Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

services.go 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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 string, params []string, rb *ResponseBuffer)
  26. help string
  27. helpShort string
  28. authRequired bool
  29. enabled func(*Config) bool // is this command enabled in the server config?
  30. minParams int
  31. maxParams int // split into at most n params, with last param containing remaining unsplit text
  32. }
  33. // looks up a command in the table of command definitions for a service, resolving aliases
  34. func lookupServiceCommand(commands map[string]*serviceCommand, command string) *serviceCommand {
  35. maxDepth := 1
  36. depth := 0
  37. for depth <= maxDepth {
  38. result, ok := commands[command]
  39. if !ok {
  40. return nil
  41. } else if result.aliasOf == "" {
  42. return result
  43. } else {
  44. command = result.aliasOf
  45. depth += 1
  46. }
  47. }
  48. return nil
  49. }
  50. // all services, by lowercase name
  51. var OragonoServices = map[string]*ircService{
  52. "nickserv": {
  53. Name: "NickServ",
  54. ShortName: "NS",
  55. CommandAliases: []string{"NICKSERV", "NS"},
  56. Commands: nickservCommands,
  57. HelpBanner: nickservHelp,
  58. },
  59. "chanserv": {
  60. Name: "ChanServ",
  61. ShortName: "CS",
  62. CommandAliases: []string{"CHANSERV", "CS"},
  63. Commands: chanservCommands,
  64. HelpBanner: chanservHelp,
  65. },
  66. "hostserv": {
  67. Name: "HostServ",
  68. ShortName: "HS",
  69. CommandAliases: []string{"HOSTSERV", "HS"},
  70. Commands: hostservCommands,
  71. HelpBanner: hostservHelp,
  72. },
  73. }
  74. // all service commands at the protocol level, by uppercase command name
  75. // e.g., NICKSERV, NS
  76. var oragonoServicesByCommandAlias map[string]*ircService
  77. // special-cased command shared by all services
  78. var servHelpCmd serviceCommand = serviceCommand{
  79. help: `Syntax: $bHELP [command]$b
  80. HELP returns information on the given command.`,
  81. helpShort: `$bHELP$b shows in-depth information about commands.`,
  82. }
  83. // generic handler for IRC commands like `/NICKSERV INFO`
  84. func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
  85. service, ok := oragonoServicesByCommandAlias[msg.Command]
  86. if !ok {
  87. server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
  88. return false
  89. }
  90. if len(msg.Params) == 0 {
  91. return false
  92. }
  93. commandName := strings.ToLower(msg.Params[0])
  94. params := msg.Params[1:]
  95. cmd := lookupServiceCommand(service.Commands, commandName)
  96. // for a maxParams command, join all final parameters together if necessary
  97. if cmd != nil && cmd.maxParams != 0 && cmd.maxParams < len(params) {
  98. newParams := make([]string, cmd.maxParams)
  99. copy(newParams, params[:cmd.maxParams-1])
  100. newParams[cmd.maxParams-1] = strings.Join(params[cmd.maxParams-1:], " ")
  101. params = newParams
  102. }
  103. serviceRunCommand(service, server, client, cmd, commandName, params, rb)
  104. return false
  105. }
  106. // generic handler for service PRIVMSG, like `/msg NickServ INFO`
  107. func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
  108. params := strings.Fields(message)
  109. if len(params) == 0 {
  110. return
  111. }
  112. // look up the service command to see how to parse it
  113. commandName := strings.ToLower(params[0])
  114. cmd := lookupServiceCommand(service.Commands, commandName)
  115. // reparse if needed
  116. if cmd != nil && cmd.maxParams != 0 {
  117. params = utils.FieldsN(message, cmd.maxParams+1)[1:]
  118. } else {
  119. params = params[1:]
  120. }
  121. serviceRunCommand(service, server, client, cmd, commandName, params, rb)
  122. }
  123. // actually execute a service command
  124. func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
  125. nick := rb.target.Nick()
  126. sendNotice := func(notice string) {
  127. rb.Add(nil, service.Name, "NOTICE", nick, notice)
  128. }
  129. if cmd == nil {
  130. sendNotice(fmt.Sprintf("%s /%s HELP", client.t("Unknown command. To see available commands, run"), service.ShortName))
  131. return
  132. }
  133. if len(params) < cmd.minParams {
  134. sendNotice(fmt.Sprintf(client.t("Invalid parameters. For usage, do /msg %s HELP %s"), service.Name, strings.ToUpper(commandName)))
  135. return
  136. }
  137. if cmd.enabled != nil && !cmd.enabled(server.Config()) {
  138. sendNotice(client.t("This command has been disabled by the server administrators"))
  139. return
  140. }
  141. if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
  142. sendNotice(client.t("Command restricted"))
  143. return
  144. }
  145. if cmd.authRequired && client.Account() == "" {
  146. sendNotice(client.t("You're not logged into an account"))
  147. return
  148. }
  149. server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
  150. if commandName == "help" {
  151. serviceHelpHandler(service, server, client, params, rb)
  152. } else {
  153. cmd.handler(server, client, commandName, params, rb)
  154. }
  155. }
  156. // generic handler that displays help for service commands
  157. func serviceHelpHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) {
  158. nick := rb.target.Nick()
  159. config := server.Config()
  160. sendNotice := func(notice string) {
  161. rb.Add(nil, service.Name, "NOTICE", nick, notice)
  162. }
  163. sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
  164. if len(params) == 0 {
  165. // show general help
  166. var shownHelpLines sort.StringSlice
  167. var disabledCommands bool
  168. for _, commandInfo := range service.Commands {
  169. // skip commands user can't access
  170. if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
  171. continue
  172. }
  173. if commandInfo.aliasOf != "" {
  174. continue // don't show help lines for aliases
  175. }
  176. if commandInfo.enabled != nil && !commandInfo.enabled(config) {
  177. disabledCommands = true
  178. continue
  179. }
  180. shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
  181. }
  182. if disabledCommands {
  183. shownHelpLines = append(shownHelpLines, " "+client.t("... and other commands which have been disabled"))
  184. }
  185. // sort help lines
  186. sort.Sort(shownHelpLines)
  187. // assemble help text
  188. assembledHelpLines := strings.Join(shownHelpLines, "\n")
  189. fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(service.HelpBanner), assembledHelpLines))
  190. // push out help text
  191. for _, line := range strings.Split(fullHelp, "\n") {
  192. sendNotice(line)
  193. }
  194. } else {
  195. commandName := strings.ToLower(params[0])
  196. commandInfo := lookupServiceCommand(service.Commands, commandName)
  197. if commandInfo == nil {
  198. sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
  199. } else {
  200. for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
  201. sendNotice(line)
  202. }
  203. }
  204. }
  205. sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
  206. }
  207. func initializeServices() {
  208. // this modifies the global Commands map,
  209. // so it must be called from irc/commands.go's init()
  210. oragonoServicesByCommandAlias = make(map[string]*ircService)
  211. for serviceName, service := range OragonoServices {
  212. // make `/MSG ServiceName HELP` work correctly
  213. service.Commands["help"] = &servHelpCmd
  214. // reserve the nickname
  215. restrictedNicknames[serviceName] = true
  216. // register the protocol-level commands (NICKSERV, NS) that talk to the service
  217. var ircCmdDef Command
  218. ircCmdDef.handler = serviceCmdHandler
  219. for _, ircCmd := range service.CommandAliases {
  220. Commands[ircCmd] = ircCmdDef
  221. oragonoServicesByCommandAlias[ircCmd] = service
  222. }
  223. // force devs to write a help entry for every command
  224. for commandName, commandInfo := range service.Commands {
  225. if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") {
  226. log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
  227. }
  228. }
  229. }
  230. }