main.go 13 KB

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