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.

responsebuffer.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "runtime/debug"
  6. "time"
  7. "github.com/ergochat/ergo/irc/caps"
  8. "github.com/ergochat/ergo/irc/utils"
  9. "github.com/ergochat/irc-go/ircmsg"
  10. )
  11. const (
  12. // https://ircv3.net/specs/extensions/labeled-response.html
  13. defaultBatchType = "labeled-response"
  14. )
  15. // ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
  16. //
  17. // Using a ResponseBuffer lets you really easily implement labeled-response, since the
  18. // buffer will silently create a batch if required and label the outgoing messages as
  19. // necessary (or leave it off and simply tag the outgoing message).
  20. type ResponseBuffer struct {
  21. Label string // label if this is a labeled response batch
  22. batchID string // ID of the labeled response batch, if one has been initiated
  23. batchType string // type of the labeled response batch (currently either `labeled-response` or `chathistory`)
  24. // stack of batch IDs of nested batches, which are handled separately
  25. // from the underlying labeled-response batch. starting a new nested batch
  26. // unconditionally enqueues its batch start message; subsequent messages
  27. // are tagged with the nested batch ID, until nested batch end.
  28. // (the nested batch start itself may have no batch tag, or the batch tag of the
  29. // underlying labeled-response batch, or the batch tag of the next outermost
  30. // nested batch.)
  31. nestedBatches []string
  32. messages []ircmsg.Message
  33. finalized bool
  34. target *Client
  35. session *Session
  36. }
  37. // GetLabel returns the label from the given message.
  38. func GetLabel(msg ircmsg.Message) string {
  39. _, value := msg.GetTag(caps.LabelTagName)
  40. return value
  41. }
  42. // NewResponseBuffer returns a new ResponseBuffer.
  43. func NewResponseBuffer(session *Session) *ResponseBuffer {
  44. return &ResponseBuffer{
  45. session: session,
  46. target: session.client,
  47. batchType: defaultBatchType,
  48. }
  49. }
  50. func (rb *ResponseBuffer) AddMessage(msg ircmsg.Message) {
  51. if rb.finalized {
  52. rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
  53. debug.PrintStack()
  54. // TODO(dan): send a NOTICE to the end user with a string representation of the message,
  55. // for debugging purposes
  56. return
  57. }
  58. rb.session.setTimeTag(&msg, time.Time{})
  59. rb.setNestedBatchTag(&msg)
  60. rb.messages = append(rb.messages, msg)
  61. }
  62. func (rb *ResponseBuffer) setNestedBatchTag(msg *ircmsg.Message) {
  63. if 0 < len(rb.nestedBatches) {
  64. msg.SetTag("batch", rb.nestedBatches[len(rb.nestedBatches)-1])
  65. }
  66. }
  67. // Add adds a standard new message to our queue.
  68. func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
  69. rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
  70. }
  71. // Broadcast adds a standard new message to our queue, then sends an unlabeled copy
  72. // to all other sessions.
  73. func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) {
  74. // can't reuse the Message object because of tag pollution :-\
  75. rb.Add(tags, prefix, command, params...)
  76. for _, session := range rb.session.client.Sessions() {
  77. if session != rb.session {
  78. session.Send(tags, prefix, command, params...)
  79. }
  80. }
  81. }
  82. // AddFromClient adds a new message from a specific client to our queue.
  83. func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, params ...string) {
  84. msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
  85. if rb.session.capabilities.Has(caps.MessageTags) {
  86. msg.UpdateTags(tags)
  87. }
  88. // attach account-tag
  89. if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
  90. msg.SetTag("account", fromAccount)
  91. }
  92. // attach message-id
  93. if rb.session.capabilities.Has(caps.MessageTags) {
  94. if len(msgid) != 0 {
  95. msg.SetTag("msgid", msgid)
  96. }
  97. if isBot {
  98. msg.SetTag(caps.BotTagName, "")
  99. }
  100. }
  101. // attach server-time
  102. rb.session.setTimeTag(&msg, time)
  103. rb.AddMessage(msg)
  104. }
  105. // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
  106. func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, isBot bool, tags map[string]string, command string, target string, message utils.SplitMessage) {
  107. if message.Is512() {
  108. if message.Message == "" {
  109. // XXX this is a TAGMSG
  110. rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, isBot, tags, command, target)
  111. } else {
  112. rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, isBot, tags, command, target, message.Message)
  113. }
  114. } else {
  115. if rb.session.capabilities.Has(caps.Multiline) {
  116. batch := composeMultilineBatch(rb.session.generateBatchID(), fromNickMask, fromAccount, isBot, tags, command, target, message)
  117. rb.setNestedBatchTag(&batch[0])
  118. rb.setNestedBatchTag(&batch[len(batch)-1])
  119. rb.messages = append(rb.messages, batch...)
  120. } else {
  121. for i, messagePair := range message.Split {
  122. var msgid string
  123. if i == 0 {
  124. msgid = message.Msgid
  125. }
  126. rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, isBot, tags, command, target, messagePair.Message)
  127. }
  128. }
  129. }
  130. }
  131. func (rb *ResponseBuffer) addEchoMessage(tags map[string]string, nickMask, accountName, command, target string, message utils.SplitMessage) {
  132. // TODO fix isBot here
  133. if rb.session.capabilities.Has(caps.EchoMessage) {
  134. hasTagsCap := rb.session.capabilities.Has(caps.MessageTags)
  135. if command == "TAGMSG" {
  136. if hasTagsCap {
  137. rb.AddFromClient(message.Time, message.Msgid, nickMask, accountName, false, tags, command, target)
  138. }
  139. } else {
  140. tagsToSend := tags
  141. if !hasTagsCap {
  142. tagsToSend = nil
  143. }
  144. rb.AddSplitMessageFromClient(nickMask, accountName, false, tagsToSend, command, target, message)
  145. }
  146. }
  147. }
  148. func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
  149. if rb.batchID != "" {
  150. // batch already initialized
  151. return
  152. }
  153. rb.batchID = rb.session.generateBatchID()
  154. message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, rb.batchType)
  155. if rb.Label != "" {
  156. message.SetTag(caps.LabelTagName, rb.Label)
  157. }
  158. rb.session.SendRawMessage(message, blocking)
  159. }
  160. func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
  161. if rb.batchID == "" {
  162. // we are not sending a batch, skip this
  163. return
  164. }
  165. message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
  166. rb.session.SendRawMessage(message, blocking)
  167. }
  168. // Starts a nested batch (see the ResponseBuffer struct definition for a description of
  169. // how this works)
  170. func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
  171. if !rb.session.capabilities.Has(caps.Batch) {
  172. return
  173. }
  174. batchID = rb.session.generateBatchID()
  175. msgParams := make([]string, len(params)+2)
  176. msgParams[0] = "+" + batchID
  177. msgParams[1] = batchType
  178. copy(msgParams[2:], params)
  179. rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", msgParams...))
  180. rb.nestedBatches = append(rb.nestedBatches, batchID)
  181. return
  182. }
  183. // Ends a nested batch
  184. func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
  185. if batchID == "" {
  186. return
  187. }
  188. if 0 == len(rb.nestedBatches) || rb.nestedBatches[len(rb.nestedBatches)-1] != batchID {
  189. rb.target.server.logger.Error("internal", "inconsistent batch nesting detected")
  190. debug.PrintStack()
  191. return
  192. }
  193. rb.nestedBatches = rb.nestedBatches[0 : len(rb.nestedBatches)-1]
  194. rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID))
  195. }
  196. // Send sends all messages in the buffer to the client.
  197. // Afterwards, the buffer is in an undefined state and MUST NOT be used further.
  198. // If `blocking` is true you MUST be sending to the client from its own goroutine.
  199. func (rb *ResponseBuffer) Send(blocking bool) error {
  200. return rb.flushInternal(true, blocking)
  201. }
  202. // Flush sends all messages in the buffer to the client.
  203. // Afterwards, the buffer can still be used. Client code MUST subsequently call Send()
  204. // to ensure that the final `BATCH -` message is sent.
  205. // If `blocking` is true you MUST be sending to the client from its own goroutine.
  206. func (rb *ResponseBuffer) Flush(blocking bool) error {
  207. return rb.flushInternal(false, blocking)
  208. }
  209. // detects whether the response buffer consists of a single, unflushed nested batch,
  210. // in which case it can be collapsed down to that batch
  211. func (rb *ResponseBuffer) isCollapsible() (result bool) {
  212. // rb.batchID indicates that we already flushed some lines
  213. if rb.batchID != "" || len(rb.messages) < 2 {
  214. return false
  215. }
  216. first, last := rb.messages[0], rb.messages[len(rb.messages)-1]
  217. if first.Command != "BATCH" || last.Command != "BATCH" {
  218. return false
  219. }
  220. if len(first.Params) == 0 || len(first.Params[0]) == 0 || len(last.Params) == 0 || len(last.Params[0]) == 0 {
  221. return false
  222. }
  223. return first.Params[0][1:] == last.Params[0][1:]
  224. }
  225. // flushInternal sends the contents of the buffer, either blocking or nonblocking
  226. // It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
  227. // If `final` is true, it also sends `BATCH -` (if necessary).
  228. func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
  229. if rb.finalized {
  230. return nil
  231. }
  232. if rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != "" {
  233. if final && rb.isCollapsible() {
  234. // collapse to the outermost nested batch
  235. rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
  236. } else if !final || 2 <= len(rb.messages) {
  237. // we either have 2+ messages, or we are doing a Flush() and have to assume
  238. // there will be more messages in the future
  239. rb.sendBatchStart(blocking)
  240. } else if len(rb.messages) == 1 && rb.batchID == "" {
  241. // single labeled message
  242. rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
  243. } else if len(rb.messages) == 0 && rb.batchID == "" {
  244. // ACK message
  245. message := ircmsg.MakeMessage(nil, rb.session.client.server.name, "ACK")
  246. message.SetTag(caps.LabelTagName, rb.Label)
  247. rb.session.setTimeTag(&message, time.Time{})
  248. rb.session.SendRawMessage(message, blocking)
  249. }
  250. }
  251. // send each message out
  252. for _, message := range rb.messages {
  253. // attach batch ID, unless this message was part of a nested batch and is
  254. // already tagged
  255. if rb.batchID != "" && !message.HasTag("batch") {
  256. message.SetTag("batch", rb.batchID)
  257. }
  258. // send message out
  259. rb.session.SendRawMessage(message, blocking)
  260. }
  261. // end batch if required
  262. if final {
  263. rb.sendBatchEnd(blocking)
  264. rb.finalized = true
  265. }
  266. // clear out any existing messages
  267. rb.messages = rb.messages[:0]
  268. return nil
  269. }
  270. // Notice sends the client the given notice from the server.
  271. func (rb *ResponseBuffer) Notice(text string) {
  272. rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.Nick(), text)
  273. }