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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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
  22. batchID string
  23. messages []ircmsg.IrcMessage
  24. finalized bool
  25. target *Client
  26. session *Session
  27. }
  28. // GetLabel returns the label from the given message.
  29. func GetLabel(msg ircmsg.IrcMessage) string {
  30. _, value := msg.GetTag(caps.LabelTagName)
  31. return value
  32. }
  33. // NewResponseBuffer returns a new ResponseBuffer.
  34. func NewResponseBuffer(session *Session) *ResponseBuffer {
  35. return &ResponseBuffer{
  36. session: session,
  37. target: session.client,
  38. }
  39. }
  40. func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
  41. if rb.finalized {
  42. rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
  43. debug.PrintStack()
  44. // TODO(dan): send a NOTICE to the end user with a string representation of the message,
  45. // for debugging purposes
  46. return
  47. }
  48. rb.messages = append(rb.messages, msg)
  49. }
  50. // Add adds a standard new message to our queue.
  51. func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
  52. rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
  53. }
  54. // AddFromClient adds a new message from a specific client to our queue.
  55. func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
  56. msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
  57. msg.UpdateTags(tags)
  58. // attach account-tag
  59. if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
  60. msg.SetTag("account", fromAccount)
  61. }
  62. // attach message-id
  63. if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
  64. msg.SetTag("draft/msgid", msgid)
  65. }
  66. rb.AddMessage(msg)
  67. }
  68. // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
  69. func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
  70. if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
  71. rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
  72. } else {
  73. for _, messagePair := range message.Wrapped {
  74. rb.AddFromClient(messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message)
  75. }
  76. }
  77. }
  78. // InitializeBatch forcibly starts a batch of batch `batchType`.
  79. // Normally, Send/Flush will decide automatically whether to start a batch
  80. // of type draft/labeled-response. This allows changing the batch type
  81. // and forcing the creation of a possibly empty batch.
  82. func (rb *ResponseBuffer) InitializeBatch(batchType string, blocking bool) {
  83. rb.sendBatchStart(batchType, blocking)
  84. }
  85. func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) {
  86. if rb.batchID != "" {
  87. // batch already initialized
  88. return
  89. }
  90. // formerly this combined time.Now.UnixNano() in base 36 with an incrementing counter,
  91. // also in base 36. but let's just use a uuidv4-alike (26 base32 characters):
  92. rb.batchID = utils.GenerateSecretToken()
  93. message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
  94. if rb.Label != "" {
  95. message.SetTag(caps.LabelTagName, rb.Label)
  96. }
  97. rb.session.SendRawMessage(message, blocking)
  98. }
  99. func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
  100. if rb.batchID == "" {
  101. // we are not sending a batch, skip this
  102. return
  103. }
  104. message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
  105. rb.session.SendRawMessage(message, blocking)
  106. }
  107. // Send sends all messages in the buffer to the client.
  108. // Afterwards, the buffer is in an undefined state and MUST NOT be used further.
  109. // If `blocking` is true you MUST be sending to the client from its own goroutine.
  110. func (rb *ResponseBuffer) Send(blocking bool) error {
  111. return rb.flushInternal(true, blocking)
  112. }
  113. // Flush sends all messages in the buffer to the client.
  114. // Afterwards, the buffer can still be used. Client code MUST subsequently call Send()
  115. // to ensure that the final `BATCH -` message is sent.
  116. // If `blocking` is true you MUST be sending to the client from its own goroutine.
  117. func (rb *ResponseBuffer) Flush(blocking bool) error {
  118. return rb.flushInternal(false, blocking)
  119. }
  120. // flushInternal sends the contents of the buffer, either blocking or nonblocking
  121. // It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
  122. // If `final` is true, it also sends `BATCH -` (if necessary).
  123. func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
  124. if rb.finalized {
  125. return nil
  126. }
  127. useLabel := rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
  128. // use a batch if we have a label, and we either currently have 0 or 2+ messages,
  129. // or we are doing a Flush() and we have to assume that there will be more messages
  130. // in the future.
  131. useBatch := useLabel && (len(rb.messages) != 1 || !final)
  132. // if label but no batch, add label to first message
  133. if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" {
  134. rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
  135. } else if useBatch {
  136. rb.sendBatchStart(defaultBatchType, blocking)
  137. }
  138. // send each message out
  139. for _, message := range rb.messages {
  140. // attach server-time if needed
  141. if rb.session.capabilities.Has(caps.ServerTime) && !message.HasTag("time") {
  142. message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
  143. }
  144. // attach batch ID
  145. if rb.batchID != "" {
  146. message.SetTag("batch", rb.batchID)
  147. }
  148. // send message out
  149. rb.session.SendRawMessage(message, blocking)
  150. }
  151. // end batch if required
  152. if final {
  153. rb.sendBatchEnd(blocking)
  154. rb.finalized = true
  155. }
  156. // clear out any existing messages
  157. rb.messages = rb.messages[:0]
  158. return nil
  159. }
  160. // Notice sends the client the given notice from the server.
  161. func (rb *ResponseBuffer) Notice(text string) {
  162. rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.nick, text)
  163. }