history.go 17 KB

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