diff --git a/.gitmodules b/.gitmodules index 4efa94f9..8b137891 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1 @@ -[submodule "submodules/discordgo"] - path = submodules/discordgo - url = https://github.com/infinitybotlist/discordgo + diff --git a/apps/apps.go b/apps/apps.go index a8e8f581..e220a598 100644 --- a/apps/apps.go +++ b/apps/apps.go @@ -6,6 +6,7 @@ import ( "popplio/types" "popplio/validators/timex" + "github.com/disgoorg/snowflake/v2" "github.com/infinitybotlist/eureka/uapi" ) @@ -234,7 +235,7 @@ You can only have up to one ban appeal at any given point of time. Abusing the s Hidden: true, // We don't want it to be prominently shown ReviewLogic: reviewLogicBanAppeal, Tags: []string{"Ban Appeal"}, - Channel: func() string { + Channel: func() snowflake.ID { return state.Config.Channels.BanAppeals }, PositionDescription: func(d uapi.RouteData, p types.Position) string { diff --git a/apps/logic.go b/apps/logic.go index a0045ea7..6d9ea6b1 100644 --- a/apps/logic.go +++ b/apps/logic.go @@ -6,9 +6,12 @@ import ( "popplio/state" "popplio/teams" "popplio/types" + "popplio/validators" "strings" - "github.com/bwmarrin/discordgo" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/snowflake/v2" "github.com/infinitybotlist/eureka/dovewing" "github.com/infinitybotlist/eureka/uapi" kittycat "github.com/infinitybotlist/kittycat/go" @@ -65,15 +68,15 @@ func extraLogicResubmit(d uapi.RouteData, p types.Position, answers map[string]s } // Send an embed to the bot logs channel - _, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.BotLogs, &discordgo.MessageSend{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.BotLogs, discord.MessageCreate{ Content: state.Config.Meta.UrgentMentions, - Embeds: []*discordgo.MessageEmbed{ + Embeds: []discord.Embed{ { Title: "Bot Resubmitted!", URL: state.Config.Sites.Frontend.Parse() + "/bots/" + botID, Description: "User <@" + d.Auth.ID + "> has resubmitted their bot", Color: 0x00ff00, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Bot ID", Value: botID, @@ -85,7 +88,7 @@ func extraLogicResubmit(d uapi.RouteData, p types.Position, answers map[string]s { Name: "Reason", Value: answers["reason"], - Inline: true, + Inline: validators.Pointer(true), }, }, }, @@ -158,10 +161,16 @@ func reviewLogicBanAppeal(d uapi.RouteData, resp types.AppResponse, reason strin return errors.New("reason must be less than 384 characters") } - err := state.Discord.GuildBanDelete( + userId, err := snowflake.Parse(resp.UserID) + + if err != nil { + return fmt.Errorf("error parsing user ID: %w", err) + } + + err = state.Discord.Rest().DeleteBan( state.Config.Servers.Main, - resp.UserID, - discordgo.WithAuditLogReason("Ban appeal accepted by "+d.Auth.ID+" | "+reason), + userId, + rest.WithReason("Ban appeal accepted by "+d.Auth.ID+" | "+reason), ) if err != nil { @@ -184,9 +193,15 @@ func reviewLogicCert(d uapi.RouteData, resp types.AppResponse, reason string, ap return errors.New("bot ID not found") } + botIdSnow, err := snowflake.Parse(botID) + + if err != nil { + return fmt.Errorf("error parsing bot ID: %w", err) + } + // Get the bot var botType string - err := state.Pool.QueryRow(d.Context, "SELECT type FROM bots WHERE bot_id = $1", botID).Scan(&botType) + err = state.Pool.QueryRow(d.Context, "SELECT type FROM bots WHERE bot_id = $1", botID).Scan(&botType) if err != nil { return fmt.Errorf("error getting bot type, does the bot exist?: %w", err) @@ -208,21 +223,21 @@ func reviewLogicCert(d uapi.RouteData, resp types.AppResponse, reason string, ap } // Give roles - err = state.Discord.GuildMemberRoleAdd(state.Config.Servers.Main, botID, state.Config.Roles.CertBot) + err = state.Discord.Rest().AddMemberRole(state.Config.Servers.Main, botIdSnow, state.Config.Roles.CertBot) if err != nil { return fmt.Errorf("error giving certified bot role to bot, but successfully certified bot: %v", err) } // Send an embed to the bot logs channel - _, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.BotLogs, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.BotLogs, discord.MessageCreate{ + Embeds: []discord.Embed{ { Title: "Bot Certified!", URL: state.Config.Sites.Frontend.Parse() + "/bots/" + botID, Description: "<@" + d.Auth.ID + "> has certified bot <@" + botID + ">", Color: 0x00ff00, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Bot ID", Value: botID, @@ -232,7 +247,7 @@ func reviewLogicCert(d uapi.RouteData, resp types.AppResponse, reason string, ap Value: reason, }, }, - Footer: &discordgo.MessageEmbedFooter{ + Footer: &discord.EmbedFooter{ Text: "If you are the owner of this bot, use ibb!getbotroles to get your dev roles", }, }, @@ -251,15 +266,21 @@ func reviewLogicCert(d uapi.RouteData, resp types.AppResponse, reason string, ap } func reviewLogicStaff(d uapi.RouteData, resp types.AppResponse, reason string, approve bool) error { + userId, err := snowflake.Parse(resp.UserID) + + if err != nil { + return fmt.Errorf("error parsing user ID: %w", err) + } + if approve { - err := state.Discord.GuildMemberRoleAdd(state.Config.Servers.Main, resp.UserID, state.Config.Roles.AwaitingStaff) + err := state.Discord.Rest().AddMemberRole(state.Config.Servers.Main, userId, state.Config.Roles.AwaitingStaff) if err != nil { return err } // DM the user - dmchan, err := state.Discord.UserChannelCreate(resp.UserID) + dmchan, err := state.Discord.Rest().CreateDMChannel(userId) if err != nil { return errors.New("could not send DM, please ask the user to accept DMs from server members") @@ -269,19 +290,19 @@ func reviewLogicStaff(d uapi.RouteData, resp types.AppResponse, reason string, a return errors.New("reason must be 1024 characters or less") } - _, err = state.Discord.ChannelMessageSendComplex(dmchan.ID, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ + _, err = state.Discord.Rest().CreateMessage(dmchan.ID(), discord.MessageCreate{ + Embeds: []discord.Embed{ { Title: "Staff Application Whitelisted", Description: "Your staff application has been whitelisted for onboarding! Please ping any manager at #staff-only in our discord server to get started.", Color: 0x00ff00, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Feedback", Value: reason, }, }, - Footer: &discordgo.MessageEmbedFooter{ + Footer: &discord.EmbedFooter{ Text: "Congratulations!", }, }, @@ -300,25 +321,25 @@ func reviewLogicStaff(d uapi.RouteData, resp types.AppResponse, reason string, a } // Attempt to DM the user on denial - dmchan, err := state.Discord.UserChannelCreate(resp.UserID) + dmchan, err := state.Discord.Rest().CreateDMChannel(userId) if err != nil { return fmt.Errorf("could not create DM channel with user, please inform them manually, then deny with reason of 'MANUALLYNOTIFIED ': %w", err) } - _, err = state.Discord.ChannelMessageSendComplex(dmchan.ID, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ + _, err = state.Discord.Rest().CreateMessage(dmchan.ID(), discord.MessageCreate{ + Embeds: []discord.Embed{ { Title: "Staff Application Denied", Description: "Unfortunately, we have denied your staff application for Infinity Bot List. You may reapply later if you wish to", Color: 0x00ff00, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Reason", Value: reason, }, }, - Footer: &discordgo.MessageEmbedFooter{ + Footer: &discord.EmbedFooter{ Text: "Better luck next time?", }, }, diff --git a/config/config.go b/config/config.go index bac436fc..78f872bf 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,8 @@ package config import ( _ "embed" "strings" + + "github.com/disgoorg/snowflake/v2" ) const ( @@ -69,19 +71,19 @@ type Sites struct { } type Roles struct { - AwaitingStaff string `yaml:"awaiting_staff" default:"1029058929361174678" comment:"Awaiting Staff Role" validate:"required"` - Apps string `yaml:"apps" default:"907729844605968454" comment:"Apps Role" validate:"required"` - CertBot string `yaml:"cert_bot" default:"759468236999491594" comment:"Certified Bot Role" validate:"required"` - PremiumRoles Differs[[]string] `yaml:"premium_roles" default:"759468236999491594" comment:"Premium Roles" validate:"required"` + AwaitingStaff snowflake.ID `yaml:"awaiting_staff" default:"1029058929361174678" comment:"Awaiting Staff Role" validate:"required"` + Apps snowflake.ID `yaml:"apps" default:"907729844605968454" comment:"Apps Role" validate:"required"` + CertBot snowflake.ID `yaml:"cert_bot" default:"759468236999491594" comment:"Certified Bot Role" validate:"required"` + PremiumRoles Differs[[]snowflake.ID] `yaml:"premium_roles" default:"759468236999491594" comment:"Premium Roles" validate:"required"` } type Channels struct { - BotLogs string `yaml:"bot_logs" default:"762077915499593738" comment:"Bot Logs Channel" validate:"required"` - ModLogs string `yaml:"mod_logs" default:"911907978926493716" comment:"Mod Logs Channel" validate:"required"` - Apps string `yaml:"apps" default:"1034075132030894100" comment:"Apps Channel, should be a staff only channel" validate:"required"` - VoteLogs string `yaml:"vote_logs" default:"762077981811146752" comment:"Vote Logs Channel" validate:"required"` - BanAppeals string `yaml:"ban_appeals" default:"870950610692878337" comment:"Ban Appeals Channel" validate:"required"` - AuthLogs string `yaml:"auth_logs" default:"1075091440117498007" comment:"Auth Logs Channel" validate:"required"` + BotLogs snowflake.ID `yaml:"bot_logs" default:"762077915499593738" comment:"Bot Logs Channel" validate:"required"` + ModLogs snowflake.ID `yaml:"mod_logs" default:"911907978926493716" comment:"Mod Logs Channel" validate:"required"` + Apps snowflake.ID `yaml:"apps" default:"1034075132030894100" comment:"Apps Channel, should be a staff only channel" validate:"required"` + VoteLogs snowflake.ID `yaml:"vote_logs" default:"762077981811146752" comment:"Vote Logs Channel" validate:"required"` + BanAppeals snowflake.ID `yaml:"ban_appeals" default:"870950610692878337" comment:"Ban Appeals Channel" validate:"required"` + AuthLogs snowflake.ID `yaml:"auth_logs" default:"1075091440117498007" comment:"Auth Logs Channel" validate:"required"` } type JAPI struct { @@ -94,7 +96,7 @@ type Notifications struct { } type Servers struct { - Main string `yaml:"main" default:"758641373074423808" comment:"Main Server ID" validate:"required"` + Main snowflake.ID `yaml:"main" default:"758641373074423808" comment:"Main Server ID" validate:"required"` } type Meta struct { diff --git a/go.mod b/go.mod index 6cb32217..8eeb4bde 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,11 @@ require ( github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/disgoorg/disgo v0.18.11 // indirect + github.com/disgoorg/json v1.2.0 // indirect + github.com/disgoorg/snowflake/v2 v2.0.3 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/arch v0.9.0 // indirect ) diff --git a/go.sum b/go.sum index 509d2993..5b2e3a11 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,12 @@ github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80N github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/disgoorg/disgo v0.18.11 h1:hvWpU43PtThNeKOeP6ghx3nyjpgP0oaQz2WL3yizCIA= +github.com/disgoorg/disgo v0.18.11/go.mod h1:mkNGTSWCxIgTXCIg8GqRJedAqNw4T++xOgBOTEk9d7U= +github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= +github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= +github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= +github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= @@ -230,6 +236,8 @@ github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/routes/apps/endpoints/create_app/route.go b/routes/apps/endpoints/create_app/route.go index 525cfda5..d1f4ef21 100644 --- a/routes/apps/endpoints/create_app/route.go +++ b/routes/apps/endpoints/create_app/route.go @@ -6,15 +6,16 @@ import ( "popplio/apps" "popplio/state" "popplio/types" + "popplio/validators" "strconv" "time" + "github.com/disgoorg/disgo/discord" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/uapi" "github.com/jackc/pgx/v5" "go.uber.org/zap" - "github.com/bwmarrin/discordgo" "github.com/go-playground/validator/v10" "github.com/infinitybotlist/eureka/crypto" ) @@ -101,14 +102,14 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } var appBanned bool - err = state.Pool.QueryRow(d.Context, "SELECT app_banned FROM users WHERE user_id = $1", d.Auth.ID).Scan(&appBanned) + err = state.Pool.QueryRow(d.Context, "SELECT app_banned FROM users WHERE user_id = $1", d.Auth.ID).Scan(&appBanned) if err != nil { - state.Logger.Error("Error gettingstate.Pop banned state", zap.Error(err), zap.String("user_id", d.Auth.ID)) + state.Logger.Error("Error gettingstate.Pop banned state", zap.Error(err), zap.String("user_id", d.Auth.ID)) return uapi.DefaultResponse(http.StatusInternalServerError) } - if appBanned { + if appBanned { return uapi.HttpResponse{ Json: types.ApiError{ Message: "You are currently banned from making applications on the site", @@ -277,29 +278,29 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { channel = position.Channel() } - _, err = state.Discord.ChannelMessageSendComplex(channel, &discordgo.MessageSend{ - Content: "<@&" + state.Config.Roles.Apps + ">", - Embeds: []*discordgo.MessageEmbed{ + _, err = state.Discord.Rest().CreateMessage(channel, discord.MessageCreate{ + Content: "<@&" + state.Config.Roles.Apps.String() + ">", + Embeds: []discord.Embed{ { Title: "New " + position.Name + " Application!", URL: state.Config.Sites.Panel.Production() + "/panel/apps", Description: desc, Color: 0x00ff00, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "App ID", Value: appId, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "User ID", Value: d.Auth.ID, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Position", Value: payload.Position, - Inline: true, + Inline: validators.Pointer(true), }, }, }, diff --git a/routes/auth/endpoints/create_oauth2_login/route.go b/routes/auth/endpoints/create_oauth2_login/route.go index e343e0ff..087ce910 100644 --- a/routes/auth/endpoints/create_oauth2_login/route.go +++ b/routes/auth/endpoints/create_oauth2_login/route.go @@ -9,14 +9,15 @@ import ( "popplio/state" "popplio/types" + "popplio/validators" + "github.com/disgoorg/disgo/discord" "github.com/infinitybotlist/eureka/jsonimpl" "github.com/infinitybotlist/eureka/ratelimit" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/uapi" - "github.com/bwmarrin/discordgo" "github.com/go-playground/validator/v10" "github.com/infinitybotlist/eureka/crypto" ua "github.com/mileusna/useragent" @@ -60,30 +61,29 @@ func sendAuthLog(user oauthUser, req types.AuthorizeRequest, new bool) { state.Logger.With( zap.String("user_id", user.ID), - zap.String("channel_id", state.Config.Channels.AuthLogs), - zap.String("bot_info", state.Discord.State.User.String()), + zap.String("channel_id", state.Config.Channels.AuthLogs.String()), ).Debug("sendAuthLog: Channel Info") - _, err := state.Discord.ChannelMessageSendComplex(state.Config.Channels.AuthLogs, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ + _, err := state.Discord.Rest().CreateMessage(state.Config.Channels.AuthLogs, discord.MessageCreate{ + Embeds: []discord.Embed{ { Title: "User Login Attempt", Color: 0xff0000, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "User ID", Value: user.ID, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Username", Value: user.Username + "#" + user.Disc, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Scope", Value: req.Scope, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Banned", @@ -94,7 +94,7 @@ func sendAuthLog(user oauthUser, req types.AuthorizeRequest, new bool) { return "No" }(), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Vote Banned", @@ -105,7 +105,7 @@ func sendAuthLog(user oauthUser, req types.AuthorizeRequest, new bool) { return "No" }(), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "App Banned", @@ -116,7 +116,7 @@ func sendAuthLog(user oauthUser, req types.AuthorizeRequest, new bool) { return "No" }(), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "New User", @@ -127,13 +127,13 @@ func sendAuthLog(user oauthUser, req types.AuthorizeRequest, new bool) { return "No" }(), - Inline: true, + Inline: validators.Pointer(true), }, }, - Footer: &discordgo.MessageEmbedFooter{ + Footer: &discord.EmbedFooter{ Text: "© Copyright 2023 - Infinity Bot List", }, - Timestamp: time.Now().Format(time.RFC3339), + Timestamp: validators.Pointer(time.Now()), }, }, }) diff --git a/routes/bots/endpoints/add_bot/route.go b/routes/bots/endpoints/add_bot/route.go index 28d1a7db..e1fe94f9 100644 --- a/routes/bots/endpoints/add_bot/route.go +++ b/routes/bots/endpoints/add_bot/route.go @@ -16,6 +16,7 @@ import ( "popplio/types" "popplio/validators" + "github.com/disgoorg/disgo/discord" "github.com/google/uuid" "github.com/infinitybotlist/eureka/ratelimit" "github.com/jackc/pgx/v5/pgtype" @@ -27,7 +28,6 @@ import ( "github.com/infinitybotlist/eureka/uapi" kittycat "github.com/infinitybotlist/kittycat/go" - "github.com/bwmarrin/discordgo" "github.com/go-playground/validator/v10" ) @@ -340,25 +340,25 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { return uapi.DefaultResponse(http.StatusInternalServerError) } - _, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.BotLogs, &discordgo.MessageSend{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.BotLogs, discord.MessageCreate{ Content: state.Config.Meta.UrgentMentions, - Embeds: []*discordgo.MessageEmbed{ + Embeds: []discord.Embed{ { URL: state.Config.Sites.Frontend.Production() + "/bots/" + payload.BotID, Title: "New Bot Added", - Thumbnail: &discordgo.MessageEmbedThumbnail{ + Thumbnail: &discord.EmbedResource{ URL: metadata.Avatar, }, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Name", Value: metadata.Name, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Bot ID", Value: payload.BotID, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Owner", @@ -368,7 +368,7 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } return fmt.Sprintf("<@%s>", d.Auth.ID) }(), - Inline: true, + Inline: validators.Pointer(true), }, }, }, diff --git a/routes/bots/endpoints/delete_bot/route.go b/routes/bots/endpoints/delete_bot/route.go index 230e5b81..783f8678 100644 --- a/routes/bots/endpoints/delete_bot/route.go +++ b/routes/bots/endpoints/delete_bot/route.go @@ -6,7 +6,7 @@ import ( "popplio/state" "popplio/types" - "github.com/bwmarrin/discordgo" + "github.com/disgoorg/disgo/discord" "github.com/go-chi/chi/v5" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/uapi" @@ -58,14 +58,14 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } // Send embed to bot log channel - _, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.ModLogs, &discordgo.MessageSend{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.ModLogs, discord.MessageCreate{ Content: "", - Embeds: []*discordgo.MessageEmbed{ + Embeds: []discord.Embed{ { URL: state.Config.Sites.Frontend.Production() + "/bots/" + id, Title: "Bot Deleted", Color: 0xff0000, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Bot ID", Value: id, diff --git a/routes/bots/endpoints/patch_bot_settings/route.go b/routes/bots/endpoints/patch_bot_settings/route.go index f12333b3..3dfd4c76 100644 --- a/routes/bots/endpoints/patch_bot_settings/route.go +++ b/routes/bots/endpoints/patch_bot_settings/route.go @@ -10,12 +10,12 @@ import ( "strconv" "strings" + "github.com/disgoorg/disgo/discord" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/dovewing" "github.com/infinitybotlist/eureka/uapi" "go.uber.org/zap" - "github.com/bwmarrin/discordgo" "github.com/go-chi/chi/v5" "github.com/go-playground/validator/v10" ) @@ -133,34 +133,43 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } // Send a message to the bot logs channel - state.Discord.ChannelMessageSendComplex(state.Config.Channels.BotLogs, &discordgo.MessageSend{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.BotLogs, discord.MessageCreate{ Content: "", - Embeds: []*discordgo.MessageEmbed{ + Embeds: []discord.Embed{ { URL: state.Config.Sites.Frontend.Production() + "/bots/" + id, Title: "Bot Updated", - Thumbnail: &discordgo.MessageEmbedThumbnail{ + Thumbnail: &discord.EmbedResource{ URL: botUser.Avatar, }, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Name", Value: botUser.Username, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Bot ID", Value: "<@" + id + ">", - Inline: true, + Inline: validators.Pointer(true), }, { Name: "User", Value: fmt.Sprintf("<@%s>", d.Auth.ID), - Inline: true, + Inline: validators.Pointer(true), }, }, }, }, }) + + if err != nil { + state.Logger.Error("Error while sending embed to mod logs channel", zap.Error(err), zap.String("serverID", id)) + return uapi.HttpResponse{ + Status: http.StatusInternalServerError, + Json: types.ApiError{Message: "Internal Error: While bot update was successful, an error occurred while sending the update embed to the mod logs channel"}, + } + } + return uapi.DefaultResponse(http.StatusNoContent) } diff --git a/routes/bots/endpoints/patch_bot_team/route.go b/routes/bots/endpoints/patch_bot_team/route.go index 307b0010..98901f92 100644 --- a/routes/bots/endpoints/patch_bot_team/route.go +++ b/routes/bots/endpoints/patch_bot_team/route.go @@ -9,13 +9,13 @@ import ( "popplio/types" "popplio/validators" + "github.com/disgoorg/disgo/discord" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/uapi" kittycat "github.com/infinitybotlist/kittycat/go" "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" - "github.com/bwmarrin/discordgo" "github.com/go-chi/chi/v5" ) @@ -125,21 +125,21 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } // Send message to mod logs - state.Discord.ChannelMessageSendComplex(state.Config.Channels.ModLogs, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ + state.Discord.Rest().CreateMessage(state.Config.Channels.ModLogs, discord.MessageCreate{ + Embeds: []discord.Embed{ { URL: state.Config.Sites.Frontend.Parse() + "/bots/" + id, Title: "Bot Team Update!", - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Bot ID", Value: id, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Performed By", Value: fmt.Sprintf("<@%s>", d.Auth.ID), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Old Team", diff --git a/routes/payments/assets/booster_status.go b/routes/payments/assets/booster_status.go index f84fcd89..ee2ca637 100644 --- a/routes/payments/assets/booster_status.go +++ b/routes/payments/assets/booster_status.go @@ -4,23 +4,24 @@ import ( "popplio/state" "popplio/types" + "github.com/disgoorg/snowflake/v2" "golang.org/x/exp/slices" ) -func CheckUserBoosterStatus(id string) types.BoosterStatus { +func CheckUserBoosterStatus(id snowflake.ID) types.BoosterStatus { // Check member is a booster - m, err := state.Discord.State.Member(state.Config.Servers.Main, id) + m, ok := state.Discord.Caches().Member(state.Config.Servers.Main, id) - if err != nil { + if !ok { return types.BoosterStatus{ - Remark: "Member not found on server:" + err.Error(), + Remark: "Member not found on server", IsBooster: false, } } // Check if member has booster role roles := state.Config.Roles.PremiumRoles.Parse() - for _, role := range m.Roles { + for _, role := range m.RoleIDs { if slices.Contains(roles, role) { // Member has booster role return types.BoosterStatus{ diff --git a/routes/payments/assets/give_perks.go b/routes/payments/assets/give_perks.go index a8e7d117..8b7f67b1 100644 --- a/routes/payments/assets/give_perks.go +++ b/routes/payments/assets/give_perks.go @@ -8,7 +8,7 @@ import ( "popplio/validators" "strconv" - "github.com/bwmarrin/discordgo" + "github.com/disgoorg/disgo/discord" "go.uber.org/zap" ) @@ -132,7 +132,7 @@ func GivePerks(ctx context.Context, perkData PerkData) error { return errors.New("our database broke, please try again later") } - _, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.ModLogs, &discordgo.MessageSend{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.ModLogs, discord.MessageCreate{ Content: "<@" + perkData.UserID + "> has bought <@" + botID + "> premium for " + strconv.Itoa(perk.TimePeriod) + " hours.", }) diff --git a/routes/payments/endpoints/redeem_payment_offer/route.go b/routes/payments/endpoints/redeem_payment_offer/route.go index 2b9344d8..84d157ea 100644 --- a/routes/payments/endpoints/redeem_payment_offer/route.go +++ b/routes/payments/endpoints/redeem_payment_offer/route.go @@ -7,6 +7,7 @@ import ( "popplio/types" "time" + "github.com/disgoorg/snowflake/v2" "github.com/infinitybotlist/eureka/ratelimit" "go.uber.org/zap" @@ -119,7 +120,19 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } // Check that the user is in fact a booster - bs := assets.CheckUserBoosterStatus(d.Auth.ID) + userId, err := snowflake.Parse(d.Auth.ID) + + if err != nil { + state.Logger.Error("Error while parsing snowflake", zap.Error(err), zap.String("userID", d.Auth.ID)) + return uapi.HttpResponse{ + Status: http.StatusBadRequest, + Json: types.ApiError{ + Message: "Error: " + err.Error(), + }, + } + } + + bs := assets.CheckUserBoosterStatus(userId) if !bs.IsBooster { return uapi.HttpResponse{ diff --git a/routes/servers/endpoints/patch_server_settings/route.go b/routes/servers/endpoints/patch_server_settings/route.go index e16b44e1..ad1c236a 100644 --- a/routes/servers/endpoints/patch_server_settings/route.go +++ b/routes/servers/endpoints/patch_server_settings/route.go @@ -11,11 +11,11 @@ import ( "strconv" "strings" + "github.com/disgoorg/disgo/discord" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/uapi" "go.uber.org/zap" - "github.com/bwmarrin/discordgo" "github.com/go-chi/chi/v5" "github.com/go-playground/validator/v10" ) @@ -132,34 +132,43 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { avatar := assetmanager.AvatarInfo(assetmanager.AssetTargetTypeServer, id) // Send a message to the bot logs channel - state.Discord.ChannelMessageSendComplex(state.Config.Channels.ModLogs, &discordgo.MessageSend{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.ModLogs, discord.MessageCreate{ Content: "", - Embeds: []*discordgo.MessageEmbed{ + Embeds: []discord.Embed{ { URL: state.Config.Sites.Frontend.Production() + "/servers/" + id, Title: "Server Updated", - Thumbnail: &discordgo.MessageEmbedThumbnail{ + Thumbnail: &discord.EmbedResource{ URL: assetmanager.ResolveAssetMetadataToUrl(avatar), }, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Name", Value: name, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Server ID", Value: id, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "User", Value: fmt.Sprintf("<@%s>", d.Auth.ID), - Inline: true, + Inline: validators.Pointer(true), }, }, }, }, }) + + if err != nil { + state.Logger.Error("Error while sending embed to mod logs channel", zap.Error(err), zap.String("serverID", id)) + return uapi.HttpResponse{ + Status: http.StatusInternalServerError, + Json: types.ApiError{Message: "Internal Error: While server update was successful, an error occurred while sending the update embed to the mod logs channel"}, + } + } + return uapi.DefaultResponse(http.StatusNoContent) } diff --git a/routes/staff/endpoints/manage_app/route.go b/routes/staff/endpoints/manage_app/route.go index 98006d46..25b46e6f 100644 --- a/routes/staff/endpoints/manage_app/route.go +++ b/routes/staff/endpoints/manage_app/route.go @@ -12,6 +12,7 @@ import ( "popplio/validators" "strings" + "github.com/disgoorg/disgo/discord" kittycat "github.com/infinitybotlist/kittycat/go" docs "github.com/infinitybotlist/eureka/doclib" @@ -19,7 +20,6 @@ import ( "github.com/jackc/pgx/v5" "go.uber.org/zap" - "github.com/bwmarrin/discordgo" "github.com/go-chi/chi/v5" "github.com/go-playground/validator/v10" ) @@ -150,7 +150,7 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } } - var embeds []*discordgo.MessageEmbed + var embeds []discord.Embed if payload.Approved { if position.ReviewLogic != nil { @@ -174,37 +174,37 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { return uapi.DefaultResponse(http.StatusInternalServerError) } - embeds = []*discordgo.MessageEmbed{ + embeds = []discord.Embed{ { Title: "Application Approved", URL: state.Config.Sites.Panel.Production() + "/panel/apps", Description: fmt.Sprintf("<@%s> has approved an application by <@%s> for the position of %s", d.Auth.ID, app.UserID, app.Position), Color: 0x00ff00, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "App ID", Value: appId, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "User ID", Value: app.UserID, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Approved By", Value: fmt.Sprintf("<@%s>", d.Auth.ID), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Position", Value: app.Position, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Feedback", Value: payload.Reason, - Inline: false, + Inline: validators.Pointer(true), }, }, }, @@ -231,37 +231,37 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { return uapi.DefaultResponse(http.StatusInternalServerError) } - embeds = []*discordgo.MessageEmbed{ + embeds = []discord.Embed{ { Title: "Application Denied", URL: state.Config.Sites.Panel.Production() + "/panel/apps", Description: fmt.Sprintf("<@%s> has denied an application by <@%s> for the position of %s", d.Auth.ID, app.UserID, app.Position), Color: 0xff0000, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "App ID", Value: appId, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "User ID", Value: app.UserID, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Denied By", Value: fmt.Sprintf("<@%s>", d.Auth.ID), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Position", Value: app.Position, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Reason", Value: payload.Reason, - Inline: false, + Inline: validators.Pointer(true), }, }, }, @@ -269,40 +269,14 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { } // Send message to apps channel - _, err = state.Discord.ChannelMessageSendEmbeds(state.Config.Channels.Apps, embeds) + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.Apps, discord.MessageCreate{ + Embeds: embeds, + }) if err != nil { state.Logger.Error("Failed to send message to apps channel", zap.Error(err), zap.String("appId", appId)) return uapi.DefaultResponse(http.StatusInternalServerError) } - // Send message to user if in main server - _, err = state.Discord.State.Member(state.Config.Servers.Main, app.UserID) - - if err == nil { - dm, err := state.Discord.UserChannelCreate(app.UserID) - - if err != nil { - state.Logger.Error("Failed to create DM channel", zap.Error(err), zap.String("appId", appId)) - return uapi.HttpResponse{ - Status: http.StatusInternalServerError, - Json: types.ApiError{ - Message: "Could not send DM, but app was updated successfully", - }, - } - } - - _, err = state.Discord.ChannelMessageSendEmbeds(dm.ID, embeds) - - if err != nil { - state.Logger.Error("Failed to send message to user", zap.Error(err), zap.String("appId", appId)) - return uapi.HttpResponse{ - Status: http.StatusInternalServerError, - Json: types.ApiError{ - Message: "Could not send DM, but app was updated successfully", - }, - } - } - } return uapi.DefaultResponse(http.StatusNoContent) } diff --git a/routes/users/endpoints/check_booster_status/route.go b/routes/users/endpoints/check_booster_status/route.go index e7cdfae5..152c3195 100644 --- a/routes/users/endpoints/check_booster_status/route.go +++ b/routes/users/endpoints/check_booster_status/route.go @@ -6,6 +6,7 @@ import ( "popplio/routes/payments/assets" "popplio/types" + "github.com/disgoorg/snowflake/v2" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/uapi" @@ -32,7 +33,18 @@ func Docs() *docs.Doc { func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { id := chi.URLParam(r, "id") + idSnow, err := snowflake.Parse(id) + + if err != nil { + return uapi.HttpResponse{ + Json: types.ApiError{ + Message: "Invalid ID", + }, + Status: http.StatusBadRequest, + } + } + return uapi.HttpResponse{ - Json: assets.CheckUserBoosterStatus(id), + Json: assets.CheckUserBoosterStatus(idSnow), } } diff --git a/routes/votes/endpoints/create_user_entity_vote/route.go b/routes/votes/endpoints/create_user_entity_vote/route.go index fc4ac2d6..8dc43607 100644 --- a/routes/votes/endpoints/create_user_entity_vote/route.go +++ b/routes/votes/endpoints/create_user_entity_vote/route.go @@ -12,7 +12,7 @@ import ( "popplio/webhooks/core/drivers" "popplio/webhooks/events" - "github.com/bwmarrin/discordgo" + "github.com/disgoorg/disgo/discord" docs "github.com/infinitybotlist/eureka/doclib" "github.com/infinitybotlist/eureka/dovewing" "github.com/infinitybotlist/eureka/uapi" @@ -239,36 +239,36 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse { return } - _, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.VoteLogs, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{ + _, err = state.Discord.Rest().CreateMessage(state.Config.Channels.VoteLogs, discord.MessageCreate{ + Embeds: []discord.Embed{ { URL: entityInfo.URL, - Thumbnail: &discordgo.MessageEmbedThumbnail{ + Thumbnail: &discord.EmbedResource{ URL: entityInfo.Avatar, }, Title: "🎉 Vote Count Updated!", Description: ":heart:" + userObj.DisplayName + " has voted for " + targetType + ": " + entityInfo.Name, Color: 0x8A6BFD, - Fields: []*discordgo.MessageEmbedField{ + Fields: []discord.EmbedField{ { Name: "Vote Count:", Value: strconv.Itoa(nvc), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "Votes Added:", Value: strconv.Itoa(vi.VoteInfo.PerUser), - Inline: true, + Inline: validators.Pointer(true), }, { Name: "User ID:", Value: userObj.ID, - Inline: true, + Inline: validators.Pointer(true), }, { Name: "View " + targetType + "'s page", Value: "[View " + entityInfo.Name + "](" + entityInfo.URL + ")", - Inline: true, + Inline: validators.Pointer(true), }, }, }, diff --git a/state/discord_dovewing/discord.go b/state/discord_dovewing/discord.go deleted file mode 100644 index fc15f59b..00000000 --- a/state/discord_dovewing/discord.go +++ /dev/null @@ -1,189 +0,0 @@ -package discord_dovewing - -import ( - "context" - "errors" - "strconv" - - "github.com/bwmarrin/discordgo" - "github.com/infinitybotlist/eureka/dovewing" - "github.com/infinitybotlist/eureka/dovewing/dovetypes" -) - -var supportedBotFlags = map[string]int64{ - "BOT_HTTP_INTERACTIONS": 1 << 19, // BOT_HTTP_INTERACTIONS - "VERIFIED_BOT": 1 << 16, // VERIFIED_BOT -} - -func flagsToArray(u *discordgo.User) []string { - var arr = []string{} - - if u.Bot { - for flag, val := range supportedBotFlags { - if int64(u.PublicFlags)&val == val { - arr = append(arr, flag) - } - } - } - - return arr -} - -func discordPlatformStatus(status discordgo.Status) dovetypes.PlatformStatus { - switch status { - case discordgo.StatusOnline: - return dovetypes.PlatformStatusOnline - case discordgo.StatusIdle: - return dovetypes.PlatformStatusIdle - case discordgo.StatusDoNotDisturb: - return dovetypes.PlatformStatusDoNotDisturb - default: - return dovetypes.PlatformStatusOffline - } -} - -type DiscordState struct { - config *DiscordStateConfig // Config for the discord state - initialized bool // Whether the platform has been initted or not -} - -type DiscordStateConfig struct { - Session *discordgo.Session // Discord session - PreferredGuild string // Which guilds should be checked first for users, good if theres one guild with the majority of users - BaseState *dovewing.BaseState // Base state -} - -func (c DiscordStateConfig) New() (*DiscordState, error) { - if c.Session == nil { - return nil, errors.New("discord not enabled") - } - - if c.BaseState == nil { - return nil, errors.New("base state not provided") - } - - return &DiscordState{ - config: &c, - }, nil -} - -func (d *DiscordState) PlatformName() string { - return "discord" -} - -func (d *DiscordState) Init() error { - d.initialized = true - return nil -} - -func (d *DiscordState) Initted() bool { - return d.initialized -} - -func (d *DiscordState) GetState() *dovewing.BaseState { - return d.config.BaseState -} - -func (d *DiscordState) ValidateId(id string) (string, error) { - // Before wasting time searching state, ensure the ID is actually a valid snowflake - if _, err := strconv.ParseUint(id, 10, 64); err != nil { - return "", err - } - - // For all practical purposes, a simple length check can handle a lot of illegal IDs - if len(id) <= 16 || len(id) > 20 { - return "", errors.New("invalid snowflake") - } - - return id, nil -} - -func (d *DiscordState) PlatformSpecificCache(ctx context.Context, id string) (*dovetypes.PlatformUser, error) { - // First try for main server - if d.config.PreferredGuild != "" { - member, err := d.config.Session.State.Member(d.config.PreferredGuild, id) - - if err == nil { - p, pErr := d.config.Session.State.Presence(d.config.PreferredGuild, id) - - if pErr != nil { - p = &discordgo.Presence{ - User: member.User, - Status: discordgo.StatusOffline, - } - } - - return &dovetypes.PlatformUser{ - ID: id, - Username: member.User.Username, - Avatar: member.User.AvatarURL(""), - DisplayName: member.User.GlobalName, - Bot: member.User.Bot, - Flags: flagsToArray(member.User), - ExtraData: map[string]any{ - "nickname": member.Nick, - "mutual_guild": d.config.PreferredGuild, - "preferred_guild": true, - "public_flags": member.User.PublicFlags, - }, - Status: discordPlatformStatus(p.Status), - }, nil - } - } - - for _, guild := range d.config.Session.State.Guilds { - if guild.ID == d.config.PreferredGuild { - continue // Already checked - } - - member, err := d.config.Session.State.Member(guild.ID, id) - - if err == nil { - p, pErr := d.config.Session.State.Presence(guild.ID, id) - - if pErr != nil { - p = &discordgo.Presence{ - User: member.User, - Status: discordgo.StatusOffline, - } - } - - return &dovetypes.PlatformUser{ - ID: id, - Username: member.User.Username, - Avatar: member.User.AvatarURL(""), - DisplayName: member.User.GlobalName, - Bot: member.User.Bot, - Flags: flagsToArray(member.User), - ExtraData: map[string]any{ - "nickname": member.Nick, - "mutual_guild": guild.ID, - "preferred_guild": false, - "public_flags": member.User.PublicFlags, - }, - Status: discordPlatformStatus(p.Status), - }, nil - } - } - - return nil, nil -} - -func (d *DiscordState) GetUser(ctx context.Context, id string) (*dovetypes.PlatformUser, error) { - // Get from discord - user, err := d.config.Session.User(id) - - if err != nil { - return nil, err - } - - return &dovetypes.PlatformUser{ - ID: id, - Username: user.Username, - Avatar: user.AvatarURL(""), - DisplayName: user.GlobalName, - Bot: user.Bot, - Status: dovetypes.PlatformStatusOffline, - Flags: flagsToArray(user), - }, nil -} diff --git a/state/discord_dovewing/disgo.go b/state/discord_dovewing/disgo.go new file mode 100644 index 00000000..70838fc5 --- /dev/null +++ b/state/discord_dovewing/disgo.go @@ -0,0 +1,232 @@ +package discord_dovewing + +import ( + "context" + "errors" + "strconv" + + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" + "github.com/infinitybotlist/eureka/dovewing" + "github.com/infinitybotlist/eureka/dovewing/dovetypes" +) + +func disgoFlagsToArray(u *discord.User) []string { + var arr = []string{} + + if u.Bot { + if u.PublicFlags.Has(discord.UserFlagBotHTTPInteractions) { + arr = append(arr, "BOT_HTTP_INTERACTIONS") + } + + if u.PublicFlags.Has(discord.UserFlagVerifiedBot) { + arr = append(arr, "VERIFIED_BOT") + } + } + + return arr +} + +func disgoPlatformStatus(status discord.OnlineStatus) dovetypes.PlatformStatus { + switch status { + case discord.OnlineStatusOnline: + return dovetypes.PlatformStatusOnline + case discord.OnlineStatusIdle: + return dovetypes.PlatformStatusIdle + case discord.OnlineStatusDND: + return dovetypes.PlatformStatusDoNotDisturb + default: + return dovetypes.PlatformStatusOffline + } +} + +type DisgoState struct { + config *DisgoStateConfig // Config for the discord state + memberCache cache.GroupedCache[discord.Member] // Member cache + guildsCache cache.Cache[discord.Guild] // Guild cache + presenceCache cache.GroupedCache[discord.Presence] // Presence cache + initialized bool // Whether the platform has been initted or not +} + +type DisgoStateConfig struct { + Client bot.Client // Discord session + PreferredGuild *snowflake.ID // Which guilds should be checked first for users, good if theres one guild with the majority of users + BaseState *dovewing.BaseState // Base state +} + +func (c DisgoStateConfig) New() (*DisgoState, error) { + if c.Client == nil { + return nil, errors.New("discord not enabled") + } + + if c.BaseState == nil { + return nil, errors.New("base state not provided") + } + + return &DisgoState{ + config: &c, + }, nil +} + +func (d *DisgoState) PlatformName() string { + return "discord" +} + +func (d *DisgoState) Init() error { + caches := d.config.Client.Caches() + + if caches == nil { + return errors.New("cache not enabled") + } + + memberCache := caches.MemberCache() + + if memberCache == nil { + return errors.New("member cache not enabled") + } + + d.memberCache = memberCache + + presenceCache := caches.PresenceCache() + + if presenceCache == nil { + return errors.New("presence cache not enabled") + } + + d.presenceCache = presenceCache + + guildsCache := caches.GuildCache() + + if guildsCache == nil { + return errors.New("guild cache not enabled") + } + + d.guildsCache = guildsCache + + d.initialized = true + return nil +} + +func (d *DisgoState) Initted() bool { + return d.initialized +} + +func (d *DisgoState) GetState() *dovewing.BaseState { + return d.config.BaseState +} + +func (d *DisgoState) ValidateId(id string) (string, error) { + // Before wasting time searching state, ensure the ID is actually a valid snowflake + if _, err := strconv.ParseUint(id, 10, 64); err != nil { + return "", err + } + + // For all practical purposes, a simple length check can handle a lot of illegal IDs + if len(id) <= 16 || len(id) > 20 { + return "", errors.New("invalid snowflake") + } + + return id, nil +} + +func (d *DisgoState) PlatformSpecificCache(ctx context.Context, idStr string) (*dovetypes.PlatformUser, error) { + id, err := snowflake.Parse(idStr) + + if err != nil { + return nil, err + } + + // First try for main server + if d.config.PreferredGuild != nil { + member, ok := d.memberCache.Get(*d.config.PreferredGuild, id) + + if ok { + p, pOk := d.presenceCache.Get(*d.config.PreferredGuild, id) + + var status = discord.OnlineStatusOffline + if pOk { + status = p.Status + } + + return &dovetypes.PlatformUser{ + ID: idStr, + Username: member.User.Username, + Avatar: member.User.EffectiveAvatarURL(), + DisplayName: member.EffectiveName(), + Bot: member.User.Bot, + Flags: disgoFlagsToArray(&member.User), + ExtraData: map[string]any{ + "nickname": member.Nick, + "mutual_guild": d.config.PreferredGuild, + "preferred_guild": true, + "public_flags": member.User.PublicFlags, + }, + Status: disgoPlatformStatus(status), + }, nil + } + } + + var puser *dovetypes.PlatformUser + d.config.Client.Caches().GuildCache().ForEach(func(guild discord.Guild) { + if puser != nil || err != nil { + return + } + + member, ok := d.memberCache.Get(guild.ID, id) + + if ok { + p, pOk := d.presenceCache.Get(guild.ID, id) + + var status = discord.OnlineStatusOffline + if pOk { + status = p.Status + } + + puser = &dovetypes.PlatformUser{ + ID: idStr, + Username: member.User.Username, + Avatar: member.User.EffectiveAvatarURL(), + DisplayName: member.EffectiveName(), + Bot: member.User.Bot, + Flags: disgoFlagsToArray(&member.User), + ExtraData: map[string]any{ + "nickname": member.Nick, + "mutual_guild": guild.ID.String(), + "preferred_guild": false, + "public_flags": member.User.PublicFlags, + }, + Status: disgoPlatformStatus(status), + } + err = nil + } + }) + + return puser, err +} + +func (d *DisgoState) GetUser(ctx context.Context, idStr string) (*dovetypes.PlatformUser, error) { + // Get from discord + id, err := snowflake.Parse(idStr) + + if err != nil { + return nil, err + } + + user, err := d.config.Client.Rest().GetUser(id) + + if err != nil { + return nil, err + } + + return &dovetypes.PlatformUser{ + ID: idStr, + Username: user.Username, + Avatar: user.EffectiveAvatarURL(), + DisplayName: user.EffectiveName(), + Bot: user.Bot, + Status: dovetypes.PlatformStatusOffline, + Flags: disgoFlagsToArray(user), + }, nil +} diff --git a/state/state.go b/state/state.go index d0139cbf..f2998037 100644 --- a/state/state.go +++ b/state/state.go @@ -3,6 +3,7 @@ package state import ( "context" "io" + "log/slog" "net/http" "os" "reflect" @@ -13,11 +14,15 @@ import ( "popplio/seo" "popplio/state/discord_dovewing" + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/disgo/sharding" "github.com/infinitybotlist/eureka/dovewing/dovetypes" hredis "github.com/infinitybotlist/eureka/hotcache/redis" "github.com/infinitybotlist/eureka/ratelimit" - "github.com/bwmarrin/discordgo" "github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10/non-standard/validators" "github.com/infinitybotlist/eureka/dovewing" @@ -36,7 +41,7 @@ var ( Pool *pgxpool.Pool Paypal *paypal.Client Redis *redis.Client - Discord *discordgo.Session + Discord bot.Client Logger *zap.Logger Context = context.Background() Validator = validator.New() @@ -108,22 +113,35 @@ func Setup() { Redis = redis.NewClient(rOptions) - Discord, err = discordgo.New("Bot " + Config.DiscordAuth.Token) + Discord, err = disgo.New(Config.DiscordAuth.Token, bot.WithShardManagerConfigOpts( + sharding.WithAutoScaling(true), + sharding.WithGatewayConfigOpts( + gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildPresences, gateway.IntentGuildMembers), + gateway.WithCompress(true), + ), + ), + bot.WithEventListeners(&events.ListenerAdapter{ + OnGuildReady: func(event *events.GuildReady) { + slog.Info("guild %s ready", event.GuildID) + }, + OnGuildsReady: func(event *events.GuildsReady) { + slog.Info("guilds on shard %d ready", event.ShardID) + }, + }), + ) if err != nil { panic(err) } - Discord.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentGuildPresences | discordgo.IntentsGuildMembers - go func() { - err = Discord.Open() - if err != nil { - panic(err) + if err = Discord.OpenShardManager(Context); err != nil { + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return } if config.CurrentEnv == config.CurrentEnvProd { - err = Discord.UpdateWatchStatus(0, Config.Sites.Frontend.Parse()) + Discord.SetPresence(Context, gateway.WithWatchingActivity(Config.Sites.Frontend.Parse())) if err != nil { panic(err) @@ -145,9 +163,9 @@ func Setup() { UserExpiryTime: 8 * time.Hour, } - DovewingPlatformDiscord, err = discord_dovewing.DiscordStateConfig{ - Session: Discord, - PreferredGuild: Config.Servers.Main, + DovewingPlatformDiscord, err = discord_dovewing.DisgoStateConfig{ + Client: Discord, + PreferredGuild: &Config.Servers.Main, BaseState: &BaseDovewingState, }.New() diff --git a/types/apps.go b/types/apps.go index fbcdb769..0296ad73 100644 --- a/types/apps.go +++ b/types/apps.go @@ -4,6 +4,7 @@ import ( "popplio/validators/timex" "time" + "github.com/disgoorg/snowflake/v2" "github.com/infinitybotlist/eureka/dovewing/dovetypes" "github.com/infinitybotlist/eureka/uapi" ) @@ -27,7 +28,7 @@ type Position struct { Cooldown timex.Duration `json:"cooldown"` // Internal fields - Channel func() string `json:"-"` + Channel func() snowflake.ID `json:"-"` ExtraLogic func(d uapi.RouteData, p Position, answers map[string]string) error `json:"-"` PositionDescription func(d uapi.RouteData, p Position) string `json:"-"` // Used for custom position descriptions AllowedForBanned bool `json:"-"` // If true, banned users can apply for this position diff --git a/validators/ptr.go b/validators/ptr.go new file mode 100644 index 00000000..2240dd3c --- /dev/null +++ b/validators/ptr.go @@ -0,0 +1,5 @@ +package validators + +func Pointer[T any](v T) *T { + return &v +}