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 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. // Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "fmt"
  6. "log"
  7. "sort"
  8. "strings"
  9. "github.com/goshuirc/irc-go/ircfmt"
  10. "github.com/goshuirc/irc-go/ircmsg"
  11. "github.com/oragono/oragono/irc/utils"
  12. )
  13. // defines an IRC service, e.g., NICKSERV
  14. type ircService struct {
  15. Name string
  16. ShortName string
  17. CommandAliases []string
  18. Commands map[string]*serviceCommand
  19. HelpBanner string
  20. }
  21. // defines a command associated with a service, e.g., NICKSERV IDENTIFY
  22. type serviceCommand struct {
  23. aliasOf string // marks this command as an alias of another
  24. capabs []string // oper capabs the given user has to have to access this command
  25. handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
  26. help string
  27. helpShort string
  28. authRequired bool
  29. enabled func(*Server) bool // is this command enabled in the server config?
  30. }
  31. // looks up a command in the table of command definitions for a service, resolving aliases
  32. func lookupServiceCommand(commands map[string]*serviceCommand, command string) *serviceCommand {
  33. maxDepth := 1
  34. depth := 0
  35. for depth <= maxDepth {
  36. result, ok := commands[command]
  37. if !ok {
  38. return nil
  39. } else if result.aliasOf == "" {
  40. return result
  41. } else {
  42. command = result.aliasOf
  43. depth += 1
  44. }
  45. }
  46. return nil
  47. }
  48. // all services, by lowercase name
  49. var OragonoServices = map[string]*ircService{
  50. "nickserv": {
  51. Name: "NickServ",
  52. ShortName: "NS",
  53. CommandAliases: []string{"NICKSERV", "NS"},
  54. Commands: nickservCommands,
  55. HelpBanner: nickservHelp,
  56. },
  57. "chanserv": {
  58. Name: "ChanServ",
  59. ShortName: "CS",
  60. CommandAliases: []string{"CHANSERV", "CS"},
  61. Commands: chanservCommands,
  62. HelpBanner: chanservHelp,
  63. },
  64. "hostserv": {
  65. Name: "HostServ",
  66. ShortName: "HS",
  67. CommandAliases: []string{"HOSTSERV", "HS"},
  68. Commands: hostservCommands,
  69. HelpBanner: hostservHelp,
  70. },
  71. }
  72. // all service commands at the protocol level, by uppercase command name
  73. // e.g., NICKSERV, NS
  74. var oragonoServicesByCommandAlias map[string]*ircService
  75. // special-cased command shared by all services
  76. var servHelpCmd serviceCommand = serviceCommand{
  77. help: `Syntax: $bHELP [command]$b
  78. HELP returns information on the given command.`,
  79. helpShort: `$bHELP$b shows in-depth information about commands.`,
  80. }
  81. // this handles IRC commands like `/NICKSERV INFO`, translating into `/MSG NICKSERV INFO`
  82. func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
  83. service, ok := oragonoServicesByCommandAlias[msg.Command]
  84. if !ok {
  85. server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
  86. return false
  87. }
  88. fakePrivmsg := strings.Join(msg.Params, " ")
  89. servicePrivmsgHandler(service, server, client, fakePrivmsg, rb)
  90. return false
  91. }
  92. // generic handler for service PRIVMSG
  93. func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
  94. commandName, params := utils.ExtractParam(message)
  95. commandName = strings.ToLower(commandName)
  96. nick := rb.target.Nick()
  97. sendNotice := func(notice string) {
  98. rb.Add(nil, service.Name, "NOTICE", nick, notice)
  99. }
  100. cmd := lookupServiceCommand(service.Commands, commandName)
  101. if cmd == nil {
  102. sendNotice(fmt.Sprintf("%s /%s HELP", client.t("Unknown command. To see available commands, run"), service.ShortName))
  103. return
  104. }
  105. if cmd.enabled != nil && !cmd.enabled(server) {
  106. sendNotice(client.t("This command has been disabled by the server administrators"))
  107. return
  108. }
  109. if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
  110. sendNotice(client.t("Command restricted"))
  111. return
  112. }
  113. if cmd.authRequired && client.Account() == "" {
  114. sendNotice(client.t("You're not logged into an account"))
  115. return
  116. }
  117. server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
  118. if commandName == "help" {
  119. serviceHelpHandler(service, server, client, params, rb)
  120. } else {
  121. cmd.handler(server, client, commandName, params, rb)
  122. }
  123. }
  124. // generic handler that displays help for service commands
  125. func serviceHelpHandler(service *ircService, server *Server, client *Client, params string, rb *ResponseBuffer) {
  126. nick := rb.target.Nick()
  127. sendNotice := func(notice string) {
  128. rb.Add(nil, service.Name, "NOTICE", nick, notice)
  129. }
  130. sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
  131. if params == "" {
  132. // show general help
  133. var shownHelpLines sort.StringSlice
  134. var disabledCommands bool
  135. for _, commandInfo := range service.Commands {
  136. // skip commands user can't access
  137. if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
  138. continue
  139. }
  140. if commandInfo.aliasOf != "" {
  141. continue // don't show help lines for aliases
  142. }
  143. if commandInfo.enabled != nil && !commandInfo.enabled(server) {
  144. disabledCommands = true
  145. continue
  146. }
  147. shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
  148. }
  149. if disabledCommands {
  150. shownHelpLines = append(shownHelpLines, " "+client.t("... and other commands which have been disabled"))
  151. }
  152. // sort help lines
  153. sort.Sort(shownHelpLines)
  154. // assemble help text
  155. assembledHelpLines := strings.Join(shownHelpLines, "\n")
  156. fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(service.HelpBanner), assembledHelpLines))
  157. // push out help text
  158. for _, line := range strings.Split(fullHelp, "\n") {
  159. sendNotice(line)
  160. }
  161. } else {
  162. commandName := strings.ToLower(strings.TrimSpace(params))
  163. commandInfo := lookupServiceCommand(service.Commands, commandName)
  164. if commandInfo == nil {
  165. sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
  166. } else {
  167. for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
  168. sendNotice(line)
  169. }
  170. }
  171. }
  172. sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
  173. }
  174. func initializeServices() {
  175. // this modifies the global Commands map,
  176. // so it must be called from irc/commands.go's init()
  177. oragonoServicesByCommandAlias = make(map[string]*ircService)
  178. for serviceName, service := range OragonoServices {
  179. // make `/MSG ServiceName HELP` work correctly
  180. service.Commands["help"] = &servHelpCmd
  181. // reserve the nickname
  182. restrictedNicknames[serviceName] = true
  183. // register the protocol-level commands (NICKSERV, NS) that talk to the service
  184. var ircCmdDef Command
  185. ircCmdDef.handler = serviceCmdHandler
  186. for _, ircCmd := range service.CommandAliases {
  187. Commands[ircCmd] = ircCmdDef
  188. oragonoServicesByCommandAlias[ircCmd] = service
  189. }
  190. // force devs to write a help entry for every command
  191. for commandName, commandInfo := range service.Commands {
  192. if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") {
  193. log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
  194. }
  195. }
  196. }
  197. }