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.

chanserv.go 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. // Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
  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/oragono/oragono/irc/modes"
  10. "github.com/oragono/oragono/irc/sno"
  11. "github.com/oragono/oragono/irc/utils"
  12. )
  13. const chanservHelp = `ChanServ lets you register and manage channels.
  14. To see in-depth help for a specific ChanServ command, try:
  15. $b/CS HELP <command>$b
  16. Here are the commands you can use:
  17. %s`
  18. type csCommand struct {
  19. capabs []string // oper capabs the given user has to have to access this command
  20. handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
  21. help string
  22. helpShort string
  23. oper bool // true if the user has to be an oper to use this command
  24. }
  25. var (
  26. chanservCommands = map[string]*csCommand{
  27. "help": {
  28. help: `Syntax: $bHELP [command]$b
  29. HELP returns information on the given command.`,
  30. helpShort: `$bHELP$b shows in-depth information about commands.`,
  31. },
  32. "op": {
  33. handler: csOpHandler,
  34. help: `Syntax: $bOP #channel [nickname]$b
  35. OP makes the given nickname, or yourself, a channel admin. You can only use
  36. this command if you're the founder of the channel.`,
  37. helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
  38. },
  39. "register": {
  40. handler: csRegisterHandler,
  41. help: `Syntax: $bREGISTER #channel$b
  42. REGISTER lets you own the given channel. If you rejoin this channel, you'll be
  43. given admin privs on it. Modes set on the channel and the topic will also be
  44. remembered.`,
  45. helpShort: `$bREGISTER$b lets you own a given channel.`,
  46. },
  47. }
  48. )
  49. // csNotice sends the client a notice from ChanServ
  50. func csNotice(rb *ResponseBuffer, text string) {
  51. rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
  52. }
  53. // chanservReceiveNotice handles NOTICEs that ChanServ receives.
  54. func (server *Server) chanservNoticeHandler(client *Client, message string, rb *ResponseBuffer) {
  55. // do nothing
  56. }
  57. // chanservReceiveNotice handles NOTICEs that ChanServ receives.
  58. func (server *Server) chanservPrivmsgHandler(client *Client, message string, rb *ResponseBuffer) {
  59. commandName, params := utils.ExtractParam(message)
  60. commandName = strings.ToLower(commandName)
  61. commandInfo := chanservCommands[commandName]
  62. if commandInfo == nil {
  63. csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP"))
  64. return
  65. }
  66. if commandInfo.oper && !client.HasMode(modes.Operator) {
  67. csNotice(rb, client.t("Command restricted"))
  68. return
  69. }
  70. if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
  71. csNotice(rb, client.t("Command restricted"))
  72. return
  73. }
  74. // custom help handling here to prevent recursive init loop
  75. if commandName == "help" {
  76. csHelpHandler(server, client, commandName, params, rb)
  77. return
  78. }
  79. if commandInfo.handler == nil {
  80. csNotice(rb, client.t("Command error. Please report this to the developers"))
  81. return
  82. }
  83. server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName))
  84. commandInfo.handler(server, client, commandName, params, rb)
  85. }
  86. func csHelpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
  87. csNotice(rb, ircfmt.Unescape(client.t("*** $bChanServ HELP$b ***")))
  88. if params == "" {
  89. // show general help
  90. var shownHelpLines sort.StringSlice
  91. for _, commandInfo := range chanservCommands {
  92. // skip commands user can't access
  93. if commandInfo.oper && !client.HasMode(modes.Operator) {
  94. continue
  95. }
  96. if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
  97. continue
  98. }
  99. shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
  100. }
  101. // sort help lines
  102. sort.Sort(shownHelpLines)
  103. // assemble help text
  104. assembledHelpLines := strings.Join(shownHelpLines, "\n")
  105. fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(chanservHelp), assembledHelpLines))
  106. // push out help text
  107. for _, line := range strings.Split(fullHelp, "\n") {
  108. csNotice(rb, line)
  109. }
  110. } else {
  111. commandInfo := chanservCommands[strings.ToLower(strings.TrimSpace(params))]
  112. if commandInfo == nil {
  113. csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP"))
  114. } else {
  115. for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
  116. csNotice(rb, line)
  117. }
  118. }
  119. }
  120. csNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ HELP$b ***")))
  121. }
  122. func csOpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
  123. channelName, clientToOp := utils.ExtractParam(params)
  124. if channelName == "" {
  125. csNotice(rb, ircfmt.Unescape(client.t("Syntax: $bOP #channel [nickname]$b")))
  126. return
  127. }
  128. clientToOp = strings.TrimSpace(clientToOp)
  129. channelKey, err := CasefoldChannel(channelName)
  130. if err != nil {
  131. csNotice(rb, client.t("Channel name is not valid"))
  132. return
  133. }
  134. channelInfo := server.channels.Get(channelKey)
  135. if channelInfo == nil {
  136. csNotice(rb, client.t("Channel does not exist"))
  137. return
  138. }
  139. clientAccount := client.Account()
  140. if clientAccount == "" {
  141. csNotice(rb, client.t("You must be logged in to op on a channel"))
  142. return
  143. }
  144. if clientAccount != channelInfo.Founder() {
  145. csNotice(rb, client.t("You must be the channel founder to op"))
  146. return
  147. }
  148. var target *Client
  149. if clientToOp != "" {
  150. casefoldedNickname, err := CasefoldName(clientToOp)
  151. target = server.clients.Get(casefoldedNickname)
  152. if err != nil || target == nil {
  153. csNotice(rb, client.t("Could not find given client"))
  154. return
  155. }
  156. } else {
  157. target = client
  158. }
  159. // give them privs
  160. givenMode := modes.ChannelOperator
  161. if client == target {
  162. givenMode = modes.ChannelFounder
  163. }
  164. change := channelInfo.applyModeToMember(target, givenMode, modes.Add, client.NickCasefolded(), rb)
  165. if change != nil {
  166. //TODO(dan): we should change the name of String and make it return a slice here
  167. //TODO(dan): unify this code with code in modes.go
  168. args := append([]string{channelName}, strings.Split(change.String(), " ")...)
  169. for _, member := range channelInfo.Members() {
  170. member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...)
  171. }
  172. }
  173. csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName))
  174. server.logger.Info("chanserv", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.nick, clientToOp, channelName))
  175. server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.nickMaskString, clientToOp, channelName))
  176. }
  177. func csRegisterHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
  178. if !server.channelRegistrationEnabled {
  179. csNotice(rb, client.t("Channel registration is not enabled"))
  180. return
  181. }
  182. channelName := strings.TrimSpace(params)
  183. if channelName == "" {
  184. csNotice(rb, ircfmt.Unescape(client.t("Syntax: $bREGISTER #channel$b")))
  185. return
  186. }
  187. channelKey, err := CasefoldChannel(channelName)
  188. if err != nil {
  189. csNotice(rb, client.t("Channel name is not valid"))
  190. return
  191. }
  192. channelInfo := server.channels.Get(channelKey)
  193. if channelInfo == nil || !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) {
  194. csNotice(rb, client.t("You must be an oper on the channel to register it"))
  195. return
  196. }
  197. if client.Account() == "" {
  198. csNotice(rb, client.t("You must be logged in to register a channel"))
  199. return
  200. }
  201. // this provides the synchronization that allows exactly one registration of the channel:
  202. err = channelInfo.SetRegistered(client.Account())
  203. if err != nil {
  204. csNotice(rb, err.Error())
  205. return
  206. }
  207. // registration was successful: make the database reflect it
  208. go server.channelRegistry.StoreChannel(channelInfo, IncludeAllChannelAttrs)
  209. csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
  210. server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName))
  211. server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
  212. // give them founder privs
  213. change := channelInfo.applyModeToMember(client, modes.ChannelFounder, modes.Add, client.NickCasefolded(), rb)
  214. if change != nil {
  215. //TODO(dan): we should change the name of String and make it return a slice here
  216. //TODO(dan): unify this code with code in modes.go
  217. args := append([]string{channelName}, strings.Split(change.String(), " ")...)
  218. for _, member := range channelInfo.Members() {
  219. member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...)
  220. }
  221. }
  222. }