You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

services.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "bytes"
  6. "fmt"
  7. "log"
  8. "sort"
  9. "strings"
  10. "github.com/goshuirc/irc-go/ircfmt"
  11. "github.com/goshuirc/irc-go/ircmsg"
  12. "github.com/oragono/oragono/irc/utils"
  13. )
  14. // defines an IRC service, e.g., NICKSERV
  15. type ircService struct {
  16. Name string
  17. ShortName string
  18. prefix string
  19. CommandAliases []string
  20. Commands map[string]*serviceCommand
  21. HelpBanner string
  22. }
  23. // defines a command associated with a service, e.g., NICKSERV IDENTIFY
  24. type serviceCommand struct {
  25. aliasOf string // marks this command as an alias of another
  26. capabs []string // oper capabs the given user has to have to access this command
  27. handler func(server *Server, client *Client, command string, params []string, rb *ResponseBuffer)
  28. help string
  29. helpStrings []string
  30. helpShort string
  31. enabled func(*Config) bool // is this command enabled in the server config?
  32. authRequired bool
  33. hidden bool
  34. minParams int
  35. maxParams int // optional, if set it's an error if the user passes more than this many params
  36. unsplitFinalParam bool // split into at most maxParams, with last param containing unsplit text
  37. }
  38. // looks up a command in the table of command definitions for a service, resolving aliases
  39. func lookupServiceCommand(commands map[string]*serviceCommand, command string) *serviceCommand {
  40. maxDepth := 1
  41. depth := 0
  42. for depth <= maxDepth {
  43. result, ok := commands[command]
  44. if !ok {
  45. return nil
  46. } else if result.aliasOf == "" {
  47. return result
  48. } else {
  49. command = result.aliasOf
  50. depth += 1
  51. }
  52. }
  53. return nil
  54. }
  55. // all services, by lowercase name
  56. var OragonoServices = map[string]*ircService{
  57. "nickserv": {
  58. Name: "NickServ",
  59. ShortName: "NS",
  60. CommandAliases: []string{"NICKSERV", "NS"},
  61. Commands: nickservCommands,
  62. HelpBanner: nickservHelp,
  63. },
  64. "chanserv": {
  65. Name: "ChanServ",
  66. ShortName: "CS",
  67. CommandAliases: []string{"CHANSERV", "CS"},
  68. Commands: chanservCommands,
  69. HelpBanner: chanservHelp,
  70. },
  71. "hostserv": {
  72. Name: "HostServ",
  73. ShortName: "HS",
  74. CommandAliases: []string{"HOSTSERV", "HS"},
  75. Commands: hostservCommands,
  76. HelpBanner: hostservHelp,
  77. },
  78. }
  79. // all service commands at the protocol level, by uppercase command name
  80. // e.g., NICKSERV, NS
  81. var oragonoServicesByCommandAlias map[string]*ircService
  82. // special-cased command shared by all services
  83. var servHelpCmd serviceCommand = serviceCommand{
  84. help: `Syntax: $bHELP [command]$b
  85. HELP returns information on the given command.`,
  86. helpShort: `$bHELP$b shows in-depth information about commands.`,
  87. }
  88. // generic handler for IRC commands like `/NICKSERV INFO`
  89. func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
  90. service, ok := oragonoServicesByCommandAlias[msg.Command]
  91. if !ok {
  92. server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
  93. return false
  94. }
  95. if len(msg.Params) == 0 {
  96. return false
  97. }
  98. commandName := strings.ToLower(msg.Params[0])
  99. params := msg.Params[1:]
  100. cmd := lookupServiceCommand(service.Commands, commandName)
  101. // for a maxParams command, join all final parameters together if necessary
  102. if cmd != nil && cmd.unsplitFinalParam && cmd.maxParams < len(params) {
  103. newParams := make([]string, cmd.maxParams)
  104. copy(newParams, params[:cmd.maxParams-1])
  105. newParams[cmd.maxParams-1] = strings.Join(params[cmd.maxParams-1:], " ")
  106. params = newParams
  107. }
  108. serviceRunCommand(service, server, client, cmd, commandName, params, rb)
  109. return false
  110. }
  111. // generic handler for service PRIVMSG, like `/msg NickServ INFO`
  112. func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
  113. params := strings.Fields(message)
  114. if len(params) == 0 {
  115. return
  116. }
  117. // look up the service command to see how to parse it
  118. commandName := strings.ToLower(params[0])
  119. cmd := lookupServiceCommand(service.Commands, commandName)
  120. // reparse if needed
  121. if cmd != nil && cmd.unsplitFinalParam {
  122. params = utils.FieldsN(message, cmd.maxParams+1)[1:]
  123. } else {
  124. params = params[1:]
  125. }
  126. serviceRunCommand(service, server, client, cmd, commandName, params, rb)
  127. }
  128. // actually execute a service command
  129. func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
  130. nick := rb.target.Nick()
  131. sendNotice := func(notice string) {
  132. rb.Add(nil, service.prefix, "NOTICE", nick, notice)
  133. }
  134. if cmd == nil {
  135. sendNotice(fmt.Sprintf(client.t("Unknown command. To see available commands, run: /%s HELP"), service.ShortName))
  136. return
  137. }
  138. if len(params) < cmd.minParams || (0 < cmd.maxParams && cmd.maxParams < len(params)) {
  139. sendNotice(fmt.Sprintf(client.t("Invalid parameters. For usage, do /msg %[1]s HELP %[2]s"), service.Name, strings.ToUpper(commandName)))
  140. return
  141. }
  142. if cmd.enabled != nil && !cmd.enabled(server.Config()) {
  143. sendNotice(client.t("This command has been disabled by the server administrators"))
  144. return
  145. }
  146. if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
  147. sendNotice(client.t("Command restricted"))
  148. return
  149. }
  150. if cmd.authRequired && client.Account() == "" {
  151. sendNotice(client.t("You're not logged into an account"))
  152. return
  153. }
  154. server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
  155. if commandName == "help" {
  156. serviceHelpHandler(service, server, client, params, rb)
  157. } else {
  158. cmd.handler(server, client, commandName, params, rb)
  159. }
  160. }
  161. // generic handler that displays help for service commands
  162. func serviceHelpHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) {
  163. nick := rb.target.Nick()
  164. config := server.Config()
  165. sendNotice := func(notice string) {
  166. rb.Add(nil, service.prefix, "NOTICE", nick, notice)
  167. }
  168. sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
  169. if len(params) == 0 {
  170. helpBannerLines := strings.Split(client.t(service.HelpBanner), "\n")
  171. helpBannerLines = append(helpBannerLines, []string{
  172. "",
  173. client.t("To see in-depth help for a specific command, try:"),
  174. ircfmt.Unescape(fmt.Sprintf(client.t(" $b/msg %s HELP <command>$b"), service.Name)),
  175. "",
  176. client.t("Here are the commands you can use:"),
  177. }...)
  178. // show general help
  179. var shownHelpLines sort.StringSlice
  180. var disabledCommands bool
  181. for _, commandInfo := range service.Commands {
  182. // skip commands user can't access
  183. if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
  184. continue
  185. }
  186. if commandInfo.aliasOf != "" || commandInfo.hidden {
  187. continue // don't show help lines for aliases
  188. }
  189. if commandInfo.enabled != nil && !commandInfo.enabled(config) {
  190. disabledCommands = true
  191. continue
  192. }
  193. shownHelpLines = append(shownHelpLines, ircfmt.Unescape(" "+client.t(commandInfo.helpShort)))
  194. }
  195. if disabledCommands {
  196. shownHelpLines = append(shownHelpLines, " "+client.t("... and other commands which have been disabled"))
  197. }
  198. // sort help lines
  199. sort.Sort(shownHelpLines)
  200. // push out help text
  201. for _, line := range helpBannerLines {
  202. sendNotice(line)
  203. }
  204. for _, line := range shownHelpLines {
  205. sendNotice(line)
  206. }
  207. } else {
  208. commandName := strings.ToLower(params[0])
  209. commandInfo := lookupServiceCommand(service.Commands, commandName)
  210. if commandInfo == nil {
  211. sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
  212. } else {
  213. helpStrings := commandInfo.helpStrings
  214. if helpStrings == nil {
  215. hsArray := [1]string{commandInfo.help}
  216. helpStrings = hsArray[:]
  217. }
  218. for i, helpString := range helpStrings {
  219. if 0 < i {
  220. sendNotice("")
  221. }
  222. for _, line := range strings.Split(ircfmt.Unescape(client.t(helpString)), "\n") {
  223. sendNotice(line)
  224. }
  225. }
  226. }
  227. }
  228. sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
  229. }
  230. func makeServiceHelpTextGenerator(cmd string, banner string) func(*Client) string {
  231. return func(client *Client) string {
  232. var buf bytes.Buffer
  233. fmt.Fprintf(&buf, client.t("%s <subcommand> [params]"), cmd)
  234. buf.WriteRune('\n')
  235. buf.WriteString(client.t(banner)) // may contain newlines, that's fine
  236. buf.WriteRune('\n')
  237. fmt.Fprintf(&buf, client.t("For more details, try /%s HELP"), cmd)
  238. return buf.String()
  239. }
  240. }
  241. func initializeServices() {
  242. // this modifies the global Commands map,
  243. // so it must be called from irc/commands.go's init()
  244. oragonoServicesByCommandAlias = make(map[string]*ircService)
  245. for serviceName, service := range OragonoServices {
  246. service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name)
  247. // make `/MSG ServiceName HELP` work correctly
  248. service.Commands["help"] = &servHelpCmd
  249. // reserve the nickname
  250. restrictedNicknames = append(restrictedNicknames, service.Name)
  251. // register the protocol-level commands (NICKSERV, NS) that talk to the service,
  252. // and their associated help entries
  253. var ircCmdDef Command
  254. ircCmdDef.handler = serviceCmdHandler
  255. for _, ircCmd := range service.CommandAliases {
  256. Commands[ircCmd] = ircCmdDef
  257. oragonoServicesByCommandAlias[ircCmd] = service
  258. Help[strings.ToLower(ircCmd)] = HelpEntry{
  259. textGenerator: makeServiceHelpTextGenerator(ircCmd, service.HelpBanner),
  260. }
  261. }
  262. // force devs to write a help entry for every command
  263. for commandName, commandInfo := range service.Commands {
  264. if commandInfo.aliasOf == "" && !commandInfo.hidden {
  265. if (commandInfo.help == "" && commandInfo.helpStrings == nil) || commandInfo.helpShort == "" {
  266. log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
  267. }
  268. }
  269. if commandInfo.maxParams == 0 && commandInfo.unsplitFinalParam {
  270. log.Fatal("unsplitFinalParam requires use of maxParams")
  271. }
  272. }
  273. }
  274. for _, restrictedNickname := range restrictedNicknames {
  275. cfName, err := CasefoldName(restrictedNickname)
  276. if err != nil {
  277. panic(err)
  278. }
  279. restrictedCasefoldedNicks[cfName] = true
  280. skeleton, err := Skeleton(restrictedNickname)
  281. if err != nil {
  282. panic(err)
  283. }
  284. restrictedSkeletons[skeleton] = true
  285. }
  286. }