plugin.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. // Package admin defines administrative bot commands. This package is
  4. // also used for the initial IRC login and channel joins.
  5. package admin
  6. import (
  7. "encoding/json"
  8. "log"
  9. "math"
  10. "os"
  11. "strconv"
  12. "strings"
  13. "syscall"
  14. "time"
  15. "notabug.org/mouz/bot/app"
  16. "notabug.org/mouz/bot/app/util"
  17. "notabug.org/mouz/bot/irc"
  18. "notabug.org/mouz/bot/irc/cmd"
  19. "notabug.org/mouz/bot/irc/proto"
  20. "notabug.org/mouz/bot/plugins"
  21. )
  22. // lastRestart defines the timestamp at which the bot was last restarted.
  23. var lastRestart = time.Now()
  24. func init() { plugins.Register(&plugin{}) }
  25. type plugin struct {
  26. cmd *cmd.Set
  27. // This will store the bot's profile, but only as a subset of
  28. // the full interface. We only need access to some parts.
  29. profile interface {
  30. WhitelistAdd(string)
  31. WhitelistRemove(string)
  32. Whitelist() []string
  33. Nickname() string
  34. SetNickname(string)
  35. NickservPassword() string
  36. SetNickservPassword(string)
  37. Channels() []irc.Channel
  38. }
  39. }
  40. // Load initializes the module and loads any internal resources
  41. // which may be required.
  42. func (p *plugin) Load(prof irc.Profile) error {
  43. p.profile = prof
  44. p.cmd = cmd.New(
  45. prof.CommandPrefix(),
  46. prof.IsWhitelisted,
  47. )
  48. p.cmd.Bind(TextFloodName, false, p.cmdFlood).
  49. Add(TextFloodCount, true, cmd.RegAny)
  50. // Two aliases for the same command. Can be invoked through
  51. // !help or !<bot nickname>
  52. p.cmd.Bind(TextHelpName, false, p.cmdHelp)
  53. p.cmd.Bind(prof.Nickname(), false, p.cmdHelp)
  54. p.cmd.Bind(TextNickName, true, p.cmdNick).
  55. Add(TextNickNickName, true, cmd.RegAny).
  56. Add(TextNickPassName, false, cmd.RegAny)
  57. p.cmd.Bind(TextJoinName, true, p.cmdJoin).
  58. Add(TextJoinChannelName, true, cmd.RegChannel).
  59. Add(TextJoinKeyName, false, cmd.RegAny)
  60. p.cmd.Bind(TextPartName, true, p.cmdPart).
  61. Add(TextPartChannelName, true, cmd.RegChannel)
  62. p.cmd.Bind(TextNoopName, true, p.cmdNoop).
  63. Add(TextNoopChannelName, false, cmd.RegChannel)
  64. p.cmd.Bind(TextAuthListName, false, p.cmdAuthList)
  65. p.cmd.Bind(TextAuthorizeName, true, p.cmdAuthorize).
  66. Add(TextAuthorizeMaskName, true, cmd.RegAny)
  67. p.cmd.Bind(TextDeauthorizeName, true, p.cmdDeauthorize).
  68. Add(TextDeauthorizeMaskName, true, cmd.RegAny)
  69. p.cmd.Bind(TextReloadName, true, p.cmdReload)
  70. p.cmd.Bind(TextVersionName, false, p.cmdVersion)
  71. return nil
  72. }
  73. // Unload cleans the module up and unloads any internal resources.
  74. func (p *plugin) Unload(prof irc.Profile) error {
  75. p.profile = nil
  76. return nil
  77. }
  78. // Dispatch sends the given, incoming IRC message to the plugin for
  79. // processing as it sees fit.
  80. func (p *plugin) Dispatch(w irc.ResponseWriter, r *irc.Request) {
  81. switch r.Type {
  82. case "375", "422": // received START_MOTD or NO_MOTD
  83. p.onFinalizeLogin(w, r)
  84. case "433":
  85. p.onNickInUse(w, r)
  86. case "PRIVMSG":
  87. p.cmd.Dispatch(w, r)
  88. }
  89. }
  90. // onFinalizeLogin is called to complete the login sequence. It is
  91. // triggered when we receive either the STARTMOTD or NOMOTD messages.
  92. // It identifies to nickserv and joins channels defined in the
  93. // profile.
  94. func (p *plugin) onFinalizeLogin(w irc.ResponseWriter, r *irc.Request) {
  95. if len(p.profile.NickservPassword()) > 0 {
  96. proto.PrivMsg(w, "nickserv", "IDENTIFY %s", p.profile.NickservPassword())
  97. }
  98. proto.Join(w, p.profile.Channels()...)
  99. }
  100. // onNickInUse signals that our nick is in use. If we can regain it, do so.
  101. // Otherwise, change ours.
  102. func (p *plugin) onNickInUse(w irc.ResponseWriter, r *irc.Request) {
  103. pr := p.profile
  104. if len(pr.NickservPassword()) > 0 {
  105. log.Println("[admin] Nick in use: trying to recover")
  106. proto.Recover(w, pr.Nickname(), pr.NickservPassword())
  107. return
  108. }
  109. pr.SetNickname(pr.Nickname() + "_")
  110. log.Println("[admin] Nick in use: changing nick to:", pr.Nickname())
  111. proto.Nick(w, pr.Nickname())
  112. }
  113. // cmdFlood
  114. func (p *plugin) cmdFlood(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  115. j, _ := json.MarshalIndent(r, "", " ")
  116. log.Println("[admin] cmdFlood() was called. Request:\n" + string(j))
  117. count := params.Uint(0)
  118. var i uint64
  119. for i = 0; i < count; i++ {
  120. proto.PrivMsg(w, r.Target, TextFloodDisplay, r.SenderName, i)
  121. }
  122. }
  123. // cmdHelp presents the user with a list of commands, also pointing them to
  124. // a resource where the full bot help can be read.
  125. func (p *plugin) cmdHelp(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  126. proto.PrivMsg(w, r.SenderName, TextHelpAll)
  127. for i := 1; i <= len(CommandsHelp); i++ {
  128. proto.PrivMsg(w, r.SenderName, "%s", CommandsHelp[i])
  129. }
  130. proto.PrivMsg(w, r.SenderName, TextHelpAdmins)
  131. }
  132. // cmdNick allows the bot to change its name.
  133. func (p *plugin) cmdNick(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  134. p.profile.SetNickname(params.String(0))
  135. if params.Len() > 1 {
  136. proto.Nick(w, params.String(0), params.String(1))
  137. p.profile.SetNickservPassword(params.String(1))
  138. } else {
  139. proto.Nick(w, params.String(0))
  140. }
  141. }
  142. // cmdJoin makes the bot join a new channel.
  143. func (p *plugin) cmdJoin(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  144. var channel irc.Channel
  145. channel.Name = params.String(0)
  146. if params.Len() > 1 {
  147. channel.Key = params.String(1)
  148. }
  149. proto.Join(w, channel)
  150. }
  151. // cmdPart makes the bot leave a given channel.
  152. func (p *plugin) cmdPart(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  153. proto.Part(w, irc.Channel{
  154. Name: params.String(0),
  155. })
  156. }
  157. // cmdNoop makes the bot de-op itself.
  158. func (p *plugin) cmdNoop(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  159. var channel_name string
  160. if params.Len() > 0 {
  161. channel_name = params.String(0)
  162. } else {
  163. if r.FromChannel() {
  164. channel_name = r.Target
  165. } else {
  166. // ugly?
  167. proto.PrivMsg(w, r.SenderName, cmd.TextMissingParameters, r.Data[1:])
  168. return
  169. }
  170. }
  171. proto.Mode(w, channel_name, "-o", p.profile.Nickname())
  172. }
  173. // cmdAuthList lists all whitelisted users.
  174. func (p *plugin) cmdAuthList(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  175. list := p.profile.Whitelist()
  176. out := strings.Join(list, ", ")
  177. proto.PrivMsg(w, r.SenderName, TextAuthListDisplay, out)
  178. }
  179. // cmdAuthorize adds a new whitelisted user.
  180. func (p *plugin) cmdAuthorize(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  181. p.profile.WhitelistAdd(params.String(0))
  182. proto.PrivMsg(w, r.SenderName, TextAuthorizeDisplay, params.String(0))
  183. }
  184. // cmdDeauthorize removes a user from the whitelist.
  185. func (p *plugin) cmdDeauthorize(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  186. p.profile.WhitelistRemove(params.String(0))
  187. proto.PrivMsg(w, r.SenderName, TextDeauthorizeDisplay, params.String(0))
  188. }
  189. // cmdReload forces the bot to fork itself. This is achieved by
  190. // sending SIGUSR1 to the current process.
  191. func (p *plugin) cmdReload(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  192. syscall.Kill(os.Getpid(), syscall.SIGUSR1)
  193. }
  194. // cmdVersion prints version information.
  195. func (p *plugin) cmdVersion(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  196. rev, _ := strconv.ParseInt(app.VersionRevision, 10, 64)
  197. stamp := time.Unix(rev, 0)
  198. hours := math.Abs(time.Since(lastRestart).Hours())
  199. days := hours / 24
  200. udays := math.Floor(days)
  201. uhours := 24 * (days - udays)
  202. uminutes := 60 * (uhours - math.Floor(uhours))
  203. useconds := 60 * (uminutes - math.Floor(uminutes))
  204. proto.PrivMsg(
  205. w, r.Target,
  206. TextVersionDisplay,
  207. r.SenderName,
  208. util.Bold(p.profile.Nickname()),
  209. util.Bold("%d.%d", app.VersionMajor, app.VersionMinor),
  210. stamp.Format(TextDateFormat),
  211. stamp.Format(TextTimeFormat),
  212. udays,
  213. uhours,
  214. uminutes,
  215. useconds,
  216. )
  217. }