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.

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