3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-26 22:09:24 +01:00
ergo/irc/chanserv.go

302 lines
9.9 KiB
Go
Raw Normal View History

// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"bytes"
"fmt"
"hash/crc32"
2018-05-23 21:35:50 +02:00
"sort"
"strconv"
"strings"
"time"
2017-06-15 18:14:19 +02:00
"github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/modes"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/sno"
)
const chanservHelp = `ChanServ lets you register and manage channels.`
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
}
var (
2018-04-19 08:48:19 +02:00
chanservCommands = map[string]*serviceCommand{
"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,
},
"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,
help: `Syntax: $bUNREGISTER #channel [code]$b
2018-06-04 11:02:22 +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,
},
"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
},
}
)
// 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)
}
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
}
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)
})
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 {
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 {
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 {
csNotice(rb, client.t("Channel does not exist"))
return
}
2019-01-04 04:32:07 +01:00
channelName := channelInfo.Name()
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"))
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 {
csNotice(rb, client.t("Could not find given client"))
return
}
} else {
target = client
}
// give them privs
givenMode := modes.ChannelOperator
2019-02-17 12:51:48 +01:00
if clientAccount == target.Account() {
givenMode = modes.ChannelFounder
}
2019-02-17 12:51:48 +01:00
change := channelInfo.applyModeToMember(client, givenMode, modes.Add, target.NickCasefolded(), rb)
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...)
}
}
csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName))
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))
}
2019-01-04 04:32:07 +01:00
func csRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channelName := params[0]
channelKey, err := CasefoldChannel(channelName)
if err != nil {
csNotice(rb, client.t("Channel name is not valid"))
return
}
channelInfo := server.channels.Get(channelKey)
if channelInfo == nil || !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) {
csNotice(rb, client.t("You must be an oper on the channel to register it"))
return
}
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
}
// 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)
if err != nil {
csNotice(rb, err.Error())
return
}
csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
2019-01-04 04:32:07 +01:00
server.logger.Info("services", 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))
// give them founder privs
2018-04-23 00:47:10 +02:00
change := channelInfo.applyModeToMember(client, modes.ChannelFounder, modes.Add, client.NickCasefolded(), rb)
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-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
}
founder := channel.Founder()
if founder == "" {
csNotice(rb, client.t("That channel is not registered"))
return
2018-06-04 11:02:22 +02: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)
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 %[1]s %[2]s"), channelKey, expectedCode))
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))
}
// 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())))
}