1// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2// released under the MIT license
3
4package irc
5
6import (
7	"fmt"
8	"regexp"
9	"sort"
10	"strings"
11	"time"
12
13	"github.com/ergochat/ergo/irc/modes"
14	"github.com/ergochat/ergo/irc/sno"
15	"github.com/ergochat/ergo/irc/utils"
16	"github.com/ergochat/irc-go/ircfmt"
17)
18
19const chanservHelp = `ChanServ lets you register and manage channels.`
20
21func chanregEnabled(config *Config) bool {
22	return config.Channels.Registration.Enabled
23}
24
25var (
26	chanservCommands = map[string]*serviceCommand{
27		"op": {
28			handler: csOpHandler,
29			help: `Syntax: $bOP #channel [nickname]$b
30
31OP makes the given nickname, or yourself, a channel admin. You can only use
32this command if you're a founder or in the AMODEs of the channel.`,
33			helpShort:    `$bOP$b makes the given user (or yourself) a channel admin.`,
34			authRequired: true,
35			enabled:      chanregEnabled,
36			minParams:    1,
37		},
38		"deop": {
39			handler: csDeopHandler,
40			help: `Syntax: $bDEOP #channel [nickname]$b
41
42DEOP removes the given nickname, or yourself, the channel admin. You can only use
43this command if you're the founder of the channel.`,
44			helpShort: `$bDEOP$b removes the given user (or yourself) from a channel admin.`,
45			enabled:   chanregEnabled,
46			minParams: 1,
47		},
48		"register": {
49			handler: csRegisterHandler,
50			help: `Syntax: $bREGISTER #channel$b
51
52REGISTER lets you own the given channel. If you rejoin this channel, you'll be
53given admin privs on it. Modes set on the channel and the topic will also be
54remembered.`,
55			helpShort:    `$bREGISTER$b lets you own a given channel.`,
56			authRequired: true,
57			enabled:      chanregEnabled,
58			minParams:    1,
59		},
60		"unregister": {
61			handler: csUnregisterHandler,
62			help: `Syntax: $bUNREGISTER #channel [code]$b
63
64UNREGISTER deletes a channel registration, allowing someone else to claim it.
65To prevent accidental unregistrations, a verification code is required;
66invoking the command without a code will display the necessary code.`,
67			helpShort: `$bUNREGISTER$b deletes a channel registration.`,
68			enabled:   chanregEnabled,
69			minParams: 1,
70		},
71		"drop": {
72			aliasOf: "unregister",
73		},
74		"amode": {
75			handler: csAmodeHandler,
76			help: `Syntax: $bAMODE #channel [mode change] [account]$b
77
78AMODE lists or modifies persistent mode settings that affect channel members.
79For example, $bAMODE #channel +o dan$b grants the holder of the "dan"
80account the +o operator mode every time they join #channel. To list current
81accounts and modes, use $bAMODE #channel$b. Note that users are always
82referenced by their registered account names, not their nicknames.
83The permissions hierarchy for adding and removing modes is the same as in
84the ordinary /MODE command.`,
85			helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`,
86			enabled:   chanregEnabled,
87			minParams: 1,
88		},
89		"clear": {
90			handler: csClearHandler,
91			help: `Syntax: $bCLEAR #channel target$b
92
93CLEAR removes users or settings from a channel. Specifically:
94
95$bCLEAR #channel users$b kicks all users except for you.
96$bCLEAR #channel access$b resets all stored bans, invites, ban exceptions,
97and persistent user-mode grants made with CS AMODE.`,
98			helpShort: `$bCLEAR$b removes users or settings from a channel.`,
99			enabled:   chanregEnabled,
100			minParams: 2,
101		},
102		"transfer": {
103			handler: csTransferHandler,
104			help: `Syntax: $bTRANSFER [accept] #channel user [code]$b
105
106TRANSFER transfers ownership of a channel from one user to another.
107To prevent accidental transfers, a verification code is required. For
108example, $bTRANSFER #channel alice$b displays the required confirmation
109code, then $bTRANSFER #channel alice 2930242125$b initiates the transfer.
110Unless you are an IRC operator with the correct permissions, alice must
111then accept the transfer, which she can do with $bTRANSFER accept #channel$b.
112To cancel a pending transfer, transfer the channel to yourself.`,
113			helpShort: `$bTRANSFER$b transfers ownership of a channel to another user.`,
114			enabled:   chanregEnabled,
115			minParams: 2,
116		},
117		"purge": {
118			handler: csPurgeHandler,
119			help: `Syntax: $bPURGE <ADD | DEL | LIST> #channel [code] [reason]$b
120
121PURGE ADD blacklists a channel from the server, making it impossible to join
122or otherwise interact with the channel. If the channel currently has members,
123they will be kicked from it. PURGE may also be applied preemptively to
124channels that do not currently have members. A purge can be undone with
125PURGE DEL. To list purged channels, use PURGE LIST.`,
126			helpShort:         `$bPURGE$b blacklists a channel from the server.`,
127			capabs:            []string{"chanreg"},
128			minParams:         1,
129			maxParams:         3,
130			unsplitFinalParam: true,
131		},
132		"list": {
133			handler: csListHandler,
134			help: `Syntax: $bLIST [regex]$b
135
136LIST returns the list of registered channels, which match the given regex.
137If no regex is provided, all registered channels are returned.`,
138			helpShort: `$bLIST$b searches the list of registered channels.`,
139			capabs:    []string{"chanreg"},
140			minParams: 0,
141		},
142		"info": {
143			handler: csInfoHandler,
144			help: `Syntax: $INFO #channel$b
145
146INFO displays info about a registered channel.`,
147			helpShort: `$bINFO$b displays info about a registered channel.`,
148			enabled:   chanregEnabled,
149		},
150		"get": {
151			handler: csGetHandler,
152			help: `Syntax: $bGET #channel <setting>$b
153
154GET queries the current values of the channel settings. For more information
155on the settings and their possible values, see HELP SET.`,
156			helpShort: `$bGET$b queries the current values of a channel's settings`,
157			enabled:   chanregEnabled,
158			minParams: 2,
159		},
160		"set": {
161			handler:   csSetHandler,
162			helpShort: `$bSET$b modifies a channel's settings`,
163			// these are broken out as separate strings so they can be translated separately
164			helpStrings: []string{
165				`Syntax $bSET #channel <setting> <value>$b
166
167SET modifies a channel's settings. The following settings are available:`,
168
169				`$bHISTORY$b
170'history' lets you control how channel history is stored. Your options are:
1711. 'off'        [no history]
1722. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
1733. 'on'         [history stored in a permanent database, if available]
1744. 'default'    [use the server default]`,
175				`$bQUERY-CUTOFF$b
176'query-cutoff' lets you restrict how much channel history can be retrieved
177by unprivileged users. Your options are:
1781. 'none'               [no restrictions]
1792. 'registration-time'  [users can view history from after their account was
180                         registered, plus a grace period]
1813. 'join-time'          [users can view history from after they joined the
182                         channel; note that history will be effectively
183                         unavailable to clients that are not always-on]
1844. 'default'            [use the server default]`,
185			},
186			enabled:   chanregEnabled,
187			minParams: 3,
188		},
189		"howtoban": {
190			handler:   csHowToBanHandler,
191			helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`,
192			help: `Syntax: $bHOWTOBAN #channel <nick>
193
194The best way to ban a user from a channel will depend on how they are
195connected to the server. $bHOWTOBAN$b suggests a ban command that will
196(ideally) prevent the user from returning to the channel.`,
197			enabled:   chanregEnabled,
198			minParams: 2,
199		},
200	}
201)
202
203func csAmodeHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
204	channelName := params[0]
205
206	channel := server.channels.Get(channelName)
207	if channel == nil {
208		service.Notice(rb, client.t("Channel does not exist"))
209		return
210	} else if channel.Founder() == "" {
211		service.Notice(rb, client.t("Channel is not registered"))
212		return
213	}
214
215	modeChanges, unknown := modes.ParseChannelModeChanges(params[1:]...)
216	var change modes.ModeChange
217	if len(modeChanges) > 1 || len(unknown) > 0 {
218		service.Notice(rb, client.t("Invalid mode change"))
219		return
220	} else if len(modeChanges) == 1 {
221		change = modeChanges[0]
222	} else {
223		change = modes.ModeChange{Op: modes.List}
224	}
225
226	// normalize and validate the account argument
227	accountIsValid := false
228	change.Arg, _ = CasefoldName(change.Arg)
229	switch change.Op {
230	case modes.List:
231		accountIsValid = true
232	case modes.Add:
233		// if we're adding a mode, the account must exist
234		if change.Arg != "" {
235			_, err := server.accounts.LoadAccount(change.Arg)
236			accountIsValid = (err == nil)
237		}
238	case modes.Remove:
239		// allow removal of accounts that may have been deleted
240		accountIsValid = (change.Arg != "")
241	}
242	if !accountIsValid {
243		service.Notice(rb, client.t("Account does not exist"))
244		return
245	}
246
247	affectedModes, err := channel.ProcessAccountToUmodeChange(client, change)
248
249	if err == errInsufficientPrivs {
250		service.Notice(rb, client.t("Insufficient privileges"))
251		return
252	} else if err != nil {
253		service.Notice(rb, client.t("Internal error"))
254		return
255	}
256
257	switch change.Op {
258	case modes.List:
259		// sort the persistent modes in descending order of priority
260		sort.Slice(affectedModes, func(i, j int) bool {
261			return umodeGreaterThan(affectedModes[i].Mode, affectedModes[j].Mode)
262		})
263		service.Notice(rb, fmt.Sprintf(client.t("Channel %[1]s has %[2]d persistent modes set"), channelName, len(affectedModes)))
264		for _, modeChange := range affectedModes {
265			service.Notice(rb, fmt.Sprintf(client.t("Account %[1]s receives mode +%[2]s"), modeChange.Arg, string(modeChange.Mode)))
266		}
267	case modes.Add, modes.Remove:
268		if len(affectedModes) > 0 {
269			service.Notice(rb, fmt.Sprintf(client.t("Successfully set persistent mode %[1]s on %[2]s"), strings.Join([]string{string(change.Op), string(change.Mode)}, ""), change.Arg))
270			// #729: apply change to current membership
271			for _, member := range channel.Members() {
272				if member.Account() == change.Arg {
273					applied, change := channel.applyModeToMember(client, change, rb)
274					if applied {
275						announceCmodeChanges(channel, modes.ModeChanges{change}, server.name, "*", "", false, rb)
276					}
277				}
278			}
279		} else {
280			service.Notice(rb, client.t("No changes were made"))
281		}
282	}
283}
284
285func csOpHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
286	channelInfo := server.channels.Get(params[0])
287	if channelInfo == nil {
288		service.Notice(rb, client.t("Channel does not exist"))
289		return
290	}
291	channelName := channelInfo.Name()
292	founder := channelInfo.Founder()
293
294	clientAccount := client.Account()
295	if clientAccount == "" {
296		service.Notice(rb, client.t("You're not logged into an account"))
297		return
298	}
299
300	var target *Client
301	if len(params) > 1 {
302		target = server.clients.Get(params[1])
303		if target == nil {
304			service.Notice(rb, client.t("Could not find given client"))
305			return
306		}
307	} else {
308		target = client
309	}
310
311	var givenMode modes.Mode
312	if target == client {
313		if clientAccount == founder {
314			givenMode = modes.ChannelFounder
315		} else {
316			givenMode = channelInfo.getAmode(clientAccount)
317			if givenMode == modes.Mode(0) {
318				service.Notice(rb, client.t("You don't have any stored privileges on that channel"))
319				return
320			}
321		}
322	} else {
323		if clientAccount == founder {
324			givenMode = modes.ChannelOperator
325		} else {
326			service.Notice(rb, client.t("Only the channel founder can do this"))
327			return
328		}
329	}
330
331	applied, change := channelInfo.applyModeToMember(client,
332		modes.ModeChange{Mode: givenMode,
333			Op:  modes.Add,
334			Arg: target.NickCasefolded(),
335		},
336		rb)
337	if applied {
338		announceCmodeChanges(channelInfo, modes.ModeChanges{change}, server.name, "*", "", false, rb)
339	}
340
341	service.Notice(rb, client.t("Successfully granted operator privileges"))
342
343	tnick := target.Nick()
344	server.logger.Info("services", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.Nick(), tnick, channelName))
345	server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.NickMaskString(), tnick, channelName))
346}
347
348func csDeopHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
349	channel := server.channels.Get(params[0])
350	if channel == nil {
351		service.Notice(rb, client.t("Channel does not exist"))
352		return
353	}
354	if !channel.hasClient(client) {
355		service.Notice(rb, client.t("You're not on that channel"))
356		return
357	}
358
359	var target *Client
360	if len(params) > 1 {
361		target = server.clients.Get(params[1])
362		if target == nil {
363			service.Notice(rb, client.t("Could not find given client"))
364			return
365		}
366	} else {
367		target = client
368	}
369
370	present, _, cumodes := channel.ClientStatus(target)
371	if !present || len(cumodes) == 0 {
372		service.Notice(rb, client.t("Target has no privileges to remove"))
373		return
374	}
375
376	tnick := target.Nick()
377	modeChanges := make(modes.ModeChanges, len(cumodes))
378	for i, mode := range cumodes {
379		modeChanges[i] = modes.ModeChange{
380			Mode: mode,
381			Op:   modes.Remove,
382			Arg:  tnick,
383		}
384	}
385
386	// use the user's own permissions for the check, then announce
387	// the changes as coming from chanserv
388	applied := channel.ApplyChannelModeChanges(client, false, modeChanges, rb)
389	details := client.Details()
390	isBot := client.HasMode(modes.Bot)
391	announceCmodeChanges(channel, applied, details.nickMask, details.accountName, details.account, isBot, rb)
392
393	if len(applied) == 0 {
394		return
395	}
396
397	service.Notice(rb, client.t("Successfully removed operator privileges"))
398}
399
400func csRegisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
401	if server.Config().Channels.Registration.OperatorOnly && !client.HasRoleCapabs("chanreg") {
402		service.Notice(rb, client.t("Channel registration is restricted to server operators"))
403		return
404	}
405	channelName := params[0]
406	channelInfo := server.channels.Get(channelName)
407	if channelInfo == nil {
408		service.Notice(rb, client.t("No such channel"))
409		return
410	}
411	if !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) {
412		service.Notice(rb, client.t("You must be an oper on the channel to register it"))
413		return
414	}
415
416	account := client.Account()
417	if !checkChanLimit(service, client, rb) {
418		return
419	}
420
421	// this provides the synchronization that allows exactly one registration of the channel:
422	err := server.channels.SetRegistered(channelName, account)
423	if err != nil {
424		service.Notice(rb, err.Error())
425		return
426	}
427
428	service.Notice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
429
430	server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName))
431	server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
432
433	// give them founder privs
434	applied, change := channelInfo.applyModeToMember(client,
435		modes.ModeChange{
436			Mode: modes.ChannelFounder,
437			Op:   modes.Add,
438			Arg:  client.NickCasefolded(),
439		},
440		rb)
441	if applied {
442		announceCmodeChanges(channelInfo, modes.ModeChanges{change}, service.prefix, "*", "", false, rb)
443	}
444}
445
446// check whether a client has already registered too many channels
447func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
448	account := client.Account()
449	channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
450	ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
451	if !ok {
452		service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
453	}
454	return
455}
456
457func csPrivsCheck(service *ircService, channel RegisteredChannel, client *Client, rb *ResponseBuffer) (success bool) {
458	founder := channel.Founder
459	if founder == "" {
460		service.Notice(rb, client.t("That channel is not registered"))
461		return false
462	}
463	if client.HasRoleCapabs("chanreg") {
464		return true
465	}
466	if founder != client.Account() {
467		service.Notice(rb, client.t("Insufficient privileges"))
468		return false
469	}
470	return true
471}
472
473func csUnregisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
474	channelName := params[0]
475	var verificationCode string
476	if len(params) > 1 {
477		verificationCode = params[1]
478	}
479
480	channel := server.channels.Get(channelName)
481	if channel == nil {
482		service.Notice(rb, client.t("No such channel"))
483		return
484	}
485
486	info := channel.ExportRegistration(0)
487	channelKey := info.NameCasefolded
488	if !csPrivsCheck(service, info, client, rb) {
489		return
490	}
491
492	expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt)
493	if expectedCode != verificationCode {
494		service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b")))
495		service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS UNREGISTER %s %s", channelKey, expectedCode)))
496		return
497	}
498
499	server.channels.SetUnregistered(channelKey, info.Founder)
500	service.Notice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey))
501}
502
503func csClearHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
504	channel := server.channels.Get(params[0])
505	if channel == nil {
506		service.Notice(rb, client.t("Channel does not exist"))
507		return
508	}
509	if !csPrivsCheck(service, channel.ExportRegistration(0), client, rb) {
510		return
511	}
512
513	switch strings.ToLower(params[1]) {
514	case "access":
515		channel.resetAccess()
516		service.Notice(rb, client.t("Successfully reset channel access"))
517	case "users":
518		for _, target := range channel.Members() {
519			if target != client {
520				channel.Kick(client, target, "Cleared by ChanServ", rb, true)
521			}
522		}
523	default:
524		service.Notice(rb, client.t("Invalid parameters"))
525	}
526
527}
528
529func csTransferHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
530	if strings.ToLower(params[0]) == "accept" {
531		processTransferAccept(service, client, params[1], rb)
532		return
533	}
534	chname := params[0]
535	channel := server.channels.Get(chname)
536	if channel == nil {
537		service.Notice(rb, client.t("Channel does not exist"))
538		return
539	}
540	regInfo := channel.ExportRegistration(0)
541	chname = regInfo.Name
542	account := client.Account()
543	isFounder := account != "" && account == regInfo.Founder
544	oper := client.Oper()
545	hasPrivs := oper.HasRoleCapab("chanreg")
546	if !isFounder && !hasPrivs {
547		service.Notice(rb, client.t("Insufficient privileges"))
548		return
549	}
550	target := params[1]
551	targetAccount, err := server.accounts.LoadAccount(params[1])
552	if err != nil {
553		service.Notice(rb, client.t("Account does not exist"))
554		return
555	}
556	if targetAccount.NameCasefolded != account {
557		expectedCode := utils.ConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
558		codeValidated := 2 < len(params) && params[2] == expectedCode
559		if !codeValidated {
560			service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b")))
561			service.Notice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode))
562			return
563		}
564	}
565	if !isFounder {
566		message := fmt.Sprintf("Operator %s ran CS TRANSFER on %s to account %s", oper.Name, chname, target)
567		server.snomasks.Send(sno.LocalOpers, message)
568		server.logger.Info("opers", message)
569	}
570	status, err := channel.Transfer(client, target, hasPrivs)
571	if err == nil {
572		switch status {
573		case channelTransferComplete:
574			service.Notice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target))
575		case channelTransferPending:
576			sendTransferPendingNotice(service, server, target, chname)
577			service.Notice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target))
578		case channelTransferCancelled:
579			service.Notice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname))
580		}
581	} else {
582		switch err {
583		case errChannelNotOwnedByAccount:
584			service.Notice(rb, client.t("You don't own that channel"))
585		default:
586			service.Notice(rb, client.t("Could not transfer channel"))
587		}
588	}
589}
590
591func sendTransferPendingNotice(service *ircService, server *Server, account, chname string) {
592	clients := server.accounts.AccountToClients(account)
593	if len(clients) == 0 {
594		return
595	}
596	var client *Client
597	for _, candidate := range clients {
598		client = candidate
599		if candidate.NickCasefolded() == candidate.Account() {
600			break // prefer the login where the nick is the account
601		}
602	}
603	client.Send(nil, service.prefix, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %[1]s. To accept, /CS TRANSFER ACCEPT %[1]s"), chname))
604}
605
606func processTransferAccept(service *ircService, client *Client, chname string, rb *ResponseBuffer) {
607	channel := client.server.channels.Get(chname)
608	if channel == nil {
609		service.Notice(rb, client.t("Channel does not exist"))
610		return
611	}
612	if !checkChanLimit(service, client, rb) {
613		return
614	}
615	switch channel.AcceptTransfer(client) {
616	case nil:
617		service.Notice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name()))
618	case errChannelTransferNotOffered:
619		service.Notice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name()))
620	default:
621		service.Notice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name()))
622	}
623}
624
625func csPurgeHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
626	oper := client.Oper()
627	if oper == nil {
628		return // should be impossible because you need oper capabs for this
629	}
630
631	switch strings.ToLower(params[0]) {
632	case "add":
633		csPurgeAddHandler(service, client, params[1:], oper.Name, rb)
634	case "del", "remove":
635		csPurgeDelHandler(service, client, params[1:], oper.Name, rb)
636	case "list":
637		csPurgeListHandler(service, client, rb)
638	default:
639		service.Notice(rb, client.t("Invalid parameters"))
640	}
641}
642
643func csPurgeAddHandler(service *ircService, client *Client, params []string, operName string, rb *ResponseBuffer) {
644	if len(params) == 0 {
645		service.Notice(rb, client.t("Invalid parameters"))
646		return
647	}
648
649	chname := params[0]
650	params = params[1:]
651	channel := client.server.channels.Get(chname) // possibly nil
652	var ctime time.Time
653	if channel != nil {
654		chname = channel.Name()
655		ctime = channel.Ctime()
656	}
657	code := utils.ConfirmationCode(chname, ctime)
658
659	if len(params) == 0 || params[0] != code {
660		service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to empty this channel and remove it from the server.$b")))
661		service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/CS PURGE ADD %s %s", chname, code)))
662		return
663	}
664	params = params[1:]
665
666	var reason string
667	if 1 < len(params) {
668		reason = params[1]
669	}
670
671	purgeRecord := ChannelPurgeRecord{
672		Oper:     operName,
673		PurgedAt: time.Now().UTC(),
674		Reason:   reason,
675	}
676	switch client.server.channels.Purge(chname, purgeRecord) {
677	case nil:
678		if channel != nil { // channel need not exist to be purged
679			for _, target := range channel.Members() {
680				channel.Kick(client, target, "Cleared by ChanServ", rb, true)
681			}
682		}
683		service.Notice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname))
684	case errInvalidChannelName:
685		service.Notice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname))
686	default:
687		service.Notice(rb, client.t("An error occurred"))
688	}
689}
690
691func csPurgeDelHandler(service *ircService, client *Client, params []string, operName string, rb *ResponseBuffer) {
692	if len(params) == 0 {
693		service.Notice(rb, client.t("Invalid parameters"))
694		return
695	}
696
697	chname := params[0]
698	switch client.server.channels.Unpurge(chname) {
699	case nil:
700		service.Notice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
701	case errNoSuchChannel:
702		service.Notice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
703	default:
704		service.Notice(rb, client.t("An error occurred"))
705	}
706}
707
708func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer) {
709	l := client.server.channels.ListPurged()
710	service.Notice(rb, fmt.Sprintf(client.t("There are %d purged channel(s)."), len(l)))
711	for i, c := range l {
712		service.Notice(rb, fmt.Sprintf("%d: %s", i+1, c))
713	}
714}
715
716func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
717	if !client.HasRoleCapabs("chanreg") {
718		service.Notice(rb, client.t("Insufficient privileges"))
719		return
720	}
721
722	var searchRegex *regexp.Regexp
723	if len(params) > 0 {
724		var err error
725		searchRegex, err = regexp.Compile(params[0])
726		if err != nil {
727			service.Notice(rb, client.t("Invalid regex"))
728			return
729		}
730	}
731
732	service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
733
734	channels := server.channelRegistry.AllChannels()
735	for _, channel := range channels {
736		if searchRegex == nil || searchRegex.MatchString(channel) {
737			service.Notice(rb, fmt.Sprintf("    %s", channel))
738		}
739	}
740
741	service.Notice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ LIST$b ***")))
742}
743
744func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
745	if len(params) == 0 {
746		// #765
747		listRegisteredChannels(service, client.Account(), rb)
748		return
749	}
750
751	chname, err := CasefoldChannel(params[0])
752	if err != nil {
753		service.Notice(rb, client.t("Invalid channel name"))
754		return
755	}
756
757	// purge status
758	if client.HasRoleCapabs("chanreg") {
759		purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
760		if err == nil {
761			service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
762			service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
763			service.Notice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
764			if purgeRecord.Reason != "" {
765				service.Notice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
766			}
767		}
768	} else {
769		if server.channels.IsPurged(chname) {
770			service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
771		}
772	}
773
774	var chinfo RegisteredChannel
775	channel := server.channels.Get(params[0])
776	if channel != nil {
777		chinfo = channel.ExportRegistration(0)
778	} else {
779		chinfo, err = server.channelRegistry.LoadChannel(chname)
780		if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
781			service.Notice(rb, client.t("An error occurred"))
782			return
783		}
784	}
785
786	// channel exists but is unregistered, or doesn't exist:
787	if chinfo.Founder == "" {
788		service.Notice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
789		return
790	}
791	service.Notice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
792	service.Notice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
793	service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
794}
795
796func displayChannelSetting(service *ircService, settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {
797	config := client.server.Config()
798
799	switch strings.ToLower(settingName) {
800	case "history":
801		effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
802		service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
803		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
804	case "query-cutoff":
805		effectiveValue := settings.QueryCutoff
806		if effectiveValue == HistoryCutoffDefault {
807			effectiveValue = config.History.Restrictions.queryCutoff
808		}
809		service.Notice(rb, fmt.Sprintf(client.t("The stored channel history query cutoff setting is: %s"), historyCutoffToString(settings.QueryCutoff)))
810		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history query cutoff setting is: %s"), historyCutoffToString(effectiveValue)))
811	default:
812		service.Notice(rb, client.t("Invalid params"))
813	}
814}
815
816func csGetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
817	chname, setting := params[0], params[1]
818	channel := server.channels.Get(chname)
819	if channel == nil {
820		service.Notice(rb, client.t("No such channel"))
821		return
822	}
823	info := channel.ExportRegistration(IncludeSettings)
824	if !csPrivsCheck(service, info, client, rb) {
825		return
826	}
827
828	displayChannelSetting(service, setting, info.Settings, client, rb)
829}
830
831func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
832	chname, setting, value := params[0], params[1], params[2]
833	channel := server.channels.Get(chname)
834	if channel == nil {
835		service.Notice(rb, client.t("No such channel"))
836		return
837	}
838	info := channel.ExportRegistration(IncludeSettings)
839	settings := info.Settings
840	if !csPrivsCheck(service, info, client, rb) {
841		return
842	}
843
844	var err error
845	switch strings.ToLower(setting) {
846	case "history":
847		settings.History, err = historyStatusFromString(value)
848		if err != nil {
849			err = errInvalidParams
850			break
851		}
852		channel.SetSettings(settings)
853		channel.resizeHistory(server.Config())
854	case "query-cutoff":
855		settings.QueryCutoff, err = historyCutoffFromString(value)
856		if err != nil {
857			err = errInvalidParams
858			break
859		}
860		channel.SetSettings(settings)
861	}
862
863	switch err {
864	case nil:
865		service.Notice(rb, client.t("Successfully changed the channel settings"))
866		displayChannelSetting(service, setting, settings, client, rb)
867	case errInvalidParams:
868		service.Notice(rb, client.t("Invalid parameters"))
869	default:
870		server.logger.Error("internal", "CS SET error:", err.Error())
871		service.Notice(rb, client.t("An error occurred"))
872	}
873}
874
875func csHowToBanHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
876	success := false
877	defer func() {
878		if success {
879			service.Notice(rb, client.t("Note that if the user is currently in the channel, you must /KICK them after you ban them"))
880		}
881	}()
882
883	chname, nick := params[0], params[1]
884	channel := server.channels.Get(chname)
885	if channel == nil {
886		service.Notice(rb, client.t("No such channel"))
887		return
888	}
889
890	if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("samode")) {
891		service.Notice(rb, client.t("Insufficient privileges"))
892		return
893	}
894
895	var details WhoWas
896	target := server.clients.Get(nick)
897	if target == nil {
898		whowasList := server.whoWas.Find(nick, 1)
899		if len(whowasList) == 0 {
900			service.Notice(rb, client.t("No such nick"))
901			return
902		}
903		service.Notice(rb, fmt.Sprintf(client.t("Warning: %s is not currently connected to the server. Using WHOWAS data, which may be inaccurate:"), nick))
904		details = whowasList[0]
905	} else {
906		details = target.Details().WhoWas
907	}
908
909	if details.account != "" {
910		if channel.getAmode(details.account) != modes.Mode(0) {
911			service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s currently has a persistent channel privilege granted with CS AMODE. If this mode is not removed, bans will not be respected"), details.accountName))
912			return
913		} else if details.account == channel.Founder() {
914			service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s is the channel founder and cannot be banned"), details.accountName))
915			return
916		}
917	}
918
919	config := server.Config()
920	if !config.Server.Cloaks.EnabledForAlwaysOn {
921		service.Notice(rb, client.t("Warning: server.ip-cloaking.enabled-for-always-on is disabled. This reduces the precision of channel bans."))
922	}
923
924	if details.account != "" {
925		if config.Accounts.NickReservation.ForceNickEqualsAccount || target.AlwaysOn() {
926			service.Notice(rb, fmt.Sprintf(client.t("User %[1]s is authenticated and can be banned by nickname: /MODE %[2]s +b %[3]s!*@*"), details.nick, channel.Name(), details.nick))
927			success = true
928			return
929		}
930	}
931
932	ban := fmt.Sprintf("*!*@%s", strings.ToLower(details.hostname))
933	banRe, err := utils.CompileGlob(ban, false)
934	if err != nil {
935		server.logger.Error("internal", "couldn't compile ban regex", ban, err.Error())
936		service.Notice(rb, "An error occurred")
937		return
938	}
939	var collateralDamage []string
940	for _, mcl := range channel.Members() {
941		if mcl != target && banRe.MatchString(mcl.NickMaskCasefolded()) {
942			collateralDamage = append(collateralDamage, mcl.Nick())
943		}
944	}
945	service.Notice(rb, fmt.Sprintf(client.t("User %[1]s can be banned by hostname: /MODE %[2]s +b %[3]s"), details.nick, channel.Name(), ban))
946	success = true
947	if len(collateralDamage) != 0 {
948		service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
949		for _, line := range utils.BuildTokenLines(400, collateralDamage, " ") {
950			service.Notice(rb, line)
951		}
952	}
953}
954