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.

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