plugin.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. pr.SetNickname(pr.Nickname() + "_")
  105. log.Println("[admin] Nick in use: changing nick to:", pr.Nickname())
  106. proto.Nick(w, pr.Nickname())
  107. }
  108. // cmdFlood
  109. func (p *plugin) cmdFlood(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  110. j, _ := json.MarshalIndent(r, "", " ")
  111. log.Println("[admin] cmdFlood() was called. Request:\n" + string(j))
  112. count := params.Uint(0)
  113. var i uint64
  114. for i = 0; i < count; i++ {
  115. proto.PrivMsg(w, r.Target, TextFloodDisplay, r.SenderName, i)
  116. }
  117. }
  118. // cmdHelp presents the user with a list of commands, also pointing them to
  119. // a resource where the full bot help can be read.
  120. func (p *plugin) cmdHelp(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  121. proto.PrivMsg(w, r.SenderName, TextHelpAll)
  122. for i := 1; i <= len(CommandsHelp); i++ {
  123. proto.PrivMsg(w, r.SenderName, "%s", CommandsHelp[i])
  124. }
  125. proto.PrivMsg(w, r.SenderName, TextHelpAdmins)
  126. }
  127. // cmdNick allows the bot to change its name.
  128. func (p *plugin) cmdNick(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  129. p.profile.SetNickname(params.String(0))
  130. if params.Len() > 1 {
  131. proto.Nick(w, params.String(0), params.String(1))
  132. p.profile.SetNickservPassword(params.String(1))
  133. } else {
  134. proto.Nick(w, params.String(0))
  135. }
  136. }
  137. // cmdJoin makes the bot join a new channel.
  138. func (p *plugin) cmdJoin(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  139. var channel irc.Channel
  140. channel.Name = params.String(0)
  141. if params.Len() > 1 {
  142. channel.Key = params.String(1)
  143. }
  144. proto.Join(w, channel)
  145. }
  146. // cmdPart makes the bot leave a given channel.
  147. func (p *plugin) cmdPart(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  148. proto.Part(w, irc.Channel{
  149. Name: params.String(0),
  150. })
  151. }
  152. // cmdNoop makes the bot de-op itself.
  153. func (p *plugin) cmdNoop(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  154. var channel_name string
  155. if params.Len() > 0 {
  156. channel_name = params.String(0)
  157. } else {
  158. if r.FromChannel() {
  159. channel_name = r.Target
  160. } else {
  161. // ugly?
  162. proto.PrivMsg(w, r.SenderName, cmd.TextMissingParameters, r.Data[1:])
  163. return
  164. }
  165. }
  166. proto.Mode(w, channel_name, "-o", p.profile.Nickname())
  167. }
  168. // cmdAuthList lists all whitelisted users.
  169. func (p *plugin) cmdAuthList(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  170. list := p.profile.Whitelist()
  171. out := strings.Join(list, ", ")
  172. proto.PrivMsg(w, r.SenderName, TextAuthListDisplay, out)
  173. }
  174. // cmdAuthorize adds a new whitelisted user.
  175. func (p *plugin) cmdAuthorize(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  176. p.profile.WhitelistAdd(params.String(0))
  177. proto.PrivMsg(w, r.SenderName, TextAuthorizeDisplay, params.String(0))
  178. }
  179. // cmdDeauthorize removes a user from the whitelist.
  180. func (p *plugin) cmdDeauthorize(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  181. p.profile.WhitelistRemove(params.String(0))
  182. proto.PrivMsg(w, r.SenderName, TextDeauthorizeDisplay, params.String(0))
  183. }
  184. // cmdReload forces the bot to fork itself. This is achieved by
  185. // sending SIGUSR1 to the current process.
  186. func (p *plugin) cmdReload(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  187. syscall.Kill(os.Getpid(), syscall.SIGUSR1)
  188. }
  189. // cmdVersion prints version information.
  190. func (p *plugin) cmdVersion(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  191. rev, _ := strconv.ParseInt(app.VersionRevision, 10, 64)
  192. stamp := time.Unix(rev, 0)
  193. hours := math.Abs(time.Since(lastRestart).Hours())
  194. days := hours / 24
  195. udays := math.Floor(days)
  196. uhours := 24 * (days - udays)
  197. uminutes := 60 * (uhours - math.Floor(uhours))
  198. useconds := 60 * (uminutes - math.Floor(uminutes))
  199. proto.PrivMsg(
  200. w, r.Target,
  201. TextVersionDisplay,
  202. r.SenderName,
  203. util.Bold(p.profile.Nickname()),
  204. util.Bold("%d.%d", app.VersionMajor, app.VersionMinor),
  205. stamp.Format(TextDateFormat),
  206. stamp.Format(TextTimeFormat),
  207. udays,
  208. uhours,
  209. uminutes,
  210. useconds,
  211. )
  212. }