From f35e2ea77c6514b801f6474345edd27e4a31d825 Mon Sep 17 00:00:00 2001 From: eccentric Date: Wed, 13 Dec 2023 22:52:16 +0000 Subject: [PATCH] add discord bot! --- aid/aid.go | 35 ++++++-- aid/config.go | 6 ++ default.config.ini | 2 + discord/discord.go | 90 ++++++++++++++++++++ discord/embed.go | 62 ++++++++++++++ discord/handlers.go | 196 ++++++++++++++++++++++++++++++++++++++++++++ fortnite/person.go | 16 +++- go.mod | 2 + go.sum | 4 + main.go | 18 +--- person/person.go | 6 +- storage/postgres.go | 25 +++++- storage/storage.go | 19 ++++- 13 files changed, 453 insertions(+), 28 deletions(-) create mode 100644 discord/discord.go create mode 100644 discord/embed.go create mode 100644 discord/handlers.go diff --git a/aid/aid.go b/aid/aid.go index fb42a88..fb96140 100644 --- a/aid/aid.go +++ b/aid/aid.go @@ -4,6 +4,7 @@ import ( "math/rand" "os" "os/signal" + "strconv" "syscall" "github.com/goccy/go-json" @@ -27,11 +28,31 @@ func JSONParse(input string) interface{} { } func RandomString(n int) string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - - s := make([]rune, n) - for i := range s { - s[i] = letters[rand.Intn(len(letters))] - } - return string(s) + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + s := make([]rune, n) + for i := range s { + s[i] = letters[rand.Intn(len(letters))] + } + return string(s) +} + +func FormatNumber(number int) string { + str := "" + for i, char := range ReverseString(strconv.Itoa(number)) { + if i % 3 == 0 && i != 0 { + str += "," + } + str += string(char) + } + + return ReverseString(str) +} + +func ReverseString(input string) string { + str := "" + for _, char := range input { + str = string(char) + str + } + return str } \ No newline at end of file diff --git a/aid/config.go b/aid/config.go index 102f948..be64b38 100644 --- a/aid/config.go +++ b/aid/config.go @@ -17,6 +17,7 @@ type CS struct { ID string Secret string Token string + Guild string } Output struct { Level string @@ -82,6 +83,11 @@ func LoadConfig(file []byte) { panic("Discord Bot Token is empty") } + Config.Discord.Guild = cfg.Section("discord").Key("guild").String() + if Config.Discord.Guild == "" { + panic("Discord Guild ID is empty") + } + Config.API.Host = cfg.Section("api").Key("host").String() if Config.API.Host == "" { panic("API Host is empty") diff --git a/default.config.ini b/default.config.ini index a70ac33..17ea99d 100644 --- a/default.config.ini +++ b/default.config.ini @@ -13,6 +13,8 @@ id="1234567890..." secret="abcdefg..." ; discord bot token token="OTK...." +; server id +guild="1234567890..." [output] ; level of logging diff --git a/discord/discord.go b/discord/discord.go new file mode 100644 index 0000000..c2b23f1 --- /dev/null +++ b/discord/discord.go @@ -0,0 +1,90 @@ +package discord + +import ( + "strings" + + "github.com/bwmarrin/discordgo" + "github.com/ectrc/snow/aid" +) + +type DiscordCommand struct { + Command *discordgo.ApplicationCommand + Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) + AdminOnly bool +} + +type DiscordModal struct { + ID string + Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) +} + +type DiscordClient struct { + Client *discordgo.Session + Commands map[string]*DiscordCommand + Modals map[string]*DiscordModal +} + +var StaticClient *DiscordClient + +func NewDiscordClient(token string) *DiscordClient { + client, err := discordgo.New("Bot " + token) + if err != nil { + panic(err) + } + + client.Identify.Intents = discordgo.IntentsAllWithoutPrivileged + + return &DiscordClient{ + Client: client, + Commands: make(map[string]*DiscordCommand), + Modals: make(map[string]*DiscordModal), + } +} + +func IntialiseClient() { + StaticClient = NewDiscordClient(aid.Config.Discord.Token) + StaticClient.Client.AddHandler(StaticClient.readyHandler) + StaticClient.Client.AddHandler(StaticClient.interactionHandler) + + addCommands() + + for _, command := range StaticClient.Commands { + StaticClient.RegisterCommand(command) + } + + err := StaticClient.Client.Open() + if err != nil { + panic(err) + } +} + +func (c *DiscordClient) RegisterCommand(command *DiscordCommand) { + if command.AdminOnly { + adminDefaultPermission := int64(discordgo.PermissionAdministrator) + command.Command.DefaultMemberPermissions = &adminDefaultPermission + } + + _, err := c.Client.ApplicationCommandCreate(aid.Config.Discord.ID, aid.Config.Discord.Guild, command.Command) + if err != nil { + aid.Print("Failed to register command: " + command.Command.Name) + return + } +} + +func (c *DiscordClient) readyHandler(s *discordgo.Session, event *discordgo.Ready) { + aid.Print("Discord bot is ready") +} + +func (c *DiscordClient) interactionHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.Type { + case discordgo.InteractionApplicationCommand: + if command, ok := c.Commands[i.ApplicationCommandData().Name]; ok { + command.Handler(s, i) + } + + case discordgo.InteractionModalSubmit: + if modal, ok := c.Modals[strings.Split(i.ModalSubmitData().CustomID, "://")[0]]; ok { + modal.Handler(s, i) + } + } +} \ No newline at end of file diff --git a/discord/embed.go b/discord/embed.go new file mode 100644 index 0000000..5253434 --- /dev/null +++ b/discord/embed.go @@ -0,0 +1,62 @@ +package discord + +import "github.com/bwmarrin/discordgo" + +type EmbedBuilder struct { + Embed discordgo.MessageEmbed +} + +func NewEmbedBuilder() *EmbedBuilder { + return &EmbedBuilder{ + Embed: discordgo.MessageEmbed{}, + } +} + +func (e *EmbedBuilder) SetTitle(title string) *EmbedBuilder { + e.Embed.Title = title + return e +} + +func (e *EmbedBuilder) SetDescription(description string) *EmbedBuilder { + e.Embed.Description = description + return e +} + +func (e *EmbedBuilder) SetColor(color int) *EmbedBuilder { + e.Embed.Color = color + return e +} + +func (e *EmbedBuilder) SetImage(url string) *EmbedBuilder { + e.Embed.Image = &discordgo.MessageEmbedImage{ + URL: url, + } + return e +} + +func (e *EmbedBuilder) SetThumbnail(url string) *EmbedBuilder { + e.Embed.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: url, + } + return e +} + +func (e *EmbedBuilder) SetFooter(text string) *EmbedBuilder { + e.Embed.Footer = &discordgo.MessageEmbedFooter{ + Text: text, + } + return e +} + +func (e *EmbedBuilder) AddField(name, value string, inline bool) *EmbedBuilder { + e.Embed.Fields = append(e.Embed.Fields, &discordgo.MessageEmbedField{ + Name: name, + Value: value, + Inline: inline, + }) + return e +} + +func (e *EmbedBuilder) Build() *discordgo.MessageEmbed { + return &e.Embed +} \ No newline at end of file diff --git a/discord/handlers.go b/discord/handlers.go new file mode 100644 index 0000000..3758056 --- /dev/null +++ b/discord/handlers.go @@ -0,0 +1,196 @@ +package discord + +import ( + "github.com/bwmarrin/discordgo" + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/fortnite" + "github.com/ectrc/snow/person" + "github.com/ectrc/snow/storage" +) + +var ( +) + +func addCommand(command *DiscordCommand) { + StaticClient.Commands[command.Command.Name] = command +} + +func addModal(modal *DiscordModal) { + StaticClient.Modals[modal.ID] = modal +} + +func addCommands() { + if StaticClient == nil { + panic("StaticClient is nil") + } + + addCommand(&DiscordCommand{ + Command: &discordgo.ApplicationCommand{ + Name: "create", + Description: "Create an account with the bot.", + }, + Handler: createHandler, + }) + + addModal(&DiscordModal{ + ID: "create", + Handler: createModalHandler, + }) + + addCommand(&DiscordCommand{ + Command: &discordgo.ApplicationCommand{ + Name: "information", + Description: "Useful information about this server's activity! Admin Only.", + }, + Handler: informationHandler, + AdminOnly: true, + }) + + addCommand(&DiscordCommand{ + Command: &discordgo.ApplicationCommand{ + Name: "delete", + Description: "Delete your account with the bot.", + }, + Handler: deleteHandler, + }) +} + +func createHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + modal := &discordgo.InteractionResponseData{ + CustomID: "create://" + i.Member.User.ID, + Title: "Create an account", + Components: []discordgo.MessageComponent{ + &discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "display", + Label: "DISPLAY NAME", + Style: discordgo.TextInputShort, + Placeholder: "Enter your crazy display name here!", + Required: true, + MaxLength: 20, + MinLength: 2, + }, + }, + }, + }, + } + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: modal, + }) +} + +func createModalHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + data := i.ModalSubmitData() + if len(data.Components) <= 0 { + aid.Print("No components found") + return + } + + components, ok := data.Components[0].(*discordgo.ActionsRow) + if !ok { + aid.Print("Failed to assert TextInput") + return + } + + display, ok := components.Components[0].(*discordgo.TextInput) + if !ok { + aid.Print("Failed to assert TextInput") + return + } + + found := person.FindByDiscord(i.Member.User.ID) + if found != nil { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + NewEmbedBuilder().SetTitle("Account already exists").SetDescription("You already have an account with the display name: `"+ found.DisplayName +"`").SetColor(0xda373c).Build(), + }, + }, + }) + return + } + + found = person.FindByDisplay(display.Value) + if found != nil { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + NewEmbedBuilder().SetTitle("Account already exists").SetDescription("An account with that display name already exists, please try a different name.").SetColor(0xda373c).Build(), + }, + }, + }) + return + } + + account := fortnite.NewFortnitePerson(display.Value, aid.RandomString(10), false) // or aid.Config.Fortnite.Everything + discord := &storage.DB_DiscordPerson{ + ID: i.Member.User.ID, + PersonID: account.ID, + Username: i.Member.User.Username, + } + storage.Repo.SaveDiscordPerson(discord) + account.Discord = discord + account.Save() + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + NewEmbedBuilder().SetTitle("Account created").SetDescription("Your account has been created with the display name: `"+ account.DisplayName +"`").SetColor(0x2093dc).Build(), + }, + }, + }) +} + +func deleteHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + found := person.FindByDiscord(i.Member.User.ID) + if found == nil { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + NewEmbedBuilder().SetTitle("Account not found").SetDescription("You don't have an account with the bot.").SetColor(0xda373c).Build(), + }, + }, + }) + return + } + + storage.Repo.DeleteDiscordPerson(found.Discord.ID) + found.Delete() + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + NewEmbedBuilder().SetTitle("Account deleted").SetDescription("Your account has been deleted.").SetColor(0x2093dc).Build(), + }, + }, + }) +} + +func informationHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + playerCount := storage.Repo.GetPersonsCount() + totalVbucks := storage.Repo.TotalVBucks() + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + NewEmbedBuilder(). + SetTitle("Information"). + SetDescription("Useful information about this server's activity!"). + SetColor(0x2093dc). + AddField("Players Registered", aid.FormatNumber(playerCount), true). + AddField("Players Online", aid.FormatNumber(0), true). + AddField("VBucks in Circulation", aid.FormatNumber(totalVbucks), false). + Build(), + }, + }, + }) +} \ No newline at end of file diff --git a/fortnite/person.go b/fortnite/person.go index 0c616fe..9e94b91 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -22,7 +22,7 @@ var ( } ) -func NewFortnitePerson(displayName string, key string) *p.Person { +func NewFortnitePerson(displayName string, key string, everything bool) *p.Person { person := p.NewPerson() person.DisplayName = displayName person.AccessKey = key @@ -115,7 +115,7 @@ func NewFortnitePerson(displayName string, key string) *p.Person { person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("last_applied_loadout", loadout.ID)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("active_loadout_index", 0)).Save() - if aid.Config.Fortnite.Everything { + if everything { for _, item := range Cosmetics.Items { if strings.Contains(strings.ToLower(item.ID), "random") { continue @@ -130,4 +130,16 @@ func NewFortnitePerson(displayName string, key string) *p.Person { person.Save() return person +} + +func GiveEverything(person *p.Person) { + for _, item := range Cosmetics.Items { + if strings.Contains(strings.ToLower(item.ID), "random") { + continue + } + + item := p.NewItem(item.Type.BackendValue + ":" + item.ID, 1) + item.HasSeen = true + person.AthenaProfile.Items.AddItem(item).Save() + } } \ No newline at end of file diff --git a/go.mod b/go.mod index 06f176e..64f4c0d 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,9 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // indirect + github.com/bwmarrin/discordgo v0.27.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect diff --git a/go.sum b/go.sum index 30c77b8..dab4c45 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= +github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -157,6 +159,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= diff --git a/main.go b/main.go index 1e88483..5fe0b61 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,10 @@ import ( "fmt" "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/discord" "github.com/ectrc/snow/fortnite" "github.com/ectrc/snow/handlers" "github.com/ectrc/snow/storage" - "github.com/google/uuid" "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" @@ -38,23 +38,9 @@ func init() { } func init() { + discord.IntialiseClient() fortnite.PreloadCosmetics(aid.Config.Fortnite.Season) fortnite.GenerateRandomStorefront() - - if aid.Config.Database.DropAllTables { - person := fortnite.NewFortnitePerson("ac", "1") - - discord := &storage.DB_DiscordPerson{ - ID: uuid.New().String(), - PersonID: person.ID, - } - storage.Repo.SaveDiscordPerson(discord) - - // person.DiscordID = discord.ID - person.Discord = discord - person.Save() - } - fortnite.GeneratePlaylistImages() } diff --git a/person/person.go b/person/person.go index fe7e2b1..c4a3bbf 100644 --- a/person/person.go +++ b/person/person.go @@ -290,4 +290,8 @@ func (p *Person) Snapshot() *PersonSnapshot { Discord: *p.Discord, DiscordID: p.Discord.ID, } -} \ No newline at end of file +} + +func (p *Person) Delete() { + storage.Repo.DeletePerson(p.ID) +} \ No newline at end of file diff --git a/storage/postgres.go b/storage/postgres.go index 43815e3..d5771db 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -128,12 +128,35 @@ func (s *PostgresStorage) GetAllPersons() []*DB_Person { return dbPersons } +func (s *PostgresStorage) GetPersonsCount() int { + var count int64 + s.Postgres.Model(&DB_Person{}).Count(&count) + return int(count) +} + +func (s *PostgresStorage) TotalVBucks() int { + var total int64 + s.Postgres.Model(&DB_Item{}).Select("sum(quantity)").Where("template_id = ?", "Currency:MtxPurchased").Find(&total) + return int(total) +} + func (s *PostgresStorage) SavePerson(person *DB_Person) { s.Postgres.Save(person) } func (s *PostgresStorage) DeletePerson(personId string) { - s.Postgres.Delete(&DB_Person{}, "id = ?", personId) + s.Postgres. + Model(&DB_Person{}). + Preload("Profiles"). + Preload("Profiles.Loadouts"). + Preload("Profiles.Items.Variants"). + Preload("Profiles.Gifts.Loot"). + Preload("Profiles.Attributes"). + Preload("Profiles.Items"). + Preload("Profiles.Gifts"). + Preload("Profiles.Quests"). + Preload("Discord"). + Delete(&DB_Person{}, "id = ?", personId) } func (s *PostgresStorage) SaveProfile(profile *DB_Profile) { diff --git a/storage/storage.go b/storage/storage.go index dc54196..4e92828 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -7,11 +7,16 @@ var ( type Storage interface { Migrate(table interface{}, tableName string) + GetAllPersons() []*DB_Person + GetPersonsCount() int + GetPerson(personId string) *DB_Person GetPersonByDisplay(displayName string) *DB_Person GetPersonByDiscordID(discordId string) *DB_Person - GetAllPersons() []*DB_Person SavePerson(person *DB_Person) + DeletePerson(personId string) + + TotalVBucks() int SaveProfile(profile *DB_Profile) DeleteProfile(profileId string) @@ -85,10 +90,22 @@ func (r *Repository) GetAllPersons() []*DB_Person { return r.Storage.GetAllPersons() } +func (r *Repository) GetPersonsCount() int { + return r.Storage.GetPersonsCount() +} + +func (r *Repository) TotalVBucks() int { + return r.Storage.TotalVBucks() +} + func (r *Repository) SavePerson(person *DB_Person) { r.Storage.SavePerson(person) } +func (r *Repository) DeletePerson(personId string) { + r.Storage.DeletePerson(personId) +} + func (r *Repository) SaveProfile(profile *DB_Profile) { r.Storage.SaveProfile(profile) }