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

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