The new logbot, not committed from the wrong terminal window this time.
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.

312 lines
8.5 KiB

  1. package bot
  2. import (
  3. "context"
  4. "log"
  5. "strings"
  6. "time"
  7. "git.aiterp.net/gisle/irc"
  8. "git.aiterp.net/rpdata/logbot3/internal/config"
  9. "git.aiterp.net/rpdata/logbot3/internal/models/channels"
  10. "git.aiterp.net/rpdata/logbot3/internal/models/logs"
  11. "git.aiterp.net/rpdata/logbot3/internal/models/posts"
  12. )
  13. // A Channel represents a handler for a single channel that takes care of the logging and such.
  14. type Channel struct {
  15. ctx context.Context
  16. cancel context.CancelFunc
  17. ch chan ChannelPost
  18. lastPostSessionID string
  19. lastPostTime time.Time
  20. name string
  21. parentCtx context.Context
  22. parent *Bot
  23. client *irc.Client
  24. }
  25. func newChannel(parent *Bot, name string, client *irc.Client) *Channel {
  26. ctx, cancel := context.WithCancel(context.Background())
  27. return &Channel{
  28. name: name,
  29. parent: parent,
  30. parentCtx: parent.ctx,
  31. client: client,
  32. ch: make(chan ChannelPost, 8),
  33. ctx: ctx,
  34. cancel: cancel,
  35. }
  36. }
  37. func (channel *Channel) loop() {
  38. queue := make([]ChannelPost, 0, 8)
  39. minutely := time.NewTicker(time.Minute)
  40. defer channel.cancel()
  41. defer minutely.Stop()
  42. // In case of a crash, it should take ownership of sessions when rejoining.
  43. session, err := logs.FindOpen(channel.parentCtx, channel.name)
  44. if err == nil {
  45. minsUntilDeadline := 1 + int(((time.Hour * 2) - time.Since(session.LatestTime())).Minutes())
  46. if minsUntilDeadline < 1 {
  47. minsUntilDeadline = 1
  48. }
  49. channel.client.Sayf(channel.name, "Session https://aiterp.net/logs/%s will be closed in %d min if there are no new posts.", session.ID, minsUntilDeadline)
  50. channel.lastPostSessionID = session.ID
  51. channel.lastPostTime = session.LatestTime()
  52. }
  53. // If the bot is op, run the commands from the configuration.
  54. go func() {
  55. time.Sleep(time.Second)
  56. commands := config.Get().Commands.OnJoinOp
  57. target := channel.client.Channel(channel.name)
  58. if len(commands) > 0 && target != nil {
  59. me, ok := target.UserList().User(channel.client.Nick())
  60. if ok && strings.ContainsRune(me.Modes, 'o') {
  61. channel.parent.runCommands(commands, target, map[string]string{
  62. "chan": channel.name,
  63. })
  64. }
  65. }
  66. }()
  67. for {
  68. select {
  69. case post := <-channel.ch:
  70. {
  71. // Handle bot commands, or add to queue otherwise.
  72. if cmd := post.botCommand(); cmd != nil {
  73. switch cmd.Verb {
  74. case "end", "ends", "endsession":
  75. {
  76. session, err := logs.FindOpen(channel.parentCtx, channel.name)
  77. if err == logs.ErrNoneOpen {
  78. channel.client.Say(channel.name, "No open session found. Mission accomplished, I guess?")
  79. break
  80. } else if err != nil {
  81. log.Println("Could not find session:", err)
  82. break
  83. }
  84. _, err = logs.SetOpen(channel.parentCtx, session, false)
  85. if err != nil {
  86. channel.client.Say(channel.name, "Something went wrong when closing the log, please use the website instead.")
  87. log.Println("Could not set open:", err)
  88. }
  89. }
  90. case "tag", "event":
  91. {
  92. if cmd.Text == "" {
  93. channel.client.Say(channel.name, "Usage: !tag <Event Name>")
  94. }
  95. session, err := logs.FindOpen(channel.parentCtx, channel.name)
  96. if err == logs.ErrNoneOpen {
  97. channel.client.Say(channel.name, "No open session found. You can edit closed logs on the website.")
  98. break
  99. } else if err != nil {
  100. log.Println("Could not find session:", err)
  101. break
  102. }
  103. _, err = logs.SetEventName(channel.parentCtx, session, cmd.Text)
  104. if err != nil {
  105. channel.client.Say(channel.name, "Something went wrong when setting the event name, please use the website instead.")
  106. log.Println("Could not set event name:", err)
  107. }
  108. }
  109. case "register":
  110. {
  111. target := channel.client.Channel(channel.name)
  112. if target != nil {
  113. me, ok := target.UserList().User(channel.client.Nick())
  114. if !ok || !strings.ContainsRune(me.Modes, 'o') {
  115. channel.client.Say(channel.name, "This unit require operator privileges to serve this request.")
  116. break
  117. }
  118. chanServNick := config.Get().Names.ChanServ
  119. if chanServNick == "" {
  120. channel.client.Say(channel.name, "This function is not enabled.")
  121. break
  122. }
  123. channel.client.Sayf(chanServNick, "REGISTER %s", target.Name())
  124. }
  125. }
  126. }
  127. } else {
  128. queue = append(queue, post)
  129. if post.Time.After(channel.lastPostTime) {
  130. channel.lastPostTime = post.Time
  131. }
  132. }
  133. // Posts after here aren't going to be bot commands, so log its reception.
  134. log.Printf("Received %s post from %s", post.Kind, post.Nick)
  135. // Stop here if there's nothing to post.
  136. if len(queue) == 0 {
  137. break
  138. }
  139. // Buffer up posts close to one another.
  140. deadline := time.After(time.Second * 3)
  141. buffering := true
  142. for buffering {
  143. select {
  144. case post := <-channel.ch:
  145. {
  146. queue = append(queue, post)
  147. }
  148. case <-deadline:
  149. {
  150. buffering = false
  151. }
  152. }
  153. }
  154. // Select session.
  155. session, err := logs.FindOpen(channel.parentCtx, channel.name)
  156. if err == logs.ErrNoneOpen {
  157. eventName := ""
  158. channelData, err := channels.Find(channel.parentCtx, channel.name)
  159. if err == nil {
  160. eventName = channelData.EventName
  161. }
  162. session, err = logs.Add(channel.parentCtx, channel.name, queue[0].Time, true, eventName)
  163. if err != nil {
  164. channel.client.Say(channel.name, "This unit failed to open session: "+err.Error())
  165. log.Println("Failed to open session:", err)
  166. }
  167. } else if err != nil {
  168. channel.client.Say(channel.name, "This unit is unable to check active sessions: "+err.Error())
  169. break
  170. }
  171. log.Println("Selected session:", session.ID)
  172. // Remember which session was last posted to.
  173. channel.lastPostSessionID = session.ID
  174. // Post posts
  175. lastSuccess := -1
  176. for i, channelPost := range queue {
  177. post, err := posts.Add(channel.parentCtx, session, channelPost.Time, channelPost.Kind, channelPost.Nick, channelPost.Text)
  178. if err != nil {
  179. log.Println("Failed to post:", err)
  180. break
  181. }
  182. summary := ""
  183. for _, ru := range post.Text {
  184. summary += string(ru)
  185. if len(summary) > 30 {
  186. summary += "..."
  187. break
  188. }
  189. }
  190. log.Printf("Posted (id=%s, kind=%s, nick=%s, delay=%s): %s", post.ID, post.Kind, post.Nick, time.Since(post.Time), summary)
  191. lastSuccess = i
  192. }
  193. if lastSuccess >= 0 {
  194. copy(queue, queue[lastSuccess:])
  195. queue = queue[:len(queue)-(lastSuccess+1)]
  196. }
  197. }
  198. case now := <-minutely.C:
  199. if !channel.lastPostTime.IsZero() && now.Sub(channel.lastPostTime) > (time.Hour*2) {
  200. session, err := logs.FindOpen(channel.parentCtx, channel.name)
  201. if err == logs.ErrNoneOpen {
  202. log.Println(channel.name, "Log already closed.")
  203. channel.lastPostTime = time.Time{}
  204. channel.lastPostSessionID = ""
  205. break
  206. } else if err != nil {
  207. log.Println(channel.name, "Could not find session:", err)
  208. break
  209. }
  210. if session.ID != channel.lastPostSessionID {
  211. log.Println("Aborted auto-close in", channel.name, "due to session change.")
  212. channel.lastPostTime = time.Time{}
  213. channel.lastPostSessionID = ""
  214. break
  215. }
  216. if now.Sub(session.LatestTime()) < (time.Hour * 2) {
  217. log.Println("Aborted auto-close in", channel.name, "due to session being more recent.")
  218. channel.lastPostTime = session.LatestTime()
  219. break
  220. }
  221. _, err = logs.SetOpen(channel.parentCtx, session, false)
  222. if err != nil {
  223. log.Println("Could not set open:", err)
  224. break
  225. }
  226. channel.client.Sayf(channel.name, "Log session closed after 2+ hours of inactivity. See log at https://aiterp.net/logs/%s", channel.lastPostSessionID)
  227. channel.lastPostTime = time.Time{}
  228. channel.lastPostSessionID = ""
  229. }
  230. case <-channel.parentCtx.Done():
  231. {
  232. // Time to pack up shop.
  233. return
  234. }
  235. }
  236. }
  237. }
  238. func (channel *Channel) wait(ctx context.Context) error {
  239. select {
  240. case <-ctx.Done():
  241. return ctx.Err()
  242. case <-channel.ctx.Done():
  243. return nil
  244. }
  245. }
  246. // A ChannelPost is a post made to a channel.
  247. type ChannelPost struct {
  248. Kind string
  249. Time time.Time
  250. Nick string
  251. Text string
  252. Account string
  253. }
  254. func (post *ChannelPost) botCommand() *botCommand {
  255. if !strings.HasPrefix(post.Text, "!") {
  256. return nil
  257. }
  258. split := strings.SplitN(post.Text, " ", 2)
  259. if len(split) == 1 {
  260. return &botCommand{Verb: strings.ToLower(split[0][1:]), Text: ""}
  261. }
  262. return &botCommand{Verb: strings.ToLower(split[0][1:]), Text: split[1]}
  263. }
  264. type botCommand struct {
  265. Verb string
  266. Text string
  267. }