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.

message_cache.go 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "time"
  6. "github.com/goshuirc/irc-go/ircmsg"
  7. "github.com/ergochat/ergo/irc/caps"
  8. "github.com/ergochat/ergo/irc/utils"
  9. )
  10. // MessageCache caches serialized IRC messages.
  11. // First call Initialize or InitializeSplitMessage, which records
  12. // the parameters and builds the cache. Then call Send, which will
  13. // either send a cached version of the message or dispatch to another
  14. // send routine that can synthesize the necessary version on the fly.
  15. type MessageCache struct {
  16. // these cache a single-line message (e.g., JOIN, or PRIVMSG with a 512-byte message)
  17. // one version is "plain" (legacy clients with no tags) and one is "full" (client has
  18. // the message-tags cap)
  19. plain []byte
  20. fullTags []byte
  21. // these cache a multiline message (a PRIVMSG that was sent as a multiline batch)
  22. // one version is "plain" (legacy clients with no tags) and one is "full" (client has
  23. // the multiline cap)
  24. plainMultiline [][]byte
  25. fullTagsMultiline [][]byte
  26. time time.Time
  27. msgid string
  28. accountName string
  29. tags map[string]string
  30. source string
  31. command string
  32. isBot bool
  33. params []string
  34. target string
  35. splitMessage utils.SplitMessage
  36. }
  37. func addAllTags(msg *ircmsg.Message, tags map[string]string, serverTime time.Time, msgid, accountName string, isBot bool) {
  38. msg.UpdateTags(tags)
  39. msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat))
  40. if accountName != "*" {
  41. msg.SetTag("account", accountName)
  42. }
  43. if msgid != "" {
  44. msg.SetTag("msgid", msgid)
  45. }
  46. if isBot {
  47. msg.SetTag(caps.BotTagName, "")
  48. }
  49. }
  50. func (m *MessageCache) handleErr(server *Server, err error) bool {
  51. if !(err == nil || err == ircmsg.ErrorBodyTooLong) {
  52. server.logger.Error("internal", "Error assembling message for sending", err.Error())
  53. // blank these out so Send will be a no-op
  54. m.fullTags = nil
  55. m.fullTagsMultiline = nil
  56. return true
  57. }
  58. return false
  59. }
  60. func (m *MessageCache) Initialize(server *Server, serverTime time.Time, msgid string, nickmask, accountName string, isBot bool, tags map[string]string, command string, params ...string) (err error) {
  61. m.time = serverTime
  62. m.msgid = msgid
  63. m.source = nickmask
  64. m.accountName = accountName
  65. m.isBot = isBot
  66. m.tags = tags
  67. m.command = command
  68. m.params = params
  69. var msg ircmsg.Message
  70. config := server.Config()
  71. if config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[command] {
  72. msg.ForceTrailing()
  73. }
  74. msg.Prefix = nickmask
  75. msg.Command = command
  76. msg.Params = make([]string, len(params))
  77. copy(msg.Params, params)
  78. m.plain, err = msg.LineBytesStrict(false, MaxLineLen)
  79. if m.handleErr(server, err) {
  80. return
  81. }
  82. addAllTags(&msg, tags, serverTime, msgid, accountName, isBot)
  83. m.fullTags, err = msg.LineBytesStrict(false, MaxLineLen)
  84. if m.handleErr(server, err) {
  85. return
  86. }
  87. return
  88. }
  89. func (m *MessageCache) InitializeSplitMessage(server *Server, nickmask, accountName string, isBot bool, tags map[string]string, command, target string, message utils.SplitMessage) (err error) {
  90. m.time = message.Time
  91. m.msgid = message.Msgid
  92. m.source = nickmask
  93. m.accountName = accountName
  94. m.isBot = isBot
  95. m.tags = tags
  96. m.command = command
  97. m.target = target
  98. m.splitMessage = message
  99. config := server.Config()
  100. forceTrailing := config.Server.Compatibility.forceTrailing && commandsThatMustUseTrailing[command]
  101. if message.Is512() {
  102. isTagmsg := command == "TAGMSG"
  103. var msg ircmsg.Message
  104. if forceTrailing {
  105. msg.ForceTrailing()
  106. }
  107. msg.Prefix = nickmask
  108. msg.Command = command
  109. if isTagmsg {
  110. msg.Params = []string{target}
  111. } else {
  112. msg.Params = []string{target, message.Message}
  113. }
  114. m.params = msg.Params
  115. if !isTagmsg {
  116. m.plain, err = msg.LineBytesStrict(false, MaxLineLen)
  117. if m.handleErr(server, err) {
  118. return
  119. }
  120. }
  121. addAllTags(&msg, tags, message.Time, message.Msgid, accountName, isBot)
  122. m.fullTags, err = msg.LineBytesStrict(false, MaxLineLen)
  123. if m.handleErr(server, err) {
  124. return
  125. }
  126. } else {
  127. var msg ircmsg.Message
  128. if forceTrailing {
  129. msg.ForceTrailing()
  130. }
  131. msg.Prefix = nickmask
  132. msg.Command = command
  133. msg.Params = make([]string, 2)
  134. msg.Params[0] = target
  135. m.plainMultiline = make([][]byte, len(message.Split))
  136. for i, pair := range message.Split {
  137. msg.Params[1] = pair.Message
  138. m.plainMultiline[i], err = msg.LineBytesStrict(false, MaxLineLen)
  139. if m.handleErr(server, err) {
  140. return
  141. }
  142. }
  143. // we need to send the same batch ID to all recipient sessions;
  144. // ensure it doesn't collide. a half-sized token has 64 bits of entropy,
  145. // so a collision isn't expected until there are on the order of 2**32
  146. // concurrent batches being relayed:
  147. batchID := utils.GenerateSecretToken()[:utils.SecretTokenLength/2]
  148. batch := composeMultilineBatch(batchID, nickmask, accountName, isBot, tags, command, target, message)
  149. m.fullTagsMultiline = make([][]byte, len(batch))
  150. for i, msg := range batch {
  151. if forceTrailing {
  152. msg.ForceTrailing()
  153. }
  154. m.fullTagsMultiline[i], err = msg.LineBytesStrict(false, MaxLineLen)
  155. if m.handleErr(server, err) {
  156. return
  157. }
  158. }
  159. }
  160. return
  161. }
  162. func (m *MessageCache) Send(session *Session) {
  163. if m.fullTags != nil {
  164. // Initialize() path:
  165. if session.capabilities.Has(caps.MessageTags) {
  166. session.sendBytes(m.fullTags, false)
  167. } else if m.plain != nil {
  168. // plain == nil indicates a TAGMSG
  169. if !(session.capabilities.Has(caps.ServerTime) || session.capabilities.Has(caps.AccountTag)) {
  170. session.sendBytes(m.plain, false)
  171. } else {
  172. // slowpath
  173. session.sendFromClientInternal(false, m.time, m.msgid, m.source, m.accountName, m.isBot, nil, m.command, m.params...)
  174. }
  175. }
  176. } else if m.fullTagsMultiline != nil {
  177. // InitializeSplitMessage() path:
  178. if session.capabilities.Has(caps.Multiline) {
  179. for _, line := range m.fullTagsMultiline {
  180. session.sendBytes(line, false)
  181. }
  182. } else if !(session.capabilities.Has(caps.ServerTime) || session.capabilities.Has(caps.AccountTag)) {
  183. for _, line := range m.plainMultiline {
  184. session.sendBytes(line, false)
  185. }
  186. } else {
  187. // slowpath
  188. session.sendSplitMsgFromClientInternal(false, m.source, m.accountName, m.isBot, m.tags, m.command, m.target, m.splitMessage)
  189. }
  190. }
  191. }