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.

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