2017-03-11 13:01:40 +01:00
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
2018-06-05 11:23:36 +02:00
"bytes"
2017-03-11 13:01:40 +01:00
"fmt"
2018-06-05 11:23:36 +02:00
"hash/crc32"
2018-05-23 21:35:50 +02:00
"sort"
2018-06-05 11:23:36 +02:00
"strconv"
2017-03-11 13:01:40 +01:00
"strings"
2018-06-19 10:03:40 +02:00
"time"
2017-03-11 13:01:40 +01:00
2017-06-15 18:14:19 +02:00
"github.com/goshuirc/irc-go/ircfmt"
2018-02-03 11:21:32 +01:00
"github.com/oragono/oragono/irc/modes"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/sno"
2018-04-01 03:51:34 +02:00
"github.com/oragono/oragono/irc/utils"
2017-03-11 13:01:40 +01:00
)
2018-04-01 03:51:34 +02:00
const chanservHelp = ` ChanServ lets you register and manage channels .
To see in - depth help for a specific ChanServ command , try :
$ b / CS HELP < command > $ b
Here are the commands you can use :
% s `
2018-06-04 11:02:22 +02:00
func chanregEnabled ( server * Server ) bool {
return server . ChannelRegistrationEnabled ( )
}
2018-04-01 03:51:34 +02:00
var (
2018-04-19 08:48:19 +02:00
chanservCommands = map [ string ] * serviceCommand {
2018-04-01 03:51:34 +02:00
"op" : {
handler : csOpHandler ,
help : ` Syntax : $ bOP # channel [ nickname ] $ b
OP makes the given nickname , or yourself , a channel admin . You can only use
this command if you ' re the founder of the channel . ` ,
2018-04-19 08:48:19 +02:00
helpShort : ` $bOP$b makes the given user (or yourself) a channel admin. ` ,
authRequired : true ,
2018-06-04 11:02:22 +02:00
enabled : chanregEnabled ,
2018-04-01 03:51:34 +02:00
} ,
"register" : {
handler : csRegisterHandler ,
help : ` Syntax : $ bREGISTER # channel $ b
REGISTER lets you own the given channel . If you rejoin this channel , you ' ll be
given admin privs on it . Modes set on the channel and the topic will also be
remembered . ` ,
2018-04-19 08:48:19 +02:00
helpShort : ` $bREGISTER$b lets you own a given channel. ` ,
authRequired : true ,
2018-06-04 11:02:22 +02:00
enabled : chanregEnabled ,
} ,
"unregister" : {
handler : csUnregisterHandler ,
2018-06-05 11:23:36 +02:00
help : ` Syntax : $ bUNREGISTER # channel [ code ] $ b
2018-06-04 11:02:22 +02:00
2018-06-05 11:23:36 +02:00
UNREGISTER deletes a channel registration , allowing someone else to claim it .
To prevent accidental unregistrations , a verification code is required ;
invoking the command without a code will display the necessary code . ` ,
2018-06-04 11:02:22 +02:00
helpShort : ` $bUNREGISTER$b deletes a channel registration. ` ,
enabled : chanregEnabled ,
2018-04-01 03:51:34 +02:00
} ,
2018-05-23 21:35:50 +02:00
"amode" : {
handler : csAmodeHandler ,
help : ` Syntax : $ bAMODE # channel [ mode change ] [ account ] $ b
AMODE lists or modifies persistent mode settings that affect channel members .
For example , $ bAMODE # channel + o dan $ b grants the the holder of the "dan"
account the + o operator mode every time they join # channel . To list current
accounts and modes , use $ bAMODE # channel $ b . Note that users are always
referenced by their registered account names , not their nicknames . ` ,
2018-05-25 06:38:20 +02:00
helpShort : ` $bAMODE$b modifies persistent mode settings for channel members. ` ,
2018-06-04 11:02:22 +02:00
enabled : chanregEnabled ,
2018-05-23 21:35:50 +02:00
} ,
2018-04-01 03:51:34 +02:00
}
)
// csNotice sends the client a notice from ChanServ
func csNotice ( rb * ResponseBuffer , text string ) {
rb . Add ( nil , "ChanServ" , "NOTICE" , rb . target . Nick ( ) , text )
2017-03-11 13:01:40 +01:00
}
2018-05-23 21:35:50 +02:00
func csAmodeHandler ( server * Server , client * Client , command , params string , rb * ResponseBuffer ) {
channelName , modeChange := utils . ExtractParam ( params )
channel := server . channels . Get ( channelName )
if channel == nil {
csNotice ( rb , client . t ( "Channel does not exist" ) )
return
2018-05-25 06:38:20 +02:00
} else if channel . Founder ( ) == "" {
csNotice ( rb , client . t ( "Channel is not registered" ) )
2018-05-23 21:35:50 +02:00
return
}
modeChanges , unknown := modes . ParseChannelModeChanges ( strings . Fields ( modeChange ) ... )
2018-05-25 06:38:20 +02:00
var change modes . ModeChange
2018-05-23 21:35:50 +02:00
if len ( modeChanges ) > 1 || len ( unknown ) > 0 {
csNotice ( rb , client . t ( "Invalid mode change" ) )
return
2018-05-25 06:38:20 +02:00
} else if len ( modeChanges ) == 1 {
change = modeChanges [ 0 ]
} else {
change = modes . ModeChange { Op : modes . List }
2018-05-23 21:35:50 +02:00
}
2018-05-25 06:38:20 +02:00
// normalize and validate the account argument
2018-05-23 21:35:50 +02:00
accountIsValid := false
change . Arg , _ = CasefoldName ( change . Arg )
2018-05-25 06:38:20 +02:00
switch change . Op {
case modes . List :
accountIsValid = true
case modes . Add :
// if we're adding a mode, the account must exist
if change . Arg != "" {
_ , err := server . accounts . LoadAccount ( change . Arg )
accountIsValid = ( err == nil )
}
case modes . Remove :
// allow removal of accounts that may have been deleted
accountIsValid = ( change . Arg != "" )
2018-05-23 21:35:50 +02:00
}
if ! accountIsValid {
csNotice ( rb , client . t ( "Account does not exist" ) )
return
}
2018-05-25 06:38:20 +02:00
affectedModes , err := channel . ProcessAccountToUmodeChange ( client , change )
if err == errInsufficientPrivs {
csNotice ( rb , client . t ( "Insufficient privileges" ) )
return
} else if err != nil {
csNotice ( rb , client . t ( "Internal error" ) )
return
}
switch change . Op {
case modes . List :
// sort the persistent modes in descending order of priority
sort . Slice ( affectedModes , func ( i , j int ) bool {
return umodeGreaterThan ( affectedModes [ i ] . Mode , affectedModes [ j ] . Mode )
} )
csNotice ( rb , fmt . Sprintf ( client . t ( "Channel %s has %d persistent modes set" ) , channelName , len ( affectedModes ) ) )
for _ , modeChange := range affectedModes {
csNotice ( rb , fmt . Sprintf ( client . t ( "Account %s receives mode +%s" ) , modeChange . Arg , string ( modeChange . Mode ) ) )
}
case modes . Add , modes . Remove :
if len ( affectedModes ) > 0 {
csNotice ( rb , fmt . Sprintf ( client . t ( "Successfully set mode %s" ) , change . String ( ) ) )
} else {
csNotice ( rb , client . t ( "Change was a no-op" ) )
}
2018-05-23 21:35:50 +02:00
}
}
2018-04-01 03:51:34 +02:00
func csOpHandler ( server * Server , client * Client , command , params string , rb * ResponseBuffer ) {
channelName , clientToOp := utils . ExtractParam ( params )
if channelName == "" {
csNotice ( rb , ircfmt . Unescape ( client . t ( "Syntax: $bOP #channel [nickname]$b" ) ) )
return
}
clientToOp = strings . TrimSpace ( clientToOp )
2018-03-31 17:26:31 +02:00
channelKey , err := CasefoldChannel ( channelName )
if err != nil {
2018-04-01 03:51:34 +02:00
csNotice ( rb , client . t ( "Channel name is not valid" ) )
2018-03-31 17:26:31 +02:00
return
}
channelInfo := server . channels . Get ( channelKey )
2018-04-01 01:33:58 +02:00
if channelInfo == nil {
2018-04-01 03:51:34 +02:00
csNotice ( rb , client . t ( "Channel does not exist" ) )
2018-03-31 17:26:31 +02:00
return
}
clientAccount := client . Account ( )
2018-04-19 08:48:19 +02:00
if clientAccount == "" || clientAccount != channelInfo . Founder ( ) {
2018-04-01 03:51:34 +02:00
csNotice ( rb , client . t ( "You must be the channel founder to op" ) )
2018-03-31 17:26:31 +02:00
return
}
var target * Client
if clientToOp != "" {
casefoldedNickname , err := CasefoldName ( clientToOp )
target = server . clients . Get ( casefoldedNickname )
if err != nil || target == nil {
2018-04-01 03:51:34 +02:00
csNotice ( rb , client . t ( "Could not find given client" ) )
2018-03-31 17:26:31 +02:00
return
}
} else {
target = client
}
// give them privs
givenMode := modes . ChannelOperator
if client == target {
givenMode = modes . ChannelFounder
}
2018-04-23 00:47:10 +02:00
change := channelInfo . applyModeToMember ( target , givenMode , modes . Add , client . NickCasefolded ( ) , rb )
2018-03-31 17:26:31 +02:00
if change != nil {
//TODO(dan): we should change the name of String and make it return a slice here
//TODO(dan): unify this code with code in modes.go
args := append ( [ ] string { channelName } , strings . Split ( change . String ( ) , " " ) ... )
for _ , member := range channelInfo . Members ( ) {
member . Send ( nil , fmt . Sprintf ( "ChanServ!services@%s" , client . server . name ) , "MODE" , args ... )
}
}
2018-04-01 03:51:34 +02:00
csNotice ( rb , fmt . Sprintf ( client . t ( "Successfully op'd in channel %s" ) , channelName ) )
2018-03-31 17:26:31 +02:00
server . logger . Info ( "chanserv" , fmt . Sprintf ( "Client %s op'd [%s] in channel %s" , client . nick , clientToOp , channelName ) )
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 , clientToOp , channelName ) )
}
2018-04-01 03:51:34 +02:00
func csRegisterHandler ( server * Server , client * Client , command , params string , rb * ResponseBuffer ) {
channelName := strings . TrimSpace ( params )
if channelName == "" {
csNotice ( rb , ircfmt . Unescape ( client . t ( "Syntax: $bREGISTER #channel$b" ) ) )
2018-02-03 12:38:28 +01:00
return
}
2017-03-11 13:01:40 +01:00
2018-02-03 12:38:28 +01:00
channelKey , err := CasefoldChannel ( channelName )
if err != nil {
2018-04-01 03:51:34 +02:00
csNotice ( rb , client . t ( "Channel name is not valid" ) )
2018-02-03 12:38:28 +01:00
return
}
2017-03-11 13:01:40 +01:00
2018-02-03 12:38:28 +01:00
channelInfo := server . channels . Get ( channelKey )
if channelInfo == nil || ! channelInfo . ClientIsAtLeast ( client , modes . ChannelOperator ) {
2018-04-01 03:51:34 +02:00
csNotice ( rb , client . t ( "You must be an oper on the channel to register it" ) )
2018-02-03 12:38:28 +01:00
return
}
2017-03-11 13:01:40 +01:00
2018-02-03 12:38:28 +01:00
// this provides the synchronization that allows exactly one registration of the channel:
2018-02-11 11:30:40 +01:00
err = channelInfo . SetRegistered ( client . Account ( ) )
2018-02-03 12:38:28 +01:00
if err != nil {
2018-04-01 03:51:34 +02:00
csNotice ( rb , err . Error ( ) )
2018-02-03 12:38:28 +01:00
return
}
2017-03-11 13:01:40 +01:00
2018-02-03 12:38:28 +01:00
// registration was successful: make the database reflect it
2018-04-04 03:49:40 +02:00
go server . channelRegistry . StoreChannel ( channelInfo , IncludeAllChannelAttrs )
2017-03-11 13:01:40 +01:00
2018-04-01 03:51:34 +02:00
csNotice ( rb , fmt . Sprintf ( client . t ( "Channel %s successfully registered" ) , channelName ) )
2017-03-11 13:01:40 +01:00
2018-02-03 12:38:28 +01:00
server . logger . Info ( "chanserv" , fmt . Sprintf ( "Client %s registered channel %s" , client . nick , channelName ) )
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 ) )
2017-11-09 04:19:50 +01:00
2018-02-03 12:38:28 +01:00
// give them founder privs
2018-04-23 00:47:10 +02:00
change := channelInfo . applyModeToMember ( client , modes . ChannelFounder , modes . Add , client . NickCasefolded ( ) , rb )
2018-02-03 12:38:28 +01:00
if change != nil {
//TODO(dan): we should change the name of String and make it return a slice here
//TODO(dan): unify this code with code in modes.go
args := append ( [ ] string { channelName } , strings . Split ( change . String ( ) , " " ) ... )
for _ , member := range channelInfo . Members ( ) {
member . Send ( nil , fmt . Sprintf ( "ChanServ!services@%s" , client . server . name ) , "MODE" , args ... )
2017-11-09 04:19:50 +01:00
}
2017-03-11 13:01:40 +01:00
}
}
2018-06-04 11:02:22 +02:00
func csUnregisterHandler ( server * Server , client * Client , command , params string , rb * ResponseBuffer ) {
2018-06-05 11:23:36 +02:00
channelName , verificationCode := utils . ExtractParam ( params )
2018-06-04 11:02:22 +02:00
channelKey , err := CasefoldChannel ( channelName )
if channelKey == "" || err != nil {
csNotice ( rb , client . t ( "Channel name is not valid" ) )
return
}
channel := server . channels . Get ( channelKey )
if channel == nil {
csNotice ( rb , client . t ( "No such channel" ) )
return
}
hasPrivs := client . HasRoleCapabs ( "chanreg" )
if ! hasPrivs {
founder := channel . Founder ( )
hasPrivs = founder != "" && founder == client . Account ( )
}
if ! hasPrivs {
csNotice ( rb , client . t ( "Insufficient privileges" ) )
return
}
info := channel . ExportRegistration ( 0 )
2018-06-19 10:03:40 +02:00
expectedCode := unregisterConfirmationCode ( info . Name , info . RegisteredAt )
if expectedCode != verificationCode {
csNotice ( rb , ircfmt . Unescape ( client . t ( "$bWarning: unregistering this channel will remove all stored channel attributes.$b" ) ) )
csNotice ( rb , fmt . Sprintf ( client . t ( "To confirm channel unregistration, type: /CS UNREGISTER %s %s" ) , channelKey , expectedCode ) )
2018-06-05 11:23:36 +02:00
return
}
2018-06-04 11:02:22 +02:00
channel . SetUnregistered ( )
go server . channelRegistry . Delete ( channelKey , info )
csNotice ( rb , fmt . Sprintf ( client . t ( "Channel %s is now unregistered" ) , channelKey ) )
}
2018-06-19 10:03:40 +02:00
// deterministically generates a confirmation code for unregistering a channel / account
func unregisterConfirmationCode ( name string , registeredAt time . Time ) ( code string ) {
var codeInput bytes . Buffer
codeInput . WriteString ( name )
codeInput . WriteString ( strconv . FormatInt ( registeredAt . Unix ( ) , 16 ) )
return strconv . Itoa ( int ( crc32 . ChecksumIEEE ( codeInput . Bytes ( ) ) ) )
}