diff --git a/irc/chanserv.go b/irc/chanserv.go index b6c1cc23..d5ccaead 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -5,16 +5,62 @@ package irc import ( "fmt" + "sort" "strings" "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" + "github.com/oragono/oragono/irc/utils" ) -// ChanServNotice sends the client a notice from ChanServ. -func (rb *ResponseBuffer) ChanServNotice(text string) { - rb.Add(nil, fmt.Sprintf("ChanServ!services@%s", rb.target.server.name), "NOTICE", rb.target.nick, text) +const chanservHelp = `ChanServ lets you register and manage channels. + +To see in-depth help for a specific ChanServ command, try: + $b/CS HELP $b + +Here are the commands you can use: +%s` + +type csCommand struct { + capabs []string // oper capabs the given user has to have to access this command + handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer) + help string + helpShort string + oper bool // true if the user has to be an oper to use this command +} + +var ( + chanservCommands = map[string]*csCommand{ + "help": { + help: `Syntax: $bHELP [command]$b + +HELP returns information on the given command.`, + helpShort: `$bHELP$b shows in-depth information about commands.`, + }, + "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.`, + helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`, + }, + "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.`, + helpShort: `$bREGISTER$b lets you own a given channel.`, + }, + } +) + +// csNotice sends the client a notice from ChanServ +func csNotice(rb *ResponseBuffer, text string) { + rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text) } // chanservReceiveNotice handles NOTICEs that ChanServ receives. @@ -24,68 +70,115 @@ func (server *Server) chanservNoticeHandler(client *Client, message string, rb * // chanservReceiveNotice handles NOTICEs that ChanServ receives. func (server *Server) chanservPrivmsgHandler(client *Client, message string, rb *ResponseBuffer) { - var params []string - for _, p := range strings.Split(message, " ") { - if len(p) > 0 { - params = append(params, p) - } - } - if len(params) < 1 { - rb.ChanServNotice(client.t("You need to run a command")) - //TODO(dan): dump CS help here + commandName, params := utils.ExtractParam(message) + commandName = strings.ToLower(commandName) + + commandInfo := chanservCommands[commandName] + if commandInfo == nil { + csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP")) return } - command := strings.ToLower(params[0]) - server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.nick, command)) - - if command == "register" { - if len(params) < 2 { - rb.ChanServNotice(client.t("Syntax: REGISTER ")) - return - } - - server.chanservRegisterHandler(client, params[1], rb) - } else if command == "op" { - if len(params) < 2 { - rb.ChanServNotice(client.t("Syntax: OP []")) - return - } - - var clientToOp string - if 2 < len(params) { - clientToOp = params[2] - } - - server.chanservOpHandler(client, params[1], clientToOp, rb) - } else { - rb.ChanServNotice(client.t("Sorry, I don't know that command")) + if commandInfo.oper && !client.HasMode(modes.Operator) { + csNotice(rb, client.t("Command restricted")) + return } + + if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) { + csNotice(rb, client.t("Command restricted")) + return + } + + // custom help handling here to prevent recursive init loop + if commandName == "help" { + csHelpHandler(server, client, commandName, params, rb) + return + } + + if commandInfo.handler == nil { + csNotice(rb, client.t("Command error. Please report this to the developers")) + return + } + + server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName)) + + commandInfo.handler(server, client, commandName, params, rb) } -// chanservOpHandler handles the ChanServ OP subcommand. -func (server *Server) chanservOpHandler(client *Client, channelName, clientToOp string, rb *ResponseBuffer) { +func csHelpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { + csNotice(rb, ircfmt.Unescape(client.t("*** $bChanServ HELP$b ***"))) + + if params == "" { + // show general help + var shownHelpLines sort.StringSlice + for _, commandInfo := range chanservCommands { + // skip commands user can't access + if commandInfo.oper && !client.HasMode(modes.Operator) { + continue + } + if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) { + continue + } + + shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort)) + } + + // sort help lines + sort.Sort(shownHelpLines) + + // assemble help text + assembledHelpLines := strings.Join(shownHelpLines, "\n") + fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(chanservHelp), assembledHelpLines)) + + // push out help text + for _, line := range strings.Split(fullHelp, "\n") { + csNotice(rb, line) + } + } else { + commandInfo := chanservCommands[strings.ToLower(strings.TrimSpace(params))] + if commandInfo == nil { + csNotice(rb, client.t("Unknown command. To see available commands, run /cs HELP")) + } else { + for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") { + csNotice(rb, line) + } + } + } + + csNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ HELP$b ***"))) +} + +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) + channelKey, err := CasefoldChannel(channelName) if err != nil { - rb.ChanServNotice(client.t("Channel name is not valid")) + csNotice(rb, client.t("Channel name is not valid")) return } channelInfo := server.channels.Get(channelKey) if channelInfo == nil { - rb.ChanServNotice(client.t("Channel does not exist")) + csNotice(rb, client.t("Channel does not exist")) return } clientAccount := client.Account() if clientAccount == "" { - rb.ChanServNotice(client.t("You must be logged in to op on a channel")) + csNotice(rb, client.t("You must be logged in to op on a channel")) return } if clientAccount != channelInfo.Founder() { - rb.ChanServNotice(client.t("You must be the channel founder to op")) + csNotice(rb, client.t("You must be the channel founder to op")) return } @@ -94,7 +187,7 @@ func (server *Server) chanservOpHandler(client *Client, channelName, clientToOp casefoldedNickname, err := CasefoldName(clientToOp) target = server.clients.Get(casefoldedNickname) if err != nil || target == nil { - rb.ChanServNotice(client.t("Could not find given client")) + csNotice(rb, client.t("Could not find given client")) return } } else { @@ -116,47 +209,52 @@ func (server *Server) chanservOpHandler(client *Client, channelName, clientToOp } } - rb.ChanServNotice(fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName)) + csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName)) 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)) } -// chanservRegisterHandler handles the ChanServ REGISTER subcommand. -func (server *Server) chanservRegisterHandler(client *Client, channelName string, rb *ResponseBuffer) { +func csRegisterHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { if !server.channelRegistrationEnabled { - rb.ChanServNotice(client.t("Channel registration is not enabled")) + csNotice(rb, client.t("Channel registration is not enabled")) + return + } + + channelName := strings.TrimSpace(params) + if channelName == "" { + csNotice(rb, ircfmt.Unescape(client.t("Syntax: $bREGISTER #channel$b"))) return } channelKey, err := CasefoldChannel(channelName) if err != nil { - rb.ChanServNotice(client.t("Channel name is not valid")) + csNotice(rb, client.t("Channel name is not valid")) return } channelInfo := server.channels.Get(channelKey) if channelInfo == nil || !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) { - rb.ChanServNotice(client.t("You must be an oper on the channel to register it")) + csNotice(rb, client.t("You must be an oper on the channel to register it")) return } if client.Account() == "" { - rb.ChanServNotice(client.t("You must be logged in to register a channel")) + csNotice(rb, client.t("You must be logged in to register a channel")) return } // this provides the synchronization that allows exactly one registration of the channel: err = channelInfo.SetRegistered(client.Account()) if err != nil { - rb.ChanServNotice(err.Error()) + csNotice(rb, err.Error()) return } // registration was successful: make the database reflect it go server.channelRegistry.StoreChannel(channelInfo, true) - rb.ChanServNotice(fmt.Sprintf(client.t("Channel %s successfully registered"), channelName)) + csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName)) 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)) diff --git a/irc/nickserv.go b/irc/nickserv.go index 188f9dce..3bd9a99c 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -120,7 +120,7 @@ or other verification.`, } ) -// send a notice from the NickServ "nick" +// csNotice sends the client a notice from NickServ func nsNotice(rb *ResponseBuffer, text string) { rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text) } @@ -167,6 +167,8 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb return } + server.logger.Debug("nickserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName)) + commandInfo.handler(server, client, commandName, params, rb) }