discord.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/AvraamMavridis/randomcolor"
  9. "github.com/bwmarrin/discordgo"
  10. "github.com/fatih/color"
  11. "github.com/hako/durafmt"
  12. )
  13. const (
  14. discordEpoch = 1420070400000
  15. )
  16. //TODO: Clean these two
  17. func discordTimestampToSnowflake(format string, timestamp string) string {
  18. t, err := time.Parse(format, timestamp)
  19. if err == nil {
  20. return fmt.Sprint(((t.Local().UnixNano() / int64(time.Millisecond)) - discordEpoch) << 22)
  21. }
  22. log.Println(color.HiRedString("Failed to convert timestamp to discord snowflake... Format: '%s', Timestamp: '%s' - Error:\t%s",
  23. format, timestamp, err),
  24. )
  25. return ""
  26. }
  27. func discordSnowflakeToTimestamp(snowflake string, format string) string {
  28. i, err := strconv.ParseInt(snowflake, 10, 64)
  29. if err != nil {
  30. return ""
  31. }
  32. t := time.Unix(0, ((i>>22)+discordEpoch)*1000000)
  33. return t.Local().Format(format)
  34. }
  35. func getAllChannels() []string {
  36. var channels []string
  37. if config.All != nil { // ALL MODE
  38. for _, guild := range bot.State.Guilds {
  39. for _, channel := range guild.Channels {
  40. if hasPerms(channel.ID, discordgo.PermissionReadMessages) && hasPerms(channel.ID, discordgo.PermissionReadMessageHistory) {
  41. channels = append(channels, channel.ID)
  42. }
  43. }
  44. }
  45. } else { // STANDARD MODE
  46. // Compile all config channels
  47. for _, channel := range config.Channels {
  48. if channel.ChannelIDs != nil {
  49. for _, subchannel := range *channel.ChannelIDs {
  50. channels = append(channels, subchannel)
  51. }
  52. } else if isNumeric(channel.ChannelID) {
  53. channels = append(channels, channel.ChannelID)
  54. }
  55. }
  56. // Compile all channels sourced from config servers
  57. for _, server := range config.Servers {
  58. if server.ServerIDs != nil {
  59. for _, subserver := range *server.ServerIDs {
  60. guild, err := bot.State.Guild(subserver)
  61. if err == nil {
  62. for _, channel := range guild.Channels {
  63. if hasPerms(channel.ID, discordgo.PermissionReadMessageHistory) {
  64. channels = append(channels, channel.ID)
  65. }
  66. }
  67. }
  68. }
  69. } else if isNumeric(server.ServerID) {
  70. guild, err := bot.State.Guild(server.ServerID)
  71. if err == nil {
  72. for _, channel := range guild.Channels {
  73. if hasPerms(channel.ID, discordgo.PermissionReadMessageHistory) {
  74. channels = append(channels, channel.ID)
  75. }
  76. }
  77. }
  78. }
  79. }
  80. }
  81. return channels
  82. }
  83. //#region Presence
  84. func presenceKeyReplacement(input string) string {
  85. //TODO: Case-insensitive key replacement. -- If no streamlined way to do it, convert to lower to find substring location but replace normally
  86. if strings.Contains(input, "{{") && strings.Contains(input, "}}") {
  87. countInt := int64(dbDownloadCount()) + *config.InflateCount
  88. timeNow := time.Now()
  89. keys := [][]string{
  90. {"{{dgVersion}}", discordgo.VERSION},
  91. {"{{ddgVersion}}", projectVersion},
  92. {"{{apiVersion}}", discordgo.APIVersion},
  93. {"{{countNoCommas}}", fmt.Sprint(countInt)},
  94. {"{{count}}", formatNumber(countInt)},
  95. {"{{countShort}}", formatNumberShort(countInt)},
  96. {"{{numServers}}", fmt.Sprint(len(bot.State.Guilds))},
  97. {"{{numBoundChannels}}", fmt.Sprint(getBoundChannelsCount())},
  98. {"{{numBoundServers}}", fmt.Sprint(getBoundServersCount())},
  99. {"{{numAdminChannels}}", fmt.Sprint(len(config.AdminChannels))},
  100. {"{{numAdmins}}", fmt.Sprint(len(config.Admins))},
  101. {"{{timeSavedShort}}", timeLastUpdated.Format("3:04pm")},
  102. {"{{timeSavedShortTZ}}", timeLastUpdated.Format("3:04pm MST")},
  103. {"{{timeSavedMid}}", timeLastUpdated.Format("3:04pm MST 1/2/2006")},
  104. {"{{timeSavedLong}}", timeLastUpdated.Format("3:04:05pm MST - January 2, 2006")},
  105. {"{{timeSavedShort24}}", timeLastUpdated.Format("15:04")},
  106. {"{{timeSavedShortTZ24}}", timeLastUpdated.Format("15:04 MST")},
  107. {"{{timeSavedMid24}}", timeLastUpdated.Format("15:04 MST 2/1/2006")},
  108. {"{{timeSavedLong24}}", timeLastUpdated.Format("15:04:05 MST - 2 January, 2006")},
  109. {"{{timeNowShort}}", timeNow.Format("3:04pm")},
  110. {"{{timeNowShortTZ}}", timeNow.Format("3:04pm MST")},
  111. {"{{timeNowMid}}", timeNow.Format("3:04pm MST 1/2/2006")},
  112. {"{{timeNowLong}}", timeNow.Format("3:04:05pm MST - January 2, 2006")},
  113. {"{{timeNowShort24}}", timeNow.Format("15:04")},
  114. {"{{timeNowShortTZ24}}", timeNow.Format("15:04 MST")},
  115. {"{{timeNowMid24}}", timeNow.Format("15:04 MST 2/1/2006")},
  116. {"{{timeNowLong24}}", timeNow.Format("15:04:05 MST - 2 January, 2006")},
  117. {"{{uptime}}", durafmt.ParseShort(time.Since(startTime)).String()},
  118. }
  119. for _, key := range keys {
  120. if strings.Contains(input, key[0]) {
  121. input = strings.ReplaceAll(input, key[0], key[1])
  122. }
  123. }
  124. }
  125. return input
  126. }
  127. func updateDiscordPresence() {
  128. if config.PresenceEnabled {
  129. // Vars
  130. countInt := int64(dbDownloadCount()) + *config.InflateCount
  131. count := formatNumber(countInt)
  132. countShort := formatNumberShort(countInt)
  133. timeShort := timeLastUpdated.Format("3:04pm")
  134. timeLong := timeLastUpdated.Format("3:04:05pm MST - January 2, 2006")
  135. // Defaults
  136. status := fmt.Sprintf("%s - %s files", timeShort, countShort)
  137. statusDetails := timeLong
  138. statusState := fmt.Sprintf("%s files total", count)
  139. // Overwrite Presence
  140. if config.PresenceOverwrite != nil {
  141. status = *config.PresenceOverwrite
  142. if status != "" {
  143. status = presenceKeyReplacement(status)
  144. }
  145. }
  146. // Overwrite Details
  147. if config.PresenceOverwriteDetails != nil {
  148. statusDetails = *config.PresenceOverwriteDetails
  149. if statusDetails != "" {
  150. statusDetails = presenceKeyReplacement(statusDetails)
  151. }
  152. }
  153. // Overwrite State
  154. if config.PresenceOverwriteState != nil {
  155. statusState = *config.PresenceOverwriteState
  156. if statusState != "" {
  157. statusState = presenceKeyReplacement(statusState)
  158. }
  159. }
  160. // Update
  161. bot.UpdateStatusComplex(discordgo.UpdateStatusData{
  162. Game: &discordgo.Game{
  163. Name: status,
  164. Type: config.PresenceType,
  165. Details: statusDetails, // Only visible if real user
  166. State: statusState, // Only visible if real user
  167. },
  168. Status: config.PresenceStatus,
  169. })
  170. } else if config.PresenceStatus != string(discordgo.StatusOnline) {
  171. bot.UpdateStatusComplex(discordgo.UpdateStatusData{
  172. Status: config.PresenceStatus,
  173. })
  174. }
  175. }
  176. //#endregion
  177. //#region Embeds
  178. func getEmbedColor(channelID string) int {
  179. var err error
  180. var color *string
  181. var channelInfo *discordgo.Channel
  182. // Assign Defined Color
  183. if config.EmbedColor != nil {
  184. if *config.EmbedColor != "" {
  185. color = config.EmbedColor
  186. }
  187. }
  188. // Overwrite with Defined Color for Channel
  189. if isChannelRegistered(channelID) {
  190. channelConfig := getChannelConfig(channelID)
  191. if channelConfig.OverwriteEmbedColor != nil {
  192. if *channelConfig.OverwriteEmbedColor != "" {
  193. color = channelConfig.OverwriteEmbedColor
  194. }
  195. }
  196. }
  197. // Use Defined Color
  198. if color != nil {
  199. // Defined as Role, fetch role color
  200. if *color == "role" || *color == "user" {
  201. botColor := bot.State.UserColor(user.ID, channelID)
  202. if botColor != 0 {
  203. return botColor
  204. }
  205. goto color_random
  206. }
  207. // Defined as Random, jump below (not preferred method but seems to work flawlessly)
  208. if *color == "random" || *color == "rand" {
  209. goto color_random
  210. }
  211. var colorString string = *color
  212. // Input is Hex
  213. colorString = strings.ReplaceAll(colorString, "#", "")
  214. if convertedHex, err := strconv.ParseUint(colorString, 16, 64); err == nil {
  215. return int(convertedHex)
  216. }
  217. // Input is Int
  218. if convertedInt, err := strconv.Atoi(colorString); err == nil {
  219. return convertedInt
  220. }
  221. // Definition is invalid since hasn't returned, so defaults to below...
  222. }
  223. // User color
  224. channelInfo, err = bot.State.Channel(channelID)
  225. if err == nil {
  226. if channelInfo.Type != discordgo.ChannelTypeDM && channelInfo.Type != discordgo.ChannelTypeGroupDM {
  227. if bot.State.UserColor(user.ID, channelID) != 0 {
  228. return bot.State.UserColor(user.ID, channelID)
  229. }
  230. }
  231. }
  232. // Random color
  233. color_random:
  234. var randomColor string = randomcolor.GetRandomColorInHex()
  235. if convertedRandom, err := strconv.ParseUint(strings.ReplaceAll(randomColor, "#", ""), 16, 64); err == nil {
  236. return int(convertedRandom)
  237. }
  238. return 16777215 // white
  239. }
  240. // Shortcut function for quickly constructing a styled embed with Title & Description
  241. func buildEmbed(channelID string, title string, description string) *discordgo.MessageEmbed {
  242. return &discordgo.MessageEmbed{
  243. Title: title,
  244. Description: description,
  245. Color: getEmbedColor(channelID),
  246. Footer: &discordgo.MessageEmbedFooter{
  247. IconURL: projectIcon,
  248. Text: fmt.Sprintf("%s v%s", projectName, projectVersion),
  249. },
  250. }
  251. }
  252. // Shortcut function for quickly replying a styled embed with Title & Description
  253. func replyEmbed(m *discordgo.Message, title string, description string) (*discordgo.Message, error) {
  254. if m != nil {
  255. if hasPerms(m.ChannelID, discordgo.PermissionSendMessages) {
  256. return bot.ChannelMessageSendComplex(m.ChannelID,
  257. &discordgo.MessageSend{
  258. Content: m.Author.Mention(),
  259. Embed: buildEmbed(m.ChannelID, title, description),
  260. },
  261. )
  262. }
  263. log.Println(color.HiRedString(fmtBotSendPerm, m.ChannelID))
  264. }
  265. return nil, nil
  266. }
  267. type logStatusType int
  268. const (
  269. logStatusStartup logStatusType = iota
  270. logStatusReconnect
  271. logStatusExit
  272. )
  273. func logStatusLabel(status logStatusType) string {
  274. switch status {
  275. case logStatusStartup:
  276. return "has launched"
  277. case logStatusReconnect:
  278. return "has reconnected"
  279. case logStatusExit:
  280. return "is exiting"
  281. }
  282. return "<<ERROR>>"
  283. }
  284. func logStatusMessage(status logStatusType) {
  285. for _, adminChannel := range config.AdminChannels {
  286. if *adminChannel.LogStatus {
  287. var message string
  288. var label string
  289. if status == logStatusStartup || status == logStatusReconnect {
  290. label = "startup"
  291. message += fmt.Sprintf("%s %s and connected to %d server%s...\n", projectLabel, logStatusLabel(status), len(bot.State.Guilds), pluralS(len(bot.State.Guilds)))
  292. message += fmt.Sprintf("\n• Uptime is %s", uptime())
  293. message += fmt.Sprintf("\n• %s total downloads", formatNumber(int64(dbDownloadCount())))
  294. message += fmt.Sprintf("\n• Bound to %d channel%s and %d server%s", getBoundChannelsCount(), pluralS(getBoundChannelsCount()), getBoundServersCount(), pluralS(getBoundServersCount()))
  295. if config.All != nil {
  296. message += "\n• **ALL MODE ENABLED -** Bot will use all available channels"
  297. }
  298. allChannels := getAllChannels()
  299. message += fmt.Sprintf("\n• ***Listening to %s channel%s...***\n", formatNumber(int64(len(allChannels))), pluralS(len(allChannels)))
  300. if twitterConnected {
  301. message += "\n• Connected to Twitter API"
  302. }
  303. if googleDriveConnected {
  304. message += "\n• Connected to Google Drive"
  305. }
  306. } else if status == logStatusExit {
  307. label = "exit"
  308. message += fmt.Sprintf("%s %s...\n", projectLabel, logStatusLabel(status))
  309. message += fmt.Sprintf("\n• Uptime was %s", uptime())
  310. message += fmt.Sprintf("\n• %s total downloads", formatNumber(int64(dbDownloadCount())))
  311. message += fmt.Sprintf("\n• Bound to %d channel%s and %d server%s", getBoundChannelsCount(), pluralS(getBoundChannelsCount()), getBoundServersCount(), pluralS(getBoundServersCount()))
  312. }
  313. // Send
  314. if config.DebugOutput {
  315. log.Println(logPrefixDebug, color.HiCyanString("Sending log for %s to admin channel %s", label, adminChannel.ChannelID))
  316. }
  317. if hasPerms(adminChannel.ChannelID, discordgo.PermissionEmbedLinks) {
  318. bot.ChannelMessageSendEmbed(adminChannel.ChannelID, buildEmbed(adminChannel.ChannelID, "Log — Status", message))
  319. } else if hasPerms(adminChannel.ChannelID, discordgo.PermissionSendMessages) {
  320. bot.ChannelMessageSend(adminChannel.ChannelID, message)
  321. } else {
  322. log.Println(logPrefixDebug, color.HiRedString("Perms checks failed for sending status log to %s", adminChannel.ChannelID))
  323. }
  324. }
  325. }
  326. }
  327. func logErrorMessage(err string) {
  328. for _, adminChannel := range config.AdminChannels {
  329. if *adminChannel.LogErrors {
  330. // Send
  331. if hasPerms(adminChannel.ChannelID, discordgo.PermissionEmbedLinks) { // not confident this is the right permission
  332. if config.DebugOutput {
  333. log.Println(logPrefixDebug, color.HiCyanString("Sending embed log for error to %s", adminChannel.ChannelID))
  334. }
  335. bot.ChannelMessageSendEmbed(adminChannel.ChannelID, buildEmbed(adminChannel.ChannelID, "Log — Error", err))
  336. } else if hasPerms(adminChannel.ChannelID, discordgo.PermissionSendMessages) {
  337. if config.DebugOutput {
  338. log.Println(logPrefixDebug, color.HiCyanString("Sending message log for error to %s", adminChannel.ChannelID))
  339. }
  340. bot.ChannelMessageSend(adminChannel.ChannelID, err)
  341. } else {
  342. log.Println(logPrefixDebug, color.HiRedString("Perms checks failed for sending error log to %s", adminChannel.ChannelID))
  343. }
  344. }
  345. }
  346. }
  347. //#endregion
  348. //#region Permissions
  349. // Checks if message author is a specified bot admin.
  350. func isBotAdmin(m *discordgo.Message) bool {
  351. // No Admins or Admin Channels
  352. if len(config.Admins) == 0 && len(config.AdminChannels) == 0 {
  353. return true
  354. }
  355. // configurationAdminChannel.UnlockCommands Bypass
  356. if isAdminChannelRegistered(m.ChannelID) {
  357. channelConfig := getAdminChannelConfig(m.ChannelID)
  358. if *channelConfig.UnlockCommands == true {
  359. return true
  360. }
  361. }
  362. return m.Author.ID == user.ID || stringInSlice(m.Author.ID, config.Admins)
  363. }
  364. // Checks if message author is a specified bot admin OR is server admin OR has message management perms in channel
  365. func isLocalAdmin(m *discordgo.Message) bool {
  366. if m == nil {
  367. if config.DebugOutput {
  368. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to empty message"))
  369. }
  370. return true
  371. }
  372. sourceChannel, err := bot.State.Channel(m.ChannelID)
  373. if err != nil || sourceChannel == nil {
  374. if config.DebugOutput {
  375. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to an error or received empty channel info for message:\t%s", err))
  376. }
  377. return true
  378. } else if sourceChannel.Name == "" || sourceChannel.GuildID == "" {
  379. if config.DebugOutput {
  380. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to incomplete channel info"))
  381. }
  382. return true
  383. }
  384. guild, _ := bot.State.Guild(m.GuildID)
  385. localPerms, err := bot.State.UserChannelPermissions(m.Author.ID, m.ChannelID)
  386. if err != nil {
  387. if config.DebugOutput {
  388. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to error when checking permissions:\t%s", err))
  389. }
  390. return true
  391. }
  392. botSelf := m.Author.ID == user.ID
  393. botAdmin := stringInSlice(m.Author.ID, config.Admins)
  394. guildOwner := m.Author.ID == guild.OwnerID
  395. guildAdmin := localPerms&discordgo.PermissionAdministrator > 0
  396. localManageMessages := localPerms&discordgo.PermissionManageMessages > 0
  397. return botSelf || botAdmin || guildOwner || guildAdmin || localManageMessages
  398. }
  399. func hasPerms(channelID string, permission int) bool {
  400. if !config.CheckPermissions {
  401. return true
  402. }
  403. sourceChannel, err := bot.State.Channel(channelID)
  404. if sourceChannel != nil && err == nil {
  405. switch sourceChannel.Type {
  406. case discordgo.ChannelTypeDM:
  407. return true
  408. case discordgo.ChannelTypeGroupDM:
  409. return true
  410. case discordgo.ChannelTypeGuildText:
  411. perms, err := bot.UserChannelPermissions(user.ID, channelID)
  412. if err == nil {
  413. return perms&permission == permission
  414. }
  415. log.Println(color.HiRedString("Failed to check permissions (%d) for %s:\t%s", permission, channelID, err))
  416. }
  417. }
  418. return false
  419. }
  420. //#endregion
  421. //#region Labeling
  422. func getUserIdentifier(usr discordgo.User) string {
  423. return fmt.Sprintf("\"%s\"#%s", usr.Username, usr.Discriminator)
  424. }
  425. //TODO: Clean below
  426. func getChannelState(channelID string) *discordgo.Channel {
  427. sourceChannel, _ := bot.State.Channel(channelID)
  428. if sourceChannel != nil {
  429. return sourceChannel
  430. }
  431. return &discordgo.Channel{}
  432. }
  433. func getGuildState(guildID string) *discordgo.Guild {
  434. sourceGuild, _ := bot.State.Guild(guildID)
  435. if sourceGuild != nil {
  436. return sourceGuild
  437. }
  438. return &discordgo.Guild{}
  439. }
  440. func getChannelGuildID(channelID string) string {
  441. sourceChannel, _ := bot.State.Channel(channelID)
  442. if sourceChannel != nil {
  443. return sourceChannel.GuildID
  444. }
  445. return ""
  446. }
  447. func getGuildName(guildID string) string {
  448. sourceGuildName := "UNKNOWN"
  449. sourceGuild, _ := bot.State.Guild(guildID)
  450. if sourceGuild != nil && sourceGuild.Name != "" {
  451. sourceGuildName = sourceGuild.Name
  452. }
  453. return sourceGuildName
  454. }
  455. func getChannelName(channelID string) string {
  456. sourceChannelName := "unknown"
  457. sourceChannel, _ := bot.State.Channel(channelID)
  458. if sourceChannel != nil {
  459. if sourceChannel.Name != "" {
  460. sourceChannelName = sourceChannel.Name
  461. } else {
  462. switch sourceChannel.Type {
  463. case discordgo.ChannelTypeDM:
  464. sourceChannelName = "dm"
  465. case discordgo.ChannelTypeGroupDM:
  466. sourceChannelName = "group-dm"
  467. }
  468. }
  469. }
  470. return sourceChannelName
  471. }
  472. func getSourceName(guildID string, channelID string) string {
  473. guildName := getGuildName(guildID)
  474. channelName := getChannelName(channelID)
  475. if channelName == "dm" || channelName == "group-dm" {
  476. return channelName
  477. }
  478. return fmt.Sprintf("\"%s\"#%s", guildName, channelName)
  479. }
  480. //#endregion
  481. // For command case-insensitivity
  482. func messageToLower(message *discordgo.Message) *discordgo.Message {
  483. newMessage := *message
  484. newMessage.Content = strings.ToLower(newMessage.Content)
  485. return &newMessage
  486. }
  487. func fixMessage(m *discordgo.Message) *discordgo.Message {
  488. // If message content is empty (likely due to userbot/selfbot)
  489. ubIssue := "Message is corrupted due to endpoint restriction"
  490. if m.Content == "" && len(m.Attachments) == 0 && len(m.Embeds) == 0 {
  491. // Get message history
  492. mCache, err := bot.ChannelMessages(m.ChannelID, 20, "", "", "")
  493. if err == nil {
  494. if len(mCache) > 0 {
  495. for _, mCached := range mCache {
  496. if mCached.ID == m.ID {
  497. // Fix original message having empty Guild ID
  498. guildID := m.GuildID
  499. // Replace message
  500. m = mCached
  501. // ^^
  502. if m.GuildID == "" && guildID != "" {
  503. m.GuildID = guildID
  504. }
  505. // Parse commands
  506. dgr.FindAndExecute(bot, strings.ToLower(config.CommandPrefix), bot.State.User.ID, messageToLower(m))
  507. break
  508. }
  509. }
  510. } else if config.DebugOutput {
  511. log.Println(logPrefixDebug, color.RedString("%s, and an attempt to get channel messages found nothing...", ubIssue))
  512. }
  513. } else if config.DebugOutput {
  514. log.Println(logPrefixDebug, color.HiRedString("%s, and an attempt to get channel messages encountered an error:\t%s", ubIssue, err))
  515. }
  516. }
  517. if m.Content == "" && len(m.Attachments) == 0 && len(m.Embeds) == 0 {
  518. if config.DebugOutput {
  519. log.Println(logPrefixDebug, color.YellowString("%s, and attempts to fix seem to have failed...", ubIssue))
  520. }
  521. }
  522. return m
  523. }