main.go 12 KB

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