Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

histserv.go 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "bufio"
  6. "fmt"
  7. "os"
  8. "runtime/debug"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/oragono/oragono/irc/history"
  13. "github.com/oragono/oragono/irc/utils"
  14. )
  15. const (
  16. histservHelp = `HistServ provides commands related to history.`
  17. histServMask = "HistServ!HistServ@localhost"
  18. )
  19. func histservEnabled(config *Config) bool {
  20. return config.History.Enabled
  21. }
  22. func historyComplianceEnabled(config *Config) bool {
  23. return config.History.Enabled && config.History.Persistent.Enabled && config.History.Retention.EnableAccountIndexing
  24. }
  25. var (
  26. histservCommands = map[string]*serviceCommand{
  27. "forget": {
  28. handler: histservForgetHandler,
  29. help: `Syntax: $bFORGET <account>$b
  30. FORGET deletes all history messages sent by an account.`,
  31. helpShort: `$bFORGET$b deletes all history messages sent by an account.`,
  32. capabs: []string{"history"},
  33. enabled: histservEnabled,
  34. minParams: 1,
  35. maxParams: 1,
  36. },
  37. "delete": {
  38. handler: histservDeleteHandler,
  39. help: `Syntax: $bDELETE [target] <msgid>$b
  40. DELETE deletes an individual message by its msgid. The target is a channel
  41. name or nickname; depending on the history implementation, this may or may not
  42. be necessary to locate the message.`,
  43. helpShort: `$bDELETE$b deletes an individual message by its msgid.`,
  44. enabled: histservEnabled,
  45. minParams: 1,
  46. maxParams: 2,
  47. },
  48. "export": {
  49. handler: histservExportHandler,
  50. help: `Syntax: $bEXPORT <account>$b
  51. EXPORT exports all messages sent by an account as JSON. This can be used at
  52. the request of the account holder.`,
  53. helpShort: `$bEXPORT$b exports all messages sent by an account as JSON.`,
  54. enabled: historyComplianceEnabled,
  55. capabs: []string{"history"},
  56. minParams: 1,
  57. maxParams: 1,
  58. },
  59. "play": {
  60. handler: histservPlayHandler,
  61. help: `Syntax: $bPLAY <target> [limit]$b
  62. PLAY plays back history messages, rendering them into direct messages from
  63. HistServ. 'target' is a channel name (or 'me' for direct messages), and 'limit'
  64. is a message count or a time duration. Note that message playback may be
  65. incomplete or degraded, relative to direct playback from /HISTORY or
  66. CHATHISTORY.`,
  67. helpShort: `$bPLAY$b plays back history messages.`,
  68. enabled: histservEnabled,
  69. minParams: 1,
  70. maxParams: 2,
  71. },
  72. }
  73. )
  74. // histNotice sends the client a notice from HistServ
  75. func histNotice(rb *ResponseBuffer, text string) {
  76. rb.Add(nil, histServMask, "NOTICE", rb.target.Nick(), text)
  77. }
  78. func histservForgetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
  79. accountName := server.accounts.AccountToAccountName(params[0])
  80. if accountName == "" {
  81. histNotice(rb, client.t("Could not look up account name, proceeding anyway"))
  82. accountName = params[0]
  83. }
  84. server.ForgetHistory(accountName)
  85. histNotice(rb, fmt.Sprintf(client.t("Enqueued account %s for message deletion"), accountName))
  86. }
  87. func histservDeleteHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
  88. var target, msgid string
  89. if len(params) == 1 {
  90. msgid = params[0]
  91. } else {
  92. target, msgid = params[0], params[1]
  93. }
  94. accountName := "*"
  95. hasPrivs := client.HasRoleCapabs("history")
  96. if !hasPrivs {
  97. accountName = client.AccountName()
  98. if !(server.Config().History.Retention.AllowIndividualDelete && accountName != "*") {
  99. histNotice(rb, client.t("Insufficient privileges"))
  100. return
  101. }
  102. }
  103. err := server.DeleteMessage(target, msgid, accountName)
  104. if err == nil {
  105. histNotice(rb, client.t("Successfully deleted message"))
  106. } else {
  107. if hasPrivs {
  108. histNotice(rb, fmt.Sprintf(client.t("Error deleting message: %v"), err))
  109. } else {
  110. histNotice(rb, client.t("Could not delete message"))
  111. }
  112. }
  113. }
  114. func histservExportHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
  115. cfAccount, err := CasefoldName(params[0])
  116. if err != nil {
  117. histNotice(rb, client.t("Invalid account name"))
  118. return
  119. }
  120. config := server.Config()
  121. // don't include the account name in the filename because of escaping concerns
  122. filename := fmt.Sprintf("%s-%s.json", utils.GenerateSecretToken(), time.Now().UTC().Format(IRCv3TimestampFormat))
  123. pathname := config.getOutputPath(filename)
  124. outfile, err := os.Create(pathname)
  125. if err != nil {
  126. histNotice(rb, fmt.Sprintf(client.t("Error opening export file: %v"), err))
  127. } else {
  128. histNotice(rb, fmt.Sprintf(client.t("Started exporting data for account %[1]s to file %[2]s"), cfAccount, filename))
  129. }
  130. go histservExportAndNotify(server, cfAccount, outfile, filename, client.Nick())
  131. }
  132. func histservExportAndNotify(server *Server, cfAccount string, outfile *os.File, filename, alertNick string) {
  133. defer func() {
  134. if r := recover(); r != nil {
  135. server.logger.Error("history",
  136. fmt.Sprintf("Panic in history export routine: %v\n%s", r, debug.Stack()))
  137. }
  138. }()
  139. defer outfile.Close()
  140. writer := bufio.NewWriter(outfile)
  141. defer writer.Flush()
  142. server.historyDB.Export(cfAccount, writer)
  143. client := server.clients.Get(alertNick)
  144. if client != nil && client.HasRoleCapabs("history") {
  145. client.Send(nil, histServMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("Data export for %[1]s completed and written to %[2]s"), cfAccount, filename))
  146. }
  147. }
  148. func histservPlayHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
  149. items, _, err := easySelectHistory(server, client, params)
  150. if err != nil {
  151. histNotice(rb, client.t("Could not retrieve history"))
  152. return
  153. }
  154. playMessage := func(timestamp time.Time, nick, message string) {
  155. histNotice(rb, fmt.Sprintf("%s <%s> %s", timestamp.Format("15:04:05"), stripMaskFromNick(nick), message))
  156. }
  157. for _, item := range items {
  158. // TODO: support a few more of these, maybe JOIN/PART/QUIT
  159. if item.Type != history.Privmsg && item.Type != history.Notice {
  160. continue
  161. }
  162. if len(item.Message.Split) == 0 {
  163. playMessage(item.Message.Time, item.Nick, item.Message.Message)
  164. } else {
  165. for _, pair := range item.Message.Split {
  166. playMessage(item.Message.Time, item.Nick, pair.Message)
  167. }
  168. }
  169. }
  170. histNotice(rb, client.t("End of history playback"))
  171. }
  172. // handles parameter parsing and history queries for /HISTORY and /HISTSERV PLAY
  173. func easySelectHistory(server *Server, client *Client, params []string) (items []history.Item, channel *Channel, err error) {
  174. target := params[0]
  175. if strings.ToLower(target) == "me" {
  176. target = "*"
  177. }
  178. channel, sequence, err := server.GetHistorySequence(nil, client, target)
  179. if sequence == nil || err != nil {
  180. return nil, nil, errNoSuchChannel
  181. }
  182. var duration time.Duration
  183. maxChathistoryLimit := server.Config().History.ChathistoryMax
  184. limit := 100
  185. if maxChathistoryLimit < limit {
  186. limit = maxChathistoryLimit
  187. }
  188. if len(params) > 1 {
  189. providedLimit, err := strconv.Atoi(params[1])
  190. if err == nil && providedLimit != 0 {
  191. limit = providedLimit
  192. if maxChathistoryLimit < limit {
  193. limit = maxChathistoryLimit
  194. }
  195. } else if err != nil {
  196. duration, err = time.ParseDuration(params[1])
  197. if err == nil {
  198. limit = maxChathistoryLimit
  199. }
  200. }
  201. }
  202. if duration == 0 {
  203. items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
  204. } else {
  205. now := time.Now().UTC()
  206. start := history.Selector{Time: now}
  207. end := history.Selector{Time: now.Add(-duration)}
  208. items, _, err = sequence.Between(start, end, limit)
  209. }
  210. return
  211. }