history.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "log"
  6. "os"
  7. "strconv"
  8. "time"
  9. "github.com/bwmarrin/discordgo"
  10. "github.com/dustin/go-humanize"
  11. "github.com/fatih/color"
  12. "github.com/hako/durafmt"
  13. )
  14. type historyStatus int
  15. const (
  16. historyStatusWaiting historyStatus = iota
  17. historyStatusDownloading
  18. historyStatusAbortRequested
  19. historyStatusAbortCompleted
  20. historyStatusErrorReadMessageHistoryPerms
  21. historyStatusErrorRequesting
  22. historyStatusCompletedNoMoreMessages
  23. historyStatusCompletedToBeforeFilter
  24. historyStatusCompletedToSinceFilter
  25. )
  26. func historyStatusLabel(status historyStatus) string {
  27. switch status {
  28. case historyStatusWaiting:
  29. return "Waiting..."
  30. case historyStatusDownloading:
  31. return "Currently Downloading..."
  32. case historyStatusAbortRequested:
  33. return "Abort Requested..."
  34. case historyStatusAbortCompleted:
  35. return "Aborted..."
  36. case historyStatusErrorReadMessageHistoryPerms:
  37. return "ERROR: Cannot Read Message History"
  38. case historyStatusErrorRequesting:
  39. return "ERROR: Message Requests Failed"
  40. case historyStatusCompletedNoMoreMessages:
  41. return "COMPLETE: No More Messages"
  42. case historyStatusCompletedToBeforeFilter:
  43. return "COMPLETE: Exceeded Before Date Filter"
  44. case historyStatusCompletedToSinceFilter:
  45. return "COMPLETE: Exceeded Since Date Filter"
  46. default:
  47. return "Unknown"
  48. }
  49. }
  50. type historyJob struct {
  51. Status historyStatus
  52. OriginUser string
  53. OriginChannel string
  54. TargetCommandingMessage *discordgo.Message
  55. TargetChannelID string
  56. TargetBefore string
  57. TargetSince string
  58. DownloadCount int64
  59. DownloadSize int64
  60. Updated time.Time
  61. Added time.Time
  62. }
  63. var (
  64. historyJobs map[string]historyJob
  65. historyProcessing bool
  66. )
  67. func handleHistory(commandingMessage *discordgo.Message, subjectChannelID string, before string, since string) int {
  68. historyProcessing = true
  69. defer func() { historyProcessing = false }()
  70. if job, exists := historyJobs[subjectChannelID]; exists && job.Status != historyStatusWaiting {
  71. log.Println(lg("History", "", color.RedString, "History job skipped, Status: %s", historyStatusLabel(job.Status)))
  72. return -1
  73. }
  74. var err error
  75. var totalMessages int64 = 0
  76. var totalDownloads int64 = 0
  77. var totalFilesize int64 = 0
  78. var messageRequestCount int = 0
  79. var responseMsg *discordgo.Message = &discordgo.Message{}
  80. responseMsg.ID = ""
  81. responseMsg.ChannelID = subjectChannelID
  82. responseMsg.GuildID = ""
  83. var commander string = "AUTORUN"
  84. var autorun bool = true
  85. if commandingMessage != nil { // Only time commandingMessage is nil is Autorun
  86. commander = getUserIdentifier(*commandingMessage.Author)
  87. autorun = false
  88. }
  89. logPrefix := fmt.Sprintf("%s/%s: ", subjectChannelID, commander)
  90. // Send Status?
  91. var sendStatus bool = true
  92. if (autorun && !config.SendAutorunHistoryStatus) || (!autorun && !config.SendHistoryStatus) {
  93. sendStatus = false
  94. }
  95. // Check Read History perms
  96. if !hasPerms(subjectChannelID, discordgo.PermissionReadMessageHistory) {
  97. if job, exists := historyJobs[subjectChannelID]; exists {
  98. job.Status = historyStatusDownloading
  99. job.Updated = time.Now()
  100. historyJobs[subjectChannelID] = job
  101. }
  102. log.Println(lg("History", "", color.HiRedString, logPrefix+"BOT DOES NOT HAVE PERMISSION TO READ MESSAGE HISTORY!!!"))
  103. return -1
  104. }
  105. hasPermsToRespond := hasPerms(subjectChannelID, discordgo.PermissionSendMessages)
  106. if !autorun {
  107. hasPermsToRespond = hasPerms(commandingMessage.ChannelID, discordgo.PermissionSendMessages)
  108. }
  109. // Update Job Status to Downloading
  110. if job, exists := historyJobs[subjectChannelID]; exists {
  111. job.Status = historyStatusDownloading
  112. job.Updated = time.Now()
  113. historyJobs[subjectChannelID] = job
  114. }
  115. //#region Cache Files
  116. openHistoryCache := func(dirpath string, output *string) {
  117. if f, err := ioutil.ReadFile(dirpath + string(os.PathSeparator) + subjectChannelID); err == nil {
  118. *output = string(f)
  119. if !autorun && config.DebugOutput {
  120. log.Println(lg("Debug", "History", color.YellowString,
  121. logPrefix+"Found a cache file, picking up where we left off before %s...", string(f)))
  122. }
  123. }
  124. }
  125. writeHistoryCache := func(dirpath string, ID string) {
  126. if err := os.MkdirAll(dirpath, 0755); err != nil {
  127. log.Println(lg("Debug", "History", color.HiRedString,
  128. logPrefix+"Error while creating history cache folder \"%s\": %s", dirpath, err))
  129. }
  130. f, err := os.OpenFile(dirpath+string(os.PathSeparator)+subjectChannelID, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
  131. if err != nil {
  132. log.Println(lg("Debug", "History", color.RedString,
  133. logPrefix+"Failed to open cache file:\t%s", err))
  134. }
  135. if _, err = f.WriteString(ID); err != nil {
  136. log.Println(lg("Debug", "History", color.RedString,
  137. logPrefix+"Failed to write cache file:\t%s", err))
  138. } else if !autorun && config.DebugOutput {
  139. log.Println(lg("Debug", "History", color.YellowString,
  140. logPrefix+"Wrote to cache file."))
  141. }
  142. f.Close()
  143. }
  144. deleteHistoryCache := func(dirpath string) {
  145. fp := dirpath + string(os.PathSeparator) + subjectChannelID
  146. if _, err := os.Stat(fp); err == nil {
  147. err = os.Remove(fp)
  148. if err != nil {
  149. log.Println(lg("Debug", "History", color.HiRedString,
  150. logPrefix+"Encountered error deleting cache file:\t%s", err))
  151. } else if commandingMessage != nil && config.DebugOutput {
  152. log.Println(lg("Debug", "History", color.HiRedString,
  153. logPrefix+"Deleted cache file."))
  154. }
  155. }
  156. }
  157. //#endregion
  158. // Date Range Vars
  159. var sinceID = since
  160. var beforeID = before
  161. var beforeTime time.Time
  162. //#region Date Range Output
  163. rangeContent := ""
  164. if sinceID != "" {
  165. if isDate(sinceID) {
  166. sinceID = discordSnowflakeToTimestamp(sinceID, "2006-01-02")
  167. }
  168. if isNumeric(sinceID) {
  169. rangeContent += fmt.Sprintf("**Since:** `%s`\n", sinceID)
  170. }
  171. }
  172. if beforeID != "" {
  173. if isDate(beforeID) {
  174. beforeID = discordSnowflakeToTimestamp(beforeID, "2006-01-02")
  175. }
  176. if isNumeric(beforeID) {
  177. rangeContent += fmt.Sprintf("**Before:** `%s`\n", beforeID)
  178. }
  179. }
  180. if rangeContent != "" {
  181. rangeContent += "\n\n"
  182. }
  183. //#endregion
  184. if channelConfig := getSource(responseMsg); channelConfig != emptyConfig {
  185. // Overwrite Send Status
  186. if channelConfig.OverwriteSendAutorunHistoryStatus != nil {
  187. if autorun && !*channelConfig.OverwriteSendAutorunHistoryStatus {
  188. sendStatus = false
  189. }
  190. }
  191. if channelConfig.OverwriteSendHistoryStatus != nil {
  192. if !autorun && !*channelConfig.OverwriteSendHistoryStatus {
  193. sendStatus = false
  194. }
  195. }
  196. // Open Cache File?
  197. openHistoryCache(historyCacheBefore, &beforeID)
  198. openHistoryCache(historyCacheSince, &sinceID)
  199. historyStartTime := time.Now()
  200. guildName := getGuildName(getChannelGuildID(subjectChannelID))
  201. categoryName := getChannelCategoryName(subjectChannelID)
  202. channelName := getChannelName(subjectChannelID)
  203. sourceName := fmt.Sprintf("%s / %s", guildName, channelName)
  204. msgSourceDisplay := fmt.Sprintf("`Server:` **%s**\n`Channel:` #%s", guildName, channelName)
  205. if categoryName != "unknown" {
  206. sourceName = fmt.Sprintf("%s / %s / %s", guildName, categoryName, channelName)
  207. msgSourceDisplay = fmt.Sprintf("`Server:` **%s**\n`Category:` _%s_\n`Channel:` #%s",
  208. guildName, categoryName, channelName)
  209. }
  210. // Initial Status Message
  211. if sendStatus {
  212. if hasPermsToRespond {
  213. responseMsg, err = replyEmbed(commandingMessage, "Command — History", msgSourceDisplay)
  214. if err != nil {
  215. log.Println(lg("History", "", color.HiRedString,
  216. logPrefix+"Failed to send command embed message:\t%s", err))
  217. }
  218. } else {
  219. log.Println(lg("History", "", color.HiRedString,
  220. logPrefix+fmtBotSendPerm, commandingMessage.ChannelID))
  221. }
  222. }
  223. log.Println(lg("History", "", color.HiCyanString, logPrefix+"Began checking history for \"%s\"...", sourceName))
  224. lastMessageID := ""
  225. MessageRequestingLoop:
  226. for {
  227. // Next 100
  228. if beforeTime != (time.Time{}) {
  229. messageRequestCount++
  230. if beforeID != "" {
  231. writeHistoryCache(historyCacheBefore, beforeID)
  232. }
  233. if sinceID != "" {
  234. writeHistoryCache(historyCacheSince, sinceID)
  235. }
  236. // Update Status
  237. log.Println(lg("History", "", color.CyanString,
  238. logPrefix+"Requesting 100 more, %d downloaded (%s), %d processed — Before %s ago (%s)",
  239. totalDownloads, humanize.Bytes(uint64(totalFilesize)), totalMessages, durafmt.ParseShort(time.Since(beforeTime)).String(), beforeTime.String()[:10]))
  240. if sendStatus {
  241. status := fmt.Sprintf(
  242. "``%s:`` **%s files downloaded...** `(%s so far, avg %1.1f MB/s)`\n``"+
  243. "%s messages processed...``\n\n"+
  244. "%s\n\n"+
  245. "%s`(%d)` _Processing more messages, please wait..._",
  246. durafmt.ParseShort(time.Since(historyStartTime)).String(), formatNumber(totalDownloads),
  247. humanize.Bytes(uint64(totalFilesize)), float64(totalFilesize/humanize.MByte)/time.Since(historyStartTime).Seconds(),
  248. formatNumber(totalMessages),
  249. msgSourceDisplay, rangeContent, messageRequestCount)
  250. if responseMsg == nil {
  251. log.Println(lg("History", "", color.RedString,
  252. logPrefix+"Tried to edit status message but it doesn't exist, sending new one."))
  253. if responseMsg, err = replyEmbed(responseMsg, "Command — History", status); err != nil { // Failed to Edit Status, Send New Message
  254. log.Println(lg("History", "", color.HiRedString,
  255. logPrefix+"Failed to send replacement status message:\t%s", err))
  256. }
  257. } else {
  258. if !hasPermsToRespond {
  259. log.Println(lg("History", "", color.HiRedString,
  260. logPrefix+fmtBotSendPerm+" - %s", responseMsg.ChannelID, status))
  261. } else {
  262. // Edit Status
  263. if selfbot {
  264. responseMsg, err = bot.ChannelMessageEdit(responseMsg.ChannelID, responseMsg.ID,
  265. fmt.Sprintf("**Command — History**\n\n%s", status))
  266. } else {
  267. responseMsg, err = bot.ChannelMessageEditComplex(&discordgo.MessageEdit{
  268. ID: responseMsg.ID,
  269. Channel: responseMsg.ChannelID,
  270. Embed: buildEmbed(responseMsg.ChannelID, "Command — History", status),
  271. })
  272. }
  273. // Failed to Edit Status
  274. if err != nil {
  275. log.Println(lg("History", "", color.HiRedString,
  276. logPrefix+"Failed to edit status message, sending new one:\t%s", err))
  277. if responseMsg, err = replyEmbed(responseMsg, "Command — History", status); err != nil { // Failed to Edit Status, Send New Message
  278. log.Println(lg("History", "", color.HiRedString,
  279. logPrefix+"Failed to send replacement status message:\t%s", err))
  280. }
  281. }
  282. }
  283. }
  284. }
  285. // Update presence
  286. timeLastUpdated = time.Now()
  287. if *channelConfig.UpdatePresence {
  288. updateDiscordPresence()
  289. }
  290. }
  291. // Request More Messages
  292. msg_rq_cnt := 0
  293. request_messages:
  294. msg_rq_cnt++
  295. if messages, err := bot.ChannelMessages(subjectChannelID, 100, beforeID, sinceID, ""); err != nil {
  296. // Error requesting messages
  297. if sendStatus {
  298. if !hasPermsToRespond {
  299. log.Println(lg("History", "", color.HiRedString,
  300. logPrefix+fmtBotSendPerm, responseMsg.ChannelID))
  301. } else {
  302. _, err = replyEmbed(responseMsg, "Command — History",
  303. fmt.Sprintf("Encountered an error requesting messages for %s: %s", subjectChannelID, err.Error()))
  304. if err != nil {
  305. log.Println(lg("History", "", color.HiRedString,
  306. logPrefix+"Failed to send error message:\t%s", err))
  307. }
  308. }
  309. }
  310. log.Println(lg("History", "", color.HiRedString, logPrefix+"Error requesting messages:\t%s", err))
  311. if job, exists := historyJobs[subjectChannelID]; exists {
  312. job.Status = historyStatusErrorRequesting
  313. job.Updated = time.Now()
  314. historyJobs[subjectChannelID] = job
  315. }
  316. break MessageRequestingLoop
  317. } else {
  318. // No More Messages
  319. if len(messages) <= 0 {
  320. if msg_rq_cnt > 3 {
  321. if job, exists := historyJobs[subjectChannelID]; exists {
  322. job.Status = historyStatusCompletedNoMoreMessages
  323. job.Updated = time.Now()
  324. historyJobs[subjectChannelID] = job
  325. }
  326. break MessageRequestingLoop
  327. } else { // retry to make sure no more
  328. time.Sleep(10 * time.Millisecond)
  329. goto request_messages
  330. }
  331. }
  332. // Set New Range
  333. beforeID = messages[len(messages)-1].ID
  334. beforeTime = messages[len(messages)-1].Timestamp
  335. sinceID = ""
  336. // Process Messages
  337. if channelConfig.TypeWhileProcessing != nil && !autorun {
  338. if *channelConfig.TypeWhileProcessing && hasPermsToRespond {
  339. bot.ChannelTyping(commandingMessage.ChannelID)
  340. }
  341. }
  342. for _, message := range messages {
  343. // Ordered to Cancel
  344. if historyJobs[subjectChannelID].Status == historyStatusAbortRequested {
  345. if job, exists := historyJobs[subjectChannelID]; exists {
  346. job.Status = historyStatusAbortCompleted
  347. job.Updated = time.Now()
  348. historyJobs[subjectChannelID] = job
  349. }
  350. break MessageRequestingLoop
  351. }
  352. lastMessageID = message.ID
  353. // Check Message Range
  354. message64, _ := strconv.ParseInt(message.ID, 10, 64)
  355. if before != "" {
  356. before64, _ := strconv.ParseInt(before, 10, 64)
  357. if message64 > before64 {
  358. if job, exists := historyJobs[subjectChannelID]; exists {
  359. job.Status = historyStatusCompletedToBeforeFilter
  360. job.Updated = time.Now()
  361. historyJobs[subjectChannelID] = job
  362. }
  363. break MessageRequestingLoop
  364. }
  365. }
  366. if since != "" {
  367. since64, _ := strconv.ParseInt(since, 10, 64)
  368. if message64 < since64 {
  369. if job, exists := historyJobs[subjectChannelID]; exists {
  370. job.Status = historyStatusCompletedToSinceFilter
  371. job.Updated = time.Now()
  372. historyJobs[subjectChannelID] = job
  373. }
  374. break MessageRequestingLoop
  375. }
  376. }
  377. // Process Message
  378. downloadCount, filesize := handleMessage(message, false, true)
  379. if downloadCount > 0 {
  380. totalDownloads += downloadCount
  381. totalFilesize += filesize
  382. }
  383. totalMessages++
  384. }
  385. }
  386. }
  387. // Cache
  388. if historyJobs[subjectChannelID].Status == historyStatusCompletedNoMoreMessages {
  389. deleteHistoryCache(historyCacheBefore)
  390. writeHistoryCache(historyCacheSince, lastMessageID)
  391. }
  392. // Final log
  393. log.Println(lg("History", "", color.HiGreenString, logPrefix+"Finished history for \"%s\", %s files, %s total",
  394. sourceName, formatNumber(totalDownloads), humanize.Bytes(uint64(totalFilesize))))
  395. // Final status update
  396. if sendStatus {
  397. status := fmt.Sprintf(
  398. "``%s:`` **%s total files downloaded!** `%s total, avg %1.1f MB/s`\n"+
  399. "``%s total messages processed``\n\n"+
  400. "%s\n\n"+ // msgSourceDisplay^
  401. "**DONE!** - %s\n"+
  402. "Ran ``%d`` message history requests\n\n"+
  403. "%s_Duration was %s_",
  404. durafmt.ParseShort(time.Since(historyStartTime)).String(), formatNumber(int64(totalDownloads)),
  405. humanize.Bytes(uint64(totalFilesize)), float64(totalFilesize/humanize.MByte)/time.Since(historyStartTime).Seconds(),
  406. formatNumber(int64(totalMessages)),
  407. msgSourceDisplay,
  408. historyStatusLabel(historyJobs[subjectChannelID].Status),
  409. messageRequestCount,
  410. rangeContent, durafmt.Parse(time.Since(historyStartTime)).String(),
  411. )
  412. if !hasPermsToRespond {
  413. log.Println(lg("History", "", color.HiRedString, logPrefix+fmtBotSendPerm, responseMsg.ChannelID))
  414. } else {
  415. if responseMsg == nil {
  416. log.Println(lg("History", "", color.RedString,
  417. logPrefix+"Tried to edit status message but it doesn't exist, sending new one."))
  418. if _, err = replyEmbed(responseMsg, "Command — History", status); err != nil { // Failed to Edit Status, Send New Message
  419. log.Println(lg("History", "", color.HiRedString,
  420. logPrefix+"Failed to send replacement status message:\t%s", err))
  421. }
  422. } else {
  423. if selfbot {
  424. responseMsg, err = bot.ChannelMessageEdit(responseMsg.ChannelID, responseMsg.ID,
  425. fmt.Sprintf("**Command — History**\n\n%s", status))
  426. } else {
  427. responseMsg, err = bot.ChannelMessageEditComplex(&discordgo.MessageEdit{
  428. ID: responseMsg.ID,
  429. Channel: responseMsg.ChannelID,
  430. Embed: buildEmbed(responseMsg.ChannelID, "Command — History", status),
  431. })
  432. }
  433. // Edit failure
  434. if err != nil {
  435. log.Println(lg("History", "", color.RedString,
  436. logPrefix+"Failed to edit status message, sending new one:\t%s", err))
  437. if _, err = replyEmbed(responseMsg, "Command — History", status); err != nil {
  438. log.Println(lg("History", "", color.HiRedString,
  439. logPrefix+"Failed to send replacement status message:\t%s", err))
  440. }
  441. }
  442. }
  443. }
  444. }
  445. }
  446. return int(totalDownloads)
  447. }