Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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