From 11067ab405ff1b52bc4f73571a732f4a7c36bf32 Mon Sep 17 00:00:00 2001 From: Ashish Date: Sat, 5 Oct 2024 02:35:05 +0530 Subject: [PATCH 1/3] Added support for discord entitlements --- bot/eventsystem/events.go | 276 +++++++++++------- bot/eventsystem/gen/events_gen.go | 18 +- cmd/yagpdb/main.go | 2 + go.mod | 11 +- go.sum | 31 +- lib/discordgo/endpoints.go | 37 +++ lib/discordgo/eventhandlers.go | 144 +++++++++ lib/discordgo/events.go | 24 ++ lib/discordgo/interactions.go | 2 + lib/discordgo/restapi.go | 116 ++++++++ lib/discordgo/structs.go | 179 ++++++++++++ premium/codepremiumsource.go | 38 +++ premium/discordpremiumsource/bot.go | 214 ++++++++++++++ .../discordpremiumsource.go | 162 ++++++++++ premium/discordpremiumsource/poller.go | 111 +++++++ .../patreonpremiumsource.go | 2 +- premium/slotpremiumsource.go | 4 +- yagpdb_docker/app.example.env | 1 + 18 files changed, 1231 insertions(+), 141 deletions(-) create mode 100644 premium/discordpremiumsource/bot.go create mode 100644 premium/discordpremiumsource/discordpremiumsource.go create mode 100644 premium/discordpremiumsource/poller.go diff --git a/bot/eventsystem/events.go b/bot/eventsystem/events.go index 38512a4d95..f54ef22906 100644 --- a/bot/eventsystem/events.go +++ b/bot/eventsystem/events.go @@ -35,61 +35,67 @@ const ( EventChannelUpdate Event = 20 EventConnect Event = 21 EventDisconnect Event = 22 - EventGuildAuditLogEntryCreate Event = 23 - EventGuildBanAdd Event = 24 - EventGuildBanRemove Event = 25 - EventGuildCreate Event = 26 - EventGuildDelete Event = 27 - EventGuildEmojisUpdate Event = 28 - EventGuildIntegrationsUpdate Event = 29 - EventGuildJoinRequestDelete Event = 30 - EventGuildJoinRequestUpdate Event = 31 - EventGuildMemberAdd Event = 32 - EventGuildMemberRemove Event = 33 - EventGuildMemberUpdate Event = 34 - EventGuildMembersChunk Event = 35 - EventGuildRoleCreate Event = 36 - EventGuildRoleDelete Event = 37 - EventGuildRoleUpdate Event = 38 - EventGuildStickersUpdate Event = 39 - EventGuildUpdate Event = 40 - EventInteractionCreate Event = 41 - EventInviteCreate Event = 42 - EventInviteDelete Event = 43 - EventMessageAck Event = 44 - EventMessageCreate Event = 45 - EventMessageDelete Event = 46 - EventMessageDeleteBulk Event = 47 - EventMessageReactionAdd Event = 48 - EventMessageReactionRemove Event = 49 - EventMessageReactionRemoveAll Event = 50 - EventMessageReactionRemoveEmoji Event = 51 - EventMessageUpdate Event = 52 - EventPresenceUpdate Event = 53 - EventPresencesReplace Event = 54 - EventRateLimit Event = 55 - EventReady Event = 56 - EventRelationshipAdd Event = 57 - EventRelationshipRemove Event = 58 - EventResumed Event = 59 - EventStageInstanceCreate Event = 60 - EventStageInstanceDelete Event = 61 - EventStageInstanceUpdate Event = 62 - EventThreadCreate Event = 63 - EventThreadDelete Event = 64 - EventThreadListSync Event = 65 - EventThreadMemberUpdate Event = 66 - EventThreadMembersUpdate Event = 67 - EventThreadUpdate Event = 68 - EventTypingStart Event = 69 - EventUserGuildSettingsUpdate Event = 70 - EventUserNoteUpdate Event = 71 - EventUserSettingsUpdate Event = 72 - EventUserUpdate Event = 73 - EventVoiceChannelStatusUpdate Event = 74 - EventVoiceServerUpdate Event = 75 - EventVoiceStateUpdate Event = 76 - EventWebhooksUpdate Event = 77 + EventEntitlementCreate Event = 23 + EventEntitlementDelete Event = 24 + EventEntitlementUpdate Event = 25 + EventGuildAuditLogEntryCreate Event = 26 + EventGuildBanAdd Event = 27 + EventGuildBanRemove Event = 28 + EventGuildCreate Event = 29 + EventGuildDelete Event = 30 + EventGuildEmojisUpdate Event = 31 + EventGuildIntegrationsUpdate Event = 32 + EventGuildJoinRequestDelete Event = 33 + EventGuildJoinRequestUpdate Event = 34 + EventGuildMemberAdd Event = 35 + EventGuildMemberRemove Event = 36 + EventGuildMemberUpdate Event = 37 + EventGuildMembersChunk Event = 38 + EventGuildRoleCreate Event = 39 + EventGuildRoleDelete Event = 40 + EventGuildRoleUpdate Event = 41 + EventGuildStickersUpdate Event = 42 + EventGuildUpdate Event = 43 + EventInteractionCreate Event = 44 + EventInviteCreate Event = 45 + EventInviteDelete Event = 46 + EventMessageAck Event = 47 + EventMessageCreate Event = 48 + EventMessageDelete Event = 49 + EventMessageDeleteBulk Event = 50 + EventMessageReactionAdd Event = 51 + EventMessageReactionRemove Event = 52 + EventMessageReactionRemoveAll Event = 53 + EventMessageReactionRemoveEmoji Event = 54 + EventMessageUpdate Event = 55 + EventPresenceUpdate Event = 56 + EventPresencesReplace Event = 57 + EventRateLimit Event = 58 + EventReady Event = 59 + EventRelationshipAdd Event = 60 + EventRelationshipRemove Event = 61 + EventResumed Event = 62 + EventStageInstanceCreate Event = 63 + EventStageInstanceDelete Event = 64 + EventStageInstanceUpdate Event = 65 + EventSubscriptionCreate Event = 66 + EventSubscriptionDelete Event = 67 + EventSubscriptionUpdate Event = 68 + EventThreadCreate Event = 69 + EventThreadDelete Event = 70 + EventThreadListSync Event = 71 + EventThreadMemberUpdate Event = 72 + EventThreadMembersUpdate Event = 73 + EventThreadUpdate Event = 74 + EventTypingStart Event = 75 + EventUserGuildSettingsUpdate Event = 76 + EventUserNoteUpdate Event = 77 + EventUserSettingsUpdate Event = 78 + EventUserUpdate Event = 79 + EventVoiceChannelStatusUpdate Event = 80 + EventVoiceServerUpdate Event = 81 + EventVoiceStateUpdate Event = 82 + EventWebhooksUpdate Event = 83 ) var EventNames = []string{ @@ -116,6 +122,9 @@ var EventNames = []string{ "ChannelUpdate", "Connect", "Disconnect", + "EntitlementCreate", + "EntitlementDelete", + "EntitlementUpdate", "GuildAuditLogEntryCreate", "GuildBanAdd", "GuildBanRemove", @@ -156,6 +165,9 @@ var EventNames = []string{ "StageInstanceCreate", "StageInstanceDelete", "StageInstanceUpdate", + "SubscriptionCreate", + "SubscriptionDelete", + "SubscriptionUpdate", "ThreadCreate", "ThreadDelete", "ThreadListSync", @@ -193,6 +205,9 @@ var AllDiscordEvents = []Event{ EventChannelUpdate, EventConnect, EventDisconnect, + EventEntitlementCreate, + EventEntitlementDelete, + EventEntitlementUpdate, EventGuildAuditLogEntryCreate, EventGuildBanAdd, EventGuildBanRemove, @@ -233,6 +248,9 @@ var AllDiscordEvents = []Event{ EventStageInstanceCreate, EventStageInstanceDelete, EventStageInstanceUpdate, + EventSubscriptionCreate, + EventSubscriptionDelete, + EventSubscriptionUpdate, EventThreadCreate, EventThreadDelete, EventThreadListSync, @@ -274,6 +292,9 @@ var AllEvents = []Event{ EventChannelUpdate, EventConnect, EventDisconnect, + EventEntitlementCreate, + EventEntitlementDelete, + EventEntitlementUpdate, EventGuildAuditLogEntryCreate, EventGuildBanAdd, EventGuildBanRemove, @@ -314,6 +335,9 @@ var AllEvents = []Event{ EventStageInstanceCreate, EventStageInstanceDelete, EventStageInstanceUpdate, + EventSubscriptionCreate, + EventSubscriptionDelete, + EventSubscriptionUpdate, EventThreadCreate, EventThreadDelete, EventThreadListSync, @@ -331,7 +355,7 @@ var AllEvents = []Event{ EventWebhooksUpdate, } -var handlers = make([][][]*Handler, 78) +var handlers = make([][][]*Handler, 84) func (data *EventData) ApplicationCommandCreate() *discordgo.ApplicationCommandCreate { return data.EvtInterface.(*discordgo.ApplicationCommandCreate) @@ -378,6 +402,15 @@ func (data *EventData) Connect() *discordgo.Connect { func (data *EventData) Disconnect() *discordgo.Disconnect { return data.EvtInterface.(*discordgo.Disconnect) } +func (data *EventData) EntitlementCreate() *discordgo.EntitlementCreate { + return data.EvtInterface.(*discordgo.EntitlementCreate) +} +func (data *EventData) EntitlementDelete() *discordgo.EntitlementDelete { + return data.EvtInterface.(*discordgo.EntitlementDelete) +} +func (data *EventData) EntitlementUpdate() *discordgo.EntitlementUpdate { + return data.EvtInterface.(*discordgo.EntitlementUpdate) +} func (data *EventData) GuildAuditLogEntryCreate() *discordgo.GuildAuditLogEntryCreate { return data.EvtInterface.(*discordgo.GuildAuditLogEntryCreate) } @@ -498,6 +531,15 @@ func (data *EventData) StageInstanceDelete() *discordgo.StageInstanceDelete { func (data *EventData) StageInstanceUpdate() *discordgo.StageInstanceUpdate { return data.EvtInterface.(*discordgo.StageInstanceUpdate) } +func (data *EventData) SubscriptionCreate() *discordgo.SubscriptionCreate { + return data.EvtInterface.(*discordgo.SubscriptionCreate) +} +func (data *EventData) SubscriptionDelete() *discordgo.SubscriptionDelete { + return data.EvtInterface.(*discordgo.SubscriptionDelete) +} +func (data *EventData) SubscriptionUpdate() *discordgo.SubscriptionUpdate { + return data.EvtInterface.(*discordgo.SubscriptionUpdate) +} func (data *EventData) ThreadCreate() *discordgo.ThreadCreate { return data.EvtInterface.(*discordgo.ThreadCreate) } @@ -577,116 +619,128 @@ func fillEvent(evtData *EventData) { evtData.Type = Event(21) case *discordgo.Disconnect: evtData.Type = Event(22) - case *discordgo.GuildAuditLogEntryCreate: + case *discordgo.EntitlementCreate: evtData.Type = Event(23) - case *discordgo.GuildBanAdd: + case *discordgo.EntitlementDelete: evtData.Type = Event(24) - case *discordgo.GuildBanRemove: + case *discordgo.EntitlementUpdate: evtData.Type = Event(25) - case *discordgo.GuildCreate: + case *discordgo.GuildAuditLogEntryCreate: evtData.Type = Event(26) - case *discordgo.GuildDelete: + case *discordgo.GuildBanAdd: evtData.Type = Event(27) - case *discordgo.GuildEmojisUpdate: + case *discordgo.GuildBanRemove: evtData.Type = Event(28) - case *discordgo.GuildIntegrationsUpdate: + case *discordgo.GuildCreate: evtData.Type = Event(29) - case *discordgo.GuildJoinRequestDelete: + case *discordgo.GuildDelete: evtData.Type = Event(30) - case *discordgo.GuildJoinRequestUpdate: + case *discordgo.GuildEmojisUpdate: evtData.Type = Event(31) - case *discordgo.GuildMemberAdd: + case *discordgo.GuildIntegrationsUpdate: evtData.Type = Event(32) - case *discordgo.GuildMemberRemove: + case *discordgo.GuildJoinRequestDelete: evtData.Type = Event(33) - case *discordgo.GuildMemberUpdate: + case *discordgo.GuildJoinRequestUpdate: evtData.Type = Event(34) - case *discordgo.GuildMembersChunk: + case *discordgo.GuildMemberAdd: evtData.Type = Event(35) - case *discordgo.GuildRoleCreate: + case *discordgo.GuildMemberRemove: evtData.Type = Event(36) - case *discordgo.GuildRoleDelete: + case *discordgo.GuildMemberUpdate: evtData.Type = Event(37) - case *discordgo.GuildRoleUpdate: + case *discordgo.GuildMembersChunk: evtData.Type = Event(38) - case *discordgo.GuildStickersUpdate: + case *discordgo.GuildRoleCreate: evtData.Type = Event(39) - case *discordgo.GuildUpdate: + case *discordgo.GuildRoleDelete: evtData.Type = Event(40) - case *discordgo.InteractionCreate: + case *discordgo.GuildRoleUpdate: evtData.Type = Event(41) - case *discordgo.InviteCreate: + case *discordgo.GuildStickersUpdate: evtData.Type = Event(42) - case *discordgo.InviteDelete: + case *discordgo.GuildUpdate: evtData.Type = Event(43) - case *discordgo.MessageAck: + case *discordgo.InteractionCreate: evtData.Type = Event(44) - case *discordgo.MessageCreate: + case *discordgo.InviteCreate: evtData.Type = Event(45) - case *discordgo.MessageDelete: + case *discordgo.InviteDelete: evtData.Type = Event(46) - case *discordgo.MessageDeleteBulk: + case *discordgo.MessageAck: evtData.Type = Event(47) - case *discordgo.MessageReactionAdd: + case *discordgo.MessageCreate: evtData.Type = Event(48) - case *discordgo.MessageReactionRemove: + case *discordgo.MessageDelete: evtData.Type = Event(49) - case *discordgo.MessageReactionRemoveAll: + case *discordgo.MessageDeleteBulk: evtData.Type = Event(50) - case *discordgo.MessageReactionRemoveEmoji: + case *discordgo.MessageReactionAdd: evtData.Type = Event(51) - case *discordgo.MessageUpdate: + case *discordgo.MessageReactionRemove: evtData.Type = Event(52) - case *discordgo.PresenceUpdate: + case *discordgo.MessageReactionRemoveAll: evtData.Type = Event(53) - case *discordgo.PresencesReplace: + case *discordgo.MessageReactionRemoveEmoji: evtData.Type = Event(54) - case *discordgo.RateLimit: + case *discordgo.MessageUpdate: evtData.Type = Event(55) - case *discordgo.Ready: + case *discordgo.PresenceUpdate: evtData.Type = Event(56) - case *discordgo.RelationshipAdd: + case *discordgo.PresencesReplace: evtData.Type = Event(57) - case *discordgo.RelationshipRemove: + case *discordgo.RateLimit: evtData.Type = Event(58) - case *discordgo.Resumed: + case *discordgo.Ready: evtData.Type = Event(59) - case *discordgo.StageInstanceCreate: + case *discordgo.RelationshipAdd: evtData.Type = Event(60) - case *discordgo.StageInstanceDelete: + case *discordgo.RelationshipRemove: evtData.Type = Event(61) - case *discordgo.StageInstanceUpdate: + case *discordgo.Resumed: evtData.Type = Event(62) - case *discordgo.ThreadCreate: + case *discordgo.StageInstanceCreate: evtData.Type = Event(63) - case *discordgo.ThreadDelete: + case *discordgo.StageInstanceDelete: evtData.Type = Event(64) - case *discordgo.ThreadListSync: + case *discordgo.StageInstanceUpdate: evtData.Type = Event(65) - case *discordgo.ThreadMemberUpdate: + case *discordgo.SubscriptionCreate: evtData.Type = Event(66) - case *discordgo.ThreadMembersUpdate: + case *discordgo.SubscriptionDelete: evtData.Type = Event(67) - case *discordgo.ThreadUpdate: + case *discordgo.SubscriptionUpdate: evtData.Type = Event(68) - case *discordgo.TypingStart: + case *discordgo.ThreadCreate: evtData.Type = Event(69) - case *discordgo.UserGuildSettingsUpdate: + case *discordgo.ThreadDelete: evtData.Type = Event(70) - case *discordgo.UserNoteUpdate: + case *discordgo.ThreadListSync: evtData.Type = Event(71) - case *discordgo.UserSettingsUpdate: + case *discordgo.ThreadMemberUpdate: evtData.Type = Event(72) - case *discordgo.UserUpdate: + case *discordgo.ThreadMembersUpdate: evtData.Type = Event(73) - case *discordgo.VoiceChannelStatusUpdate: + case *discordgo.ThreadUpdate: evtData.Type = Event(74) - case *discordgo.VoiceServerUpdate: + case *discordgo.TypingStart: evtData.Type = Event(75) - case *discordgo.VoiceStateUpdate: + case *discordgo.UserGuildSettingsUpdate: evtData.Type = Event(76) - case *discordgo.WebhooksUpdate: + case *discordgo.UserNoteUpdate: evtData.Type = Event(77) + case *discordgo.UserSettingsUpdate: + evtData.Type = Event(78) + case *discordgo.UserUpdate: + evtData.Type = Event(79) + case *discordgo.VoiceChannelStatusUpdate: + evtData.Type = Event(80) + case *discordgo.VoiceServerUpdate: + evtData.Type = Event(81) + case *discordgo.VoiceStateUpdate: + evtData.Type = Event(82) + case *discordgo.WebhooksUpdate: + evtData.Type = Event(83) default: return } diff --git a/bot/eventsystem/gen/events_gen.go b/bot/eventsystem/gen/events_gen.go index 3255af1fe2..b6af6c856d 100644 --- a/bot/eventsystem/gen/events_gen.go +++ b/bot/eventsystem/gen/events_gen.go @@ -81,17 +81,17 @@ type Event struct { } var NonStandardEvents = []Event{ - Event{"NewGuild", false}, - Event{"All", false}, - Event{"AllPre", false}, - Event{"AllPost", false}, - Event{"MemberFetched", false}, + {"NewGuild", false}, + {"All", false}, + {"AllPre", false}, + {"AllPost", false}, + {"MemberFetched", false}, // Sent once a shard either resumes or readies for the first time, signifying that its now ready - Event{"YagShardReady", false}, + {"YagShardReady", false}, // Sent one or more shards is either migrated to this node or added freshly - Event{"YagShardsAdded", false}, + {"YagShardsAdded", false}, // Sent once a shard has been either migrated away or removeotherwise - Event{"YagShardRemoved", false}, + {"YagShardRemoved", false}, } var ( @@ -139,7 +139,7 @@ func main() { } names := []string{} - for name, _ := range parsedFile.Scope.Objects { + for name := range parsedFile.Scope.Objects { names = append(names, name) } sort.Strings(names) diff --git a/cmd/yagpdb/main.go b/cmd/yagpdb/main.go index f05ef7a497..4771927391 100644 --- a/cmd/yagpdb/main.go +++ b/cmd/yagpdb/main.go @@ -29,6 +29,7 @@ import ( "github.com/botlabs-gg/yagpdb/v2/moderation" "github.com/botlabs-gg/yagpdb/v2/notifications" "github.com/botlabs-gg/yagpdb/v2/premium" + "github.com/botlabs-gg/yagpdb/v2/premium/discordpremiumsource" "github.com/botlabs-gg/yagpdb/v2/premium/patreonpremiumsource" "github.com/botlabs-gg/yagpdb/v2/reddit" "github.com/botlabs-gg/yagpdb/v2/reminders" @@ -83,6 +84,7 @@ func main() { verification.RegisterPlugin() premium.RegisterPlugin() patreonpremiumsource.RegisterPlugin() + discordpremiumsource.RegisterPlugin() scheduledevents2.RegisterPlugin() twitter.RegisterPlugin() rsvp.RegisterPlugin() diff --git a/go.mod b/go.mod index 9159d2280c..e44527097e 100644 --- a/go.mod +++ b/go.mod @@ -59,12 +59,12 @@ require ( github.com/volatiletech/sqlboiler/v4 v4.14.2 github.com/volatiletech/strmangle v0.0.6 goji.io v2.0.2+incompatible - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.27.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.23.0 + golang.org/x/net v0.29.0 golang.org/x/oauth2 v0.10.0 - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 golang.org/x/time v0.3.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.131.0 @@ -90,6 +90,7 @@ require ( github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -100,7 +101,6 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/arch v0.4.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect ) @@ -167,6 +167,7 @@ require ( go.mongodb.org/mongo-driver v1.12.0 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/tools v0.25.0 google.golang.org/appengine v1.6.7 // indirect google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/go.sum b/go.sum index c9e6e5f844..da6d665e16 100644 --- a/go.sum +++ b/go.sum @@ -313,8 +313,9 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -819,8 +820,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -861,6 +862,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -918,8 +921,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -960,8 +963,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1055,16 +1058,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1079,8 +1082,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1147,6 +1150,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lib/discordgo/endpoints.go b/lib/discordgo/endpoints.go index 2b0388e567..9347c6dc85 100644 --- a/lib/discordgo/endpoints.go +++ b/lib/discordgo/endpoints.go @@ -201,6 +201,24 @@ var ( EndpointInteractionFollowupMessage = func(applicationID int64, token string, messageID int64) string { return "" } + EndpointSKUs = func(applicationID int64) string { + return "" + } + EndpointEntitlements = func(applicationID int64) string { + return "" + } + EndpointEntitlement = func(applicationID, entitlementID int64) string { + return "" + } + EndpointEntitlementConsume = func(applicationID, entitlementID int64) string { + return "" + } + EndpointSKUSubscriptions = func(skuID int64) string { + return "" + } + EndpointSKUSubscription = func(skuID, subscriptionID int64) string { + return "" + } ) func CreateEndpoints(base string) { @@ -384,6 +402,25 @@ func CreateEndpoints(base string) { return EndpointApplicationGuildCommand(aID, gID, cmdID) + "/permissions" } + EndpointSKUs = func(applicationID int64) string { + return EndpointApplicationNonOauth2(applicationID) + "/skus" + } + EndpointEntitlements = func(applicationID int64) string { + return EndpointApplicationNonOauth2(applicationID) + "/entitlements" + } + EndpointEntitlement = func(applicationID, entitlementID int64) string { + return EndpointApplicationNonOauth2(applicationID) + "/entitlements/" + StrID(entitlementID) + } + EndpointEntitlementConsume = func(applicationID, entitlementID int64) string { + return EndpointApplicationNonOauth2(applicationID) + "/entitlements/" + StrID(entitlementID) + "/consume" + } + EndpointSKUSubscriptions = func(skuID int64) string { + return EndpointAPI + "skus/" + StrID(skuID) + "/subscriptions" + } + EndpointSKUSubscription = func(skuID, subscriptionID int64) string { + return EndpointAPI + "skus/" + StrID(skuID) + "/subscriptions/" + StrID(subscriptionID) + } + EndpointInteractions = EndpointAPI + "interactions" EndpointInteractionCallback = func(interactionID int64, token string) string { return EndpointInteractions + "/" + StrID(interactionID) + "/" + token + "/callback" diff --git a/lib/discordgo/eventhandlers.go b/lib/discordgo/eventhandlers.go index 966a37c671..50235b8a46 100644 --- a/lib/discordgo/eventhandlers.go +++ b/lib/discordgo/eventhandlers.go @@ -22,6 +22,9 @@ const ( channelUpdateEventType = "CHANNEL_UPDATE" connectEventType = "__CONNECT__" disconnectEventType = "__DISCONNECT__" + entitlementCreateEventType = "ENTITLEMENT_CREATE" + entitlementDeleteEventType = "ENTITLEMENT_DELETE" + entitlementUpdateEventType = "ENTITLEMENT_UPDATE" eventEventType = "__EVENT__" guildAuditLogEntryCreateEventType = "GUILD_AUDIT_LOG_ENTRY_CREATE" guildBanAddEventType = "GUILD_BAN_ADD" @@ -63,6 +66,9 @@ const ( stageInstanceCreateEventType = "STAGE_INSTANCE_CREATE" stageInstanceDeleteEventType = "STAGE_INSTANCE_DELETE" stageInstanceUpdateEventType = "STAGE_INSTANCE_UPDATE" + subscriptionCreateEventType = "SUBSCRIPTION_CREATE" + subscriptionDeleteEventType = "SUBSCRIPTION_DELETE" + subscriptionUpdateEventType = "SUBSCRIPTION_UPDATE" threadCreateEventType = "THREAD_CREATE" threadDeleteEventType = "THREAD_DELETE" threadListSyncEventType = "THREAD_LIST_SYNC" @@ -370,6 +376,66 @@ func (eh disconnectEventHandler) Handle(s *Session, i interface{}) { } } +// entitlementCreateEventHandler is an event handler for EntitlementCreate events. +type entitlementCreateEventHandler func(*Session, *EntitlementCreate) + +// Type returns the event type for EntitlementCreate events. +func (eh entitlementCreateEventHandler) Type() string { + return entitlementCreateEventType +} + +// New returns a new instance of EntitlementCreate. +func (eh entitlementCreateEventHandler) New() interface{} { + return &EntitlementCreate{} +} + +// Handle is the handler for EntitlementCreate events. +func (eh entitlementCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*EntitlementCreate); ok { + eh(s, t) + } +} + +// entitlementDeleteEventHandler is an event handler for EntitlementDelete events. +type entitlementDeleteEventHandler func(*Session, *EntitlementDelete) + +// Type returns the event type for EntitlementDelete events. +func (eh entitlementDeleteEventHandler) Type() string { + return entitlementDeleteEventType +} + +// New returns a new instance of EntitlementDelete. +func (eh entitlementDeleteEventHandler) New() interface{} { + return &EntitlementDelete{} +} + +// Handle is the handler for EntitlementDelete events. +func (eh entitlementDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*EntitlementDelete); ok { + eh(s, t) + } +} + +// entitlementUpdateEventHandler is an event handler for EntitlementUpdate events. +type entitlementUpdateEventHandler func(*Session, *EntitlementUpdate) + +// Type returns the event type for EntitlementUpdate events. +func (eh entitlementUpdateEventHandler) Type() string { + return entitlementUpdateEventType +} + +// New returns a new instance of EntitlementUpdate. +func (eh entitlementUpdateEventHandler) New() interface{} { + return &EntitlementUpdate{} +} + +// Handle is the handler for EntitlementUpdate events. +func (eh entitlementUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*EntitlementUpdate); ok { + eh(s, t) + } +} + // eventEventHandler is an event handler for Event events. type eventEventHandler func(*Session, *Event) @@ -1180,6 +1246,66 @@ func (eh stageInstanceUpdateEventHandler) Handle(s *Session, i interface{}) { } } +// subscriptionCreateEventHandler is an event handler for SubscriptionCreate events. +type subscriptionCreateEventHandler func(*Session, *SubscriptionCreate) + +// Type returns the event type for SubscriptionCreate events. +func (eh subscriptionCreateEventHandler) Type() string { + return subscriptionCreateEventType +} + +// New returns a new instance of SubscriptionCreate. +func (eh subscriptionCreateEventHandler) New() interface{} { + return &SubscriptionCreate{} +} + +// Handle is the handler for SubscriptionCreate events. +func (eh subscriptionCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*SubscriptionCreate); ok { + eh(s, t) + } +} + +// subscriptionDeleteEventHandler is an event handler for SubscriptionDelete events. +type subscriptionDeleteEventHandler func(*Session, *SubscriptionDelete) + +// Type returns the event type for SubscriptionDelete events. +func (eh subscriptionDeleteEventHandler) Type() string { + return subscriptionDeleteEventType +} + +// New returns a new instance of SubscriptionDelete. +func (eh subscriptionDeleteEventHandler) New() interface{} { + return &SubscriptionDelete{} +} + +// Handle is the handler for SubscriptionDelete events. +func (eh subscriptionDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*SubscriptionDelete); ok { + eh(s, t) + } +} + +// subscriptionUpdateEventHandler is an event handler for SubscriptionUpdate events. +type subscriptionUpdateEventHandler func(*Session, *SubscriptionUpdate) + +// Type returns the event type for SubscriptionUpdate events. +func (eh subscriptionUpdateEventHandler) Type() string { + return subscriptionUpdateEventType +} + +// New returns a new instance of SubscriptionUpdate. +func (eh subscriptionUpdateEventHandler) New() interface{} { + return &SubscriptionUpdate{} +} + +// Handle is the handler for SubscriptionUpdate events. +func (eh subscriptionUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*SubscriptionUpdate); ok { + eh(s, t) + } +} + // threadCreateEventHandler is an event handler for ThreadCreate events. type threadCreateEventHandler func(*Session, *ThreadCreate) @@ -1514,6 +1640,12 @@ func handlerForInterface(handler interface{}) EventHandler { return connectEventHandler(v) case func(*Session, *Disconnect): return disconnectEventHandler(v) + case func(*Session, *EntitlementCreate): + return entitlementCreateEventHandler(v) + case func(*Session, *EntitlementDelete): + return entitlementDeleteEventHandler(v) + case func(*Session, *EntitlementUpdate): + return entitlementUpdateEventHandler(v) case func(*Session, *Event): return eventEventHandler(v) case func(*Session, *GuildAuditLogEntryCreate): @@ -1596,6 +1728,12 @@ func handlerForInterface(handler interface{}) EventHandler { return stageInstanceDeleteEventHandler(v) case func(*Session, *StageInstanceUpdate): return stageInstanceUpdateEventHandler(v) + case func(*Session, *SubscriptionCreate): + return subscriptionCreateEventHandler(v) + case func(*Session, *SubscriptionDelete): + return subscriptionDeleteEventHandler(v) + case func(*Session, *SubscriptionUpdate): + return subscriptionUpdateEventHandler(v) case func(*Session, *ThreadCreate): return threadCreateEventHandler(v) case func(*Session, *ThreadDelete): @@ -1645,6 +1783,9 @@ func init() { registerInterfaceProvider(channelPinsUpdateEventHandler(nil)) registerInterfaceProvider(channelTopicUpdateEventHandler(nil)) registerInterfaceProvider(channelUpdateEventHandler(nil)) + registerInterfaceProvider(entitlementCreateEventHandler(nil)) + registerInterfaceProvider(entitlementDeleteEventHandler(nil)) + registerInterfaceProvider(entitlementUpdateEventHandler(nil)) registerInterfaceProvider(guildAuditLogEntryCreateEventHandler(nil)) registerInterfaceProvider(guildBanAddEventHandler(nil)) registerInterfaceProvider(guildBanRemoveEventHandler(nil)) @@ -1684,6 +1825,9 @@ func init() { registerInterfaceProvider(stageInstanceCreateEventHandler(nil)) registerInterfaceProvider(stageInstanceDeleteEventHandler(nil)) registerInterfaceProvider(stageInstanceUpdateEventHandler(nil)) + registerInterfaceProvider(subscriptionCreateEventHandler(nil)) + registerInterfaceProvider(subscriptionDeleteEventHandler(nil)) + registerInterfaceProvider(subscriptionUpdateEventHandler(nil)) registerInterfaceProvider(threadCreateEventHandler(nil)) registerInterfaceProvider(threadDeleteEventHandler(nil)) registerInterfaceProvider(threadListSyncEventHandler(nil)) diff --git a/lib/discordgo/events.go b/lib/discordgo/events.go index cb9223287e..c3219bd557 100644 --- a/lib/discordgo/events.go +++ b/lib/discordgo/events.go @@ -579,3 +579,27 @@ type GuildJoinRequestUpdate struct{} type GuildJoinRequestDelete struct{} type VoiceChannelStatusUpdate struct{} type ChannelTopicUpdate struct{} + +// Monetization events +type EntitlementCreate struct { + *Entitlement +} +type EntitlementUpdate struct { + *Entitlement +} + +// EntitlementDelete is the data for an EntitlementDelete event. +// NOTE: Entitlements are not deleted when they expire. +type EntitlementDelete struct { + *Entitlement +} + +type SubscriptionCreate struct { + *Subscription +} +type SubscriptionUpdate struct { + *Subscription +} +type SubscriptionDelete struct { + *Subscription +} diff --git a/lib/discordgo/interactions.go b/lib/discordgo/interactions.go index 092454a149..0d6e713b4d 100644 --- a/lib/discordgo/interactions.go +++ b/lib/discordgo/interactions.go @@ -211,6 +211,8 @@ type Interaction struct { Version int `json:"version"` DataCommand *ApplicationCommandInteractionData + + Entitlements []*Entitlement `json:"entitlements"` } type interaction Interaction diff --git a/lib/discordgo/restapi.go b/lib/discordgo/restapi.go index f13fa3c8d5..47bd144d50 100644 --- a/lib/discordgo/restapi.go +++ b/lib/discordgo/restapi.go @@ -3153,3 +3153,119 @@ func (s *Session) DeleteFollowupMessage(applicationID int64, token string, messa _, err = s.RequestWithBucketID("DELETE", EndpointInteractionFollowupMessage(applicationID, token, messageID), nil, nil, EndpointInteractionFollowupMessage(0, "", 0)) return } + +// ---------------------------------------------------------------------- +// Monetization Functions +// ---------------------------------------------------------------------- + +// SKUs returns all SKUs for a given application. +// appID : The ID of the application. +func (s *Session) SKUs(applicationID int64) (skus []*SKU, err error) { + body, err := s.RequestWithBucketID("GET", EndpointSKUs(applicationID), nil, nil, EndpointSKUs(applicationID)) + if err != nil { + return + } + + err = unmarshal(body, &skus) + return +} + +// Entitlements returns all antitlements for a given app, active and expired. +// appID : The ID of the application. +// filterOptions : Optional filter options; otherwise set it to nil. +func (s *Session) Entitlements(applicationID int64, filterOptions *EntitlementFilterOptions) (entitlements []*Entitlement, err error) { + endpoint := EndpointEntitlements(applicationID) + queryParams := url.Values{} + if filterOptions != nil { + if filterOptions.UserID != 0 { + queryParams.Set("user_id", StrID(filterOptions.UserID)) + } + if len(filterOptions.SkuIDs) > 0 { + stringSkuIDs := make([]string, 0, len(filterOptions.SkuIDs)) + for _, skuID := range filterOptions.SkuIDs { + stringSkuIDs = append(stringSkuIDs, StrID(skuID)) + } + queryParams.Set("sku_ids", strings.Join(stringSkuIDs, ",")) + } + if filterOptions.BeforeID != 0 { + queryParams.Set("before", StrID(filterOptions.BeforeID)) + } + if filterOptions.AfterID != 0 { + queryParams.Set("after", StrID(filterOptions.AfterID)) + } + if filterOptions.Limit > 0 { + queryParams.Set("limit", strconv.Itoa(filterOptions.Limit)) + } + if filterOptions.GuildID != 0 { + queryParams.Set("guild_id", StrID(filterOptions.GuildID)) + } + if filterOptions.ExcludeEnded { + queryParams.Set("exclude_ended", "true") + } + } + uri := endpoint + uri = fmt.Sprintf("%s?%s", uri, queryParams.Encode()) + body, err := s.RequestWithBucketID("GET", uri, nil, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &entitlements) + return +} + +// EntitlementConsume marks a given One-Time Purchase for the user as consumed. +func (s *Session) EntitlementConsume(appID, entitlementID int64) (err error) { + _, err = s.RequestWithBucketID("POST", EndpointEntitlementConsume(appID, entitlementID), nil, nil, EndpointEntitlementConsume(appID, 0)) + return +} + +// EntitlementTestCreate creates a test entitlement to a given SKU for a given guild or user. +// Discord will act as though that user or guild has entitlement to your premium offering. +func (s *Session) EntitlementTestCreate(appID int64, data *EntitlementTest) (err error) { + endpoint := EndpointEntitlements(appID) + _, err = s.RequestWithBucketID("POST", endpoint, data, nil, endpoint) + return +} + +// EntitlementTestDelete deletes a currently-active test entitlement. Discord will act as though +// that user or guild no longer has entitlement to your premium offering. +func (s *Session) EntitlementTestDelete(appID, entitlementID int64) (err error) { + _, err = s.RequestWithBucketID("DELETE", EndpointEntitlement(appID, entitlementID), nil, nil, EndpointEntitlement(appID, 0)) + return +} + +func (s *Session) GetSKUSubscriptions(skuID int64, filterOptions *SubscriptionFilterOptions) (subscriptions []*Subscription, err error) { + endpoint := EndpointSKUSubscriptions(skuID) + queryParams := url.Values{} + if filterOptions.UserId != 0 { + queryParams.Set("user_id", StrID(filterOptions.UserId)) + } + if filterOptions.AfterID != 0 { + queryParams.Set("after", StrID(filterOptions.AfterID)) + } + if filterOptions.BeforeID != 0 { + queryParams.Set("before", StrID(filterOptions.BeforeID)) + } + if filterOptions.Limit != 0 { + queryParams.Set("limit", strconv.Itoa(filterOptions.Limit)) + } + uri := endpoint + uri = fmt.Sprintf("%s?%s", uri, queryParams.Encode()) + body, err := s.RequestWithBucketID("GET", uri, nil, nil, endpoint) + if err != nil { + return + } + err = unmarshal(body, &subscriptions) + return +} + +func (s *Session) GetSKUSubscription(skuID, subscriptionID int64) (subscription *Subscription, err error) { + endpoint := EndpointSKUSubscription(skuID, subscriptionID) + body, err := s.RequestWithBucketID("GET", endpoint, nil, nil, endpoint) + if err != nil { + return + } + err = unmarshal(body, &subscription) + return +} diff --git a/lib/discordgo/structs.go b/lib/discordgo/structs.go index bda094e72f..9401f03cdd 100644 --- a/lib/discordgo/structs.go +++ b/lib/discordgo/structs.go @@ -1703,3 +1703,182 @@ type AutoModerationAction struct { Type AutoModerationActionType `json:"type"` Metadata *AutoModerationActionMetadata `json:"metadata,omitempty"` } + +type SKUType int + +// Valid SKUType values +const ( + SKUTypeDurable SKUType = 2 + SKUTypeConsumable SKUType = 3 + SKUTypeSubscription SKUType = 5 + SKUTypeSubscriptionGroup SKUType = 6 +) + +type SKUFlags int + +const ( + // SKUFlagAvailable indicates that the SKU is available for purchase. + SKUFlagAvailable SKUFlags = 1 << 2 + // SKUFlagGuildSubscription indicates that the SKU is a guild subscription. + SKUFlagGuildSubscription SKUFlags = 1 << 7 + // SKUFlagUserSubscription indicates that the SKU is a user subscription. + SKUFlagUserSubscription SKUFlags = 1 << 8 +) + +// SKU represents a purchasable item in the Discord store. +type SKU struct { + // The ID of the SKU + ID int64 `json:"id,string"` + + // The Type of the SKU + Type SKUType `json:"type"` + + // The ID of the parent application + ApplicationID int64 `json:"application_id,string"` + + // Customer-facing name of the SKU. + Name string `json:"name"` + + // System-generated URL slug based on the SKU's name. + Slug string `json:"slug"` + + // SKUFlags combined as a bitfield. The presence of a certain flag can be checked + // by performing a bitwise AND operation between this int and the flag. + Flags SKUFlags `json:"flags"` +} + +// EntitlementType is the type of entitlement +// https://discord.com/developers/docs/monetization/entitlements#entitlement-object-entitlement-types +type EntitlementType int + +// Valid EntitlementType values +const ( + EntitlementTypePurchase = 1 + EntitlementTypePremiumSubscription = 2 + EntitlementTypeDeveloperGift = 3 + EntitlementTypeTestModePurchase = 4 + EntitlementTypeFreePurchase = 5 + EntitlementTypeUserGift = 6 + EntitlementTypePremiumPurchase = 7 + EntitlementTypeApplicationSubscription = 8 +) + +// Entitlement represents that a user or guild has access to a premium offering +// in your application. +type Entitlement struct { + // The ID of the entitlement + ID int64 `json:"id,string"` + + // The ID of the SKU + SKUID int64 `json:"sku_id,string"` + + // The ID of the parent application + ApplicationID int64 `json:"application_id,string"` + + // The ID of the user that is granted access to the entitlement's sku + // Only available for user subscriptions. + UserID int64 `json:"user_id,string,omitempty"` + + // The type of the entitlement + Type EntitlementType `json:"type"` + + // The entitlement was deleted + Deleted bool `json:"deleted"` + + // The start date at which the entitlement is valid. + // Not present when using test entitlements. + StartsAt *time.Time `json:"starts_at,omitempty"` + + // The date at which the entitlement is no longer valid. + // Not present when using test entitlements. + EndsAt *time.Time `json:"ends_at,omitempty"` + + // The ID of the guild that is granted access to the entitlement's sku. + // Only available for guild subscriptions. + GuildID int64 `json:"guild_id,string,omitempty"` + + // Whether or not the entitlement has been consumed. + // Only available for consumable items. + Consumed bool `json:"consumed,omitempty"` +} + +// EntitlementOwnerType is the type of entitlement (see EntitlementOwnerType* consts) +type EntitlementOwnerType int + +// Valid EntitlementOwnerType values +const ( + EntitlementOwnerTypeGuildSubscription EntitlementOwnerType = 1 + EntitlementOwnerTypeUserSubscription EntitlementOwnerType = 2 +) + +// EntitlementTest is used to test granting an entitlement to a user or guild +type EntitlementTest struct { + // The ID of the SKU to grant the entitlement to + SKUID int64 `json:"sku_id,string"` + + // The ID of the guild or user to grant the entitlement to + OwnerID int64 `json:"owner_id,string"` + + // OwnerType is the type of which the entitlement should be created + OwnerType EntitlementOwnerType `json:"owner_type"` +} + +// EntitlementFilterOptions are the options for filtering Entitlements +type EntitlementFilterOptions struct { + // Optional user ID to look up for. + UserID int64 + + // Optional array of SKU IDs to check for. + SkuIDs []int64 + + // Optional timestamp (snowflake) to retrieve Entitlements before this time. + BeforeID int64 + + // Optional timestamp (snowflake) to retrieve Entitlements after this time. + AfterID int64 + + // Optional maximum number of entitlements to return (1-100, default 100). + Limit int + + // Optional guild ID to look up for. + GuildID int64 + + // Optional whether or not ended entitlements should be omitted. + ExcludeEnded bool +} + +// Tells the status of a subscription +type SubscriptionStatus int + +const ( + //Subscription is Active and scheduled to renew + SubscriptionStatusActive SubscriptionStatus = 1 + //Subscription is Active but scheduled to end and not be renewed + SubscriptionStatusEnding SubscriptionStatus = 2 + //Subscription is Inactive and not being charged + SubscriptionStatusInactive SubscriptionStatus = 3 +) + +type Subscription struct { + ID int64 `json:"id,string"` + UserID int64 `json:"user_id,string"` + //list of SKUs the user has subscribed to + SKUIDS []int64 `json:"sku_ids"` + //list of entitlements granted + EntitlementIDs []int64 `json:"entitlement_ids"` + //start of the current subscription period, this is updated when the subscription is renewed, not to be confused with the start of a subscription + CurrentPeriodStart time.Time `json:"current_period_start"` + //end of the current subscription period + CurrentPeriodEnd time.Time `json:"current_period_end"` + Status SubscriptionStatus `json:"status"` + // Only present if the subscription status is Ending or Inactive, represents the time when the subscription was cancelled + CancelledAt time.Time `json:"cancelled_at,omitempty"` +} + +// SubscriptionFilterOptions are the options for filtering Subscriptions for list subscriptions API +type SubscriptionFilterOptions struct { + BeforeID int64 + AfterID int64 + Limit int + UserId int64 +} diff --git a/premium/codepremiumsource.go b/premium/codepremiumsource.go index a8d80fe134..df05d325f8 100644 --- a/premium/codepremiumsource.go +++ b/premium/codepremiumsource.go @@ -35,12 +35,50 @@ func init() { type CodePremiumSource struct{} func (ps *CodePremiumSource) Init() { + go ExpiredSlotsRemover() } func (ps *CodePremiumSource) Names() (human string, idname string) { return "Redeemed code", "code" } +func ExpiredSlotsRemover() { + ticker := time.NewTicker(time.Hour) + for { + <-ticker.C + err := RemoveExpiredSlots() + if err != nil { + logger.WithError(err).Error("Failed Removing Expired slots for code") + } + } +} + +func RemoveExpiredSlots() error { + tx, _ := common.PQ.BeginTx(context.Background(), nil) + slots, err := models.PremiumCodes(qm.Where("source = ?"), qm.Where("duration_remaining < 0"), qm.Where("permanent = false")).AllG(context.Background()) + if err != nil { + logger.WithError(err).Error("Failed getting expired codes") + return err + } + for _, slot := range slots { + err := DetachSlotFromGuild(context.Background(), slot.ID, slot.UserID.Int64) + if err != nil { + logger.WithError(err).Error("Failed detaching expired code") + tx.Rollback() + return err + } + _, err = slot.Delete(context.Background(), tx) + if err != nil { + logger.WithError(err).Error("Failed deleting expired code") + tx.Rollback() + return err + } + logger.Infof("Deleted expired code slot for user_id: %d and slot_id: %d", slot.UserID.Int64, slot.ID) + } + tx.Commit() + return nil +} + func RedeemCode(ctx context.Context, code string, userID int64) error { tx, err := common.PQ.BeginTx(ctx, nil) if err != nil { diff --git a/premium/discordpremiumsource/bot.go b/premium/discordpremiumsource/bot.go new file mode 100644 index 0000000000..de1a9c8fe5 --- /dev/null +++ b/premium/discordpremiumsource/bot.go @@ -0,0 +1,214 @@ +package discordpremiumsource + +import ( + "fmt" + "time" + + "emperror.dev/errors" + "github.com/botlabs-gg/yagpdb/v2/bot" + "github.com/botlabs-gg/yagpdb/v2/bot/eventsystem" + "github.com/botlabs-gg/yagpdb/v2/commands" + "github.com/botlabs-gg/yagpdb/v2/common" + "github.com/botlabs-gg/yagpdb/v2/lib/dcmd" + "github.com/botlabs-gg/yagpdb/v2/lib/discordgo" + "github.com/botlabs-gg/yagpdb/v2/premium" + "github.com/botlabs-gg/yagpdb/v2/premium/models" + "github.com/botlabs-gg/yagpdb/v2/stdcommands/util" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +var _ bot.BotInitHandler = (*Plugin)(nil) +var _ commands.CommandProvider = (*Plugin)(nil) + +func init() { +} + +func (p *Plugin) BotInit() { + eventsystem.AddHandlerAsyncLastLegacy(p, bot.ConcurrentEventHandler(HandleEntitlementCreate), eventsystem.EventEntitlementCreate) + eventsystem.AddHandlerAsyncLastLegacy(p, bot.ConcurrentEventHandler(HandleEntitlementUpdate), eventsystem.EventEntitlementUpdate) + eventsystem.AddHandlerAsyncLastLegacy(p, bot.ConcurrentEventHandler(HandleEntitlementDelete), eventsystem.EventEntitlementDelete) +} + +func (p *Plugin) AddCommands() { + commands.AddRootCommands(p, CmdActivateTestEntitlement, CmdDeleteTestEntitlement) +} + +func HandleEntitlementCreate(evt *eventsystem.EventData) { + entitlement := evt.EntitlementCreate() + logger.Infof("EntitlementCreate Event Received for User %d and SKUID %d", entitlement.UserID, entitlement.SKUID) + if entitlement.SKUID != int64(confDiscordPremiumSKUID.GetInt()) { + logger.Errorf("EntitlementCreate recieved for unknown SKUID %d", entitlement.SKUID) + return + } + ctx := evt.Context() + tx, err := common.PQ.BeginTx(ctx, nil) + if err != nil { + logger.Error(errors.WithMessage(err, "BeginTX")) + tx.Rollback() + return + } + slots, err := models.PremiumSlots(qm.Where("source='discord'"), qm.Where("user_id = ?", entitlement.UserID)).All(ctx, tx) + if err != nil { + logger.Error(errors.WithMessage(err, "Failed fetching PremiumSlots for EntitlementCreate")) + tx.Rollback() + return + } + if len(slots) > 0 { + logger.Infof("User %d already has PremiumSlots", entitlement.UserID) + tx.Rollback() + return + } + _, err = premium.CreatePremiumSlot(ctx, tx, entitlement.UserID, "discord", "Discord Slot #1", "Slot is available as long as subscription is Active on Discord", 1, -1, premium.PremiumTierPremium) + if err != nil { + logger.WithError(err).Error("Failed creating PremiumSlot for EntitlementCreate Event") + tx.Rollback() + return + } + err = tx.Commit() + if err != nil { + logger.WithError(err).Error("Failed committing transaction for EntitlementCreate Event") + } +} + +func HandleEntitlementUpdate(evt *eventsystem.EventData) { + entitlement := evt.EntitlementUpdate() + logger.Infof("EntitlementUpdate Event Received for User %d and SKUID %d", entitlement.UserID, entitlement.SKUID) + if entitlement.SKUID != int64(confDiscordPremiumSKUID.GetInt()) { + logger.Errorf("EntitlementUpdate recieved for unknown SKUID %d", entitlement.SKUID) + return + } + ctx := evt.Context() + tx, err := common.PQ.BeginTx(ctx, nil) + if err != nil { + logger.Error(errors.WithMessage(err, "BeginTX")) + tx.Rollback() + return + } + if entitlement.EndsAt != nil && entitlement.EndsAt.Before(time.Now()) { + tx.Rollback() + return + } + slots, err := models.PremiumSlots(qm.Where("source='discord'"), qm.Where("user_id = ?", entitlement.UserID)).All(ctx, tx) + if err != nil { + logger.Error(errors.WithMessage(err, "Failed fetching PremiumSlots for EntitlementUpdate")) + tx.Rollback() + return + } + if len(slots) == 0 { + logger.Infof("User %d has no PremiumSlots", entitlement.UserID) + tx.Rollback() + return + } + + err = premium.RemovePremiumSlots(ctx, tx, entitlement.UserID, "discord", []int64{slots[0].ID}) + if err != nil { + logger.WithError(err).Error("Failed Removing PremiumSlot for EntitlementUpdate Event") + tx.Rollback() + return + } + err = tx.Commit() + if err != nil { + logger.WithError(err).Error("Failed committing transaction for EntitlementUpdate Event") + } +} + +func HandleEntitlementDelete(evt *eventsystem.EventData) { + entitlement := evt.EntitlementDelete() + logger.Infof("EntitlementDelete Event Received for User %d and SKUID %d", entitlement.UserID, entitlement.SKUID) + if entitlement.SKUID != int64(confDiscordPremiumSKUID.GetInt()) { + logger.Errorf("EntitlementDelete recieved for unknown SKUID %d", entitlement.SKUID) + return + } + ctx := evt.Context() + tx, err := common.PQ.BeginTx(ctx, nil) + if err != nil { + logger.Error(errors.WithMessage(err, "BeginTX")) + tx.Rollback() + return + } + + slots, err := models.PremiumSlots(qm.Where("source='discord'"), qm.Where("user_id = ?", entitlement.UserID)).All(ctx, tx) + if err != nil { + logger.Error(errors.WithMessage(err, "Failed fetching PremiumSlots for EntitlementDelete")) + tx.Rollback() + return + } + if len(slots) == 0 { + logger.Infof("User %d has no PremiumSlots", entitlement.UserID) + tx.Rollback() + return + } + + err = premium.RemovePremiumSlots(ctx, tx, entitlement.UserID, "discord", []int64{slots[0].ID}) + if err != nil { + logger.WithError(err).Error("Failed Removing PremiumSlot for EntitlementDelete Event") + tx.Rollback() + return + } + err = tx.Commit() + if err != nil { + logger.WithError(err).Error("Failed committing transaction for EntitlementDelete Event") + } +} + +var CmdActivateTestEntitlement = &commands.YAGCommand{ + CmdCategory: commands.CategoryDebug, + HideFromCommandsPage: true, + Name: "activateTestEntitlement", + Description: "Enables Test Entitlement for a User. Bot Owner Only", + HideFromHelp: true, + RequiredArgs: 3, + RunInDM: true, + Arguments: []*dcmd.ArgDef{ + {Name: "User", Type: dcmd.UserID}, + }, + RunFunc: util.RequireOwner(func(data *dcmd.Data) (interface{}, error) { + userID := data.Args[0].Int64() + entitlementData := &discordgo.EntitlementTest{ + SKUID: int64(confDiscordPremiumSKUID.GetInt()), + OwnerID: userID, + OwnerType: discordgo.EntitlementOwnerTypeUserSubscription, + } + err := common.BotSession.EntitlementTestCreate(common.BotApplication.ID, entitlementData) + if err != nil { + return fmt.Sprintf("Failed enabling Entitlement for <@%d>: %s", userID, err), nil + } + return fmt.Sprintf("Enabled Entitlement for <@%d>", userID), nil + }), +} + +var CmdDeleteTestEntitlement = &commands.YAGCommand{ + CmdCategory: commands.CategoryDebug, + HideFromCommandsPage: true, + Name: "deleteTestEntitlement", + Description: "Delete Test Entitlement for a User. Bot Owner Only", + HideFromHelp: true, + RequiredArgs: 3, + RunInDM: true, + Arguments: []*dcmd.ArgDef{ + {Name: "User", Type: dcmd.UserID}, + }, + RunFunc: util.RequireOwner(func(data *dcmd.Data) (interface{}, error) { + userID := data.Args[0].Int64() + entitlements, err := common.BotSession.Entitlements(common.BotApplication.ID, &discordgo.EntitlementFilterOptions{ + UserID: userID, + SkuIDs: []int64{int64(confDiscordPremiumSKUID.GetInt())}, + ExcludeEnded: true, + }) + if err != nil { + return fmt.Sprintf("Failed fetching Entitlements for <@%d>: %s", userID, err), nil + } + if len(entitlements) < 1 { + return fmt.Sprintf("No Entitlements found for <@%d>", userID), nil + } + for _, v := range entitlements { + //if v.Type == discordgo.EntitlementTypeTestModePurchase { + err := common.BotSession.EntitlementTestDelete(common.BotApplication.ID, v.ID) + if err != nil { + return fmt.Sprintf("Failed deleting Entitlement for <@%d>: %s", userID, err), nil + } + //} + } + return fmt.Sprintf("Deleted Entitlement for <@%d>", userID), nil + }), +} diff --git a/premium/discordpremiumsource/discordpremiumsource.go b/premium/discordpremiumsource/discordpremiumsource.go new file mode 100644 index 0000000000..21be0710fb --- /dev/null +++ b/premium/discordpremiumsource/discordpremiumsource.go @@ -0,0 +1,162 @@ +package discordpremiumsource + +import ( + "context" + "fmt" + "time" + + "emperror.dev/errors" + "github.com/botlabs-gg/yagpdb/v2/common" + "github.com/botlabs-gg/yagpdb/v2/premium" + "github.com/botlabs-gg/yagpdb/v2/premium/models" + "github.com/botlabs-gg/yagpdb/v2/web" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +type DiscordPremiumSource struct{} + +var ActiveDiscordPremiumPoller *DiscordPremiumPoller + +func RegisterPlugin() { + logger.Info("Registered discord premium source") + premium.RegisterPremiumSource(&DiscordPremiumSource{}) + common.RegisterPlugin(&Plugin{}) +} + +func (ps *DiscordPremiumSource) Init() {} + +func (ps *DiscordPremiumSource) Names() (human string, idname string) { + return "Discord", "discord" +} + +var logger = common.GetPluginLogger(&Plugin{}) + +type Plugin struct{} + +func (p *Plugin) PluginInfo() *common.PluginInfo { + return &common.PluginInfo{ + Name: "Discord premium source", + SysName: "discord_premium_source", + Category: common.PluginCategoryCore, + } +} + +var _ web.Plugin = (*Plugin)(nil) + +func (p *Plugin) InitWeb() { + if confDiscordPremiumSKUID.GetInt() == 0 { + logger.Warn("No discord premium SKU ID set, not starting poller") + return + } + ActiveDiscordPremiumPoller = InitPoller() + if ActiveDiscordPremiumPoller == nil { + logger.Warn("Failed initializing discord premium poller") + return + } + go RunPoller() +} + +func RunPoller() { + ticker := time.NewTicker(time.Minute) + for { + <-ticker.C + err := UpdatePremiumSlots(context.Background()) + if err != nil { + logger.WithError(err).Error("Failed updating premium slots for discord") + } + } +} + +func UpdatePremiumSlots(ctx context.Context) error { + tx, err := common.PQ.BeginTx(ctx, nil) + if err != nil { + return errors.WithMessage(err, "BeginTX") + } + + slots, err := models.PremiumSlots(qm.Where("source='discord'"), qm.OrderBy("id desc")).All(ctx, tx) + if err != nil { + tx.Rollback() + return errors.WithMessage(err, "PremiumSlots") + } + + entitlements := ActiveDiscordPremiumPoller.GetEntitlements() + if len(entitlements) == 0 { + tx.Rollback() + return nil + } + + // Sort the slots into a map of users -> slots + sorted := make(map[int64][]*models.PremiumSlot) + for _, slot := range slots { + sorted[slot.UserID] = append(sorted[slot.UserID], slot) + } + + // Update already tracked slots + for userID, userSlots := range sorted { + slotsForPledge := 0 + for _, entitlement := range entitlements { + if entitlement.UserID == userID { + //TODO: Add Support for multiple slots via discord + slotsForPledge = 1 + break + } + } + + if slotsForPledge == len(userSlots) { + // Correct number of slots already set up + continue + } + + if slotsForPledge > len(userSlots) { + // Need to create more slots + for i := 0; i < slotsForPledge-len(userSlots); i++ { + title := fmt.Sprintf("Discord Slot #%d", 1+i+len(userSlots)) + slot, err := premium.CreatePremiumSlot(ctx, tx, userID, "discord", title, "Slot is available as long as subscription is Active on Discord", int64(i+len(userSlots)), -1, premium.PremiumTierPremium) + if err != nil { + tx.Rollback() + return errors.WithMessage(err, "CreatePremiumSlot") + } + logger.Info("Created discord premium slot #", slot.ID, slot.UserID) + } + } else if slotsForPledge < len(userSlots) { + // Need to remove slots + slotsToRemove := make([]int64, 0) + for i := 0; i < len(userSlots)-slotsForPledge; i++ { + slot := userSlots[i] + slotsToRemove = append(slotsToRemove, slot.ID) + logger.Info("Marked discord slot for deletion #", slot.ID, slot.UserID) + } + err = premium.RemovePremiumSlots(ctx, tx, userID, "discord", slotsToRemove) + if err != nil { + tx.Rollback() + return errors.WithMessage(err, "RemovePremiumSlots") + } + } + } + + // Add completely new premium slots +OUTER: + for _, v := range entitlements { + for userID := range sorted { + if userID == v.UserID { + continue OUTER + } + } + + // If we got here then that means this is a new user + slots := 1 + for i := 0; i < slots; i++ { + title := fmt.Sprintf("Discord Premium Slot #%d", i+1) + slot, err := premium.CreatePremiumSlot(ctx, tx, v.UserID, "discord", title, "Slot is available as long as subscription is Active on Discord", int64(i+1), -1, premium.PremiumTierPremium) + if err != nil { + tx.Rollback() + return errors.WithMessage(err, "new CreatePremiumSlot") + } + + logger.Info("Created new discord premium slot #", slot.ID, slot.ID) + } + } + + err = tx.Commit() + return errors.WithMessage(err, "Commit") +} diff --git a/premium/discordpremiumsource/poller.go b/premium/discordpremiumsource/poller.go new file mode 100644 index 0000000000..5d64ff9061 --- /dev/null +++ b/premium/discordpremiumsource/poller.go @@ -0,0 +1,111 @@ +package discordpremiumsource + +import ( + "sync" + "time" + + "github.com/botlabs-gg/yagpdb/v2/common" + "github.com/botlabs-gg/yagpdb/v2/common/config" + "github.com/botlabs-gg/yagpdb/v2/lib/discordgo" +) + +type DiscordPremiumPoller struct { + mu sync.RWMutex + activeEntitlements []*discordgo.Entitlement +} + +var confDiscordPremiumSKUID = config.RegisterOption("yagpdb.discord.premium.sku_id", "SKU_ID for Discord Premium", nil) + +func InitPoller() *DiscordPremiumPoller { + + discordPremiumSKUID := int64(confDiscordPremiumSKUID.GetInt()) + if discordPremiumSKUID == 0 { + DiscordPremiumDisabled(nil, "Missing Discord Premium SKU_ID, not starting poller") + return nil + } + + poller := &DiscordPremiumPoller{} + go poller.Run() + return poller +} + +func DiscordPremiumDisabled(err error, reason string) { + l := logger + + if err != nil { + l = l.WithError(err) + } + + l.Warn("Not starting discord premium integration" + reason) +} + +func (p *DiscordPremiumPoller) Run() { + logger.Info("Starting Discord Premium Poller") + ticker := time.NewTicker(time.Minute * 30) + for { + p.Poll() + <-ticker.C + } +} + +func (p *DiscordPremiumPoller) Poll() { + logger.Info("Fetching Discord SKUs") + discordPremiumSKUID := int64(confDiscordPremiumSKUID.GetInt()) + // Get your SKU data + skus, err := common.BotSession.SKUs(common.BotApplication.ID) + if err != nil || len(skus) < 1 { + logger.WithError(err).Error("Failed fetching skus") + return + } + + var is_sku_configured bool + for _, sku := range skus { + if sku.ID == discordPremiumSKUID { + is_sku_configured = true + break + } + } + + if !is_sku_configured { + logger.Error("SKU ID not found in bot's application SKUs") + return + } + + afterID := int64(0) + filterOptions := &discordgo.EntitlementFilterOptions{ + SkuIDs: []int64{discordPremiumSKUID}, + ExcludeEnded: true, + Limit: 100, + } + allEntitlements := make([]*discordgo.Entitlement, 0) + for { + entitlements, err := common.BotSession.Entitlements(common.BotApplication.ID, filterOptions) + if err != nil { + logger.WithError(err).Error("Failed fetching Entitlements") + break + } + if len(entitlements) == 0 { + logger.Infof("Finished Fetching All Entitlements, Total Active Entitlements: %d", len(allEntitlements)) + break + } + for _, entitlement := range entitlements { + if entitlement.ID > afterID { + afterID = entitlement.ID + } + allEntitlements = append(allEntitlements, entitlement) + } + filterOptions.AfterID = afterID + time.Sleep(time.Second) + } + // Swap the stored ones, this dosent mutate the existing returned slices so we dont have to do any copying on each request woo + p.mu.Lock() + p.activeEntitlements = allEntitlements + p.mu.Unlock() +} + +func (p *DiscordPremiumPoller) GetEntitlements() (entitlements []*discordgo.Entitlement) { + p.mu.RLock() + entitlements = p.activeEntitlements + p.mu.RUnlock() + return +} diff --git a/premium/patreonpremiumsource/patreonpremiumsource.go b/premium/patreonpremiumsource/patreonpremiumsource.go index 644fce92d7..14284db6b2 100644 --- a/premium/patreonpremiumsource/patreonpremiumsource.go +++ b/premium/patreonpremiumsource/patreonpremiumsource.go @@ -126,7 +126,7 @@ func UpdatePremiumSlots(ctx context.Context) error { logger.Info("Marked patreon slot for deletion #", slot.ID, slot.UserID) } - err = premium.RemovePremiumSlots(ctx, tx, userID, slotsToRemove) + err = premium.RemovePremiumSlots(ctx, tx, userID, "patreon", slotsToRemove) if err != nil { tx.Rollback() return errors.WithMessage(err, "RemovePremiumSlots") diff --git a/premium/slotpremiumsource.go b/premium/slotpremiumsource.go index 25600ff1a6..c442117d40 100644 --- a/premium/slotpremiumsource.go +++ b/premium/slotpremiumsource.go @@ -253,8 +253,8 @@ func SlotExpired(ctx context.Context, slot *models.PremiumSlot) error { // RemovePremiumSlots removes the specifues premium slots and attempts to migrate to other permanent available ones // THIS SHOULD BE USED INSIDE A TRANSACTION ONLY, AS OTHERWISE RACE CONDITIONS BE UPON THEE -func RemovePremiumSlots(ctx context.Context, exec boil.ContextExecutor, userID int64, slotsToRemove []int64) error { - userSlots, err := models.PremiumSlots(qm.Where("user_id = ?", userID), qm.OrderBy("id desc"), qm.For("UPDATE")).All(ctx, exec) +func RemovePremiumSlots(ctx context.Context, exec boil.ContextExecutor, userID int64, source string, slotsToRemove []int64) error { + userSlots, err := models.PremiumSlots(qm.Where("user_id = ?", userID), qm.Where("source = ?", source), qm.OrderBy("id desc"), qm.For("UPDATE")).All(ctx, exec) if err != nil { return errors.WithMessage(err, "models.PremiumSlots") } diff --git a/yagpdb_docker/app.example.env b/yagpdb_docker/app.example.env index 0fedd49e11..34a5d18a36 100644 --- a/yagpdb_docker/app.example.env +++ b/yagpdb_docker/app.example.env @@ -59,3 +59,4 @@ GOOGLE_APPLICATION_CREDENTIALS=path/to/credentials.json # This will be used as the pubsubhubbub (websub) verify token when receiving callbacks on new video uploads # if this gets leaked, people could spam feeds YAGPDB_YOUTUBE_VERIFY_TOKEN= +YAGPDB_DISCORD_PREMIUM_SKU_ID= \ No newline at end of file From 2e9e954e29c4c9c2b356f5812886460333831246 Mon Sep 17 00:00:00 2001 From: Ashish Date: Sat, 5 Oct 2024 19:40:04 +0530 Subject: [PATCH 2/3] more enhancements --- premium/assets/premium-perks.html | 22 +++++---- premium/discordpremiumsource/bot.go | 1 + .../discordpremiumsource.go | 5 ++- .../patreonpremiumsource.go | 4 +- premium/plugin_bot.go | 45 ++++++++++++++++++- premium/premium.go | 3 +- 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/premium/assets/premium-perks.html b/premium/assets/premium-perks.html index 7825a0abf0..8b0d924248 100644 --- a/premium/assets/premium-perks.html +++ b/premium/assets/premium-perks.html @@ -11,31 +11,35 @@

Premium Perks

-

Info

+

Information about YAGPDB Premium

Premium helps covers server cost and supports the development of the bot

Here you can find the features that premium subscription provides.

- -
+ +
+
  • Make a pledge on our Patreon. Tiers $3 and above will grant you premium slots. It can take up to 10 minutes from you making a pledge to it being processed on our side. (If you do not see your premium slots after that time, please contact us through the support server.)
+ To manage and assign your premium slots to a server, please go to the premium page +
+ Note: Premium slots are not transferable between Patreon and Discord Store.

- -
+ +
General goodies diff --git a/premium/discordpremiumsource/bot.go b/premium/discordpremiumsource/bot.go index de1a9c8fe5..5f5fc004d7 100644 --- a/premium/discordpremiumsource/bot.go +++ b/premium/discordpremiumsource/bot.go @@ -65,6 +65,7 @@ func HandleEntitlementCreate(evt *eventsystem.EventData) { return } err = tx.Commit() + go bot.SendDM(entitlement.UserID, fmt.Sprintf("You have received %d new premium slots via Discord Subscription, [Assign them to a server here](https://%s/premium)", 1, common.ConfHost.GetString())) if err != nil { logger.WithError(err).Error("Failed committing transaction for EntitlementCreate Event") } diff --git a/premium/discordpremiumsource/discordpremiumsource.go b/premium/discordpremiumsource/discordpremiumsource.go index 21be0710fb..0e6c9e153b 100644 --- a/premium/discordpremiumsource/discordpremiumsource.go +++ b/premium/discordpremiumsource/discordpremiumsource.go @@ -6,6 +6,7 @@ import ( "time" "emperror.dev/errors" + "github.com/botlabs-gg/yagpdb/v2/bot" "github.com/botlabs-gg/yagpdb/v2/common" "github.com/botlabs-gg/yagpdb/v2/premium" "github.com/botlabs-gg/yagpdb/v2/premium/models" @@ -118,6 +119,7 @@ func UpdatePremiumSlots(ctx context.Context) error { } logger.Info("Created discord premium slot #", slot.ID, slot.UserID) } + go bot.SendDM(userID, fmt.Sprintf("You have received %d new premium slots via Discord Subscription, [Assign them to a server here](https://%s/premium)", slotsForPledge-len(userSlots), common.ConfHost.GetString())) } else if slotsForPledge < len(userSlots) { // Need to remove slots slotsToRemove := make([]int64, 0) @@ -152,11 +154,10 @@ OUTER: tx.Rollback() return errors.WithMessage(err, "new CreatePremiumSlot") } - logger.Info("Created new discord premium slot #", slot.ID, slot.ID) } + go bot.SendDM(v.UserID, fmt.Sprintf("You have received %d new premium slots via Discord Subscription, [Assign them to a server here](https://%s/premium)", 1, common.ConfHost.GetString())) } - err = tx.Commit() return errors.WithMessage(err, "Commit") } diff --git a/premium/patreonpremiumsource/patreonpremiumsource.go b/premium/patreonpremiumsource/patreonpremiumsource.go index 14284db6b2..472d538ae8 100644 --- a/premium/patreonpremiumsource/patreonpremiumsource.go +++ b/premium/patreonpremiumsource/patreonpremiumsource.go @@ -6,6 +6,7 @@ import ( "time" "emperror.dev/errors" + "github.com/botlabs-gg/yagpdb/v2/bot" "github.com/botlabs-gg/yagpdb/v2/common" "github.com/botlabs-gg/yagpdb/v2/common/patreon" "github.com/botlabs-gg/yagpdb/v2/premium" @@ -113,9 +114,9 @@ func UpdatePremiumSlots(ctx context.Context) error { tx.Rollback() return errors.WithMessage(err, "CreatePremiumSlot") } - logger.Info("Created patreon premium slot #", slot.ID, slot.UserID) } + go bot.SendDM(userID, fmt.Sprintf("You have received %d new premium slots via Patreon Subscription, [Assign them to a server here](https://%s/premium)", 1, common.ConfHost.GetString())) } else if slotsForPledge < len(userSlots) { // Need to remove slots slotsToRemove := make([]int64, 0) @@ -159,6 +160,7 @@ OUTER: logger.Info("Created new patreon premium slot #", slot.ID, slot.ID) } + go bot.SendDM(v.DiscordID, fmt.Sprintf("You have received %d new premium slots via Patreon Subscription, [Assign them to a server here](https://%s/premium)", 1, common.ConfHost.GetString())) } err = tx.Commit() diff --git a/premium/plugin_bot.go b/premium/plugin_bot.go index ed629c6b2a..938d068d31 100644 --- a/premium/plugin_bot.go +++ b/premium/plugin_bot.go @@ -1,10 +1,17 @@ package premium import ( + "fmt" "time" "github.com/botlabs-gg/yagpdb/v2/bot" "github.com/botlabs-gg/yagpdb/v2/commands" + "github.com/botlabs-gg/yagpdb/v2/common" + "github.com/botlabs-gg/yagpdb/v2/lib/dcmd" + "github.com/botlabs-gg/yagpdb/v2/lib/discordgo" + "github.com/botlabs-gg/yagpdb/v2/premium/models" + "github.com/botlabs-gg/yagpdb/v2/stdcommands/util" + "github.com/volatiletech/sqlboiler/v4/queries/qm" ) var _ bot.BotInitHandler = (*Plugin)(nil) @@ -27,12 +34,46 @@ func init() { } } +var cmdPremiumStatus = &commands.YAGCommand{ + CmdCategory: commands.CategoryGeneral, + Name: "premiumstatus", + Aliases: []string{"gpc"}, + Description: "Generates premium codes. Bot Owner Only", + RequiredArgs: 0, + RunInDM: true, + SlashCommandEnabled: true, + ArgSwitches: []*dcmd.ArgDef{ + {Name: "User", Type: dcmd.UserID, Help: "Optional User to check premium status for", Default: 0}, + }, + RunFunc: util.RequireOwner(func(data *dcmd.Data) (interface{}, error) { + userID := int64(data.Switches["User"].Int()) + if userID == 0 { + userID = data.Author.ID + } + if confAllGuildsPremium.GetBool() { + return "All guilds are premium, have fun!", nil + } + premiumSlots, err := models.PremiumSlots(qm.Where("user_id=?", userID)).AllG(data.Context()) + if err != nil { + return "Failed Fetching Premium Slots ", err + } + embed := &discordgo.MessageEmbed{} + if len(premiumSlots) < 1 { + embed.Title = "No Premium Slots Found" + embed.Description = fmt.Sprintf("<@%d> doesn have any premium slots! [Learn how to get premium](https://%s/premium-perks)", userID, common.ConfHost.GetString()) + return embed, nil + } + embed.Title = fmt.Sprintf("User has %d Premium Slots!", len(premiumSlots)) + embed.Description = fmt.Sprintf("<@%d> has %d premium slots! [Manage your premium slots](https://%s/premium)", userID, len(premiumSlots), common.ConfHost.GetString()) + return embed, nil + }), +} + func (p *Plugin) BotInit() { - // bot.State.CustomLimitProvider = p } func (p *Plugin) AddCommands() { - commands.AddRootCommands(p, cmdGenerateCode) + commands.AddRootCommands(p, cmdGenerateCode, cmdPremiumStatus) } const ( diff --git a/premium/premium.go b/premium/premium.go index 46107f2121..0513938392 100644 --- a/premium/premium.go +++ b/premium/premium.go @@ -44,7 +44,8 @@ func (p PremiumTier) String() string { } var ( - confAllGuildsPremium = config.RegisterOption("yagpdb.premium.all_guilds_premium", "All servers have premium", false) + confAllGuildsPremium = config.RegisterOption("yagpdb.premium.all_guilds_premium", "All servers have premium", false) + confDiscordPremiumSKUID = config.RegisterOption("yagpdb.discord.premium.sku_id", "SKU_ID for Discord Premium", nil) ) var logger = common.GetPluginLogger(&Plugin{}) From 3d53f30c52d6ea814c012608640517227e3a3621 Mon Sep 17 00:00:00 2001 From: Ashish Date: Sat, 5 Oct 2024 19:44:15 +0530 Subject: [PATCH 3/3] more enhancements --- premium/codepremiumsource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/premium/codepremiumsource.go b/premium/codepremiumsource.go index df05d325f8..68a92784da 100644 --- a/premium/codepremiumsource.go +++ b/premium/codepremiumsource.go @@ -55,7 +55,7 @@ func ExpiredSlotsRemover() { func RemoveExpiredSlots() error { tx, _ := common.PQ.BeginTx(context.Background(), nil) - slots, err := models.PremiumCodes(qm.Where("source = ?"), qm.Where("duration_remaining < 0"), qm.Where("permanent = false")).AllG(context.Background()) + slots, err := models.PremiumCodes(qm.Where("source = 'code'"), qm.Where("duration_remaining < 0"), qm.Where("permanent = false")).AllG(context.Background()) if err != nil { logger.WithError(err).Error("Failed getting expired codes") return err