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

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