Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

services.go 11KB

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