main.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/signal"
  8. "strings"
  9. "time"
  10. "github.com/bwmarrin/discordgo"
  11. )
  12. // Bot parameters
  13. var (
  14. GuildID = flag.String("guild", "", "Test guild ID. If not passed - bot registers commands globally")
  15. BotToken = flag.String("token", "", "Bot access token")
  16. RemoveCommands = flag.Bool("rmcmd", true, "Remove all commands after shutdowning or not")
  17. )
  18. var s *discordgo.Session
  19. func init() { flag.Parse() }
  20. func init() {
  21. var err error
  22. s, err = discordgo.New("Bot " + *BotToken)
  23. if err != nil {
  24. log.Fatalf("Invalid bot parameters: %v", err)
  25. }
  26. }
  27. var (
  28. integerOptionMinValue = 1.0
  29. commands = []*discordgo.ApplicationCommand{
  30. {
  31. Name: "basic-command",
  32. // All commands and options must have a description
  33. // Commands/options without description will fail the registration
  34. // of the command.
  35. Description: "Basic command",
  36. },
  37. {
  38. Name: "basic-command-with-files",
  39. Description: "Basic command with files",
  40. },
  41. {
  42. Name: "options",
  43. Description: "Command for demonstrating options",
  44. Options: []*discordgo.ApplicationCommandOption{
  45. {
  46. Type: discordgo.ApplicationCommandOptionString,
  47. Name: "string-option",
  48. Description: "String option",
  49. Required: true,
  50. },
  51. {
  52. Type: discordgo.ApplicationCommandOptionInteger,
  53. Name: "integer-option",
  54. Description: "Integer option",
  55. MinValue: &integerOptionMinValue,
  56. MaxValue: 10,
  57. Required: true,
  58. },
  59. {
  60. Type: discordgo.ApplicationCommandOptionNumber,
  61. Name: "number-option",
  62. Description: "Float option",
  63. MaxValue: 10.1,
  64. Required: true,
  65. },
  66. {
  67. Type: discordgo.ApplicationCommandOptionBoolean,
  68. Name: "bool-option",
  69. Description: "Boolean option",
  70. Required: true,
  71. },
  72. // Required options must be listed first since optional parameters
  73. // always come after when they're used.
  74. // The same concept applies to Discord's Slash-commands API
  75. {
  76. Type: discordgo.ApplicationCommandOptionChannel,
  77. Name: "channel-option",
  78. Description: "Channel option",
  79. // Channel type mask
  80. ChannelTypes: []discordgo.ChannelType{
  81. discordgo.ChannelTypeGuildText,
  82. discordgo.ChannelTypeGuildVoice,
  83. },
  84. Required: false,
  85. },
  86. {
  87. Type: discordgo.ApplicationCommandOptionUser,
  88. Name: "user-option",
  89. Description: "User option",
  90. Required: false,
  91. },
  92. {
  93. Type: discordgo.ApplicationCommandOptionRole,
  94. Name: "role-option",
  95. Description: "Role option",
  96. Required: false,
  97. },
  98. },
  99. },
  100. {
  101. Name: "subcommands",
  102. Description: "Subcommands and command groups example",
  103. Options: []*discordgo.ApplicationCommandOption{
  104. // When a command has subcommands/subcommand groups
  105. // It must not have top-level options, they aren't accesible in the UI
  106. // in this case (at least not yet), so if a command has
  107. // subcommands/subcommand any groups registering top-level options
  108. // will cause the registration of the command to fail
  109. {
  110. Name: "scmd-grp",
  111. Description: "Subcommands group",
  112. Options: []*discordgo.ApplicationCommandOption{
  113. // Also, subcommand groups aren't capable of
  114. // containing options, by the name of them, you can see
  115. // they can only contain subcommands
  116. {
  117. Name: "nst-subcmd",
  118. Description: "Nested subcommand",
  119. Type: discordgo.ApplicationCommandOptionSubCommand,
  120. },
  121. },
  122. Type: discordgo.ApplicationCommandOptionSubCommandGroup,
  123. },
  124. // Also, you can create both subcommand groups and subcommands
  125. // in the command at the same time. But, there's some limits to
  126. // nesting, count of subcommands (top level and nested) and options.
  127. // Read the intro of slash-commands docs on Discord dev portal
  128. // to get more information
  129. {
  130. Name: "subcmd",
  131. Description: "Top-level subcommand",
  132. Type: discordgo.ApplicationCommandOptionSubCommand,
  133. },
  134. },
  135. },
  136. {
  137. Name: "responses",
  138. Description: "Interaction responses testing initiative",
  139. Options: []*discordgo.ApplicationCommandOption{
  140. {
  141. Name: "resp-type",
  142. Description: "Response type",
  143. Type: discordgo.ApplicationCommandOptionInteger,
  144. Choices: []*discordgo.ApplicationCommandOptionChoice{
  145. {
  146. Name: "Channel message with source",
  147. Value: 4,
  148. },
  149. {
  150. Name: "Deferred response With Source",
  151. Value: 5,
  152. },
  153. },
  154. Required: true,
  155. },
  156. },
  157. },
  158. {
  159. Name: "followups",
  160. Description: "Followup messages",
  161. },
  162. }
  163. commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
  164. "basic-command": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  165. s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  166. Type: discordgo.InteractionResponseChannelMessageWithSource,
  167. Data: &discordgo.InteractionResponseData{
  168. Content: "Hey there! Congratulations, you just executed your first slash command",
  169. },
  170. })
  171. },
  172. "basic-command-with-files": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  173. s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  174. Type: discordgo.InteractionResponseChannelMessageWithSource,
  175. Data: &discordgo.InteractionResponseData{
  176. Content: "Hey there! Congratulations, you just executed your first slash command with a file in the response",
  177. Files: []*discordgo.File{
  178. {
  179. ContentType: "text/plain",
  180. Name: "test.txt",
  181. Reader: strings.NewReader("Hello Discord!!"),
  182. },
  183. },
  184. },
  185. })
  186. },
  187. "options": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  188. margs := []interface{}{
  189. // Here we need to convert raw interface{} value to wanted type.
  190. // Also, as you can see, here is used utility functions to convert the value
  191. // to particular type. Yeah, you can use just switch type,
  192. // but this is much simpler
  193. i.ApplicationCommandData().Options[0].StringValue(),
  194. i.ApplicationCommandData().Options[1].IntValue(),
  195. i.ApplicationCommandData().Options[2].FloatValue(),
  196. i.ApplicationCommandData().Options[3].BoolValue(),
  197. }
  198. msgformat :=
  199. ` Now you just learned how to use command options. Take a look to the value of which you've just entered:
  200. > string_option: %s
  201. > integer_option: %d
  202. > number_option: %f
  203. > bool_option: %v
  204. `
  205. if len(i.ApplicationCommandData().Options) >= 5 {
  206. margs = append(margs, i.ApplicationCommandData().Options[4].ChannelValue(nil).ID)
  207. msgformat += "> channel-option: <#%s>\n"
  208. }
  209. if len(i.ApplicationCommandData().Options) >= 6 {
  210. margs = append(margs, i.ApplicationCommandData().Options[5].UserValue(nil).ID)
  211. msgformat += "> user-option: <@%s>\n"
  212. }
  213. if len(i.ApplicationCommandData().Options) >= 7 {
  214. margs = append(margs, i.ApplicationCommandData().Options[6].RoleValue(nil, "").ID)
  215. msgformat += "> role-option: <@&%s>\n"
  216. }
  217. s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  218. // Ignore type for now, we'll discuss them in "responses" part
  219. Type: discordgo.InteractionResponseChannelMessageWithSource,
  220. Data: &discordgo.InteractionResponseData{
  221. Content: fmt.Sprintf(
  222. msgformat,
  223. margs...,
  224. ),
  225. },
  226. })
  227. },
  228. "subcommands": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  229. content := ""
  230. // As you can see, the name of subcommand (nested, top-level) or subcommand group
  231. // is provided through arguments.
  232. switch i.ApplicationCommandData().Options[0].Name {
  233. case "subcmd":
  234. content =
  235. "The top-level subcommand is executed. Now try to execute the nested one."
  236. default:
  237. if i.ApplicationCommandData().Options[0].Name != "scmd-grp" {
  238. return
  239. }
  240. switch i.ApplicationCommandData().Options[0].Options[0].Name {
  241. case "nst-subcmd":
  242. content = "Nice, now you know how to execute nested commands too"
  243. default:
  244. // I added this in the case something might go wrong
  245. content = "Oops, something gone wrong.\n" +
  246. "Hol' up, you aren't supposed to see this message."
  247. }
  248. }
  249. s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  250. Type: discordgo.InteractionResponseChannelMessageWithSource,
  251. Data: &discordgo.InteractionResponseData{
  252. Content: content,
  253. },
  254. })
  255. },
  256. "responses": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  257. // Responses to a command are very important.
  258. // First of all, because you need to react to the interaction
  259. // by sending the response in 3 seconds after receiving, otherwise
  260. // interaction will be considered invalid and you can no longer
  261. // use the interaction token and ID for responding to the user's request
  262. content := ""
  263. // As you can see, the response type names used here are pretty self-explanatory,
  264. // but for those who want more information see the official documentation
  265. switch i.ApplicationCommandData().Options[0].IntValue() {
  266. case int64(discordgo.InteractionResponseChannelMessageWithSource):
  267. content =
  268. "You just responded to an interaction, sent a message and showed the original one. " +
  269. "Congratulations!"
  270. content +=
  271. "\nAlso... you can edit your response, wait 5 seconds and this message will be changed"
  272. default:
  273. err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  274. Type: discordgo.InteractionResponseType(i.ApplicationCommandData().Options[0].IntValue()),
  275. })
  276. if err != nil {
  277. s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
  278. Content: "Something went wrong",
  279. })
  280. }
  281. return
  282. }
  283. err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  284. Type: discordgo.InteractionResponseType(i.ApplicationCommandData().Options[0].IntValue()),
  285. Data: &discordgo.InteractionResponseData{
  286. Content: content,
  287. },
  288. })
  289. if err != nil {
  290. s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
  291. Content: "Something went wrong",
  292. })
  293. return
  294. }
  295. time.AfterFunc(time.Second*5, func() {
  296. _, err = s.InteractionResponseEdit(s.State.User.ID, i.Interaction, &discordgo.WebhookEdit{
  297. Content: content + "\n\nWell, now you know how to create and edit responses. " +
  298. "But you still don't know how to delete them... so... wait 10 seconds and this " +
  299. "message will be deleted.",
  300. })
  301. if err != nil {
  302. s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
  303. Content: "Something went wrong",
  304. })
  305. return
  306. }
  307. time.Sleep(time.Second * 10)
  308. s.InteractionResponseDelete(s.State.User.ID, i.Interaction)
  309. })
  310. },
  311. "followups": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  312. // Followup messages are basically regular messages (you can create as many of them as you wish)
  313. // but work as they are created by webhooks and their functionality
  314. // is for handling additional messages after sending a response.
  315. s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
  316. Type: discordgo.InteractionResponseChannelMessageWithSource,
  317. Data: &discordgo.InteractionResponseData{
  318. // Note: this isn't documented, but you can use that if you want to.
  319. // This flag just allows you to create messages visible only for the caller of the command
  320. // (user who triggered the command)
  321. Flags: 1 << 6,
  322. Content: "Surprise!",
  323. },
  324. })
  325. msg, err := s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
  326. Content: "Followup message has been created, after 5 seconds it will be edited",
  327. })
  328. if err != nil {
  329. s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
  330. Content: "Something went wrong",
  331. })
  332. return
  333. }
  334. time.Sleep(time.Second * 5)
  335. s.FollowupMessageEdit(s.State.User.ID, i.Interaction, msg.ID, &discordgo.WebhookEdit{
  336. Content: "Now the original message is gone and after 10 seconds this message will ~~self-destruct~~ be deleted.",
  337. })
  338. time.Sleep(time.Second * 10)
  339. s.FollowupMessageDelete(s.State.User.ID, i.Interaction, msg.ID)
  340. s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
  341. Content: "For those, who didn't skip anything and followed tutorial along fairly, " +
  342. "take a unicorn :unicorn: as reward!\n" +
  343. "Also, as bonus... look at the original interaction response :D",
  344. })
  345. },
  346. }
  347. )
  348. func init() {
  349. s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
  350. if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
  351. h(s, i)
  352. }
  353. })
  354. }
  355. func main() {
  356. s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
  357. log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
  358. })
  359. err := s.Open()
  360. if err != nil {
  361. log.Fatalf("Cannot open the session: %v", err)
  362. }
  363. log.Println("Adding commands...")
  364. registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
  365. for i, v := range commands {
  366. cmd, err := s.ApplicationCommandCreate(s.State.User.ID, *GuildID, v)
  367. if err != nil {
  368. log.Panicf("Cannot create '%v' command: %v", v.Name, err)
  369. }
  370. registeredCommands[i] = cmd
  371. }
  372. defer s.Close()
  373. stop := make(chan os.Signal, 1)
  374. signal.Notify(stop, os.Interrupt)
  375. log.Println("Press Ctrl+C to exit")
  376. <-stop
  377. if *RemoveCommands {
  378. log.Println("Removing commands...")
  379. // // We need to fetch the commands, since deleting requires the command ID.
  380. // // We are doing this from the returned commands on line 375, because using
  381. // // this will delete all the commands, which might not be desirable, so we
  382. // // are deleting only the commands that we added.
  383. // registeredCommands, err := s.ApplicationCommands(s.State.User.ID, *GuildID)
  384. // if err != nil {
  385. // log.Fatalf("Could not fetch registered commands: %v", err)
  386. // }
  387. for _, v := range registeredCommands {
  388. err := s.ApplicationCommandDelete(s.State.User.ID, *GuildID, v.ID)
  389. if err != nil {
  390. log.Panicf("Cannot delete '%v' command: %v", v.Name, err)
  391. }
  392. }
  393. }
  394. log.Println("Gracefully shutdowning")
  395. }