2018-04-19 08:48:19 +02:00
|
|
|
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
|
|
// released under the MIT license
|
|
|
|
|
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2018-08-06 15:21:29 +02:00
|
|
|
"log"
|
2018-04-19 08:48:19 +02:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/goshuirc/irc-go/ircfmt"
|
|
|
|
"github.com/goshuirc/irc-go/ircmsg"
|
2019-01-04 16:03:12 +01:00
|
|
|
"github.com/oragono/oragono/irc/utils"
|
2018-04-19 08:48:19 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// defines an IRC service, e.g., NICKSERV
|
|
|
|
type ircService struct {
|
|
|
|
Name string
|
|
|
|
ShortName string
|
2019-05-19 10:27:44 +02:00
|
|
|
prefix string
|
2018-04-19 08:48:19 +02:00
|
|
|
CommandAliases []string
|
|
|
|
Commands map[string]*serviceCommand
|
|
|
|
HelpBanner string
|
|
|
|
}
|
|
|
|
|
|
|
|
// defines a command associated with a service, e.g., NICKSERV IDENTIFY
|
|
|
|
type serviceCommand struct {
|
2018-08-06 15:21:29 +02:00
|
|
|
aliasOf string // marks this command as an alias of another
|
2018-04-19 08:48:19 +02:00
|
|
|
capabs []string // oper capabs the given user has to have to access this command
|
2019-01-04 04:32:07 +01:00
|
|
|
handler func(server *Server, client *Client, command string, params []string, rb *ResponseBuffer)
|
2018-04-19 08:48:19 +02:00
|
|
|
help string
|
|
|
|
helpShort string
|
|
|
|
authRequired bool
|
2019-05-19 10:27:44 +02:00
|
|
|
hidden bool
|
2019-01-04 04:32:07 +01:00
|
|
|
enabled func(*Config) bool // is this command enabled in the server config?
|
|
|
|
minParams int
|
2019-01-04 16:03:12 +01:00
|
|
|
maxParams int // split into at most n params, with last param containing remaining unsplit text
|
2018-04-19 08:48:19 +02:00
|
|
|
}
|
|
|
|
|
2018-08-06 15:21:29 +02:00
|
|
|
// looks up a command in the table of command definitions for a service, resolving aliases
|
|
|
|
func lookupServiceCommand(commands map[string]*serviceCommand, command string) *serviceCommand {
|
|
|
|
maxDepth := 1
|
|
|
|
depth := 0
|
|
|
|
for depth <= maxDepth {
|
|
|
|
result, ok := commands[command]
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
} else if result.aliasOf == "" {
|
|
|
|
return result
|
|
|
|
} else {
|
|
|
|
command = result.aliasOf
|
|
|
|
depth += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-04-19 08:48:19 +02:00
|
|
|
// all services, by lowercase name
|
|
|
|
var OragonoServices = map[string]*ircService{
|
|
|
|
"nickserv": {
|
|
|
|
Name: "NickServ",
|
|
|
|
ShortName: "NS",
|
|
|
|
CommandAliases: []string{"NICKSERV", "NS"},
|
|
|
|
Commands: nickservCommands,
|
|
|
|
HelpBanner: nickservHelp,
|
|
|
|
},
|
|
|
|
"chanserv": {
|
|
|
|
Name: "ChanServ",
|
|
|
|
ShortName: "CS",
|
|
|
|
CommandAliases: []string{"CHANSERV", "CS"},
|
|
|
|
Commands: chanservCommands,
|
|
|
|
HelpBanner: chanservHelp,
|
|
|
|
},
|
|
|
|
"hostserv": {
|
|
|
|
Name: "HostServ",
|
|
|
|
ShortName: "HS",
|
|
|
|
CommandAliases: []string{"HOSTSERV", "HS"},
|
|
|
|
Commands: hostservCommands,
|
|
|
|
HelpBanner: hostservHelp,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// all service commands at the protocol level, by uppercase command name
|
|
|
|
// e.g., NICKSERV, NS
|
|
|
|
var oragonoServicesByCommandAlias map[string]*ircService
|
|
|
|
|
|
|
|
// special-cased command shared by all services
|
|
|
|
var servHelpCmd serviceCommand = serviceCommand{
|
|
|
|
help: `Syntax: $bHELP [command]$b
|
|
|
|
|
|
|
|
HELP returns information on the given command.`,
|
|
|
|
helpShort: `$bHELP$b shows in-depth information about commands.`,
|
|
|
|
}
|
|
|
|
|
2019-01-04 04:32:07 +01:00
|
|
|
// generic handler for IRC commands like `/NICKSERV INFO`
|
2018-04-19 08:48:19 +02:00
|
|
|
func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
|
|
|
service, ok := oragonoServicesByCommandAlias[msg.Command]
|
|
|
|
if !ok {
|
|
|
|
server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-01-04 16:03:12 +01:00
|
|
|
if len(msg.Params) == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
commandName := strings.ToLower(msg.Params[0])
|
|
|
|
params := msg.Params[1:]
|
|
|
|
cmd := lookupServiceCommand(service.Commands, commandName)
|
|
|
|
// for a maxParams command, join all final parameters together if necessary
|
|
|
|
if cmd != nil && cmd.maxParams != 0 && cmd.maxParams < len(params) {
|
|
|
|
newParams := make([]string, cmd.maxParams)
|
|
|
|
copy(newParams, params[:cmd.maxParams-1])
|
|
|
|
newParams[cmd.maxParams-1] = strings.Join(params[cmd.maxParams-1:], " ")
|
|
|
|
params = newParams
|
|
|
|
}
|
|
|
|
serviceRunCommand(service, server, client, cmd, commandName, params, rb)
|
2018-04-19 08:48:19 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-01-04 04:32:07 +01:00
|
|
|
// generic handler for service PRIVMSG, like `/msg NickServ INFO`
|
2018-04-19 08:48:19 +02:00
|
|
|
func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
|
2019-01-04 16:03:12 +01:00
|
|
|
params := strings.Fields(message)
|
2019-01-04 04:32:07 +01:00
|
|
|
if len(params) == 0 {
|
|
|
|
return
|
|
|
|
}
|
2019-01-04 16:03:12 +01:00
|
|
|
|
|
|
|
// look up the service command to see how to parse it
|
2019-01-04 04:32:07 +01:00
|
|
|
commandName := strings.ToLower(params[0])
|
2019-01-04 16:03:12 +01:00
|
|
|
cmd := lookupServiceCommand(service.Commands, commandName)
|
|
|
|
// reparse if needed
|
|
|
|
if cmd != nil && cmd.maxParams != 0 {
|
|
|
|
params = utils.FieldsN(message, cmd.maxParams+1)[1:]
|
|
|
|
} else {
|
|
|
|
params = params[1:]
|
|
|
|
}
|
|
|
|
serviceRunCommand(service, server, client, cmd, commandName, params, rb)
|
|
|
|
}
|
2018-04-19 08:48:19 +02:00
|
|
|
|
2019-01-04 16:03:12 +01:00
|
|
|
// actually execute a service command
|
|
|
|
func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
|
2018-04-19 08:48:19 +02:00
|
|
|
nick := rb.target.Nick()
|
|
|
|
sendNotice := func(notice string) {
|
2019-05-19 10:27:44 +02:00
|
|
|
rb.Add(nil, service.prefix, "NOTICE", nick, notice)
|
2018-04-19 08:48:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if cmd == nil {
|
2019-02-22 03:37:11 +01:00
|
|
|
sendNotice(fmt.Sprintf(client.t("Unknown command. To see available commands, run: /%s HELP"), service.ShortName))
|
2018-04-19 08:48:19 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-01-04 04:32:07 +01:00
|
|
|
if len(params) < cmd.minParams {
|
2019-02-22 03:37:11 +01:00
|
|
|
sendNotice(fmt.Sprintf(client.t("Invalid parameters. For usage, do /msg %[1]s HELP %[2]s"), service.Name, strings.ToUpper(commandName)))
|
2019-01-04 04:32:07 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if cmd.enabled != nil && !cmd.enabled(server.Config()) {
|
2018-04-19 08:48:19 +02:00
|
|
|
sendNotice(client.t("This command has been disabled by the server administrators"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
|
|
|
|
sendNotice(client.t("Command restricted"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if cmd.authRequired && client.Account() == "" {
|
|
|
|
sendNotice(client.t("You're not logged into an account"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
|
|
|
|
if commandName == "help" {
|
|
|
|
serviceHelpHandler(service, server, client, params, rb)
|
|
|
|
} else {
|
|
|
|
cmd.handler(server, client, commandName, params, rb)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generic handler that displays help for service commands
|
2019-01-04 04:32:07 +01:00
|
|
|
func serviceHelpHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) {
|
2018-04-19 08:48:19 +02:00
|
|
|
nick := rb.target.Nick()
|
2019-01-04 04:32:07 +01:00
|
|
|
config := server.Config()
|
2018-04-19 08:48:19 +02:00
|
|
|
sendNotice := func(notice string) {
|
2019-05-19 10:27:44 +02:00
|
|
|
rb.Add(nil, service.prefix, "NOTICE", nick, notice)
|
2018-04-19 08:48:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
|
|
|
|
|
2019-01-04 04:32:07 +01:00
|
|
|
if len(params) == 0 {
|
2018-04-19 08:48:19 +02:00
|
|
|
// show general help
|
|
|
|
var shownHelpLines sort.StringSlice
|
2018-05-19 01:00:22 +02:00
|
|
|
var disabledCommands bool
|
2018-04-19 08:48:19 +02:00
|
|
|
for _, commandInfo := range service.Commands {
|
|
|
|
// skip commands user can't access
|
|
|
|
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
|
|
|
continue
|
|
|
|
}
|
2019-05-19 10:27:44 +02:00
|
|
|
if commandInfo.aliasOf != "" || commandInfo.hidden {
|
2018-08-06 15:21:29 +02:00
|
|
|
continue // don't show help lines for aliases
|
|
|
|
}
|
2019-01-04 04:32:07 +01:00
|
|
|
if commandInfo.enabled != nil && !commandInfo.enabled(config) {
|
2018-05-19 01:00:22 +02:00
|
|
|
disabledCommands = true
|
2018-04-19 08:48:19 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
|
|
|
|
}
|
|
|
|
|
2018-05-19 01:00:22 +02:00
|
|
|
if disabledCommands {
|
|
|
|
shownHelpLines = append(shownHelpLines, " "+client.t("... and other commands which have been disabled"))
|
|
|
|
}
|
|
|
|
|
2018-04-19 08:48:19 +02:00
|
|
|
// sort help lines
|
|
|
|
sort.Sort(shownHelpLines)
|
|
|
|
|
|
|
|
// assemble help text
|
|
|
|
assembledHelpLines := strings.Join(shownHelpLines, "\n")
|
|
|
|
fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(service.HelpBanner), assembledHelpLines))
|
|
|
|
|
|
|
|
// push out help text
|
|
|
|
for _, line := range strings.Split(fullHelp, "\n") {
|
|
|
|
sendNotice(line)
|
|
|
|
}
|
|
|
|
} else {
|
2019-01-04 04:32:07 +01:00
|
|
|
commandName := strings.ToLower(params[0])
|
2018-08-06 15:21:29 +02:00
|
|
|
commandInfo := lookupServiceCommand(service.Commands, commandName)
|
2018-04-19 08:48:19 +02:00
|
|
|
if commandInfo == nil {
|
|
|
|
sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
|
|
|
|
} else {
|
|
|
|
for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
|
|
|
|
sendNotice(line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
|
|
|
|
}
|
|
|
|
|
|
|
|
func initializeServices() {
|
|
|
|
// this modifies the global Commands map,
|
|
|
|
// so it must be called from irc/commands.go's init()
|
|
|
|
oragonoServicesByCommandAlias = make(map[string]*ircService)
|
|
|
|
|
|
|
|
for serviceName, service := range OragonoServices {
|
2019-05-19 10:27:44 +02:00
|
|
|
service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name)
|
|
|
|
|
2018-04-19 08:48:19 +02:00
|
|
|
// make `/MSG ServiceName HELP` work correctly
|
|
|
|
service.Commands["help"] = &servHelpCmd
|
|
|
|
|
|
|
|
// reserve the nickname
|
|
|
|
restrictedNicknames[serviceName] = true
|
|
|
|
|
|
|
|
// register the protocol-level commands (NICKSERV, NS) that talk to the service
|
|
|
|
var ircCmdDef Command
|
|
|
|
ircCmdDef.handler = serviceCmdHandler
|
|
|
|
for _, ircCmd := range service.CommandAliases {
|
|
|
|
Commands[ircCmd] = ircCmdDef
|
|
|
|
oragonoServicesByCommandAlias[ircCmd] = service
|
|
|
|
}
|
2018-08-06 15:21:29 +02:00
|
|
|
|
|
|
|
// force devs to write a help entry for every command
|
|
|
|
for commandName, commandInfo := range service.Commands {
|
2019-05-19 10:27:44 +02:00
|
|
|
if commandInfo.aliasOf == "" && !commandInfo.hidden && (commandInfo.help == "" || commandInfo.helpShort == "") {
|
2018-08-06 15:21:29 +02:00
|
|
|
log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
|
|
|
|
}
|
|
|
|
}
|
2018-04-19 08:48:19 +02:00
|
|
|
}
|
|
|
|
}
|