plugin.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. // Package stats retains a listing of user host names, mapped to
  4. // nicknames they have ever been seen using, along with some other,
  5. // rudimentary user statistics.
  6. //
  7. // This is intended to make it easier to pick out trolls, trying to
  8. // present themselves as new users. While this is by no means fool-proof,
  9. // it keeps the majority out.
  10. package stats
  11. import (
  12. "fmt"
  13. "log"
  14. "path/filepath"
  15. "regexp"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "time"
  20. "github.com/monkeybird/autimaat/app/util"
  21. "github.com/monkeybird/autimaat/irc"
  22. "github.com/monkeybird/autimaat/irc/cmd"
  23. "github.com/monkeybird/autimaat/irc/proto"
  24. "github.com/monkeybird/autimaat/plugins"
  25. )
  26. // SaveInterval determines the time interval after which we save stats data to disk.
  27. const SaveInterval = time.Minute * 10
  28. func init() { plugins.Register(&plugin{}) }
  29. type plugin struct {
  30. m sync.RWMutex
  31. cmd *cmd.Set
  32. file string
  33. users UserList
  34. quitOnce sync.Once
  35. quit chan struct{}
  36. }
  37. // Load initializes the module and loads any internal resources
  38. // which may be required.
  39. func (p *plugin) Load(prof irc.Profile) error {
  40. p.m.Lock()
  41. defer p.m.Unlock()
  42. p.quit = make(chan struct{})
  43. p.file = filepath.Join(prof.Root(), "stats.dat")
  44. p.cmd = cmd.New(prof.CommandPrefix(), nil)
  45. p.cmd.Bind(TextWhoisName, false, p.cmdWhois).
  46. Add(TextNick, true, cmd.RegAny)
  47. p.cmd.Bind(TextFirstOn, false, p.cmdFirstOn).
  48. Add(TextNick, true, cmd.RegAny)
  49. p.cmd.Bind(TextLastOn, false, p.cmdLastOn).
  50. Add(TextNick, true, cmd.RegAny)
  51. go p.periodicSave()
  52. return util.ReadFile(p.file, &p.users, true)
  53. }
  54. // Unload cleans the module up and unloads any internal resources.
  55. func (p *plugin) Unload(prof irc.Profile) error {
  56. p.quitOnce.Do(func() {
  57. close(p.quit)
  58. p.saveFile()
  59. })
  60. return nil
  61. }
  62. // Dispatch sends the given, incoming IRC message to the plugin for
  63. // processing as it sees fit.
  64. func (p *plugin) Dispatch(w irc.ResponseWriter, r *irc.Request) {
  65. p.cmd.Dispatch(w, r)
  66. mask := filterMibbit(r.SenderMask)
  67. p.m.Lock()
  68. usr := p.users.Get(mask)
  69. usr.AddNickname(r.SenderName)
  70. p.m.Unlock()
  71. }
  72. // periodicSave periodically saves the stats data to disk.
  73. func (p *plugin) periodicSave() {
  74. for {
  75. select {
  76. case <-p.quit:
  77. return
  78. case <-time.After(SaveInterval):
  79. p.saveFile()
  80. }
  81. }
  82. }
  83. // saveFile saes the user data to disk.
  84. func (p *plugin) saveFile() {
  85. p.m.RLock()
  86. err := util.WriteFile(p.file, p.users, true)
  87. p.m.RUnlock()
  88. if err != nil {
  89. log.Println("[stats] save:", err)
  90. }
  91. }
  92. // cmdWhois presents the caller with a list of usernames known for a specific
  93. // user or hostmask.
  94. func (p *plugin) cmdWhois(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  95. p.m.RLock()
  96. defer p.m.RUnlock()
  97. query := filterMibbit(params.String(0))
  98. set := p.users.Find(query, 3)
  99. if len(set) == 0 {
  100. proto.PrivMsg(w, r.SenderName, TextWhoisUnknownUser, r.SenderName,
  101. util.Bold(params.String(0)))
  102. return
  103. }
  104. for _, usr := range set {
  105. proto.PrivMsg(w, r.SenderName,
  106. TextWhoisDisplay,
  107. r.SenderName,
  108. util.Bold(usr.Hostmask),
  109. usr.FirstSeen.Format(TextDateFormat),
  110. strings.Join(usr.Nicknames, ", "),
  111. )
  112. }
  113. }
  114. // cmdFirstOn tells the caller when a specific user was first seen online.
  115. func (p *plugin) cmdFirstOn(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  116. p.m.RLock()
  117. defer p.m.RUnlock()
  118. query := filterMibbit(params.String(0))
  119. set := p.users.Find(query, 3)
  120. if len(set) == 0 {
  121. proto.PrivMsg(w, r.SenderName, TextUnknownUser, r.SenderName,
  122. util.Bold(params.String(0)))
  123. return
  124. }
  125. for _, usr := range set {
  126. proto.PrivMsg(w, r.SenderName,
  127. TextFirstOnDisplay,
  128. r.SenderName,
  129. strings.Join(usr.Nicknames, ", "),
  130. util.Bold(usr.Hostmask),
  131. usr.FirstSeen.Format(TextDateFormat),
  132. usr.FirstSeen.Format(TextTimeFormat),
  133. FormatDuration(time.Since(usr.FirstSeen)),
  134. )
  135. }
  136. }
  137. // cmdLastOn tells the caller when a specific user was last seen online.
  138. func (p *plugin) cmdLastOn(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  139. p.m.RLock()
  140. defer p.m.RUnlock()
  141. query := filterMibbit(params.String(0))
  142. set := p.users.Find(query, 3)
  143. if len(set) == 0 {
  144. proto.PrivMsg(w, r.SenderName, TextUnknownUser, r.SenderName,
  145. util.Bold(params.String(0)))
  146. return
  147. }
  148. for _, usr := range set {
  149. proto.PrivMsg(w, r.SenderName,
  150. TextLastOnDisplay,
  151. r.SenderName,
  152. strings.Join(usr.Nicknames, ", "),
  153. util.Bold(usr.Hostmask),
  154. usr.LastSeen.Format(TextDateFormat),
  155. usr.LastSeen.Format(TextTimeFormat),
  156. FormatDuration(time.Since(usr.LastSeen)),
  157. )
  158. }
  159. }
  160. // regMibbit seeks to identify Mibbit hostmasks.
  161. var regMibbit = regexp.MustCompile(`\.mibbit\.com$`)
  162. // filterMibbit checks if the given value is a hostmask originating
  163. // from mibbit.com. If so, it extracts te user's actual IP from it and
  164. // returns that as the new hostmask to be used.
  165. func filterMibbit(v string) string {
  166. if !regMibbit.MatchString(v) {
  167. return v
  168. }
  169. idx := strings.Index(v, "@")
  170. if idx == -1 {
  171. return v
  172. }
  173. addr := strings.TrimSpace(v[:idx])
  174. if len(addr) != 8 {
  175. return v
  176. }
  177. a, ea := strconv.ParseUint(addr[:2], 16, 8)
  178. b, eb := strconv.ParseUint(addr[2:4], 16, 8)
  179. c, ec := strconv.ParseUint(addr[4:6], 16, 8)
  180. d, ed := strconv.ParseUint(addr[6:], 16, 8)
  181. if ea != nil || eb != nil || ec != nil || ed != nil {
  182. return v
  183. }
  184. return fmt.Sprintf("%d.%d.%d.%d", a, b, c, d)
  185. }