history.go 17 KB

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