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 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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/goshuirc/irc-go/ircmsg"
  8. "github.com/oragono/oragono/irc/caps"
  9. "github.com/oragono/oragono/irc/utils"
  10. )
  11. const (
  12. // https://ircv3.net/specs/extensions/labeled-response.html
  13. defaultBatchType = "draft/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 (possibly `history` 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.IrcMessage
  33. finalized bool
  34. target *Client
  35. session *Session
  36. }
  37. // GetLabel returns the label from the given message.
  38. func GetLabel(msg ircmsg.IrcMessage) 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.IrcMessage) {
  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. if 0 < len(rb.nestedBatches) {
  59. msg.SetTag("batch", rb.nestedBatches[len(rb.nestedBatches)-1])
  60. }
  61. rb.messages = append(rb.messages, msg)
  62. }
  63. // Add adds a standard new message to our queue.
  64. func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
  65. rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
  66. }
  67. // AddFromClient adds a new message from a specific client to our queue.
  68. func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
  69. msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
  70. if rb.session.capabilities.Has(caps.MessageTags) {
  71. msg.UpdateTags(tags)
  72. }
  73. // attach account-tag
  74. if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
  75. msg.SetTag("account", fromAccount)
  76. }
  77. // attach message-id
  78. if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
  79. msg.SetTag("msgid", msgid)
  80. }
  81. // attach server-time
  82. if rb.session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
  83. msg.SetTag("time", time.UTC().Format(IRCv3TimestampFormat))
  84. }
  85. rb.AddMessage(msg)
  86. }
  87. // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
  88. func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
  89. if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
  90. rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
  91. } else {
  92. for _, messagePair := range message.Wrapped {
  93. rb.AddFromClient(message.Time, messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message)
  94. }
  95. }
  96. }
  97. // ForceBatchStart forcibly starts a batch of batch `batchType`.
  98. // Normally, Send/Flush will decide automatically whether to start a batch
  99. // of type draft/labeled-response. This allows changing the batch type
  100. // and forcing the creation of a possibly empty batch.
  101. func (rb *ResponseBuffer) ForceBatchStart(batchType string, blocking bool) {
  102. rb.batchType = batchType
  103. rb.sendBatchStart(blocking)
  104. }
  105. func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
  106. if rb.batchID != "" {
  107. // batch already initialized
  108. return
  109. }
  110. rb.batchID = utils.GenerateSecretToken()
  111. message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, rb.batchType)
  112. if rb.Label != "" {
  113. message.SetTag(caps.LabelTagName, rb.Label)
  114. }
  115. rb.session.SendRawMessage(message, blocking)
  116. }
  117. func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
  118. if rb.batchID == "" {
  119. // we are not sending a batch, skip this
  120. return
  121. }
  122. message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
  123. rb.session.SendRawMessage(message, blocking)
  124. }
  125. // Starts a nested batch (see the ResponseBuffer struct definition for a description of
  126. // how this works)
  127. func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
  128. batchID = utils.GenerateSecretToken()
  129. msgParams := make([]string, len(params)+2)
  130. msgParams[0] = "+" + batchID
  131. msgParams[1] = batchType
  132. copy(msgParams[2:], params)
  133. rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", msgParams...))
  134. rb.nestedBatches = append(rb.nestedBatches, batchID)
  135. return
  136. }
  137. // Ends a nested batch
  138. func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
  139. if batchID == "" {
  140. return
  141. }
  142. if 0 == len(rb.nestedBatches) || rb.nestedBatches[len(rb.nestedBatches)-1] != batchID {
  143. rb.target.server.logger.Error("internal", "inconsistent batch nesting detected")
  144. debug.PrintStack()
  145. return
  146. }
  147. rb.nestedBatches = rb.nestedBatches[0 : len(rb.nestedBatches)-1]
  148. rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID))
  149. }
  150. // Convenience to start a nested batch for history lines, at the highest level
  151. // supported by the client (`history`, `chathistory`, or no batch, in descending order).
  152. func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
  153. var batchType string
  154. if rb.session.capabilities.Has(caps.EventPlayback) {
  155. batchType = "history"
  156. } else if rb.session.capabilities.Has(caps.Batch) {
  157. batchType = "chathistory"
  158. }
  159. if batchType != "" {
  160. batchID = rb.StartNestedBatch(batchType, params...)
  161. }
  162. return
  163. }
  164. // Send sends all messages in the buffer to the client.
  165. // Afterwards, the buffer is in an undefined state and MUST NOT be used further.
  166. // If `blocking` is true you MUST be sending to the client from its own goroutine.
  167. func (rb *ResponseBuffer) Send(blocking bool) error {
  168. return rb.flushInternal(true, blocking)
  169. }
  170. // Flush sends all messages in the buffer to the client.
  171. // Afterwards, the buffer can still be used. Client code MUST subsequently call Send()
  172. // to ensure that the final `BATCH -` message is sent.
  173. // If `blocking` is true you MUST be sending to the client from its own goroutine.
  174. func (rb *ResponseBuffer) Flush(blocking bool) error {
  175. return rb.flushInternal(false, blocking)
  176. }
  177. // flushInternal sends the contents of the buffer, either blocking or nonblocking
  178. // It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
  179. // If `final` is true, it also sends `BATCH -` (if necessary).
  180. func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
  181. if rb.finalized {
  182. return nil
  183. }
  184. useLabel := rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
  185. // use a batch if we have a label, and we either currently have 0 or 2+ messages,
  186. // or we are doing a Flush() and we have to assume that there will be more messages
  187. // in the future.
  188. useBatch := useLabel && (len(rb.messages) != 1 || !final)
  189. // if label but no batch, add label to first message
  190. if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" {
  191. rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
  192. } else if useBatch {
  193. rb.sendBatchStart(blocking)
  194. }
  195. // send each message out
  196. for _, message := range rb.messages {
  197. // attach server-time if needed
  198. if rb.session.capabilities.Has(caps.ServerTime) && !message.HasTag("time") {
  199. message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
  200. }
  201. // attach batch ID, unless this message was part of a nested batch and is
  202. // already tagged
  203. if rb.batchID != "" && !message.HasTag("batch") {
  204. message.SetTag("batch", rb.batchID)
  205. }
  206. // send message out
  207. rb.session.SendRawMessage(message, blocking)
  208. }
  209. // end batch if required
  210. if final {
  211. rb.sendBatchEnd(blocking)
  212. rb.finalized = true
  213. }
  214. // clear out any existing messages
  215. rb.messages = rb.messages[:0]
  216. return nil
  217. }
  218. // Notice sends the client the given notice from the server.
  219. func (rb *ResponseBuffer) Notice(text string) {
  220. rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.nick, text)
  221. }