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.

znc.go 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. // Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/ergochat/ergo/irc/history"
  10. "github.com/ergochat/ergo/irc/utils"
  11. )
  12. const (
  13. // #829, also see "Case 2" in the "three cases" below:
  14. zncPlaybackCommandExpiration = time.Second * 30
  15. zncPrefix = "*playback!znc@znc.in"
  16. maxDMTargetsForAutoplay = 128
  17. )
  18. type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
  19. var zncHandlers = map[string]zncCommandHandler{
  20. "*playback": zncPlaybackHandler,
  21. }
  22. func zncPrivmsgHandler(client *Client, command string, privmsg string, rb *ResponseBuffer) {
  23. zncModuleHandler(client, command, strings.Fields(privmsg), rb)
  24. }
  25. func zncModuleHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
  26. command = strings.ToLower(command)
  27. if subHandler, ok := zncHandlers[command]; ok {
  28. subHandler(client, command, params, rb)
  29. } else {
  30. nick := rb.target.Nick()
  31. rb.Add(nil, client.server.name, "NOTICE", nick, fmt.Sprintf(client.t("Oragono does not emulate the ZNC module %s"), command))
  32. rb.Add(nil, "*status!znc@znc.in", "NOTICE", nick, fmt.Sprintf(client.t("No such module [%s]"), command))
  33. }
  34. }
  35. // "number of seconds (floating point for millisecond precision) elapsed since January 1, 1970"
  36. func zncWireTimeToTime(str string) (result time.Time) {
  37. var secondsPortion, fracPortion string
  38. dot := strings.IndexByte(str, '.')
  39. if dot == -1 {
  40. secondsPortion = str
  41. } else {
  42. secondsPortion = str[:dot]
  43. fracPortion = str[dot+1:]
  44. }
  45. seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
  46. // truncate to nanosecond resolution if necessary
  47. if len(fracPortion) > 9 {
  48. fracPortion = fracPortion[:9]
  49. }
  50. fracSeconds, _ := strconv.ParseInt(fracPortion, 10, 64)
  51. for i := 0; i < (9 - len(fracPortion)); i++ {
  52. fracSeconds *= 10
  53. }
  54. return time.Unix(seconds, fracSeconds).UTC()
  55. }
  56. func timeToZncWireTime(t time.Time) (result string) {
  57. secs := t.Unix()
  58. nano := t.UnixNano() - (secs * 1000000000)
  59. return fmt.Sprintf("%d.%d", secs, nano)
  60. }
  61. type zncPlaybackTimes struct {
  62. start time.Time
  63. end time.Time
  64. targets utils.HashSet[string] // nil for "*" (everything), otherwise the channel names
  65. setAt time.Time
  66. }
  67. func (z *zncPlaybackTimes) ValidFor(target string) bool {
  68. if z == nil {
  69. return false
  70. }
  71. if time.Now().Sub(z.setAt) > zncPlaybackCommandExpiration {
  72. return false
  73. }
  74. if z.targets == nil {
  75. return true
  76. }
  77. return z.targets.Has(target)
  78. }
  79. // https://wiki.znc.in/Playback
  80. func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
  81. if len(params) == 0 {
  82. return
  83. }
  84. switch strings.ToLower(params[0]) {
  85. case "play":
  86. zncPlaybackPlayHandler(client, command, params, rb)
  87. case "list":
  88. zncPlaybackListHandler(client, command, params, rb)
  89. default:
  90. return
  91. }
  92. }
  93. // PRIVMSG *playback :play <target> [lower_bound] [upper_bound]
  94. // e.g., PRIVMSG *playback :play * 1558374442
  95. func zncPlaybackPlayHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
  96. if len(params) < 2 || len(params) > 4 {
  97. return
  98. }
  99. targetString := params[1]
  100. now := time.Now().UTC()
  101. var start, end time.Time
  102. switch len(params) {
  103. case 2:
  104. // #1205: this should have the same semantics as `LATEST *`
  105. case 3:
  106. // #831: this should have the same semantics as `LATEST timestamp=qux`,
  107. // or equivalently `BETWEEN timestamp=$now timestamp=qux`, as opposed to
  108. // `AFTER timestamp=qux` (this matters in the case where there are
  109. // more than znc-maxmessages available)
  110. start = now
  111. end = zncWireTimeToTime(params[2])
  112. case 4:
  113. start = zncWireTimeToTime(params[2])
  114. end = zncWireTimeToTime(params[3])
  115. }
  116. var targets utils.HashSet[string]
  117. var nickTargets []string
  118. // three cases:
  119. // 1. the user's PMs get played back immediately upon receiving this
  120. // 2. if this is a new connection (from the server's POV), save the information
  121. // and use it to process subsequent joins. (This is the Textual behavior:
  122. // first send the playback PRIVMSG, then send the JOIN lines.)
  123. // 3. if this is a reattach (from the server's POV), immediately play back
  124. // history for channels that the client is already joined to. In this scenario,
  125. // there are three total attempts to play the history:
  126. // 3.1. During the initial reattach (no-op because the *playback privmsg
  127. // hasn't been received yet, but they negotiated the znc.in/playback
  128. // cap so we know we're going to receive it later)
  129. // 3.2 Upon receiving the *playback privmsg, i.e., now: we should play
  130. // the relevant history lines
  131. // 3.3 When the client sends a subsequent redundant JOIN line for those
  132. // channels; redundant JOIN is a complete no-op so we won't replay twice
  133. playPrivmsgs := false
  134. if params[1] == "*" {
  135. playPrivmsgs = true // XXX nil `targets` means "every channel"
  136. } else {
  137. targets = make(utils.HashSet[string])
  138. for _, targetName := range strings.Split(targetString, ",") {
  139. if strings.HasPrefix(targetName, "#") {
  140. if cfTarget, err := CasefoldChannel(targetName); err == nil {
  141. targets.Add(cfTarget)
  142. }
  143. } else {
  144. if cfNick, err := CasefoldName(targetName); err == nil {
  145. nickTargets = append(nickTargets, cfNick)
  146. }
  147. }
  148. }
  149. }
  150. if playPrivmsgs {
  151. zncPlayPrivmsgsFromAll(client, rb, start, end)
  152. }
  153. rb.session.zncPlaybackTimes = &zncPlaybackTimes{
  154. start: start,
  155. end: end,
  156. targets: targets,
  157. setAt: time.Now().UTC(),
  158. }
  159. for _, channel := range client.Channels() {
  160. if targets == nil || targets.Has(channel.NameCasefolded()) {
  161. channel.autoReplayHistory(client, rb, "")
  162. rb.Flush(true)
  163. }
  164. }
  165. for _, cfNick := range nickTargets {
  166. zncPlayPrivmsgsFrom(client, rb, cfNick, start, end)
  167. rb.Flush(true)
  168. }
  169. }
  170. func zncPlayPrivmsgsFrom(client *Client, rb *ResponseBuffer, target string, start, end time.Time) {
  171. _, sequence, err := client.server.GetHistorySequence(nil, client, target)
  172. if sequence == nil || err != nil {
  173. return
  174. }
  175. zncMax := client.server.Config().History.ZNCMax
  176. items, err := sequence.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax)
  177. if err == nil && len(items) != 0 {
  178. client.replayPrivmsgHistory(rb, items, target, false)
  179. }
  180. }
  181. func zncPlayPrivmsgsFromAll(client *Client, rb *ResponseBuffer, start, end time.Time) {
  182. zncMax := client.server.Config().History.ZNCMax
  183. items, err := client.privmsgsBetween(start, end, maxDMTargetsForAutoplay, zncMax)
  184. if err == nil && len(items) != 0 {
  185. client.replayPrivmsgHistory(rb, items, "", false)
  186. }
  187. }
  188. // PRIVMSG *playback :list
  189. func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
  190. limit := client.server.Config().History.ChathistoryMax
  191. correspondents, err := client.listTargets(history.Selector{}, history.Selector{}, limit)
  192. if err != nil {
  193. client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
  194. return
  195. }
  196. nick := client.Nick()
  197. for _, correspondent := range correspondents {
  198. stamp := timeToZncWireTime(correspondent.Time)
  199. unfoldedTarget := client.server.UnfoldName(correspondent.CfName)
  200. rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", unfoldedTarget, stamp))
  201. }
  202. }