discord.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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. DISCORD_EPOCH = 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)) - DISCORD_EPOCH) << 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)+DISCORD_EPOCH)*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 hasPerms(m.ChannelID, discordgo.PermissionSendMessages) {
  255. return bot.ChannelMessageSendComplex(m.ChannelID,
  256. &discordgo.MessageSend{
  257. Content: m.Author.Mention(),
  258. Embed: buildEmbed(m.ChannelID, title, description),
  259. },
  260. )
  261. } else {
  262. log.Println(color.HiRedString(fmtBotSendPerm, m.ChannelID))
  263. return nil, nil
  264. }
  265. }
  266. func logStatusMessage(status string) {
  267. for _, adminChannel := range config.AdminChannels {
  268. if *adminChannel.LogStatus {
  269. message := fmt.Sprintf("%s has %s and connected to %d server(s)...\n", projectLabel, status, len(bot.State.Guilds))
  270. message += fmt.Sprintf("\n• Uptime is %s", uptime())
  271. message += fmt.Sprintf("\n• %s total downloads", formatNumber(int64(dbDownloadCount())))
  272. message += fmt.Sprintf("\n• Bound to %d channel(s) and %d server(s)", getBoundChannelsCount(), getBoundServersCount())
  273. if config.All != nil {
  274. message += "\n• **ALL MODE ENABLED -** Bot will use all available channels"
  275. }
  276. message += fmt.Sprintf("\n• ***Listening to %s channel(s)...***\n", formatNumber(int64(len(getAllChannels()))))
  277. if twitterConnected {
  278. message += "\n• Connected to Twitter API"
  279. }
  280. if googleDriveConnected {
  281. message += "\n• Connected to Google Drive"
  282. }
  283. // Send
  284. if hasPerms(adminChannel.ChannelID, discordgo.PermissionEmbedLinks) { // not confident this is the right permission
  285. if config.DebugOutput {
  286. log.Println(logPrefixDebug, color.HiCyanString("Sending embed log for startup to %s", adminChannel.ChannelID))
  287. }
  288. bot.ChannelMessageSendEmbed(adminChannel.ChannelID, buildEmbed(adminChannel.ChannelID, "Log — Status", message))
  289. } else if hasPerms(adminChannel.ChannelID, discordgo.PermissionSendMessages) {
  290. if config.DebugOutput {
  291. log.Println(logPrefixDebug, color.HiCyanString("Sending message log for startup to %s", adminChannel.ChannelID))
  292. }
  293. bot.ChannelMessageSend(adminChannel.ChannelID, message)
  294. } else {
  295. log.Println(logPrefixDebug, color.HiRedString("Perms checks failed for sending status log to %s", adminChannel.ChannelID))
  296. }
  297. }
  298. }
  299. }
  300. func logErrorMessage(err string) {
  301. for _, adminChannel := range config.AdminChannels {
  302. if *adminChannel.LogErrors {
  303. message := fmt.Sprintf("***ERROR ENCOUNTERED:***\n%s", err)
  304. // Send
  305. if hasPerms(adminChannel.ChannelID, discordgo.PermissionEmbedLinks) { // not confident this is the right permission
  306. if config.DebugOutput {
  307. log.Println(logPrefixDebug, color.HiCyanString("Sending embed log for error to %s", adminChannel.ChannelID))
  308. }
  309. bot.ChannelMessageSendEmbed(adminChannel.ChannelID, buildEmbed(adminChannel.ChannelID, "Log — Status", message))
  310. } else if hasPerms(adminChannel.ChannelID, discordgo.PermissionSendMessages) {
  311. if config.DebugOutput {
  312. log.Println(logPrefixDebug, color.HiCyanString("Sending message log for error to %s", adminChannel.ChannelID))
  313. }
  314. bot.ChannelMessageSend(adminChannel.ChannelID, message)
  315. } else {
  316. log.Println(logPrefixDebug, color.HiRedString("Perms checks failed for sending error log to %s", adminChannel.ChannelID))
  317. }
  318. }
  319. }
  320. }
  321. //#endregion
  322. //#region Permissions
  323. // Checks if message author is a specified bot admin.
  324. func isBotAdmin(m *discordgo.Message) bool {
  325. // No Admins or Admin Channels
  326. if len(config.Admins) == 0 && len(config.AdminChannels) == 0 {
  327. return true
  328. }
  329. // configurationAdminChannel.UnlockCommands Bypass
  330. if isAdminChannelRegistered(m.ChannelID) {
  331. channelConfig := getAdminChannelConfig(m.ChannelID)
  332. if *channelConfig.UnlockCommands == true {
  333. return true
  334. }
  335. }
  336. return m.Author.ID == user.ID || stringInSlice(m.Author.ID, config.Admins)
  337. }
  338. // Checks if message author is a specified bot admin OR is server admin OR has message management perms in channel
  339. func isLocalAdmin(m *discordgo.Message) bool {
  340. if m == nil {
  341. if config.DebugOutput {
  342. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to empty message"))
  343. }
  344. return true
  345. }
  346. sourceChannel, err := bot.State.Channel(m.ChannelID)
  347. if err != nil || sourceChannel == nil {
  348. if config.DebugOutput {
  349. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to an error or received empty channel info for message:\t%s", err))
  350. }
  351. return true
  352. } else if sourceChannel.Name == "" || sourceChannel.GuildID == "" {
  353. if config.DebugOutput {
  354. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to incomplete channel info"))
  355. }
  356. return true
  357. }
  358. guild, _ := bot.State.Guild(m.GuildID)
  359. localPerms, err := bot.State.UserChannelPermissions(m.Author.ID, m.ChannelID)
  360. if err != nil {
  361. if config.DebugOutput {
  362. log.Println(logPrefixDebug, color.YellowString("isLocalAdmin check failed due to error when checking permissions:\t%s", err))
  363. }
  364. return true
  365. }
  366. botSelf := m.Author.ID == user.ID
  367. botAdmin := stringInSlice(m.Author.ID, config.Admins)
  368. guildOwner := m.Author.ID == guild.OwnerID
  369. guildAdmin := localPerms&discordgo.PermissionAdministrator > 0
  370. localManageMessages := localPerms&discordgo.PermissionManageMessages > 0
  371. return botSelf || botAdmin || guildOwner || guildAdmin || localManageMessages
  372. }
  373. func hasPerms(channelID string, permission int) bool {
  374. if !config.CheckPermissions {
  375. return true
  376. }
  377. sourceChannel, err := bot.State.Channel(channelID)
  378. if sourceChannel != nil && err == nil {
  379. switch sourceChannel.Type {
  380. case discordgo.ChannelTypeDM:
  381. return true
  382. case discordgo.ChannelTypeGroupDM:
  383. return true
  384. case discordgo.ChannelTypeGuildText:
  385. perms, err := bot.UserChannelPermissions(user.ID, channelID)
  386. if err == nil {
  387. return perms&permission == permission
  388. }
  389. log.Println(color.HiRedString("Failed to check permissions (%d) for %s:\t%s", permission, channelID, err))
  390. }
  391. }
  392. return false
  393. }
  394. //#endregion
  395. //#region Labeling
  396. func getUserIdentifier(usr discordgo.User) string {
  397. return fmt.Sprintf("\"%s\"#%s", usr.Username, usr.Discriminator)
  398. }
  399. func getGuildName(guildID string) string {
  400. sourceGuildName := "UNKNOWN"
  401. sourceGuild, _ := bot.State.Guild(guildID)
  402. if sourceGuild != nil && sourceGuild.Name != "" {
  403. sourceGuildName = sourceGuild.Name
  404. }
  405. return sourceGuildName
  406. }
  407. func getChannelName(channelID string) string {
  408. sourceChannelName := "unknown"
  409. sourceChannel, _ := bot.State.Channel(channelID)
  410. if sourceChannel != nil {
  411. if sourceChannel.Name != "" {
  412. sourceChannelName = sourceChannel.Name
  413. } else {
  414. switch sourceChannel.Type {
  415. case discordgo.ChannelTypeDM:
  416. sourceChannelName = "dm"
  417. case discordgo.ChannelTypeGroupDM:
  418. sourceChannelName = "group-dm"
  419. }
  420. }
  421. }
  422. return sourceChannelName
  423. }
  424. func getSourceName(guildID string, channelID string) string {
  425. guildName := getGuildName(guildID)
  426. channelName := getChannelName(channelID)
  427. if channelName == "dm" || channelName == "group-dm" {
  428. return channelName
  429. }
  430. return fmt.Sprintf("\"%s\"#%s", guildName, channelName)
  431. }
  432. //#endregion
  433. // For command case-insensitivity
  434. func messageToLower(message *discordgo.Message) *discordgo.Message {
  435. newMessage := *message
  436. newMessage.Content = strings.ToLower(newMessage.Content)
  437. return &newMessage
  438. }