commands.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. "github.com/Necroforger/dgrouter/exrouter"
  11. "github.com/bwmarrin/discordgo"
  12. "github.com/fatih/color"
  13. "github.com/hako/durafmt"
  14. "github.com/kennygrant/sanitize"
  15. )
  16. // Multiple use messages to save space and make cleaner.
  17. //TODO: Implement this for more?
  18. const (
  19. cmderrLackingLocalAdminPerms = "You do not have permission to use this command.\n" +
  20. "\nTo use this command you must:" +
  21. "\n• Be set as a bot administrator (in the settings)" +
  22. "\n• Own this Discord Server" +
  23. "\n• Have Server Administrator Permissions"
  24. cmderrLackingBotAdminPerms = "You do not have permission to use this command. Your User ID must be set as a bot administrator in the settings file."
  25. cmderrChannelNotRegistered = "Specified channel is not registered in the bot settings."
  26. cmderrHistoryCancelled = "History cataloging was cancelled."
  27. )
  28. func handleCommands() *exrouter.Route {
  29. router := exrouter.New()
  30. //#region Utility Commands
  31. router.On("ping", func(ctx *exrouter.Context) {
  32. logPrefixHere := color.CyanString("[dgrouter:ping]")
  33. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  34. if isCommandableChannel(ctx.Msg) {
  35. beforePong := time.Now()
  36. pong, err := ctx.Reply("Pong!")
  37. if err != nil {
  38. log.Println(logPrefixHere, color.HiRedString("Error sending pong message:\t%s", err))
  39. } else {
  40. afterPong := time.Now()
  41. latency := bot.HeartbeatLatency().Milliseconds()
  42. roundtrip := afterPong.Sub(beforePong).Milliseconds()
  43. mention := ctx.Msg.Author.Mention()
  44. content := fmt.Sprintf("**Latency:** ``%dms`` — **Roundtrip:** ``%dms``",
  45. latency,
  46. roundtrip,
  47. )
  48. if pong != nil {
  49. _, err := bot.ChannelMessageEditComplex(&discordgo.MessageEdit{
  50. ID: pong.ID,
  51. Channel: pong.ChannelID,
  52. Content: &mention,
  53. Embed: buildEmbed(ctx.Msg.ChannelID, "Command — Ping", content),
  54. })
  55. // Failed to edit pong
  56. if err != nil {
  57. log.Println(logPrefixHere, color.HiRedString("Failed to edit pong message, sending new one:\t%s", err))
  58. _, err := replyEmbed(pong, "Command — Ping", content)
  59. // Failed to send new pong
  60. if err != nil {
  61. log.Println(logPrefixHere, color.HiRedString("Failed to send replacement pong message:\t%s", err))
  62. }
  63. }
  64. }
  65. // Log
  66. log.Println(logPrefixHere, color.HiCyanString("%s pinged bot - Latency: %dms, Roundtrip: %dms",
  67. getUserIdentifier(*ctx.Msg.Author),
  68. latency,
  69. roundtrip),
  70. )
  71. }
  72. }
  73. } else {
  74. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  75. }
  76. }).Cat("Utility").Alias("test").Desc("Pings the bot")
  77. router.On("help", func(ctx *exrouter.Context) {
  78. logPrefixHere := color.CyanString("[dgrouter:help]")
  79. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  80. if isGlobalCommandAllowed(ctx.Msg) {
  81. text := ""
  82. for _, cmd := range router.Routes {
  83. if cmd.Category != "Admin" || isBotAdmin(ctx.Msg) {
  84. text += fmt.Sprintf("• \"%s\" : %s",
  85. cmd.Name,
  86. cmd.Description,
  87. )
  88. if len(cmd.Aliases) > 0 {
  89. text += fmt.Sprintf("\n— Aliases: \"%s\"", strings.Join(cmd.Aliases, "\", \""))
  90. }
  91. text += "\n\n"
  92. }
  93. }
  94. _, err := replyEmbed(ctx.Msg, "Command — Help", fmt.Sprintf("Use commands as ``\"%s<command> <arguments?>\"``\n```%s```\n%s", config.CommandPrefix, text, projectRepoURL))
  95. // Failed to send
  96. if err != nil {
  97. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  98. }
  99. log.Println(logPrefixHere, color.HiCyanString("%s asked for help", getUserIdentifier(*ctx.Msg.Author)))
  100. }
  101. } else {
  102. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  103. }
  104. }).Cat("Utility").Alias("commands").Desc("Outputs this help menu")
  105. //#endregion
  106. //#region Info Commands
  107. router.On("status", func(ctx *exrouter.Context) {
  108. logPrefixHere := color.CyanString("[dgrouter:status]")
  109. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  110. if isCommandableChannel(ctx.Msg) {
  111. message := fmt.Sprintf("• **Uptime —** %s\n"+
  112. "• **Started at —** %s\n"+
  113. "• **Joined Servers —** %d\n"+
  114. "• **Bound Channels —** %d\n"+
  115. "• **Bound Servers —** %d\n"+
  116. "• **Admin Channels —** %d\n"+
  117. "• **Heartbeat Latency —** %dms",
  118. durafmt.Parse(time.Since(startTime)).String(),
  119. startTime.Format("03:04:05pm on Monday, January 2, 2006 (MST)"),
  120. len(bot.State.Guilds),
  121. getBoundChannelsCount(),
  122. getBoundServersCount(),
  123. len(config.AdminChannels),
  124. bot.HeartbeatLatency().Milliseconds(),
  125. )
  126. if isChannelRegistered(ctx.Msg.ChannelID) {
  127. configJson, _ := json.MarshalIndent(getChannelConfig(ctx.Msg.ChannelID), "", "\t")
  128. message = message + fmt.Sprintf("\n• **Channel Settings...** ```%s```", string(configJson))
  129. }
  130. _, err := replyEmbed(ctx.Msg, "Command — Status", message)
  131. // Failed to send
  132. if err != nil {
  133. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  134. }
  135. log.Println(logPrefixHere, color.HiCyanString("%s requested status report", getUserIdentifier(*ctx.Msg.Author)))
  136. }
  137. } else {
  138. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  139. }
  140. }).Cat("Info").Desc("Displays info regarding the current status of the bot")
  141. router.On("stats", func(ctx *exrouter.Context) {
  142. logPrefixHere := color.CyanString("[dgrouter:stats]")
  143. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  144. if isChannelRegistered(ctx.Msg.ChannelID) {
  145. channelConfig := getChannelConfig(ctx.Msg.ChannelID)
  146. if *channelConfig.AllowCommands {
  147. content := fmt.Sprintf("• **Total Downloads —** %s\n"+
  148. "• **Downloads in this Channel —** %s",
  149. formatNumber(int64(dbDownloadCount())),
  150. formatNumber(int64(dbDownloadCountByChannel(ctx.Msg.ChannelID))),
  151. )
  152. //TODO: Count in channel by users
  153. _, err := replyEmbed(ctx.Msg, "Command — Stats", content)
  154. // Failed to send
  155. if err != nil {
  156. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  157. }
  158. log.Println(logPrefixHere, color.HiCyanString("%s requested stats", getUserIdentifier(*ctx.Msg.Author)))
  159. }
  160. }
  161. } else {
  162. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  163. }
  164. }).Cat("Info").Desc("Outputs statistics regarding this channel")
  165. router.On("info", func(ctx *exrouter.Context) {
  166. logPrefixHere := color.CyanString("[dgrouter:info]")
  167. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  168. if isGlobalCommandAllowed(ctx.Msg) {
  169. content := fmt.Sprintf("Here is some useful info...\n\n"+
  170. "• **Your User ID —** `%s`\n"+
  171. "• **Bots User ID —** `%s`\n"+
  172. "• **This Channel ID —** `%s`\n"+
  173. "• **This Server ID —** `%s`"+
  174. "\n\nRemember to remove any spaces when copying to settings.",
  175. ctx.Msg.Author.ID, user.ID, ctx.Msg.ChannelID, ctx.Msg.GuildID)
  176. _, err := replyEmbed(ctx.Msg, "Command — Info", content)
  177. // Failed to send
  178. if err != nil {
  179. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  180. }
  181. log.Println(logPrefixHere, color.HiCyanString("%s requested info", getUserIdentifier(*ctx.Msg.Author)))
  182. } else {
  183. log.Println(logPrefixHere, color.HiRedString("%s tried using the info command but commands are disabled here", getUserIdentifier(*ctx.Msg.Author)))
  184. }
  185. } else {
  186. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  187. }
  188. }).Cat("Info").Desc("Displays info regarding Discord IDs")
  189. //#endregion
  190. //#region Admin Commands
  191. router.On("history", func(ctx *exrouter.Context) {
  192. logPrefixHere := color.CyanString("[dgrouter:history]")
  193. // Vars
  194. var channels []string
  195. var before string
  196. var beforeID string
  197. var since string
  198. var sinceID string
  199. var stop bool
  200. // Keys
  201. beforeKey := "--before="
  202. sinceKey := "--since="
  203. // Parse Args
  204. for k, v := range ctx.Args {
  205. // Skip "history" segment
  206. if k == 0 {
  207. continue
  208. }
  209. // Actually Parse Args
  210. if strings.Contains(strings.ToLower(v), beforeKey) {
  211. before = strings.ReplaceAll(strings.ToLower(v), beforeKey, "")
  212. if isDate(before) {
  213. beforeID = discordTimestampToSnowflake("2006-01-02", before)
  214. } else if isNumeric(before) {
  215. beforeID = before
  216. }
  217. if config.DebugOutput {
  218. log.Println(logPrefixDebug, logPrefixHere, color.CyanString("Date range applied, before %s", beforeID))
  219. }
  220. } else if strings.Contains(strings.ToLower(v), sinceKey) {
  221. since = strings.ReplaceAll(strings.ToLower(v), sinceKey, "")
  222. if isDate(since) {
  223. sinceID = discordTimestampToSnowflake("2006-01-02", since)
  224. } else if isNumeric(since) {
  225. sinceID = since
  226. }
  227. if config.DebugOutput {
  228. log.Println(logPrefixDebug, logPrefixHere, color.CyanString("Date range applied, since %s", sinceID))
  229. }
  230. } else if strings.Contains(strings.ToLower(v), "cancel") || strings.Contains(strings.ToLower(v), "stop") {
  231. stop = true
  232. } else {
  233. // Actual Source ID(s)
  234. targets := strings.Split(ctx.Args.Get(k), ",")
  235. for _, target := range targets {
  236. if isNumeric(target) {
  237. // Test/Use if number is guild
  238. guild, err := bot.State.Guild(target)
  239. if err == nil {
  240. if config.DebugOutput {
  241. log.Println(logPrefixHere, logPrefixDebug, color.YellowString("Specified target %s is a guild: \"%s\", adding all channels...", target, guild.Name))
  242. }
  243. for _, ch := range guild.Channels {
  244. channels = append(channels, ch.ID)
  245. if config.DebugOutput {
  246. log.Println(logPrefixHere, logPrefixDebug, color.YellowString("Added %s (#%s in \"%s\") to history queue", ch.ID, ch.Name, guild.Name))
  247. }
  248. }
  249. } else { // Test/Use if number is channel
  250. ch, err := bot.State.Channel(target)
  251. if err == nil {
  252. channels = append(channels, target)
  253. if config.DebugOutput {
  254. log.Println(logPrefixHere, logPrefixDebug, color.YellowString("Added %s (#%s in %s) to history queue", ch.ID, ch.Name, ch.GuildID))
  255. }
  256. }
  257. }
  258. } else if strings.Contains(strings.ToLower(target), "all") {
  259. channels = getAllChannels()
  260. }
  261. }
  262. }
  263. }
  264. if len(channels) == 0 { // Local
  265. channels = append(channels, ctx.Msg.ChannelID)
  266. }
  267. // Foreach Channel
  268. for _, channel := range channels {
  269. if config.DebugOutput {
  270. log.Println(logPrefixHere, logPrefixDebug, color.YellowString("Processing %s...", channel))
  271. }
  272. // Registered check
  273. if isCommandableChannel(ctx.Msg) {
  274. // Permission check
  275. if isBotAdmin(ctx.Msg) || isLocalAdmin(ctx.Msg) {
  276. // Run
  277. if !stop {
  278. _, historyCommandIsSet := historyStatus[channel]
  279. if !historyCommandIsSet || historyStatus[channel] == "" {
  280. if config.AsynchronousHistory {
  281. go handleHistory(ctx.Msg, channel, beforeID, sinceID)
  282. } else {
  283. handleHistory(ctx.Msg, channel, beforeID, sinceID)
  284. }
  285. } else { // ALREADY RUNNING
  286. log.Println(logPrefixHere, color.CyanString("%s tried using history command but history is already running for %s...", getUserIdentifier(*ctx.Msg.Author), channel))
  287. }
  288. } else if historyStatus[channel] == "downloading" {
  289. historyStatus[channel] = "cancel"
  290. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  291. _, err := replyEmbed(ctx.Msg, "Command — History", cmderrHistoryCancelled)
  292. if err != nil {
  293. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  294. }
  295. } else {
  296. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, channel))
  297. }
  298. log.Println(logPrefixHere, color.CyanString("%s cancelled history cataloging for \"%s\"", getUserIdentifier(*ctx.Msg.Author), channel))
  299. }
  300. } else { // DOES NOT HAVE PERMISSION
  301. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  302. _, err := replyEmbed(ctx.Msg, "Command — History", cmderrLackingLocalAdminPerms)
  303. if err != nil {
  304. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  305. }
  306. } else {
  307. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, channel))
  308. }
  309. log.Println(logPrefixHere, color.CyanString("%s tried to cache history for %s but lacked proper permission.", getUserIdentifier(*ctx.Msg.Author), channel))
  310. }
  311. } else { // CHANNEL NOT REGISTERED
  312. log.Println(logPrefixHere, color.CyanString("%s tried to catalog history for \"%s\" but channel is not registered...", getUserIdentifier(*ctx.Msg.Author), channel))
  313. }
  314. }
  315. }).Alias("catalog", "cache").Cat("Admin").Desc("Catalogs history for this channel")
  316. router.On("exit", func(ctx *exrouter.Context) {
  317. logPrefixHere := color.CyanString("[dgrouter:exit]")
  318. if isCommandableChannel(ctx.Msg) {
  319. if isBotAdmin(ctx.Msg) {
  320. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  321. _, err := replyEmbed(ctx.Msg, "Command — Exit", "Exiting...")
  322. if err != nil {
  323. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  324. }
  325. } else {
  326. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  327. }
  328. log.Println(logPrefixHere, color.HiCyanString("%s (bot admin) requested exit, goodbye...", getUserIdentifier(*ctx.Msg.Author)))
  329. properExit()
  330. } else {
  331. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  332. _, err := replyEmbed(ctx.Msg, "Command — Exit", cmderrLackingBotAdminPerms)
  333. if err != nil {
  334. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  335. }
  336. } else {
  337. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  338. }
  339. log.Println(logPrefixHere, color.HiCyanString("%s tried to exit but lacked bot admin perms.", getUserIdentifier(*ctx.Msg.Author)))
  340. }
  341. }
  342. }).Alias("reload", "kill").Cat("Admin").Desc("Kills the bot")
  343. router.On("emojis", func(ctx *exrouter.Context) {
  344. logPrefixHere := color.CyanString("[dgrouter:emojis]")
  345. if isGlobalCommandAllowed(ctx.Msg) {
  346. if isBotAdmin(ctx.Msg) {
  347. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  348. args := ctx.Args.After(1)
  349. // Determine which guild(s)
  350. guilds := []string{ctx.Msg.GuildID}
  351. if args != "" {
  352. guilds = nil
  353. _guilds := strings.Split(args, ",")
  354. if len(_guilds) > 0 {
  355. for _, guild := range _guilds {
  356. guild = strings.TrimSpace(guild)
  357. guilds = append(guilds, guild)
  358. }
  359. }
  360. }
  361. for _, guild := range guilds {
  362. i := 0
  363. s := 0
  364. guildName := guild
  365. guildNameO := guild
  366. guildInfo, err := bot.Guild(guild)
  367. if err == nil {
  368. guildName = sanitize.Name(guildInfo.Name)
  369. guildNameO = guildInfo.Name
  370. }
  371. destination := "emojis" + string(os.PathSeparator) + guildName + string(os.PathSeparator)
  372. err = os.MkdirAll(destination, 0755)
  373. if err != nil {
  374. log.Println(logPrefixHere, color.HiRedString("Error while creating destination folder \"%s\": %s", destination, err))
  375. } else {
  376. emojis, err := bot.GuildEmojis(guild)
  377. if err == nil {
  378. for _, emoji := range emojis {
  379. var message discordgo.Message
  380. message.ChannelID = ctx.Msg.ChannelID
  381. url := "https://cdn.discordapp.com/emojis/" + emoji.ID
  382. status := startDownload(
  383. downloadRequestStruct{
  384. InputURL: url,
  385. Filename: emoji.ID,
  386. Path: destination,
  387. Message: &message,
  388. FileTime: time.Now(),
  389. HistoryCmd: false,
  390. EmojiCmd: true,
  391. })
  392. if status.Status == downloadSuccess {
  393. i++
  394. } else {
  395. s++
  396. log.Println(logPrefixHere, color.HiRedString("Failed to download emoji \"%s\": \t[%d - %s] %v", url, status.Status, getDownloadStatusString(status.Status), status.Error))
  397. }
  398. }
  399. destinationOut := destination
  400. abs, err := filepath.Abs(destination)
  401. if err == nil {
  402. destinationOut = abs
  403. }
  404. _, err = replyEmbed(ctx.Msg, "Command — Emojis",
  405. fmt.Sprintf("`%d` emojis downloaded, `%d` skipped or failed\n• Destination: `%s`\n• Server: `%s`",
  406. i, s, destinationOut, guildNameO,
  407. ),
  408. )
  409. if err != nil {
  410. log.Println(logPrefixHere, color.HiRedString("Failed to send status message for emoji downloads:\t%s", err))
  411. }
  412. } else {
  413. log.Println(err)
  414. }
  415. }
  416. }
  417. }
  418. } else {
  419. if hasPerms(ctx.Msg.ChannelID, discordgo.PermissionSendMessages) {
  420. _, err := replyEmbed(ctx.Msg, "Command — Emojis", cmderrLackingBotAdminPerms)
  421. if err != nil {
  422. log.Println(logPrefixHere, color.HiRedString("Failed to send command embed message (requested by %s)...\t%s", getUserIdentifier(*ctx.Msg.Author), err))
  423. }
  424. } else {
  425. log.Println(logPrefixHere, color.HiRedString(fmtBotSendPerm, ctx.Msg.ChannelID))
  426. }
  427. log.Println(logPrefixHere, color.HiCyanString("%s tried to download emojis but lacked bot admin perms.", getUserIdentifier(*ctx.Msg.Author)))
  428. }
  429. }
  430. }).Cat("Admin").Desc("Saves all server emojis to download destination")
  431. //#endregion
  432. // Handler for Command Router
  433. bot.AddHandler(func(_ *discordgo.Session, m *discordgo.MessageCreate) {
  434. //NOTE: This setup makes it case-insensitive but message content will be lowercase, currently case sensitivity is not necessary.
  435. router.FindAndExecute(bot, strings.ToLower(config.CommandPrefix), bot.State.User.ID, messageToLower(m.Message))
  436. })
  437. return router
  438. }