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"
2017-03-11 13:01:40 +01:00
)
2019-07-02 18:36:32 +02:00
const chanservHelp = ` ChanServ lets you register and manage channels. `
2018-04-01 03:51:34 +02:00
2019-01-04 04:32:07 +01:00
func chanregEnabled ( config * Config ) bool {
return config . Channels . Registration . Enabled
2018-06-04 11:02:22 +02:00
}
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 ,
2019-01-04 04:32:07 +01:00
minParams : 1 ,
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 ,
2019-01-04 04:32:07 +01:00
minParams : 1 ,
2018-06-04 11:02:22 +02:00
} ,
"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 ,
2019-01-04 04:32:07 +01:00
minParams : 1 ,
2018-04-01 03:51:34 +02:00
} ,
2018-08-06 15:21:29 +02:00
"drop" : {
aliasOf : "unregister" ,
} ,
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 ,
2019-01-04 04:32:07 +01:00
minParams : 1 ,
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 ) {
2019-05-19 10:27:44 +02:00
rb . Add ( nil , "ChanServ!ChanServ@localhost" , "NOTICE" , rb . target . Nick ( ) , text )
2017-03-11 13:01:40 +01:00
}
2019-01-04 04:32:07 +01:00
func csAmodeHandler ( server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
channelName := params [ 0 ]
2018-05-23 21:35:50 +02:00
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
}
2019-01-04 16:03:12 +01:00
modeChanges , unknown := modes . ParseChannelModeChanges ( params [ 1 : ] ... )
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 )
} )
2019-02-22 03:37:11 +01:00
csNotice ( rb , fmt . Sprintf ( client . t ( "Channel %[1]s has %[2]d persistent modes set" ) , channelName , len ( affectedModes ) ) )
2018-05-25 06:38:20 +02:00
for _ , modeChange := range affectedModes {
2019-02-22 03:37:11 +01:00
csNotice ( rb , fmt . Sprintf ( client . t ( "Account %[1]s receives mode +%[2]s" ) , modeChange . Arg , string ( modeChange . Mode ) ) )
2018-05-25 06:38:20 +02:00
}
case modes . Add , modes . Remove :
if len ( affectedModes ) > 0 {
csNotice ( rb , fmt . Sprintf ( client . t ( "Successfully set mode %s" ) , change . String ( ) ) )
} else {
2019-02-22 03:37:11 +01:00
csNotice ( rb , client . t ( "No changes were made" ) )
2018-05-25 06:38:20 +02:00
}
2018-05-23 21:35:50 +02:00
}
}
2019-01-04 04:32:07 +01:00
func csOpHandler ( server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
channelInfo := server . channels . Get ( params [ 0 ] )
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
}
2019-01-04 04:32:07 +01:00
channelName := channelInfo . Name ( )
2018-03-31 17:26:31 +02:00
clientAccount := client . Account ( )
2018-04-19 08:48:19 +02:00
if clientAccount == "" || clientAccount != channelInfo . Founder ( ) {
2019-05-17 05:41:32 +02:00
csNotice ( rb , client . t ( "Only the channel founder can do this" ) )
2018-03-31 17:26:31 +02:00
return
}
var target * Client
2019-01-04 04:32:07 +01:00
if len ( params ) > 1 {
target = server . clients . Get ( params [ 1 ] )
if 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
2019-02-17 12:51:48 +01:00
if clientAccount == target . Account ( ) {
2018-03-31 17:26:31 +02:00
givenMode = modes . ChannelFounder
}
2019-02-17 12:51:48 +01:00
change := channelInfo . applyModeToMember ( client , givenMode , modes . Add , target . 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
2019-01-04 04:32:07 +01:00
tnick := target . Nick ( )
server . logger . Info ( "services" , fmt . Sprintf ( "Client %s op'd [%s] in channel %s" , client . Nick ( ) , tnick , 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 ( ) , tnick , channelName ) )
2018-03-31 17:26:31 +02:00
}
2019-01-04 04:32:07 +01:00
func csRegisterHandler ( server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
channelName := params [ 0 ]
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
2019-02-06 10:32:04 +01:00
account := client . Account ( )
channelsAlreadyRegistered := server . accounts . ChannelsForAccount ( account )
if server . Config ( ) . Channels . Registration . MaxChannelsPerAccount <= len ( channelsAlreadyRegistered ) {
csNotice ( rb , client . t ( "You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER" ) )
return
}
2018-02-03 12:38:28 +01:00
// this provides the synchronization that allows exactly one registration of the channel:
2019-03-12 00:24:45 +01:00
err = server . channels . SetRegistered ( channelKey , 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-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
2019-01-04 04:32:07 +01:00
server . logger . Info ( "services" , fmt . Sprintf ( "Client %s registered channel %s" , client . nick , channelName ) )
2018-02-03 12:38:28 +01:00
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
2019-01-04 04:32:07 +01:00
func csUnregisterHandler ( server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
channelName := params [ 0 ]
var verificationCode string
if len ( params ) > 1 {
verificationCode = params [ 1 ]
}
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
}
2019-02-12 05:30:49 +01:00
founder := channel . Founder ( )
if founder == "" {
csNotice ( rb , client . t ( "That channel is not registered" ) )
return
2018-06-04 11:02:22 +02:00
}
2019-02-12 05:30:49 +01:00
hasPrivs := client . HasRoleCapabs ( "chanreg" ) || founder == client . Account ( )
2018-06-04 11:02:22 +02:00
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" ) ) )
2019-02-22 03:37:11 +01:00
csNotice ( rb , fmt . Sprintf ( client . t ( "To confirm channel unregistration, type: /CS UNREGISTER %[1]s %[2]s" ) , channelKey , expectedCode ) )
2018-06-05 11:23:36 +02:00
return
}
2019-03-12 00:24:45 +01:00
server . channels . SetUnregistered ( channelKey , founder )
2018-06-04 11:02:22 +02:00
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 ( ) ) ) )
}