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

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