From 863e6690a16c16d94bbf9fe64f6ddc9a0ca0af7e Mon Sep 17 00:00:00 2001 From: Blane Thompson Date: Tue, 7 Sep 2021 13:40:58 +0100 Subject: [PATCH] Linting, working on shardgroup and shards --- .dockerignore | 4 +- .gitattributes | 2 +- .github/workflows/codeql-analysis.yml | 142 ++--- build.sh | 24 +- cmd/main.go | 110 ++-- discord/structs/application.go | 504 +++++++-------- discord/structs/channel.go | 224 +++---- discord/structs/embed.go | 142 ++--- discord/structs/emoji.go | 34 +- discord/structs/events.go | 710 ++++++++++----------- discord/structs/gateway.go | 271 ++++---- discord/structs/guild.go | 332 +++++----- discord/structs/http.go | 54 +- discord/structs/invites.go | 22 +- discord/structs/member.go | 40 +- discord/structs/message.go | 298 ++++----- discord/structs/presence.go | 174 ++--- discord/structs/role.go | 50 +- discord/structs/sticker.go | 74 +-- discord/structs/template.go | 6 +- discord/structs/user.go | 108 ++-- discord/structs/webhook.go | 84 +-- generate-protobuf.sh | 20 +- go.mod | 4 + go.sum | 50 +- internal/analytics.go | 48 +- internal/client.go | 270 ++++---- internal/errors.go | 60 +- internal/grpc.go | 8 +- internal/manager.go | 765 +++++++++++----------- internal/messaging.go | 304 ++++----- internal/sandwich.go | 872 +++++++++++++------------- internal/shard.go | 618 +++++++++++++++++- internal/shardgroup.go | 337 ++++++---- internal/state.go | 164 ++--- internal/utils.go | 142 ++--- internal/webhook.go | 135 ++-- messaging/kafka.go | 200 +++--- messaging/redis.go | 156 ++--- messaging/stan.go | 236 +++---- messaging/utils.go | 36 +- structs/http.go | 38 +- structs/messaging.go | 48 +- structs/state.go | 16 +- structs/status.go | 2 +- structure.txt | 18 +- 46 files changed, 4325 insertions(+), 3631 deletions(-) diff --git a/.dockerignore b/.dockerignore index 939746a..966bb2c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,2 @@ -.git -web/node_modules +.git +web/node_modules diff --git a/.gitattributes b/.gitattributes index ad3bf33..824a267 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ -*.js linguist-detectable=false +*.js linguist-detectable=false *.map linguist-detectable=false \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6191654..f9556c5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,71 +1,71 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '24 15 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '24 15 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/build.sh b/build.sh index 788bcdc..f21105e 100644 --- a/build.sh +++ b/build.sh @@ -1,12 +1,12 @@ -echo "Build GO Executable" -go build -v -o sandwich cmd/main.go - -echo "Build Web Distributable" -#!cd web -#!yarn lint -#!yarn build --modern -#!cd .. - -echo "Docker build and push" -docker build --tag 1345/sandwich-daemon:latest . -docker push 1345/sandwich-daemon:latest +echo "Build GO Executable" +go build -v -o sandwich cmd/main.go + +echo "Build Web Distributable" +#!cd web +#!yarn lint +#!yarn build --modern +#!cd .. + +echo "Docker build and push" +docker build --tag 1345/sandwich-daemon:latest . +docker push 1345/sandwich-daemon:latest diff --git a/cmd/main.go b/cmd/main.go index c1ccdc5..0042c78 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,55 +1,55 @@ -package main - -import ( - "flag" - "os" - "os/signal" - "syscall" - "time" - - internal "github.com/WelcomerTeam/Sandwich-Daemon/next/internal" - "github.com/rs/zerolog" -) - -func main() { - lFlag := flag.String("level", "info", "Global log level to use (debug/info/warn/error/fatal/panic/no/disabled/trace) (default: info)") - lConfigurationLocation := flag.String("configuration", "sandwich.yaml", "Path of configuration file (default: sandwich.yaml)") - lPoolConcurrency := flag.Int("concurrency", 512, "Total number of events that can be processed concurrently (default: 512)") - - flag.Parse() - - // Parse flag and configure logging - level, err := zerolog.ParseLevel(*lFlag) - if err != nil { - level = zerolog.InfoLevel - } - - zerolog.SetGlobalLevel(level) - - consoleWriter := zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: time.Stamp, - } - - log := zerolog.New(consoleWriter).With().Timestamp().Logger() - - // Sandwich initialization - sandwich, err := internal.NewSandwich(consoleWriter, *lConfigurationLocation, *lPoolConcurrency) - if err != nil { - log.Panic().Err(err).Msg("Cannot create sandwich") - } - - err = sandwich.Open() - if err != nil { - log.Panic().Err(err).Msg("Cannot open sandwich") - } - - signalCh := make(chan os.Signal, 1) - signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) - <-signalCh - - err = sandwich.Close() - if err != nil { - sandwich.Logger.Warn().Err(err).Msg("Exception whilst closing sandwich") - } -} +package main + +import ( + "flag" + "os" + "os/signal" + "syscall" + "time" + + internal "github.com/WelcomerTeam/Sandwich-Daemon/next/internal" + "github.com/rs/zerolog" +) + +func main() { + lFlag := flag.String("level", "info", "Global log level to use (debug/info/warn/error/fatal/panic/no/disabled/trace) (default: info)") + lConfigurationLocation := flag.String("configuration", "sandwich.yaml", "Path of configuration file (default: sandwich.yaml)") + lPoolConcurrency := flag.Int("concurrency", 512, "Total number of events that can be processed concurrently (default: 512)") + + flag.Parse() + + // Parse flag and configure logging + level, err := zerolog.ParseLevel(*lFlag) + if err != nil { + level = zerolog.InfoLevel + } + + zerolog.SetGlobalLevel(level) + + consoleWriter := zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.Stamp, + } + + log := zerolog.New(consoleWriter).With().Timestamp().Logger() + + // Sandwich initialization + sandwich, err := internal.NewSandwich(consoleWriter, *lConfigurationLocation, *lPoolConcurrency) + if err != nil { + log.Panic().Err(err).Msg("Cannot create sandwich") + } + + err = sandwich.Open() + if err != nil { + log.Panic().Err(err).Msg("Cannot open sandwich") + } + + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-signalCh + + err = sandwich.Close() + if err != nil { + sandwich.Logger.Warn().Err(err).Msg("Exception whilst closing sandwich") + } +} diff --git a/discord/structs/application.go b/discord/structs/application.go index 1028bd1..03f2c80 100644 --- a/discord/structs/application.go +++ b/discord/structs/application.go @@ -1,252 +1,252 @@ -package discord - -import ( - "github.com/WelcomerTeam/RealRock/snowflake" -) - -// application.go represents the application object and interactions. - -// ApplicationTeamMemberState represents the state of a member in a team. -type ApplicationTeamMemberState uint8 - -const ( - ApplicationTeamMemberStateInvited ApplicationTeamMemberState = 1 + iota - ApplicationTeamMemberStateAccepted -) - -// ApplicationCommandType represents the different types of application command. -type ApplicationCommandType uint8 - -const ( - ApplicationCommandTypeChatInput ApplicationCommandType = 1 + iota - ApplicationCommandTypeUser - ApplicationCommandTypeMessage -) - -// ApplicationCommandOptionType represents the different types of options. -type ApplicationCommandOptionType uint8 - -const ( - ApplicationCommandOptionTypeSubCommand ApplicationCommandOptionType = 1 + iota - ApplicationCommandOptionTypeSubCommandGroup - ApplicationCommandOptionTypeString - ApplicationCommandOptionTypeInteger - ApplicationCommandOptionTypeBoolean - ApplicationCommandOptionTypeUser - ApplicationCommandOptionTypeChannel - ApplicationCommandOptionTypeRole - ApplicationCommandOptionTypeMentionable - ApplicationCommandOptionTypeNumber -) - -// InteractionType represents the type of interaction. -type InteractionType uint8 - -const ( - InteractionTypePing InteractionType = 1 + iota - InteractionTypeApplicationCommand - InteractionTypeMessageComponent -) - -// IntegrationType represents the type of integration -type IntegrationType string - -const ( - IntegrationTypeTwitch IntegrationType = "twitch" - IntegrationTypeYoutube IntegrationType = "youtube" - IntegrationTypeDiscord IntegrationType = "discord" -) - -// IntegrationExpireBehavior represents the integration expiration -type IntegrationExpireBehavior uint8 - -const ( - IntegrationExpireBehaviorRemoveRole IntegrationExpireBehavior = iota - IntegrationExpireBehaviorKick -) - -// InteractionComponentType represents the type of component. -type InteractionComponentType uint8 - -const ( - InteractionComponentTypeActionRow InteractionComponentType = 1 + iota - InteractionComponentTypeButton - InteractionComponentTypeSelectMenu -) - -// InteractionComponentStyle represents the style of a component -type InteractionComponentStyle uint8 - -const ( - InteractionComponentStylePrimary InteractionComponentStyle = 1 + iota - InteractionComponentStyleSecondary - InteractionComponentStyleSuccess - InteractionComponentStyleDanger - InteractionComponentStyleLink -) - -// Application response from REST. -type Application struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Icon string `json:"icon,omitempty"` - Description string `json:"description"` - - RPCOrigins []string `json:"rpc_origins,omitempty"` - BotPublic bool `json:"bot_public"` - BotRequireCodeGrant bool `json:"bot_require_code_grant"` - TermsOfServiceURL string `json:"terms_of_service,omitempty"` - PrivacyPolicyURL string `json:"privacy_policy_url,omitempty"` - Owner *User `json:"owner,omitempty"` - Summary string `json:"summary,omitempty"` - VerifyKey string `json:"verify_key,omitempty"` - Team *ApplicationTeam `json:"team,omitempty"` - GuildID snowflake.ID `json:"guild_id,omitempty"` - - PrimarySKUID snowflake.ID `json:"primary_sku_id,omitempty"` - Slug string `json:"slug,omitempty"` - CoverImage string `json:"cover_image,omitempty"` - Flags int64 `json:"flags"` -} - -// ApplicationTeam represents the team of an application. -type ApplicationTeam struct { - Icon string `json:"icon,omitempty"` - ID snowflake.ID `json:"id"` - Members []*ApplicationTeamMember `json:"members"` - Name string `json:"name"` - OwnerUserID snowflake.ID `json:"owner_user_id"` -} - -// ApplicationTeamMembers represents a member of a team. -type ApplicationTeamMember struct { - MembershipState ApplicationTeamMemberState `json:"membership_state"` - Permissions []string `json:"permissions"` - TeamID snowflake.ID `json:"team_id"` - User *User `json:"user"` -} - -// ApplicationCommand represents an application's command. -type ApplicationCommand struct { - ID snowflake.ID `json:"id"` - Type *ApplicationCommandType `json:"type,omitempty"` - ApplicationID snowflake.ID `json:"application_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Name string `json:"name"` - Description string `json:"description"` - Options []*ApplicationCommandOption `json:"options,omitempty"` - DefaultPermission *bool `json:"default_permission"` -} - -// ApplicationCommandOption represents the options for an application command. -type ApplicationCommandOption struct { - Type ApplicationCommandOptionType `json:"type"` - Name string `json:"name"` - Description string `json:"description"` - Required *bool `json:"required,omitempty"` - Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"` - Options []*ApplicationCommandOption `json:"options,omitempty"` -} - -// ApplicationCommandOptionChoice represents the different choices. -type ApplicationCommandOptionChoice struct { - Name string `json:"name"` - Value interface{} `json:"value"` -} - -// Interaction represents the structure of an interaction. -type Interaction struct { - ID snowflake.ID `json:"id"` - ApplicationID snowflake.ID `json:"application_id"` - Type *InteractionType `json:"type,omitempty"` - Data *InteractionData `json:"data,omitempty"` - - GuildID snowflake.ID `json:"guild_id,omitempty"` - ChannelID snowflake.ID `json:"channel_id,omitempty"` - Member *Member `json:"member,omitempty"` - User *User `json:"user,omitempty"` - Token string `json:"token"` - Version int `json:"version"` - Message *Message `json:"message,omitempty"` -} - -// InteractionData represents the structure of interaction data. -type InteractionData struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Type ApplicationCommandType `json:"type"` - Resolved *InteractionResolvedData `json:"resolved,omitempty"` - Options []InteractionDataOption `json:"option,omitempty"` - CustomID *string `json:"custom_id,omitempty"` - ComponentType *ApplicationCommandType `json:"component_type,omitempty"` - Values []ApplicationSelectOption `json:"values,omitempty"` - TargetID *snowflake.ID `json:"target_id,omitempty"` -} - -// InteractionDataOption represents the structure of an interaction option. -type InteractionDataOption struct { - Name string `json:"name"` - Type ApplicationCommandOptionType `json:"type"` - Value interface{} `json:"value"` - Options []InteractionDataOption `json:"options,omitempty"` -} - -// InteractionResolvedData represents any extra payload data for an interaction. -type InteractionResolvedData struct { - Users []*User `json:"users,omitempty"` - Members []*Member `json:"members,omitempty"` - Roles []*Role `json:"roles,omitempty"` - Channels []*Channel `json:"channels,omitempty"` - Messages []*Message `json:"messages,omitempty"` -} - -// ApplicationSelectOption represents the structure of select options. -type ApplicationSelectOption struct { - Label string `json:"label"` - Value string `json:"value"` - Description *string `json:"description,omitempty"` - Emoji *Emoji `json:"emoji,omitempty"` - Default *bool `json:"default,omitempty"` -} - -// Integration represents the structure of an integration. -type Integration struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Type IntegrationType `json:"type"` - Enabled bool `json:"enabled"` - Syncing *bool `json:"syncing"` - RoleID *snowflake.ID `json:"role_id,omitempty"` - EnableEmoticons *bool `json:"enable_emoticons,omitempty"` - - ExpireBehavior *IntegrationExpireBehavior `json:"expire_behavior,omitempty"` - ExpireGracePeriod int `json:"expire_grace_period"` - User *User `json:"user,omitempty"` - Account IntegrationAccount `json:"account"` - SyncedAt *string `json:"synced_at,omitempty"` - SubscriberCount int `json:"subscriber_count,omitempty"` - Revoked *bool `json:"revoked,omitempty"` - Application *Application `json:"application,omitempty"` -} - -// IntegrationAccount represents the account of the integration. -type IntegrationAccount struct { - ID string `json:"id"` - Name string `json:"name"` -} - -// InteractionComponent represents the structure of a component. -type InteractionComponent struct { - Type InteractionComponentType `json:"type"` - CustomID *string `json:"custom_id,omitempty"` - Disabled *bool `json:"disabled,omitempty"` - Style *InteractionComponentStyle `json:"style,omitempty"` - Label *string `json:"label,omitempty"` - Emoji *Emoji `json:"emoji,omitempty"` - URL *string `json:"url,omitempty"` - Options []*ApplicationSelectOption `json:"options"` - Placeholder *string `json:"placeholder,omitempty"` - MinValues *int `json:"min_values,omitempty"` - MaxValues *int `json:"max_values,omitempty"` - Components []*InteractionComponent `json:"components,omitempty"` -} +package discord + +import ( + "github.com/WelcomerTeam/RealRock/snowflake" +) + +// application.go represents the application object and interactions. + +// ApplicationTeamMemberState represents the state of a member in a team. +type ApplicationTeamMemberState uint8 + +const ( + ApplicationTeamMemberStateInvited ApplicationTeamMemberState = 1 + iota + ApplicationTeamMemberStateAccepted +) + +// ApplicationCommandType represents the different types of application command. +type ApplicationCommandType uint8 + +const ( + ApplicationCommandTypeChatInput ApplicationCommandType = 1 + iota + ApplicationCommandTypeUser + ApplicationCommandTypeMessage +) + +// ApplicationCommandOptionType represents the different types of options. +type ApplicationCommandOptionType uint8 + +const ( + ApplicationCommandOptionTypeSubCommand ApplicationCommandOptionType = 1 + iota + ApplicationCommandOptionTypeSubCommandGroup + ApplicationCommandOptionTypeString + ApplicationCommandOptionTypeInteger + ApplicationCommandOptionTypeBoolean + ApplicationCommandOptionTypeUser + ApplicationCommandOptionTypeChannel + ApplicationCommandOptionTypeRole + ApplicationCommandOptionTypeMentionable + ApplicationCommandOptionTypeNumber +) + +// InteractionType represents the type of interaction. +type InteractionType uint8 + +const ( + InteractionTypePing InteractionType = 1 + iota + InteractionTypeApplicationCommand + InteractionTypeMessageComponent +) + +// IntegrationType represents the type of integration +type IntegrationType string + +const ( + IntegrationTypeTwitch IntegrationType = "twitch" + IntegrationTypeYoutube IntegrationType = "youtube" + IntegrationTypeDiscord IntegrationType = "discord" +) + +// IntegrationExpireBehavior represents the integration expiration +type IntegrationExpireBehavior uint8 + +const ( + IntegrationExpireBehaviorRemoveRole IntegrationExpireBehavior = iota + IntegrationExpireBehaviorKick +) + +// InteractionComponentType represents the type of component. +type InteractionComponentType uint8 + +const ( + InteractionComponentTypeActionRow InteractionComponentType = 1 + iota + InteractionComponentTypeButton + InteractionComponentTypeSelectMenu +) + +// InteractionComponentStyle represents the style of a component +type InteractionComponentStyle uint8 + +const ( + InteractionComponentStylePrimary InteractionComponentStyle = 1 + iota + InteractionComponentStyleSecondary + InteractionComponentStyleSuccess + InteractionComponentStyleDanger + InteractionComponentStyleLink +) + +// Application response from REST. +type Application struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Icon string `json:"icon,omitempty"` + Description string `json:"description"` + + RPCOrigins []string `json:"rpc_origins,omitempty"` + BotPublic bool `json:"bot_public"` + BotRequireCodeGrant bool `json:"bot_require_code_grant"` + TermsOfServiceURL string `json:"terms_of_service,omitempty"` + PrivacyPolicyURL string `json:"privacy_policy_url,omitempty"` + Owner *User `json:"owner,omitempty"` + Summary string `json:"summary,omitempty"` + VerifyKey string `json:"verify_key,omitempty"` + Team *ApplicationTeam `json:"team,omitempty"` + GuildID snowflake.ID `json:"guild_id,omitempty"` + + PrimarySKUID snowflake.ID `json:"primary_sku_id,omitempty"` + Slug string `json:"slug,omitempty"` + CoverImage string `json:"cover_image,omitempty"` + Flags int64 `json:"flags"` +} + +// ApplicationTeam represents the team of an application. +type ApplicationTeam struct { + Icon string `json:"icon,omitempty"` + ID snowflake.ID `json:"id"` + Members []*ApplicationTeamMember `json:"members"` + Name string `json:"name"` + OwnerUserID snowflake.ID `json:"owner_user_id"` +} + +// ApplicationTeamMembers represents a member of a team. +type ApplicationTeamMember struct { + MembershipState ApplicationTeamMemberState `json:"membership_state"` + Permissions []string `json:"permissions"` + TeamID snowflake.ID `json:"team_id"` + User *User `json:"user"` +} + +// ApplicationCommand represents an application's command. +type ApplicationCommand struct { + ID snowflake.ID `json:"id"` + Type *ApplicationCommandType `json:"type,omitempty"` + ApplicationID snowflake.ID `json:"application_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Options []*ApplicationCommandOption `json:"options,omitempty"` + DefaultPermission *bool `json:"default_permission"` +} + +// ApplicationCommandOption represents the options for an application command. +type ApplicationCommandOption struct { + Type ApplicationCommandOptionType `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + Required *bool `json:"required,omitempty"` + Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"` + Options []*ApplicationCommandOption `json:"options,omitempty"` +} + +// ApplicationCommandOptionChoice represents the different choices. +type ApplicationCommandOptionChoice struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +// Interaction represents the structure of an interaction. +type Interaction struct { + ID snowflake.ID `json:"id"` + ApplicationID snowflake.ID `json:"application_id"` + Type *InteractionType `json:"type,omitempty"` + Data *InteractionData `json:"data,omitempty"` + + GuildID snowflake.ID `json:"guild_id,omitempty"` + ChannelID snowflake.ID `json:"channel_id,omitempty"` + Member *Member `json:"member,omitempty"` + User *User `json:"user,omitempty"` + Token string `json:"token"` + Version int `json:"version"` + Message *Message `json:"message,omitempty"` +} + +// InteractionData represents the structure of interaction data. +type InteractionData struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Type ApplicationCommandType `json:"type"` + Resolved *InteractionResolvedData `json:"resolved,omitempty"` + Options []InteractionDataOption `json:"option,omitempty"` + CustomID *string `json:"custom_id,omitempty"` + ComponentType *ApplicationCommandType `json:"component_type,omitempty"` + Values []ApplicationSelectOption `json:"values,omitempty"` + TargetID *snowflake.ID `json:"target_id,omitempty"` +} + +// InteractionDataOption represents the structure of an interaction option. +type InteractionDataOption struct { + Name string `json:"name"` + Type ApplicationCommandOptionType `json:"type"` + Value interface{} `json:"value"` + Options []InteractionDataOption `json:"options,omitempty"` +} + +// InteractionResolvedData represents any extra payload data for an interaction. +type InteractionResolvedData struct { + Users []*User `json:"users,omitempty"` + Members []*Member `json:"members,omitempty"` + Roles []*Role `json:"roles,omitempty"` + Channels []*Channel `json:"channels,omitempty"` + Messages []*Message `json:"messages,omitempty"` +} + +// ApplicationSelectOption represents the structure of select options. +type ApplicationSelectOption struct { + Label string `json:"label"` + Value string `json:"value"` + Description *string `json:"description,omitempty"` + Emoji *Emoji `json:"emoji,omitempty"` + Default *bool `json:"default,omitempty"` +} + +// Integration represents the structure of an integration. +type Integration struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Type IntegrationType `json:"type"` + Enabled bool `json:"enabled"` + Syncing *bool `json:"syncing"` + RoleID *snowflake.ID `json:"role_id,omitempty"` + EnableEmoticons *bool `json:"enable_emoticons,omitempty"` + + ExpireBehavior *IntegrationExpireBehavior `json:"expire_behavior,omitempty"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user,omitempty"` + Account IntegrationAccount `json:"account"` + SyncedAt *string `json:"synced_at,omitempty"` + SubscriberCount int `json:"subscriber_count,omitempty"` + Revoked *bool `json:"revoked,omitempty"` + Application *Application `json:"application,omitempty"` +} + +// IntegrationAccount represents the account of the integration. +type IntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// InteractionComponent represents the structure of a component. +type InteractionComponent struct { + Type InteractionComponentType `json:"type"` + CustomID *string `json:"custom_id,omitempty"` + Disabled *bool `json:"disabled,omitempty"` + Style *InteractionComponentStyle `json:"style,omitempty"` + Label *string `json:"label,omitempty"` + Emoji *Emoji `json:"emoji,omitempty"` + URL *string `json:"url,omitempty"` + Options []*ApplicationSelectOption `json:"options"` + Placeholder *string `json:"placeholder,omitempty"` + MinValues *int `json:"min_values,omitempty"` + MaxValues *int `json:"max_values,omitempty"` + Components []*InteractionComponent `json:"components,omitempty"` +} diff --git a/discord/structs/channel.go b/discord/structs/channel.go index e23074e..58c83c7 100644 --- a/discord/structs/channel.go +++ b/discord/structs/channel.go @@ -1,112 +1,112 @@ -package discord - -import ( - "github.com/WelcomerTeam/RealRock/snowflake" -) - -// channel.go contains the information relating to channels - -// ChannelType represents a channel's type. -type ChannelType uint8 - -const ( - ChannelTypeGuildText ChannelType = iota - ChannelTypeDM - ChannelTypeGuildVoice - ChannelTypeGroupDM - ChannelTypeGuildCategory - ChannelTypeGuildNews - ChannelTypeGuildStore - _ - _ - _ - ChannelTypeGuildNewsThread - ChannelTypeGuildPublicThread - ChannelTypeGuildPrivateThread - ChannelTypeGuildStageVoice -) - -// VideoQualityMode represents the quality of the video -type VideoQualityMode uint8 - -const ( - VideoQualityModeAuto VideoQualityMode = 1 + iota - VideoqualityModeFull -) - -// StageChannelPrivacyLevel represents the privacy level of a stage channel -type StageChannelPrivacyLevel uint8 - -const ( - StageChannelPrivacyLevelPublic StageChannelPrivacyLevel = 1 + iota - StageChannelPrivacyLevelGuildOnly -) - -// Channel represents a Discord channel. -type Channel struct { - ID snowflake.ID `json:"id"` - Type ChannelType `json:"type"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Position *int `json:"position,omitempty"` - PermissionOverwrites []*ChannelOverwrite `json:"permission_overwrites,omitempty"` - Name *string `json:"name,omitempty"` - Topic *string `json:"topic,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` - LastMessageID *string `json:"last_message_id,omitempty"` - Bitrate *int `json:"bitrate,omitempty"` - UserLimit *int `json:"user_limit,omitempty"` - RateLimitPerUser *int `json:"rate_limit_per_user,omitempty"` - Recipients []*User `json:"recipients,omitempty"` - Icon *string `json:"icon,omitempty"` - OwnerID *snowflake.ID `json:"owner_id,omitempty"` - ApplicationID *snowflake.ID `json:"application_id,omitempty"` - ParentID *snowflake.ID `json:"parent_id,omitempty"` - LastPinTimestamp *string `json:"last_pin_timestamp,omitempty"` - - RTCRegion *string `json:"rtc_region,omitempty"` - VideoQualityMode *VideoQualityMode `json:"video_quality_mode,omitempty"` - - // Threads - MessageCount *int `json:"message_count,omitempty"` - MemberCount *int `json:"member_count,omitempty"` - ThreadMetadata *ThreadMetadata `json:"thread_metadata,omitempty"` - ThreadMember *ThreadMember `json:"member,omitempty"` - DefaultAutoArchiveDuration int `json:"default_auto_archive_duration"` - - // Slash Commands - Permissions *string `json:"permissions,omitempty"` -} - -// ChannelOverwrite represents a permission overwrite for a channel. -type ChannelOverwrite struct { - ID snowflake.ID `json:"id"` - Type int `json:"type"` - Allow int64 `json:"allow"` - Deny int64 `json:"deny"` -} - -// ThreadMetadata contains thread-specific channel fields. -type ThreadMetadata struct { - Archived bool `json:"archived"` - AutoArchiveDuration int `json:"auto_archive_duration"` - ArchiveTimestamp string `json:"archive_timestamp"` - Locked *bool `json:"locked,omitempty"` -} - -// ThreadMember is used to indicate whether a user has joined a thread or not. -type ThreadMember struct { - ID *snowflake.ID `json:"id,omitempty"` - UserID *snowflake.ID `json:"user_id,omitempty"` - JoinTimestamp string `json:"join_timestamp"` - Flags int `json:"flags"` -} - -// StageInstance represents a stage channel instance. -type StageInstance struct { - ID snowflake.ID `json:"id"` - GuildID snowflake.ID `json:"guild_id"` - ChannelID snowflake.ID `json:"channel_id"` - Topic string `json:"topic"` - PrivacyLabel StageChannelPrivacyLevel `json:"privacy_level"` - DiscoverableDisabled bool `json:"discoverable_disabled"` -} +package discord + +import ( + "github.com/WelcomerTeam/RealRock/snowflake" +) + +// channel.go contains the information relating to channels + +// ChannelType represents a channel's type. +type ChannelType uint8 + +const ( + ChannelTypeGuildText ChannelType = iota + ChannelTypeDM + ChannelTypeGuildVoice + ChannelTypeGroupDM + ChannelTypeGuildCategory + ChannelTypeGuildNews + ChannelTypeGuildStore + _ + _ + _ + ChannelTypeGuildNewsThread + ChannelTypeGuildPublicThread + ChannelTypeGuildPrivateThread + ChannelTypeGuildStageVoice +) + +// VideoQualityMode represents the quality of the video +type VideoQualityMode uint8 + +const ( + VideoQualityModeAuto VideoQualityMode = 1 + iota + VideoqualityModeFull +) + +// StageChannelPrivacyLevel represents the privacy level of a stage channel +type StageChannelPrivacyLevel uint8 + +const ( + StageChannelPrivacyLevelPublic StageChannelPrivacyLevel = 1 + iota + StageChannelPrivacyLevelGuildOnly +) + +// Channel represents a Discord channel. +type Channel struct { + ID snowflake.ID `json:"id"` + Type ChannelType `json:"type"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Position *int `json:"position,omitempty"` + PermissionOverwrites []*ChannelOverwrite `json:"permission_overwrites,omitempty"` + Name *string `json:"name,omitempty"` + Topic *string `json:"topic,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` + LastMessageID *string `json:"last_message_id,omitempty"` + Bitrate *int `json:"bitrate,omitempty"` + UserLimit *int `json:"user_limit,omitempty"` + RateLimitPerUser *int `json:"rate_limit_per_user,omitempty"` + Recipients []*User `json:"recipients,omitempty"` + Icon *string `json:"icon,omitempty"` + OwnerID *snowflake.ID `json:"owner_id,omitempty"` + ApplicationID *snowflake.ID `json:"application_id,omitempty"` + ParentID *snowflake.ID `json:"parent_id,omitempty"` + LastPinTimestamp *string `json:"last_pin_timestamp,omitempty"` + + RTCRegion *string `json:"rtc_region,omitempty"` + VideoQualityMode *VideoQualityMode `json:"video_quality_mode,omitempty"` + + // Threads + MessageCount *int `json:"message_count,omitempty"` + MemberCount *int `json:"member_count,omitempty"` + ThreadMetadata *ThreadMetadata `json:"thread_metadata,omitempty"` + ThreadMember *ThreadMember `json:"member,omitempty"` + DefaultAutoArchiveDuration int `json:"default_auto_archive_duration"` + + // Slash Commands + Permissions *string `json:"permissions,omitempty"` +} + +// ChannelOverwrite represents a permission overwrite for a channel. +type ChannelOverwrite struct { + ID snowflake.ID `json:"id"` + Type int `json:"type"` + Allow int64 `json:"allow"` + Deny int64 `json:"deny"` +} + +// ThreadMetadata contains thread-specific channel fields. +type ThreadMetadata struct { + Archived bool `json:"archived"` + AutoArchiveDuration int `json:"auto_archive_duration"` + ArchiveTimestamp string `json:"archive_timestamp"` + Locked *bool `json:"locked,omitempty"` +} + +// ThreadMember is used to indicate whether a user has joined a thread or not. +type ThreadMember struct { + ID *snowflake.ID `json:"id,omitempty"` + UserID *snowflake.ID `json:"user_id,omitempty"` + JoinTimestamp string `json:"join_timestamp"` + Flags int `json:"flags"` +} + +// StageInstance represents a stage channel instance. +type StageInstance struct { + ID snowflake.ID `json:"id"` + GuildID snowflake.ID `json:"guild_id"` + ChannelID snowflake.ID `json:"channel_id"` + Topic string `json:"topic"` + PrivacyLabel StageChannelPrivacyLevel `json:"privacy_level"` + DiscoverableDisabled bool `json:"discoverable_disabled"` +} diff --git a/discord/structs/embed.go b/discord/structs/embed.go index 95b4522..17c1fe7 100644 --- a/discord/structs/embed.go +++ b/discord/structs/embed.go @@ -1,71 +1,71 @@ -package discord - -// embed.go contains all structures for constructing embeds - -// Embed represents a message embed on Discord. -type Embed struct { - Title string `json:"title,omitempty"` - Type string `json:"type,omitempty"` - Description string `json:"description,omitempty"` - URL string `json:"url,omitempty"` - Timestamp string `json:"timestamp,omitempty"` - Color int `json:"color,omitempty"` - Footer *EmbedFooter `json:"footer,omitempty"` - Image *EmbedImage `json:"image,omitempty"` - Thumbnail *EmbedThumbnail `json:"thumbnail,omitempty"` - Video *EmbedVideo `json:"video,omitempty"` - Provider *EmbedProvider `json:"provider,omitempty"` - Author *EmbedAuthor `json:"author,omitempty"` - Fields []*EmbedField `json:"fields,omitempty"` -} - -// EmbedFooter represents the footer of an embed. -type EmbedFooter struct { - Text string `json:"text"` - IconURL *string `json:"icon_url,omitempty"` - ProxyIconURL *string `json:"proxy_icon_url,omitempty"` -} - -// EmbedImage represents an image in an embed. -type EmbedImage struct { - URL *string `json:"url,omitempty"` - ProxyURL *string `json:"proxy_url,omitempty"` - Height int `json:"height,omitempty"` - Width int `json:"width,omitempty"` -} - -// EmbedThumbnail represents the thumbnail of an embed. -type EmbedThumbnail struct { - URL *string `json:"url,omitempty"` - ProxyURL *string `json:"proxy_url,omitempty"` - Height int `json:"height,omitempty"` - Width int `json:"width,omitempty"` -} - -// EmbedVideo represents the video of an embed. -type EmbedVideo struct { - URL *string `json:"url,omitempty"` - Height int `json:"height,omitempty"` - Width int `json:"width,omitempty"` -} - -// EmbedProvider represents the provider of an embed. -type EmbedProvider struct { - Name *string `json:"name,omitempty"` - URL *string `json:"url,omitempty"` -} - -// EmbedAuthor represents the author of an embed. -type EmbedAuthor struct { - Name *string `json:"name,omitempty"` - URL *string `json:"url,omitempty"` - IconURL *string `json:"icon_url,omitempty"` - ProxyIconURL *string `json:"proxy_icon_url,omitempty"` -} - -// EmbedField represents a field in an embed. -type EmbedField struct { - Name string `json:"name"` - Value string `json:"value"` - Inline bool `json:"inline,omitempty"` -} +package discord + +// embed.go contains all structures for constructing embeds + +// Embed represents a message embed on Discord. +type Embed struct { + Title string `json:"title,omitempty"` + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + URL string `json:"url,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Color int `json:"color,omitempty"` + Footer *EmbedFooter `json:"footer,omitempty"` + Image *EmbedImage `json:"image,omitempty"` + Thumbnail *EmbedThumbnail `json:"thumbnail,omitempty"` + Video *EmbedVideo `json:"video,omitempty"` + Provider *EmbedProvider `json:"provider,omitempty"` + Author *EmbedAuthor `json:"author,omitempty"` + Fields []*EmbedField `json:"fields,omitempty"` +} + +// EmbedFooter represents the footer of an embed. +type EmbedFooter struct { + Text string `json:"text"` + IconURL *string `json:"icon_url,omitempty"` + ProxyIconURL *string `json:"proxy_icon_url,omitempty"` +} + +// EmbedImage represents an image in an embed. +type EmbedImage struct { + URL *string `json:"url,omitempty"` + ProxyURL *string `json:"proxy_url,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` +} + +// EmbedThumbnail represents the thumbnail of an embed. +type EmbedThumbnail struct { + URL *string `json:"url,omitempty"` + ProxyURL *string `json:"proxy_url,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` +} + +// EmbedVideo represents the video of an embed. +type EmbedVideo struct { + URL *string `json:"url,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` +} + +// EmbedProvider represents the provider of an embed. +type EmbedProvider struct { + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` +} + +// EmbedAuthor represents the author of an embed. +type EmbedAuthor struct { + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` + IconURL *string `json:"icon_url,omitempty"` + ProxyIconURL *string `json:"proxy_icon_url,omitempty"` +} + +// EmbedField represents a field in an embed. +type EmbedField struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline,omitempty"` +} diff --git a/discord/structs/emoji.go b/discord/structs/emoji.go index b521d92..a886fac 100644 --- a/discord/structs/emoji.go +++ b/discord/structs/emoji.go @@ -1,17 +1,17 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// emoji.go contains all structures for emojis. - -// Emoji represents an Emoji on discord. -type Emoji struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Roles []snowflake.ID `json:"roles,omitempty"` - User *User `json:"user,omitempty"` - RequireColons *bool `json:"require_colons,omitempty"` - Managed *bool `json:"managed,omitempty"` - Animated *bool `json:"animated,omitempty"` - Available *bool `json:"available,omitempty"` -} +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// emoji.go contains all structures for emojis. + +// Emoji represents an Emoji on discord. +type Emoji struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Roles []snowflake.ID `json:"roles,omitempty"` + User *User `json:"user,omitempty"` + RequireColons *bool `json:"require_colons,omitempty"` + Managed *bool `json:"managed,omitempty"` + Animated *bool `json:"animated,omitempty"` + Available *bool `json:"available,omitempty"` +} diff --git a/discord/structs/events.go b/discord/structs/events.go index bfa70d5..8171d1f 100644 --- a/discord/structs/events.go +++ b/discord/structs/events.go @@ -1,355 +1,355 @@ -package discord - -import ( - "time" - - "github.com/WelcomerTeam/RealRock/snowflake" -) - -// events.go contains the structures of all received events from discord - -// Empty structure -type void struct{} - -// Hello represents a hello event when connecting. -type Hello struct { - HeartbeatInterval time.Duration `json:"heartbeat_interal"` -} - -// Ready represents when the client has completed the initial handshake. -type Ready struct { - Version int `json:"version"` - User User `json:"user"` - Guilds []*UnavailableGuild `json:"guilds"` - SessionID string `json:"session_id"` - Shard []int `json:"shard,omitempty"` - Application Application `json:"application"` -} - -// Resumed represents the response to a resume event. -type Resumed void - -// Reconnect represents the reconnect event. -type Reconnect void - -// Invalid Session represents the invalid session event. -type InvalidSession struct { - Resumable bool `json:"d"` -} - -// ApplicationCommandCreate represents the application command create event. -type ApplicationCommandCreate *ApplicationCommand - -// ApplicationCommandUpdate represents the application command update event. -type ApplicationCommandUpdate *ApplicationCommand - -// ApplicationCommandDelete represents the application command delete event. -type ApplicationCommandDelete *ApplicationCommand - -// ChannelCreate represents a channel create event. -type ChannelCreate struct { - *Channel -} - -// ChannelUpdate represents a channel update event. -type ChannelUpdate struct { - *Channel -} - -// ChannelDelete represents a channel delete event. -type ChannelDelete struct { - *Channel -} - -// ChannelPinsUpdate represents a channel pins update event. -type ChannelPisnUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - ChannelID snowflake.ID `json:"channel_id"` - LastPinTimestamp *string `json:"last_pin_timestamp,omitempty"` -} - -// ThreadCreate represents a thread create event. -type ThreadCreate struct { - *Channel -} - -// ThreadUpdate represents a thread update event. -type ThreadUpdate struct { - *Channel -} - -// ThreadDelete represents a thread delete event. -type ThreadDelete struct { - *Channel -} - -// ThreadListSync represents a thread list sync event. -type ThreadListSync struct { - GuildID snowflake.ID `json:"guild_id"` - ChannelIDs []snowflake.ID `json:"channel_ids,omitempty"` - Threads []*Channel `json:"threads"` - Members []*ThreadMember `json:"members"` -} - -// ThreadMemberUpdate represents a thread member update event. -type ThreadMemberUpdate *ThreadMember - -// ThreadMembersUpdate represents a thread members update event. -type ThreadMembersUpdate struct { - ID snowflake.ID `json:"id"` - GuildID snowflake.ID `json:"guild_id"` - MemberCount int `json:"member_count"` - AddedMembers []*ThreadMember `json:"added_members,omitempty"` - RemovedMemberIDs []snowflake.ID `json:"removed_member_ids,omitempty"` -} - -// GuildCreate represents a guild create event. -type GuildCreate struct { - *Guild - Lazy bool `json:"-"` // Internal use. -} - -// GuildUpdate represents a guild update event. -type GuildUpdate *Guild - -// GuildDelete represents a guild delete event. -type GuildDelete *UnavailableGuild - -// GuildBanAdd represents a guild ban add event. -type GuildBanAdd struct { - GuildID snowflake.ID `json:"guild_id"` - User *User `json:"user"` -} - -// GuildBanRemove represents a guild ban remove event. -type GuildBanRemove struct { - GuildID snowflake.ID `json:"guild_id"` - User *User `json:"user"` -} - -// GuildEmojisUpdate represents a guild emojis update event. -type GuildEmojisUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - Emojis []*Emoji `json:"emojis"` -} - -// GuildStickersUpdate represents a guild stickers update event. -type GuildStickersUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - Stickers []*Sticker `json:"stickers"` -} - -// GuildIntegrationsUpdate represents a guild integrations update event. -type GuildIntegrationsUpdate struct { - GuildID snowflake.ID `json:"guild_id"` -} - -// GuildMemberAdd represents a guild member add event. -type GuildMemberAdd struct { - *Member - GuildID snowflake.ID `json:"guild_id"` -} - -// GuildMemberRemove represents a guild member remove event. -type GuildMemberRemove struct { - GuildID snowflake.ID `json:"guild_id"` - User *User `json:"user"` -} - -// GuildMemberUpdate represents a guild member update event. -type GuildMemberUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - Roles []snowflake.ID `json:"roles"` - User *User `json:"user"` - Nick string `json:"nick"` - JoinedAt string `json:"joined_at"` - PremiumSince *string `json:"premium_since,omitempty"` - Deaf *bool `json:"deaf,omitempty"` - Mute *bool `json:"mute,omitempty"` - Pending *bool `json:"pending,omitempty"` -} - -// GuildMembersChunk represents a guild members chunk event. -type GuildMembersChunk struct { - GuildID snowflake.ID `json:"guild_id"` - Members []*Member `json:"members"` - ChunkIndex int `json:"chunk_index"` - ChunkCount int `json:"chunk_count"` - NotFound []snowflake.ID `json:"not_found,omitempty"` - Presences []PresenceStatus `json:"presences,omitempty"` - Nonce *string `json:"nonce,omitempty"` -} - -// GuildRoleCreate represents a guild role create event. -type GuildRoleCreate struct { - GuildID snowflake.ID `json:"guild_id"` - Role *Role `json:"role"` -} - -// GuildRoleUpdate represents a guild role update event. -type GuildRoleUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - Role *Role `json:"role"` // TODO: type -} - -// GuildRoleDelete represents a guild role delete event. -type GuildRoleDelete struct { - GuildID snowflake.ID `json:"guild_id"` - RoleID snowflake.ID `json:"role_id"` -} - -// IntegrationCreate represents the integration create event. -type IntegrationCreate struct { - *Integration - GuildID snowflake.ID `json:"guild_id"` -} - -// IntegrationUpdate represents the integration update event. -type IntegrationUpdate struct { - *Integration - GuildID snowflake.ID `json:"guild_id"` -} - -// IntegrationDelete represents the integration delete event. -type IntegrationDelete struct { - ID snowflake.ID `json:"id"` - GuildID snowflake.ID `json:"guild_id"` - ApplicationID *snowflake.ID `json:"application_id"` -} - -// InteractionCreate represents the interaction create event. -type InteractionCreate *Interaction - -// InviteCreate represents the invite create event. -type InviteCreate struct { - ChannelID snowflake.ID `json:"channel_id"` - Code string `json:"code"` - CreatedAt string `json:"created_at"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Inviter *User `json:"inviter,omitempty"` - MaxAge int `json:"max_age"` - MaxUses int `json:"max_uses"` - TargetType *InviteTargetType `json:"target_type,omitempty"` - TargetUser *User `json:"target_user,omitempty"` - TargetApplication *Application `json:"target_application"` - Temporary bool `json:"temporary"` - Uses int `json:"uses"` -} - -// InviteDelete represents the invite delete event. -type InviteDelete struct { - ChannelID snowflake.ID `json:"channel_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Code string `json:"code"` -} - -// MessageCreate represents the message update event. -type MessageUpdate *Message - -// MessageCreate represents the message delete event. -type MessageDelete struct { - ID snowflake.ID `json:"id"` - ChannelID snowflake.ID `json:"channel_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` -} - -// MessageCreate represents the message bulk delete event. -type MessageDeleteBulk struct { - IDs []snowflake.ID `json:"ids"` - ChannelID snowflake.ID `json:"channel_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` -} - -// MessageReactionAdd represents a message reaction add event. -type MessageReactionAdd struct { - UserID snowflake.ID `json:"user_id"` - ChannelID snowflake.ID `json:"channel_id"` - MessageID snowflake.ID `json:"message_id"` - GuildID snowflake.ID `json:"guild_id,omitempty"` - Member *Member `json:"member,omitempty"` - Emoji *Emoji `json:"emoji"` -} - -// MessageReactionRemove represents a message reaction remove event. -type MessageReactionRemove struct { - UserID snowflake.ID `json:"user_id"` - ChannelID snowflake.ID `json:"channel_id"` - MessageID snowflake.ID `json:"message_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Emoji *Emoji `json:"emoji"` -} - -// MessageReactionRemoveAll represents a message reaction remove all event. -type MessageReactionRemoveAll struct { - ChannelID snowflake.ID `json:"channel_id"` - MessageID snowflake.ID `json:"message_id"` - GuildID snowflake.ID `json:"guild_id,omitempty"` -} - -// MessageReactionRemoveEmoji represents a message reaction remove emoji event. -type MessageReactionRemoveEmoji struct { - ChannelID snowflake.ID `json:"channel_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - MessageID snowflake.ID `json:"message_id"` - Emoji *Emoji `json:"emoji"` -} - -// PresenceUpdate represents a presence update event. -type PresenceUpdate struct { - User *User `json:"user"` - GuildID snowflake.ID `json:"guild_id"` - Status PresenceStatus `json:"status"` - Activities []*Activity `json:"activities"` - ClientStatus *ClientStatus `json:"clienbt_status"` -} - -// StageInstanceCreate represents a stage instance create event. -type StageInstanceCreate *StageInstance - -// StageInstanceUpdate represents a stage instance update event. -type StageInstanceUpdate *StageInstance - -// StageInstanceDelete represents a stage instance delete event. -type StageInstanceDelete *StageInstance - -// TypingStart represents a typing start event. -type TypingStart struct { - ChannelID snowflake.ID `json:"channel_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - UserID snowflake.ID `json:"user_id"` - Timestamp int `json:"timestamp"` - Member *Member `json:"member,omitempty"` -} - -// UserUpdate represents a user update event. -type UserUpdate *User - -// VoiceStateUpdate represents the voice state update event. -type VoiceStateUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - ChannelID *snowflake.ID `json:"channel_id,omitempty"` - UserID snowflake.ID `json:"user_id"` - Member *Member `json:"member,omitempty"` - SessionID string `json:"session_id"` - Deaf bool `json:"deaf"` - Mute bool `json:"mute"` - SelfDeaf bool `json:"self_deaf"` - SelfMute bool `json:"self_mute"` - SelfStream *bool `json:"self_stream,omitempty"` - SelfVideo bool `json:"self_video"` - Suppress bool `json:"suppress"` - RequestToSpeakTimestamp string `json:"request_to_speak_timestamp"` -} - -// VoiceServerUpdate represents a voice server update event. -type VoiceServerUpdate struct { - Token string `json:"token"` - GuildID snowflake.ID `json:"guild_id"` - Endpoint string `json:"endpoint"` -} - -// WebhookUpdate represents a webhook update packet. -type WebhookUpdate struct { - GuildID snowflake.ID `json:"guild_id"` - ChannelID snowflake.ID `json:"channel_id"` -} +package discord + +import ( + "time" + + "github.com/WelcomerTeam/RealRock/snowflake" +) + +// events.go contains the structures of all received events from discord + +// Empty structure +type void struct{} + +// Hello represents a hello event when connecting. +type Hello struct { + HeartbeatInterval time.Duration `json:"heartbeat_interal"` +} + +// Ready represents when the client has completed the initial handshake. +type Ready struct { + Version int `json:"version"` + User User `json:"user"` + Guilds []*UnavailableGuild `json:"guilds"` + SessionID string `json:"session_id"` + Shard []int `json:"shard,omitempty"` + Application Application `json:"application"` +} + +// Resumed represents the response to a resume event. +type Resumed void + +// Reconnect represents the reconnect event. +type Reconnect void + +// Invalid Session represents the invalid session event. +type InvalidSession struct { + Resumable bool `json:"d"` +} + +// ApplicationCommandCreate represents the application command create event. +type ApplicationCommandCreate *ApplicationCommand + +// ApplicationCommandUpdate represents the application command update event. +type ApplicationCommandUpdate *ApplicationCommand + +// ApplicationCommandDelete represents the application command delete event. +type ApplicationCommandDelete *ApplicationCommand + +// ChannelCreate represents a channel create event. +type ChannelCreate struct { + *Channel +} + +// ChannelUpdate represents a channel update event. +type ChannelUpdate struct { + *Channel +} + +// ChannelDelete represents a channel delete event. +type ChannelDelete struct { + *Channel +} + +// ChannelPinsUpdate represents a channel pins update event. +type ChannelPisnUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + ChannelID snowflake.ID `json:"channel_id"` + LastPinTimestamp *string `json:"last_pin_timestamp,omitempty"` +} + +// ThreadCreate represents a thread create event. +type ThreadCreate struct { + *Channel +} + +// ThreadUpdate represents a thread update event. +type ThreadUpdate struct { + *Channel +} + +// ThreadDelete represents a thread delete event. +type ThreadDelete struct { + *Channel +} + +// ThreadListSync represents a thread list sync event. +type ThreadListSync struct { + GuildID snowflake.ID `json:"guild_id"` + ChannelIDs []snowflake.ID `json:"channel_ids,omitempty"` + Threads []*Channel `json:"threads"` + Members []*ThreadMember `json:"members"` +} + +// ThreadMemberUpdate represents a thread member update event. +type ThreadMemberUpdate *ThreadMember + +// ThreadMembersUpdate represents a thread members update event. +type ThreadMembersUpdate struct { + ID snowflake.ID `json:"id"` + GuildID snowflake.ID `json:"guild_id"` + MemberCount int `json:"member_count"` + AddedMembers []*ThreadMember `json:"added_members,omitempty"` + RemovedMemberIDs []snowflake.ID `json:"removed_member_ids,omitempty"` +} + +// GuildCreate represents a guild create event. +type GuildCreate struct { + *Guild + Lazy bool `json:"-"` // Internal use. +} + +// GuildUpdate represents a guild update event. +type GuildUpdate *Guild + +// GuildDelete represents a guild delete event. +type GuildDelete *UnavailableGuild + +// GuildBanAdd represents a guild ban add event. +type GuildBanAdd struct { + GuildID snowflake.ID `json:"guild_id"` + User *User `json:"user"` +} + +// GuildBanRemove represents a guild ban remove event. +type GuildBanRemove struct { + GuildID snowflake.ID `json:"guild_id"` + User *User `json:"user"` +} + +// GuildEmojisUpdate represents a guild emojis update event. +type GuildEmojisUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + Emojis []*Emoji `json:"emojis"` +} + +// GuildStickersUpdate represents a guild stickers update event. +type GuildStickersUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + Stickers []*Sticker `json:"stickers"` +} + +// GuildIntegrationsUpdate represents a guild integrations update event. +type GuildIntegrationsUpdate struct { + GuildID snowflake.ID `json:"guild_id"` +} + +// GuildMemberAdd represents a guild member add event. +type GuildMemberAdd struct { + *Member + GuildID snowflake.ID `json:"guild_id"` +} + +// GuildMemberRemove represents a guild member remove event. +type GuildMemberRemove struct { + GuildID snowflake.ID `json:"guild_id"` + User *User `json:"user"` +} + +// GuildMemberUpdate represents a guild member update event. +type GuildMemberUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + Roles []snowflake.ID `json:"roles"` + User *User `json:"user"` + Nick string `json:"nick"` + JoinedAt string `json:"joined_at"` + PremiumSince *string `json:"premium_since,omitempty"` + Deaf *bool `json:"deaf,omitempty"` + Mute *bool `json:"mute,omitempty"` + Pending *bool `json:"pending,omitempty"` +} + +// GuildMembersChunk represents a guild members chunk event. +type GuildMembersChunk struct { + GuildID snowflake.ID `json:"guild_id"` + Members []*Member `json:"members"` + ChunkIndex int `json:"chunk_index"` + ChunkCount int `json:"chunk_count"` + NotFound []snowflake.ID `json:"not_found,omitempty"` + Presences []PresenceStatus `json:"presences,omitempty"` + Nonce *string `json:"nonce,omitempty"` +} + +// GuildRoleCreate represents a guild role create event. +type GuildRoleCreate struct { + GuildID snowflake.ID `json:"guild_id"` + Role *Role `json:"role"` +} + +// GuildRoleUpdate represents a guild role update event. +type GuildRoleUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + Role *Role `json:"role"` // TODO: type +} + +// GuildRoleDelete represents a guild role delete event. +type GuildRoleDelete struct { + GuildID snowflake.ID `json:"guild_id"` + RoleID snowflake.ID `json:"role_id"` +} + +// IntegrationCreate represents the integration create event. +type IntegrationCreate struct { + *Integration + GuildID snowflake.ID `json:"guild_id"` +} + +// IntegrationUpdate represents the integration update event. +type IntegrationUpdate struct { + *Integration + GuildID snowflake.ID `json:"guild_id"` +} + +// IntegrationDelete represents the integration delete event. +type IntegrationDelete struct { + ID snowflake.ID `json:"id"` + GuildID snowflake.ID `json:"guild_id"` + ApplicationID *snowflake.ID `json:"application_id"` +} + +// InteractionCreate represents the interaction create event. +type InteractionCreate *Interaction + +// InviteCreate represents the invite create event. +type InviteCreate struct { + ChannelID snowflake.ID `json:"channel_id"` + Code string `json:"code"` + CreatedAt string `json:"created_at"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Inviter *User `json:"inviter,omitempty"` + MaxAge int `json:"max_age"` + MaxUses int `json:"max_uses"` + TargetType *InviteTargetType `json:"target_type,omitempty"` + TargetUser *User `json:"target_user,omitempty"` + TargetApplication *Application `json:"target_application"` + Temporary bool `json:"temporary"` + Uses int `json:"uses"` +} + +// InviteDelete represents the invite delete event. +type InviteDelete struct { + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Code string `json:"code"` +} + +// MessageCreate represents the message update event. +type MessageUpdate *Message + +// MessageCreate represents the message delete event. +type MessageDelete struct { + ID snowflake.ID `json:"id"` + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` +} + +// MessageCreate represents the message bulk delete event. +type MessageDeleteBulk struct { + IDs []snowflake.ID `json:"ids"` + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` +} + +// MessageReactionAdd represents a message reaction add event. +type MessageReactionAdd struct { + UserID snowflake.ID `json:"user_id"` + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID snowflake.ID `json:"guild_id,omitempty"` + Member *Member `json:"member,omitempty"` + Emoji *Emoji `json:"emoji"` +} + +// MessageReactionRemove represents a message reaction remove event. +type MessageReactionRemove struct { + UserID snowflake.ID `json:"user_id"` + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Emoji *Emoji `json:"emoji"` +} + +// MessageReactionRemoveAll represents a message reaction remove all event. +type MessageReactionRemoveAll struct { + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID snowflake.ID `json:"guild_id,omitempty"` +} + +// MessageReactionRemoveEmoji represents a message reaction remove emoji event. +type MessageReactionRemoveEmoji struct { + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + MessageID snowflake.ID `json:"message_id"` + Emoji *Emoji `json:"emoji"` +} + +// PresenceUpdate represents a presence update event. +type PresenceUpdate struct { + User *User `json:"user"` + GuildID snowflake.ID `json:"guild_id"` + Status PresenceStatus `json:"status"` + Activities []*Activity `json:"activities"` + ClientStatus *ClientStatus `json:"clienbt_status"` +} + +// StageInstanceCreate represents a stage instance create event. +type StageInstanceCreate *StageInstance + +// StageInstanceUpdate represents a stage instance update event. +type StageInstanceUpdate *StageInstance + +// StageInstanceDelete represents a stage instance delete event. +type StageInstanceDelete *StageInstance + +// TypingStart represents a typing start event. +type TypingStart struct { + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + UserID snowflake.ID `json:"user_id"` + Timestamp int `json:"timestamp"` + Member *Member `json:"member,omitempty"` +} + +// UserUpdate represents a user update event. +type UserUpdate *User + +// VoiceStateUpdate represents the voice state update event. +type VoiceStateUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + ChannelID *snowflake.ID `json:"channel_id,omitempty"` + UserID snowflake.ID `json:"user_id"` + Member *Member `json:"member,omitempty"` + SessionID string `json:"session_id"` + Deaf bool `json:"deaf"` + Mute bool `json:"mute"` + SelfDeaf bool `json:"self_deaf"` + SelfMute bool `json:"self_mute"` + SelfStream *bool `json:"self_stream,omitempty"` + SelfVideo bool `json:"self_video"` + Suppress bool `json:"suppress"` + RequestToSpeakTimestamp string `json:"request_to_speak_timestamp"` +} + +// VoiceServerUpdate represents a voice server update event. +type VoiceServerUpdate struct { + Token string `json:"token"` + GuildID snowflake.ID `json:"guild_id"` + Endpoint string `json:"endpoint"` +} + +// WebhookUpdate represents a webhook update packet. +type WebhookUpdate struct { + GuildID snowflake.ID `json:"guild_id"` + ChannelID snowflake.ID `json:"channel_id"` +} diff --git a/discord/structs/gateway.go b/discord/structs/gateway.go index 298bc73..b2cbde6 100644 --- a/discord/structs/gateway.go +++ b/discord/structs/gateway.go @@ -1,141 +1,130 @@ -package discord - -import ( - "encoding/json" - "time" - - "github.com/WelcomerTeam/RealRock/snowflake" -) - -// gateway.go contains all structures for interacting with discord's gateway and contains -// all events and structures we send to discord. - -// GatewayOp represents the operation codes of a gateway message. -type GatewayOp uint8 - -const ( - GatewayOpDispatch GatewayOp = iota - GatewayOpHeartbeat - GatewayOpIdentify - GatewayOpStatusUpdate - GatewayOpVoiceStateUpdate - _ - GatewayOpResume - GatewayOpReconnect - GatewayOpRequestGuildMembers - GatewayOpInvalidSession - GatewayOpHello - GatewayOpHeartbeatACK -) - -// IntentFlag represents a bitflag for intents. -type GatewayIntent uint32 - -const ( - IntentGuilds GatewayIntent = 1 << iota - IntentGuildMembers - IntentGuildBans - IntentGuildEmojis - IntentGuildIntegrations - IntentGuildWebhooks - IntentGuildInvites - IntentGuildVoiceStates - IntentGuildPresences - IntentGuildMessages - IntentGuildMessageReactions - IntentGuildMessageTyping - IntentDirectMessages - IntentDirectMessageReactions - IntentDirectMessageTyping -) - -// Gateway close codes -const ( - CloseUnknownError = 4000 + iota - CloseUnknownOpCode - CloseDecodeError - CloseNotAuthenticated - CloseAuthenticationFailed - CloseAlreadyAuthenticated - _ - CloseInvalidSeq - CloseRateLimited - CloseSessionTimeout - CloseInvalidShard - CloseShardingRequired - CloseInvalidAPIVersion - CloseInvalidIntents - CloseDisallowedIntents -) - -// GatewayPayload represents the base payload received from discord gateway. -type GatewayPayload struct { - Op GatewayOp `json:"op"` - Data json.RawMessage `json:"d"` - Sequence int64 `json:"s"` - Type string `json:"t"` - - // Used internally for trace tracking - traceTime time.Time `json:"-"` - traces map[string]int64 `json:"-"` -} - -// AddTrace adds a trace entry to a GatewayPayload. -func (gp *GatewayPayload) AddTrace(name string, now time.Time) { - gp.traces[name] = now.Sub(gp.traceTime).Milliseconds() - gp.traceTime = now -} - -// SentPayload represents the base payload we send to discords gateway. -type SentPayload struct { - Op GatewayOp `json:"op"` - Data interface{} `json:"d"` -} - -// Gateway Commands - -// Identify represents the initial handshake with the gateway. -type Identify struct { - Token string `json:"token"` - Properties *IdentifyProperties `json:"properties"` - Compress bool `json:"compress"` - LargeThreshold *int `json:"large_threshold,omitempty"` - Shard [2]int `json:"shard,omitempty"` - Presence *UpdateStatus `json:"presence,omitempty"` - Intents int64 `json:"intents"` -} - -// IdentifyProperties are the extra properties sent in the identify packet. -type IdentifyProperties struct { - OS string `json:"$os"` - Browser string `json:"$browser"` - Device string `json:"$device"` -} - -// Resume resumes a dropped gateway connection. -type Resume struct { - Token string `json:"token"` - SessionID string `json:"session_id"` - Sequence int64 `json:"seq"` -} - -// Heartbeat represents the heartbeat packet. -type Heartbeat int - -// Request guild members requests members for a guild. -type RequestGuildMembers struct { - GuildID snowflake.ID `json:"guild_id"` - Query string `json:"query"` - Limit int `json:"limit"` - Presences bool `json:"presences"` - Nonce string `json:"nonce"` - UserIDs []snowflake.ID `json:"user_ids"` -} - -// Update Presence updates a client's presence. -type UpdateStatus struct { - Since *int `json:"since,omitempty"` - Game *Activity `json:"game,omitempty"` - Status string `json:"status"` - AFK bool `json:"afk"` -} +package discord + +import ( + "encoding/json" + + "github.com/WelcomerTeam/RealRock/snowflake" +) + +// gateway.go contains all structures for interacting with discord's gateway and contains +// all events and structures we send to discord. + +// GatewayOp represents the operation codes of a gateway message. +type GatewayOp uint8 + +const ( + GatewayOpDispatch GatewayOp = iota + GatewayOpHeartbeat + GatewayOpIdentify + GatewayOpStatusUpdate + GatewayOpVoiceStateUpdate + _ + GatewayOpResume + GatewayOpReconnect + GatewayOpRequestGuildMembers + GatewayOpInvalidSession + GatewayOpHello + GatewayOpHeartbeatACK +) + +// IntentFlag represents a bitflag for intents. +type GatewayIntent uint32 + +const ( + IntentGuilds GatewayIntent = 1 << iota + IntentGuildMembers + IntentGuildBans + IntentGuildEmojis + IntentGuildIntegrations + IntentGuildWebhooks + IntentGuildInvites + IntentGuildVoiceStates + IntentGuildPresences + IntentGuildMessages + IntentGuildMessageReactions + IntentGuildMessageTyping + IntentDirectMessages + IntentDirectMessageReactions + IntentDirectMessageTyping +) + +// Gateway close codes +const ( + CloseUnknownError = 4000 + iota + CloseUnknownOpCode + CloseDecodeError + CloseNotAuthenticated + CloseAuthenticationFailed + CloseAlreadyAuthenticated + _ + CloseInvalidSeq + CloseRateLimited + CloseSessionTimeout + CloseInvalidShard + CloseShardingRequired + CloseInvalidAPIVersion + CloseInvalidIntents + CloseDisallowedIntents +) + +// GatewayPayload represents the base payload received from discord gateway. +type GatewayPayload struct { + Op GatewayOp `json:"op"` + Data json.RawMessage `json:"d"` + Sequence int64 `json:"s"` + Type string `json:"t"` +} + +// SentPayload represents the base payload we send to discords gateway. +type SentPayload struct { + Op GatewayOp `json:"op"` + Data interface{} `json:"d"` +} + +// Gateway Commands + +// Identify represents the initial handshake with the gateway. +type Identify struct { + Token string `json:"token"` + Properties *IdentifyProperties `json:"properties"` + Compress bool `json:"compress"` + LargeThreshold int `json:"large_threshold"` + Shard [2]int `json:"shard,omitempty"` + Presence *UpdateStatus `json:"presence,omitempty"` + Intents int64 `json:"intents"` +} + +// IdentifyProperties are the extra properties sent in the identify packet. +type IdentifyProperties struct { + OS string `json:"$os"` + Browser string `json:"$browser"` + Device string `json:"$device"` +} + +// Resume resumes a dropped gateway connection. +type Resume struct { + Token string `json:"token"` + SessionID string `json:"session_id"` + Sequence int64 `json:"seq"` +} + +// Heartbeat represents the heartbeat packet. +type Heartbeat int + +// Request guild members requests members for a guild. +type RequestGuildMembers struct { + GuildID snowflake.ID `json:"guild_id"` + Query string `json:"query"` + Limit int `json:"limit"` + Presences bool `json:"presences"` + Nonce string `json:"nonce"` + UserIDs []snowflake.ID `json:"user_ids"` +} + +// Update Presence updates a client's presence. +type UpdateStatus struct { + Since *int `json:"since,omitempty"` + Game *Activity `json:"game,omitempty"` + Status string `json:"status"` + AFK bool `json:"afk"` +} diff --git a/discord/structs/guild.go b/discord/structs/guild.go index 274ba28..b8f554a 100644 --- a/discord/structs/guild.go +++ b/discord/structs/guild.go @@ -1,166 +1,166 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// guild.go contains the structures to represent a guild. - -// MessageNotificationLevel represents a guild's message notification level. -type MessageNotificationLevel int - -// Message notification levels. -const ( - MessageNotificationsAllMessages MessageNotificationLevel = iota - MessageNotificationsOnlyMentions -) - -// ExplicitContentFilterLevel represents a guild's explicit content filter level. -type ExplicitContentFilterLevel int - -// Explicit content filter levels. -const ( - ExplicitContentFilterDisabled ExplicitContentFilterLevel = iota - ExplicitContentFilterMembersWithoutRoles - ExplicitContentFilterAllMembers -) - -// MFALevel represents a guild's MFA level. -type MFALevel uint8 - -// MFA levels. -const ( - MFALevelNone MFALevel = iota - MFALevelElevated -) - -// VerificationLevel represents a guild's verification level. -type VerificationLevel uint8 - -const ( - VerificationLevelNone VerificationLevel = iota - VerificationLevelLow - VerificationLevelMedium - VerificationLevelHigh - VerificationLevelVeryHigh -) - -// SystemChannelFlags represents the flags of a system channel. -type SystemChannelFlags uint8 - -const ( - SystemChannelFlagsSuppressJoin SystemChannelFlags = 1 << iota - SystemChannelFlagsPremiumSubscriptions -) - -// PremiumTier represents the current boosting tier of a guild. -type PremiumTier uint8 - -const ( - PremiumTierNone PremiumTier = iota - PremiumTier1 - PremiumTier2 - PremiumTier3 -) - -// GuildNSFWLevelType represents the level of the guild. -type GuildNSFWLevelType uint8 - -const ( - GuildNSFWLevelTypDefault GuildNSFWLevelType = iota - GuildNSFWLevelTypeExplicit - GuildNSFWLevelTypeSafe - GuildNSFWLevelTypeAgeRestricted -) - -// Guild represents a guild on Discord. -type Guild struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Icon string `json:"icon"` - IconHash *string `json:"icon_hash,omitempty"` - Splash string `json:"splash"` - DiscoverySplash string `json:"discovery_splash"` - - Owner *bool `json:"owner,omitempty"` - OwnerID *snowflake.ID `json:"owner_id,omitempty"` - Permissions *int `json:"permissions,omitempty"` - Region string `json:"region"` - - AFKChannelID *snowflake.ID `json:"afk_channel_id,omitempty"` - AFKTimeout int `json:"afk_timeout"` - - WidgetEnabled *bool `json:"widget_enabled,omitempty"` - WidgetChannelID *snowflake.ID `json:"widget_channel_id,omitempty"` - - VerificationLevel VerificationLevel `json:"verification_level"` - DefaultMessageNotifications MessageNotificationLevel `json:"default_message_notifications"` - ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"` - - Roles []*Role `json:"roles"` - Emojis []*Emoji `json:"emojis"` - Features []string `json:"features"` - - MFALevel MFALevel `json:"mfa_level"` - ApplicationID *snowflake.ID `json:"application_id,omitempty"` - SystemChannelID *snowflake.ID `json:"system_channel_id,omitempty"` - SystemChannelFlags *SystemChannelFlags `json:"system_channel_flags,omitempty"` - RulesChannelID *snowflake.ID `json:"rules_channel_id,omitempty"` - - JoinedAt *string `json:"joined_at,omitempty"` - Large *bool `json:"large,omitempty"` - Unavailable *bool `json:"unavailable,omitempty"` - MemberCount *int `json:"member_count,omitempty"` - - VoiceStates []*VoiceState `json:"voice_states,omitempty"` - Members []*GuildMember `json:"members,omitempty"` - Channels []*Channel `json:"channels,omitempty"` - Presences []*Activity `json:"presences,omitempty"` - - MaxPresences *int `json:"max_presences,omitempty"` - MaxMembers *int `json:"max_members,omitempty"` - VanityURLCode *string `json:"vanity_url_code,omitempty"` - Description *string `json:"description,omitempty"` - Banner *string `json:"banner,omitempty"` - PremiumTier *PremiumTier `json:"premium_tier,omitempty"` - - PremiumSubscriptionCount *int `json:"premium_subscription_count,omitempty"` - PreferredLocale *string `json:"preferred_locale,omitempty"` - PublicUpdatesChannelID *snowflake.ID `json:"public_updates_channel_id,omitempty"` - MaxVideoChannelUsers *int `json:"max_video_channel_users,omitempty"` - ApproximateMemberCount *int `json:"approximate_member_count,omitempty"` - ApproximatePresenceCount *int `json:"approximate_presence_count,omitempty"` - - NSFWLevel *GuildNSFWLevelType `json:"nsfw_level"` - StageInstances []*StageInstance `json:"stage_instances,omitempty"` - Stickers []*Sticker `json:"stickers"` -} - -// UnavailableGuild represents an unavailable guild. -type UnavailableGuild struct { - ID snowflake.ID `json:"id"` - Unavailable bool `json:"unavailable"` -} - -// GuildMember represents a guild member on Discord. -type GuildMember struct { - User *User `json:"user"` - Nick *string `json:"nick,omitempty"` - - Roles []snowflake.ID `json:"roles"` - JoinedAt string `json:"joined_at"` - Deaf bool `json:"deaf"` - Mute bool `json:"mute"` -} - -// VoiceState represents the voice state on Discord. -type VoiceState struct { - GuildID *snowflake.ID `json:"guild_id,omitempty"` - ChannelID snowflake.ID `json:"channel_id"` - UserID snowflake.ID `json:"user_id"` - Member *GuildMember `json:"member,omitempty"` - SessionID string `json:"session_id"` - Deaf bool `json:"deaf"` - Mute bool `json:"mute"` - SelfDeaf bool `json:"self_deaf"` - SelfMute bool `json:"self_mute"` - Suppress bool `json:"suppress"` -} +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// guild.go contains the structures to represent a guild. + +// MessageNotificationLevel represents a guild's message notification level. +type MessageNotificationLevel int + +// Message notification levels. +const ( + MessageNotificationsAllMessages MessageNotificationLevel = iota + MessageNotificationsOnlyMentions +) + +// ExplicitContentFilterLevel represents a guild's explicit content filter level. +type ExplicitContentFilterLevel int + +// Explicit content filter levels. +const ( + ExplicitContentFilterDisabled ExplicitContentFilterLevel = iota + ExplicitContentFilterMembersWithoutRoles + ExplicitContentFilterAllMembers +) + +// MFALevel represents a guild's MFA level. +type MFALevel uint8 + +// MFA levels. +const ( + MFALevelNone MFALevel = iota + MFALevelElevated +) + +// VerificationLevel represents a guild's verification level. +type VerificationLevel uint8 + +const ( + VerificationLevelNone VerificationLevel = iota + VerificationLevelLow + VerificationLevelMedium + VerificationLevelHigh + VerificationLevelVeryHigh +) + +// SystemChannelFlags represents the flags of a system channel. +type SystemChannelFlags uint8 + +const ( + SystemChannelFlagsSuppressJoin SystemChannelFlags = 1 << iota + SystemChannelFlagsPremiumSubscriptions +) + +// PremiumTier represents the current boosting tier of a guild. +type PremiumTier uint8 + +const ( + PremiumTierNone PremiumTier = iota + PremiumTier1 + PremiumTier2 + PremiumTier3 +) + +// GuildNSFWLevelType represents the level of the guild. +type GuildNSFWLevelType uint8 + +const ( + GuildNSFWLevelTypDefault GuildNSFWLevelType = iota + GuildNSFWLevelTypeExplicit + GuildNSFWLevelTypeSafe + GuildNSFWLevelTypeAgeRestricted +) + +// Guild represents a guild on Discord. +type Guild struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + IconHash *string `json:"icon_hash,omitempty"` + Splash string `json:"splash"` + DiscoverySplash string `json:"discovery_splash"` + + Owner *bool `json:"owner,omitempty"` + OwnerID *snowflake.ID `json:"owner_id,omitempty"` + Permissions *int `json:"permissions,omitempty"` + Region string `json:"region"` + + AFKChannelID *snowflake.ID `json:"afk_channel_id,omitempty"` + AFKTimeout int `json:"afk_timeout"` + + WidgetEnabled *bool `json:"widget_enabled,omitempty"` + WidgetChannelID *snowflake.ID `json:"widget_channel_id,omitempty"` + + VerificationLevel VerificationLevel `json:"verification_level"` + DefaultMessageNotifications MessageNotificationLevel `json:"default_message_notifications"` + ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"` + + Roles []*Role `json:"roles"` + Emojis []*Emoji `json:"emojis"` + Features []string `json:"features"` + + MFALevel MFALevel `json:"mfa_level"` + ApplicationID *snowflake.ID `json:"application_id,omitempty"` + SystemChannelID *snowflake.ID `json:"system_channel_id,omitempty"` + SystemChannelFlags *SystemChannelFlags `json:"system_channel_flags,omitempty"` + RulesChannelID *snowflake.ID `json:"rules_channel_id,omitempty"` + + JoinedAt *string `json:"joined_at,omitempty"` + Large *bool `json:"large,omitempty"` + Unavailable *bool `json:"unavailable,omitempty"` + MemberCount *int `json:"member_count,omitempty"` + + VoiceStates []*VoiceState `json:"voice_states,omitempty"` + Members []*GuildMember `json:"members,omitempty"` + Channels []*Channel `json:"channels,omitempty"` + Presences []*Activity `json:"presences,omitempty"` + + MaxPresences *int `json:"max_presences,omitempty"` + MaxMembers *int `json:"max_members,omitempty"` + VanityURLCode *string `json:"vanity_url_code,omitempty"` + Description *string `json:"description,omitempty"` + Banner *string `json:"banner,omitempty"` + PremiumTier *PremiumTier `json:"premium_tier,omitempty"` + + PremiumSubscriptionCount *int `json:"premium_subscription_count,omitempty"` + PreferredLocale *string `json:"preferred_locale,omitempty"` + PublicUpdatesChannelID *snowflake.ID `json:"public_updates_channel_id,omitempty"` + MaxVideoChannelUsers *int `json:"max_video_channel_users,omitempty"` + ApproximateMemberCount *int `json:"approximate_member_count,omitempty"` + ApproximatePresenceCount *int `json:"approximate_presence_count,omitempty"` + + NSFWLevel *GuildNSFWLevelType `json:"nsfw_level"` + StageInstances []*StageInstance `json:"stage_instances,omitempty"` + Stickers []*Sticker `json:"stickers"` +} + +// UnavailableGuild represents an unavailable guild. +type UnavailableGuild struct { + ID snowflake.ID `json:"id"` + Unavailable bool `json:"unavailable"` +} + +// GuildMember represents a guild member on Discord. +type GuildMember struct { + User *User `json:"user"` + Nick *string `json:"nick,omitempty"` + + Roles []snowflake.ID `json:"roles"` + JoinedAt string `json:"joined_at"` + Deaf bool `json:"deaf"` + Mute bool `json:"mute"` +} + +// VoiceState represents the voice state on Discord. +type VoiceState struct { + GuildID *snowflake.ID `json:"guild_id,omitempty"` + ChannelID snowflake.ID `json:"channel_id"` + UserID snowflake.ID `json:"user_id"` + Member *GuildMember `json:"member,omitempty"` + SessionID string `json:"session_id"` + Deaf bool `json:"deaf"` + Mute bool `json:"mute"` + SelfDeaf bool `json:"self_deaf"` + SelfMute bool `json:"self_mute"` + Suppress bool `json:"suppress"` +} diff --git a/discord/structs/http.go b/discord/structs/http.go index a21e60b..b031806 100644 --- a/discord/structs/http.go +++ b/discord/structs/http.go @@ -1,27 +1,27 @@ -package discord - -// http.go represents the structures of common endpoints we use. - -// Gateway represents a GET /gateway response. -type Gateway struct { - URL string `json:"url"` -} - -// GatewayBot represents a GET /gateway/bot response. -type GatewayBot struct { - URL string `json:"url"` - Shards int `json:"shards"` - SessionStartLimit struct { - Total int `json:"total"` - Remaining int `json:"remaining"` - ResetAfter int `json:"reset_after"` - MaxConcurrency int `json:"max_concurrency"` - } `json:"session_start_limit"` -} - -// TooManyRequests represents the payload of a TooManyRequests response. -type TooManyRequests struct { - Message string `json:"message"` - RetryAfter int `json:"retry_after"` - Global bool `json:"global"` -} +package discord + +// http.go represents the structures of common endpoints we use. + +// Gateway represents a GET /gateway response. +type Gateway struct { + URL string `json:"url"` +} + +// GatewayBot represents a GET /gateway/bot response. +type GatewayBot struct { + URL string `json:"url"` + Shards int `json:"shards"` + SessionStartLimit struct { + Total int `json:"total"` + Remaining int `json:"remaining"` + ResetAfter int `json:"reset_after"` + MaxConcurrency int `json:"max_concurrency"` + } `json:"session_start_limit"` +} + +// TooManyRequests represents the payload of a TooManyRequests response. +type TooManyRequests struct { + Message string `json:"message"` + RetryAfter int `json:"retry_after"` + Global bool `json:"global"` +} diff --git a/discord/structs/invites.go b/discord/structs/invites.go index 7916e49..0ca80ca 100644 --- a/discord/structs/invites.go +++ b/discord/structs/invites.go @@ -1,11 +1,11 @@ -package discord - -// invites.go contains all structures for invites. - -// ` represents the type of an invites target -type InviteTargetType uint8 - -const ( - InviteTargetTypeStream InviteTargetType = 1 + iota - InviteTargetTypeEmbeddedApplication -) +package discord + +// invites.go contains all structures for invites. + +// ` represents the type of an invites target +type InviteTargetType uint8 + +const ( + InviteTargetTypeStream InviteTargetType = 1 + iota + InviteTargetTypeEmbeddedApplication +) diff --git a/discord/structs/member.go b/discord/structs/member.go index cfebd2f..731f52a 100644 --- a/discord/structs/member.go +++ b/discord/structs/member.go @@ -1,20 +1,20 @@ -package discord - -import ( - "github.com/WelcomerTeam/RealRock/snowflake" -) - -// member.go contains all structures that represent a guild member. - -// GuildMember represents a guild member on Discord. -type Member struct { - User *User `json:"user"` - Nick *string `json:"nick,omitempty"` - Roles []snowflake.ID `json:"roles"` - JoinedAt string `json:"joined_at"` - PremiumSince *string `json:"premium_since,omitempty"` - Deaf bool `json:"deaf"` - Mute bool `json:"mute"` - Pending *bool `json:"pending,omitempty"` - Permissions *string `json:"permissions,omitempty"` -} +package discord + +import ( + "github.com/WelcomerTeam/RealRock/snowflake" +) + +// member.go contains all structures that represent a guild member. + +// GuildMember represents a guild member on Discord. +type Member struct { + User *User `json:"user"` + Nick *string `json:"nick,omitempty"` + Roles []snowflake.ID `json:"roles"` + JoinedAt string `json:"joined_at"` + PremiumSince *string `json:"premium_since,omitempty"` + Deaf bool `json:"deaf"` + Mute bool `json:"mute"` + Pending *bool `json:"pending,omitempty"` + Permissions *string `json:"permissions,omitempty"` +} diff --git a/discord/structs/message.go b/discord/structs/message.go index 7ec5cbf..c4526ea 100644 --- a/discord/structs/message.go +++ b/discord/structs/message.go @@ -1,149 +1,149 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// message.go contains the structure that represents a discord message. - -// MessageType represents the type of message that has been sent. -type MessageType uint8 - -const ( - MessageTypeDefault MessageType = iota - MessageTypeRecipientAdd - MessageTypeRecipientRemove - MessageTypeCall - MessageTypeChannelNameChange - MessageTypeChannelIconChange - MessageTypeChannelPinnedMessage - MessageTypeGuildMemberJoin - MessageTypeUserPremiumGuildSubscription - MessageTypeUserPremiumGuildSubscriptionTier1 - MessageTypeUserPremiumGuildSubscriptionTier2 - MessageTypeUserPremiumGuildSubscriptionTier3 - MessageTypeChannelFollowAdd - _ - MessageTypeGuildDiscoveryDisqualified - MessageTypeGuildDiscoveryRequalified - MessageTypeGuildDiscoveryGracePeriodInitialWarning - MessageTypeGuildDiscoveryGracePeriodFinalWarning - MessageTypeThreadCreated - MessageTypeReply - MessageTypeApplicationCommand - MessageTypeThreadStarterMessage - MessageTypeGuildInviteReminder -) - -// MessageFlags represents the extra information on a message. -type MessageFlags uint8 - -const ( - MessageFlagCrossposted MessageFlags = 1 << iota - MessageFlagIsCrosspost - MessageFlagSuppressEmbeds - MessageFlagSourceMessageDeleted - MessageFlagUrgent - MessageFlagHasThread - MessageFlaEphemeral - MessageFlagLoading -) - -// MessageAllowedMentionsType represents all the allowed mention types -type MessageAllowedMentionsType string - -const ( - MessageAllowedMentionsTypeRoles MessageAllowedMentionsType = "roles" - MessageAllowedMentionsTypeUsers MessageAllowedMentionsType = "users" - MessageAllowedMentionsTypeEveryone MessageAllowedMentionsType = "everyone" -) - -// MessageActivityType represents the type of message activity. -type MessageActivityType uint8 - -const ( - MessageActivityTypeJoin MessageActivityType = 1 + iota - MessageActivityTypeSpectate - MessageActivityTypeListen - MessageActivityTypeJoinRequest -) - -// Message represents a message on Discord. -type Message struct { - ID snowflake.ID `json:"id"` - ChannelID snowflake.ID `json:"channel_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Author *User `json:"author"` - Member *Member `json:"member,omitempty"` - - Content string `json:"content"` - Timestamp string `json:"timestamp"` - EditedTimestamp string `json:"edited_timestamp"` - TTS bool `json:"tts"` - - MentionEveryone bool `json:"mention_everyone"` - Mentions []*User `json:"mentions"` - MentionRoles []snowflake.ID `json:"mention_roles"` - MentionChannels []*MessageChannelMention `json:"mention_channels,omitempty"` - - Attachments []*MessageAttachment `json:"attachments"` - Embeds []*Embed `json:"embeds"` - Reactions []*MessageReaction `json:"reactions"` - Nonce *snowflake.ID `json:"nonce,omitempty"` - Pinned bool `json:"pinned"` - WebhookID *snowflake.ID `json:"webhook_id,omitempty"` - Type MessageType `json:"type"` - Activity *MessageActivity `json:"activity"` - Application *Application `json:"application"` - MessageReference []*MessageReference `json:"message_referenced,omitempty"` - Flags *MessageFlags `json:"flags,omitempty"` - Components []*InteractionComponent - Stickers []*Sticker `json:"stickers,omitempty"` - ReferencedMessage *Message `json:"referenced_message,omitempty"` -} - -// MessageChannelMention represents a mentioned channel. -type MessageChannelMention struct { - ID snowflake.ID `json:"id"` - GuildID snowflake.ID `json:"guild_id"` - Type ChannelType `json:"type"` - Name string `json:"name"` -} - -// MessageReference represents crossposted messages or replys. -type MessageReference struct { - ID *snowflake.ID `json:"message_id,omitempty"` - ChannelID *snowflake.ID `json:"channel_id,omitempty"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - FailIfNotExists *bool `json:"fail_if_not_exists,omitempty"` -} - -// MessageReaction represents a reaction to a message on Discord. -type MessageReaction struct { - Count int `json:"count"` - Me bool `json:"me"` - Emoji *Emoji `json:"emoji"` -} - -// MessageAllowedMentions is the structure of the allowed mentions entry. -type MessageAllowedMentions struct { - Parse []MessageAllowedMentionsType `json:"parse"` - Roles []snowflake.ID `json:"roles"` - Users []snowflake.ID `json:"users"` - RepliedUser bool `json:"replied_user"` -} - -// MessageAttachment represents a message attachment on discord. -type MessageAttachment struct { - ID snowflake.ID `json:"id"` - Filename string `json:"filename"` - Size int `json:"size"` - URL string `json:"url"` - ProxyURL string `json:"proxy_url"` - Height int `json:"height"` - Width int `json:"width"` -} - -// MessageActivity represents a message activity on Discord. -type MessageActivity struct { - Type MessageActivityType `json:"type"` - PartyID *string `json:"party_id,omitempty"` -} +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// message.go contains the structure that represents a discord message. + +// MessageType represents the type of message that has been sent. +type MessageType uint8 + +const ( + MessageTypeDefault MessageType = iota + MessageTypeRecipientAdd + MessageTypeRecipientRemove + MessageTypeCall + MessageTypeChannelNameChange + MessageTypeChannelIconChange + MessageTypeChannelPinnedMessage + MessageTypeGuildMemberJoin + MessageTypeUserPremiumGuildSubscription + MessageTypeUserPremiumGuildSubscriptionTier1 + MessageTypeUserPremiumGuildSubscriptionTier2 + MessageTypeUserPremiumGuildSubscriptionTier3 + MessageTypeChannelFollowAdd + _ + MessageTypeGuildDiscoveryDisqualified + MessageTypeGuildDiscoveryRequalified + MessageTypeGuildDiscoveryGracePeriodInitialWarning + MessageTypeGuildDiscoveryGracePeriodFinalWarning + MessageTypeThreadCreated + MessageTypeReply + MessageTypeApplicationCommand + MessageTypeThreadStarterMessage + MessageTypeGuildInviteReminder +) + +// MessageFlags represents the extra information on a message. +type MessageFlags uint8 + +const ( + MessageFlagCrossposted MessageFlags = 1 << iota + MessageFlagIsCrosspost + MessageFlagSuppressEmbeds + MessageFlagSourceMessageDeleted + MessageFlagUrgent + MessageFlagHasThread + MessageFlaEphemeral + MessageFlagLoading +) + +// MessageAllowedMentionsType represents all the allowed mention types +type MessageAllowedMentionsType string + +const ( + MessageAllowedMentionsTypeRoles MessageAllowedMentionsType = "roles" + MessageAllowedMentionsTypeUsers MessageAllowedMentionsType = "users" + MessageAllowedMentionsTypeEveryone MessageAllowedMentionsType = "everyone" +) + +// MessageActivityType represents the type of message activity. +type MessageActivityType uint8 + +const ( + MessageActivityTypeJoin MessageActivityType = 1 + iota + MessageActivityTypeSpectate + MessageActivityTypeListen + MessageActivityTypeJoinRequest +) + +// Message represents a message on Discord. +type Message struct { + ID snowflake.ID `json:"id"` + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Author *User `json:"author"` + Member *Member `json:"member,omitempty"` + + Content string `json:"content"` + Timestamp string `json:"timestamp"` + EditedTimestamp string `json:"edited_timestamp"` + TTS bool `json:"tts"` + + MentionEveryone bool `json:"mention_everyone"` + Mentions []*User `json:"mentions"` + MentionRoles []snowflake.ID `json:"mention_roles"` + MentionChannels []*MessageChannelMention `json:"mention_channels,omitempty"` + + Attachments []*MessageAttachment `json:"attachments"` + Embeds []*Embed `json:"embeds"` + Reactions []*MessageReaction `json:"reactions"` + Nonce *snowflake.ID `json:"nonce,omitempty"` + Pinned bool `json:"pinned"` + WebhookID *snowflake.ID `json:"webhook_id,omitempty"` + Type MessageType `json:"type"` + Activity *MessageActivity `json:"activity"` + Application *Application `json:"application"` + MessageReference []*MessageReference `json:"message_referenced,omitempty"` + Flags *MessageFlags `json:"flags,omitempty"` + Components []*InteractionComponent + Stickers []*Sticker `json:"stickers,omitempty"` + ReferencedMessage *Message `json:"referenced_message,omitempty"` +} + +// MessageChannelMention represents a mentioned channel. +type MessageChannelMention struct { + ID snowflake.ID `json:"id"` + GuildID snowflake.ID `json:"guild_id"` + Type ChannelType `json:"type"` + Name string `json:"name"` +} + +// MessageReference represents crossposted messages or replys. +type MessageReference struct { + ID *snowflake.ID `json:"message_id,omitempty"` + ChannelID *snowflake.ID `json:"channel_id,omitempty"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + FailIfNotExists *bool `json:"fail_if_not_exists,omitempty"` +} + +// MessageReaction represents a reaction to a message on Discord. +type MessageReaction struct { + Count int `json:"count"` + Me bool `json:"me"` + Emoji *Emoji `json:"emoji"` +} + +// MessageAllowedMentions is the structure of the allowed mentions entry. +type MessageAllowedMentions struct { + Parse []MessageAllowedMentionsType `json:"parse"` + Roles []snowflake.ID `json:"roles"` + Users []snowflake.ID `json:"users"` + RepliedUser bool `json:"replied_user"` +} + +// MessageAttachment represents a message attachment on discord. +type MessageAttachment struct { + ID snowflake.ID `json:"id"` + Filename string `json:"filename"` + Size int `json:"size"` + URL string `json:"url"` + ProxyURL string `json:"proxy_url"` + Height int `json:"height"` + Width int `json:"width"` +} + +// MessageActivity represents a message activity on Discord. +type MessageActivity struct { + Type MessageActivityType `json:"type"` + PartyID *string `json:"party_id,omitempty"` +} diff --git a/discord/structs/presence.go b/discord/structs/presence.go index 5fade52..265673f 100644 --- a/discord/structs/presence.go +++ b/discord/structs/presence.go @@ -1,87 +1,87 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// PresenceStatus represents a presence's status. -type PresenceStatus string - -// Presence statuses. -const ( - PresenceStatusIdle PresenceStatus = "idle" - PresenceStatusDND PresenceStatus = "dnd" - PresenceStatusOnline PresenceStatus = "online" - PresenceStatusOffline PresenceStatus = "offline" -) - -// ActivityType represents an activity's type. -type ActivityType int - -// Activity types. -const ( - ActivityTypeGame ActivityType = iota - ActivityTypeStreaming - ActivityTypeListening -) - -// ActivityFlag represents an activity's flags. -type ActivityFlag int - -// Activity flags. -const ( - ActivityFlagInstance ActivityFlag = 1 << iota - ActivityFlagJoin - ActivityFlagSpectate - ActivityFlagJoinRequest - ActivityFlagSync - ActivityFlagPlay -) - -// Activity represents an activity as sent as part of other packets. -type Activity struct { - Name string `json:"name"` - Type ActivityType `json:"type"` - URL *string `json:"url,omitempty"` - Timestamps *Timestamps `json:"timestamps,omitempty"` - ApplicationID *snowflake.ID `json:"application_id"` - Details *string `json:"details,omitempty"` - State *string `json:"state,omitempty"` - Party *Party `json:"party,omitempty"` - Assets *Assets `json:"assets,omitempty"` - Secrets *Secrets `json:"secrets,omitempty"` - Instance *bool `json:"instance,omitempty"` - Flags *ActivityFlag `json:"flags,omitempty"` -} - -// Timestamps represents the starting and ending timestamp of an activity. -type Timestamps struct { - Start *int `json:"start,omitempty"` - End *int `json:"end,omitempty"` -} - -// Party represents an activity's current party information. -type Party struct { - ID *string `json:"id,omitempty"` - Size []int `json:"size,omitempty"` -} - -// Assets represents an activity's images and their hover texts. -type Assets struct { - LargeImage *string `json:"large_image,omitempty"` - LargeText *string `json:"large_text,omitempty"` - SmallImage *string `json:"small_image,omitempty"` - SmallText *string `json:"small_text,omitempty"` -} - -// Secrets represents an activity's secrets for Rich Presence joining and spectating. -type Secrets struct { - Join *string `json:"join,omitempty"` - Spectate *string `json:"spectate,omitempty"` - Match *string `json:"match,omitempty"` -} - -// ClientStatus represent's the status of a client. -type ClientStatus struct { - Desktop *string `json:"desktop,omitempty"` - Mobile *string `json:"mobile,omitempty"` - Web *string `json:"web,omitempty"` -} +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// PresenceStatus represents a presence's status. +type PresenceStatus string + +// Presence statuses. +const ( + PresenceStatusIdle PresenceStatus = "idle" + PresenceStatusDND PresenceStatus = "dnd" + PresenceStatusOnline PresenceStatus = "online" + PresenceStatusOffline PresenceStatus = "offline" +) + +// ActivityType represents an activity's type. +type ActivityType int + +// Activity types. +const ( + ActivityTypeGame ActivityType = iota + ActivityTypeStreaming + ActivityTypeListening +) + +// ActivityFlag represents an activity's flags. +type ActivityFlag int + +// Activity flags. +const ( + ActivityFlagInstance ActivityFlag = 1 << iota + ActivityFlagJoin + ActivityFlagSpectate + ActivityFlagJoinRequest + ActivityFlagSync + ActivityFlagPlay +) + +// Activity represents an activity as sent as part of other packets. +type Activity struct { + Name string `json:"name"` + Type ActivityType `json:"type"` + URL *string `json:"url,omitempty"` + Timestamps *Timestamps `json:"timestamps,omitempty"` + ApplicationID *snowflake.ID `json:"application_id"` + Details *string `json:"details,omitempty"` + State *string `json:"state,omitempty"` + Party *Party `json:"party,omitempty"` + Assets *Assets `json:"assets,omitempty"` + Secrets *Secrets `json:"secrets,omitempty"` + Instance *bool `json:"instance,omitempty"` + Flags *ActivityFlag `json:"flags,omitempty"` +} + +// Timestamps represents the starting and ending timestamp of an activity. +type Timestamps struct { + Start *int `json:"start,omitempty"` + End *int `json:"end,omitempty"` +} + +// Party represents an activity's current party information. +type Party struct { + ID *string `json:"id,omitempty"` + Size []int `json:"size,omitempty"` +} + +// Assets represents an activity's images and their hover texts. +type Assets struct { + LargeImage *string `json:"large_image,omitempty"` + LargeText *string `json:"large_text,omitempty"` + SmallImage *string `json:"small_image,omitempty"` + SmallText *string `json:"small_text,omitempty"` +} + +// Secrets represents an activity's secrets for Rich Presence joining and spectating. +type Secrets struct { + Join *string `json:"join,omitempty"` + Spectate *string `json:"spectate,omitempty"` + Match *string `json:"match,omitempty"` +} + +// ClientStatus represent's the status of a client. +type ClientStatus struct { + Desktop *string `json:"desktop,omitempty"` + Mobile *string `json:"mobile,omitempty"` + Web *string `json:"web,omitempty"` +} diff --git a/discord/structs/role.go b/discord/structs/role.go index a380e02..b68496d 100644 --- a/discord/structs/role.go +++ b/discord/structs/role.go @@ -1,25 +1,25 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// role.go represents all structures for a discord guild role. - -// Role represents a role on Discord. -type Role struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Color int `json:"color"` - Hoist bool `json:"hoist"` - Position int `json:"position"` - Permissions int `json:"permissions"` - Managed bool `json:"managed"` - Mentionable bool `json:"mentionable"` - Tags []*RoleTag `json:"tags,omitempty"` -} - -// RoleTag represents extra information about a role. -type RoleTag struct { - BotID *snowflake.ID `json:"bot_id,omitempty"` - IntegrationID *snowflake.ID `json:"integration_id,omitempty"` - PremiumSubscriber *bool `json:"premium_subscriber,omitempty"` -} +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// role.go represents all structures for a discord guild role. + +// Role represents a role on Discord. +type Role struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Color int `json:"color"` + Hoist bool `json:"hoist"` + Position int `json:"position"` + Permissions int `json:"permissions"` + Managed bool `json:"managed"` + Mentionable bool `json:"mentionable"` + Tags []*RoleTag `json:"tags,omitempty"` +} + +// RoleTag represents extra information about a role. +type RoleTag struct { + BotID *snowflake.ID `json:"bot_id,omitempty"` + IntegrationID *snowflake.ID `json:"integration_id,omitempty"` + PremiumSubscriber *bool `json:"premium_subscriber,omitempty"` +} diff --git a/discord/structs/sticker.go b/discord/structs/sticker.go index 3069e9c..0c149f0 100644 --- a/discord/structs/sticker.go +++ b/discord/structs/sticker.go @@ -1,37 +1,37 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// sticker represents all structures for a sticker. - -// Sticker represents a sticker object. -type Sticker struct { - ID snowflake.ID `json:"id"` - PackID *snowflake.ID `json:"pack_id,omitempty"` - Name string `json:"name"` - Description string `json:"description"` - Tags []string `json:"tags,omitempty"` - Type *StickerType `json:"type"` - FormatType *StickerFormatType `json:"format_type"` - Available *bool `json:"available,omitempty"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - User *User `json:"user,omitempty"` - SortValue *int `json:"sort_value,omitempty"` -} - -// StickerType represents the type of sticker. -type StickerType uint8 - -const ( - StickerTypeStandard StickerType = 1 + iota - StickerTypeGuild -) - -// StickerFormatType represents the sticker format. -type StickerFormatType uint8 - -const ( - StickerFormatTypePNG StickerFormatType = 1 + iota - StickerFormatTypeAPNG - StickerFormatTypeLOTTIE -) +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// sticker represents all structures for a sticker. + +// Sticker represents a sticker object. +type Sticker struct { + ID snowflake.ID `json:"id"` + PackID *snowflake.ID `json:"pack_id,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` + Type *StickerType `json:"type"` + FormatType *StickerFormatType `json:"format_type"` + Available *bool `json:"available,omitempty"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + User *User `json:"user,omitempty"` + SortValue *int `json:"sort_value,omitempty"` +} + +// StickerType represents the type of sticker. +type StickerType uint8 + +const ( + StickerTypeStandard StickerType = 1 + iota + StickerTypeGuild +) + +// StickerFormatType represents the sticker format. +type StickerFormatType uint8 + +const ( + StickerFormatTypePNG StickerFormatType = 1 + iota + StickerFormatTypeAPNG + StickerFormatTypeLOTTIE +) diff --git a/discord/structs/template.go b/discord/structs/template.go index cd97dde..1e7bb2d 100644 --- a/discord/structs/template.go +++ b/discord/structs/template.go @@ -1,3 +1,3 @@ -package discord - -// template.go represents all structures relating to guild templates. +package discord + +// template.go represents all structures relating to guild templates. diff --git a/discord/structs/user.go b/discord/structs/user.go index fd7bc06..905461b 100644 --- a/discord/structs/user.go +++ b/discord/structs/user.go @@ -1,54 +1,54 @@ -package discord - -import "github.com/WelcomerTeam/RealRock/snowflake" - -// user.go represents all structures for a discord user. - -// UserFlags represents the flags on a user's account. -type UserFlags int - -// User flags. -const ( - UserFlagsNone UserFlags = 1 << iota - UserFlagsDiscordEmployee - UserFlagsPartneredServerOwner - UserFlagsHypeSquadEvents - UserFlagsBugHunterLevel1 - UserFlagsHouseBravery - UserFlagsHouseBrilliance - UserFlagsHouseBalance - UserFlagsEarlySupporter - UserFlagsTeamUser - UserFlagsSystem - UserFlagsBugHunterLevel2 - UserFlagsVerifiedBot - UserFlagsEarlyVerifiedBotDeveloper -) - -// UserPremiumType represents the type of Nitro on a user's account. -type UserPremiumType int - -// User premium type. -const ( - UserPremiumTypeNone UserPremiumType = iota - UserPremiumTypeNitroClassic - UserPremiumTypeNitro -) - -// User represents a user on Discord. -type User struct { - ID snowflake.ID `json:"id"` - Username string `json:"username"` - Discriminator string `json:"discriminator"` - Avatar *string `json:"avatar,omitempty"` - Bot *bool `json:"bot,omitempty"` - System *bool `json:"system,omitempty"` - MFAEnabled *bool `json:"mfa_enabled,omitempty"` - Banner *string `json:"banner,omitempty"` - Locale *string `json:"locale,omitempty"` - Verified *bool `json:"verified,omitempty"` - Email *string `json:"email,omitempty"` - Flags *UserFlags `json:"flags,omitempty"` - PremiumType *UserPremiumType `json:"premium_type,omitempty"` - PublicFlags *UserFlags `json:"public_flags,omitempty"` -} +package discord + +import "github.com/WelcomerTeam/RealRock/snowflake" + +// user.go represents all structures for a discord user. + +// UserFlags represents the flags on a user's account. +type UserFlags int + +// User flags. +const ( + UserFlagsNone UserFlags = 1 << iota + UserFlagsDiscordEmployee + UserFlagsPartneredServerOwner + UserFlagsHypeSquadEvents + UserFlagsBugHunterLevel1 + UserFlagsHouseBravery + UserFlagsHouseBrilliance + UserFlagsHouseBalance + UserFlagsEarlySupporter + UserFlagsTeamUser + UserFlagsSystem + UserFlagsBugHunterLevel2 + UserFlagsVerifiedBot + UserFlagsEarlyVerifiedBotDeveloper +) + +// UserPremiumType represents the type of Nitro on a user's account. +type UserPremiumType int + +// User premium type. +const ( + UserPremiumTypeNone UserPremiumType = iota + UserPremiumTypeNitroClassic + UserPremiumTypeNitro +) + +// User represents a user on Discord. +type User struct { + ID snowflake.ID `json:"id"` + Username string `json:"username"` + Discriminator string `json:"discriminator"` + Avatar *string `json:"avatar,omitempty"` + Bot *bool `json:"bot,omitempty"` + System *bool `json:"system,omitempty"` + MFAEnabled *bool `json:"mfa_enabled,omitempty"` + Banner *string `json:"banner,omitempty"` + Locale *string `json:"locale,omitempty"` + Verified *bool `json:"verified,omitempty"` + Email *string `json:"email,omitempty"` + Flags *UserFlags `json:"flags,omitempty"` + PremiumType *UserPremiumType `json:"premium_type,omitempty"` + PublicFlags *UserFlags `json:"public_flags,omitempty"` +} diff --git a/discord/structs/webhook.go b/discord/structs/webhook.go index d88cd27..5150959 100644 --- a/discord/structs/webhook.go +++ b/discord/structs/webhook.go @@ -1,42 +1,42 @@ -package discord - -import ( - "github.com/WelcomerTeam/RealRock/snowflake" -) - -// webhook.go represents all structures to create a webhook and interact with it. - -// WebhookType is the type of webhook. -type WebhookType uint8 - -// Webhook type. -const ( - WebhookTypeIncoming WebhookType = iota + 1 - WebhookTypeChannelFollower -) - -// Webhook represents a webhook on Discord. -type Webhook struct { - ID snowflake.ID `json:"id"` - Type WebhookType `json:"type"` - - GuildID *snowflake.ID `json:"guild_id,omitempty"` - ChannelID *snowflake.ID `json:"channel_id,omitempty"` - User *User `json:"user,omitempty"` - Name string `json:"name"` - Avatar string `json:"avatar"` - Token string `json:"token"` - ApplicationID *snowflake.ID `json:"application_id,omitempty"` -} - -// WebhookMessage represents a message on Discord for webhooks. -type WebhookMessage struct { - Content *string `json:"content,omitempty"` - Username *string `json:"username,omitempty"` - AvatarURL *string `json:"avatar_url,omitempty"` - TTS *bool `json:"tts,omitempty"` - Embeds []*Embed `json:"embeds,omitempty"` - AllowedMentions []*MessageAllowedMentions `json:"allowed_mentions,omitempty"` - Components []*InteractionComponent `json:"components,omitempty"` - // PayloadJSON *json.RawMessage `json:"payload_json,omitempty"` -} +package discord + +import ( + "github.com/WelcomerTeam/RealRock/snowflake" +) + +// webhook.go represents all structures to create a webhook and interact with it. + +// WebhookType is the type of webhook. +type WebhookType uint8 + +// Webhook type. +const ( + WebhookTypeIncoming WebhookType = iota + 1 + WebhookTypeChannelFollower +) + +// Webhook represents a webhook on Discord. +type Webhook struct { + ID snowflake.ID `json:"id"` + Type WebhookType `json:"type"` + + GuildID *snowflake.ID `json:"guild_id,omitempty"` + ChannelID *snowflake.ID `json:"channel_id,omitempty"` + User *User `json:"user,omitempty"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Token string `json:"token"` + ApplicationID *snowflake.ID `json:"application_id,omitempty"` +} + +// WebhookMessage represents a message on Discord for webhooks. +type WebhookMessage struct { + Content *string `json:"content,omitempty"` + Username *string `json:"username,omitempty"` + AvatarURL *string `json:"avatar_url,omitempty"` + TTS *bool `json:"tts,omitempty"` + Embeds []*Embed `json:"embeds,omitempty"` + AllowedMentions []*MessageAllowedMentions `json:"allowed_mentions,omitempty"` + Components []*InteractionComponent `json:"components,omitempty"` + // PayloadJSON *json.RawMessage `json:"payload_json,omitempty"` +} diff --git a/generate-protobuf.sh b/generate-protobuf.sh index 4210ece..e06a981 100644 --- a/generate-protobuf.sh +++ b/generate-protobuf.sh @@ -1,11 +1,11 @@ -export GO111MODULE=on - -go get google.golang.org/protobuf/cmd/protoc-gen-go \ - google.golang.org/grpc/cmd/protoc-gen-go-grpc - -export PATH="$PATH:$(go env GOPATH)/bin" - -protoc --proto_path=protobuf \ - --go_out=protobuf --go_opt=paths=source_relative \ - --go-grpc_out=protobuf --go-grpc_opt=paths=source_relative \ +export GO111MODULE=on + +go get google.golang.org/protobuf/cmd/protoc-gen-go \ + google.golang.org/grpc/cmd/protoc-gen-go-grpc + +export PATH="$PATH:$(go env GOPATH)/bin" + +protoc --proto_path=protobuf \ + --go_out=protobuf --go_opt=paths=source_relative \ + --go-grpc_out=protobuf --go-grpc_opt=paths=source_relative \ protobuf/gateway.proto \ No newline at end of file diff --git a/go.mod b/go.mod index af45e6c..5e5b1d0 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/BurntSushi/toml v0.4.1 // indirect github.com/WelcomerTeam/RealRock v0.0.0-20210814150330-a411f684a7e9 + github.com/WelcomerTeam/czlib v0.0.0-20210907121728-d7ed7721c904 // indirect github.com/andybalholm/brotli v1.0.0 github.com/go-redis/redis/v8 v8.11.3 github.com/json-iterator/go v1.1.11 @@ -12,13 +13,16 @@ require ( github.com/nats-io/nats-streaming-server v0.22.1 // indirect github.com/nats-io/nats.go v1.11.1-0.20210623165838-4b75fc59ae30 github.com/nats-io/stan.go v0.10.0 + github.com/prometheus/client_golang v0.9.2 github.com/rs/zerolog v1.23.0 github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873 github.com/segmentio/kafka-go v0.4.17 github.com/vmihailenco/msgpack v4.0.4+incompatible go.uber.org/atomic v1.9.0 + golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/appengine v1.6.7 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + nhooyr.io/websocket v1.8.7 ) diff --git a/go.sum b/go.sum index 0543197..0e18a55 100644 --- a/go.sum +++ b/go.sum @@ -3,10 +3,13 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/WelcomerTeam/RealRock v0.0.0-20210814150330-a411f684a7e9 h1:1nbaug0RNGy+ah63UT5AjeUnrBQ8klr5xHlx+8tGhaI= github.com/WelcomerTeam/RealRock v0.0.0-20210814150330-a411f684a7e9/go.mod h1:GstUihsC3fhjBQvacUZ9AC4SWysOCw73kNEjItrPl74= +github.com/WelcomerTeam/czlib v0.0.0-20210907121728-d7ed7721c904 h1:WV4Ok6b0/kgczuLAgGTNtLVOB5JIqdfzJzgOpdlDM8Q= +github.com/WelcomerTeam/czlib v0.0.0-20210907121728-d7ed7721c904/go.mod h1:rCfCrg0xPnEoVKPXk+GNyHgzTWMzJjhnPaAfDe7UPJE= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -28,15 +31,34 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8= github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -57,6 +79,8 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= @@ -73,11 +97,13 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/raft v1.3.1 h1:zDT8ke8y2aP4wf9zPTB2uSIeavJ3Hx/ceY4jxI2JxuY= github.com/hashicorp/raft v1.3.1/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.12 h1:famVnQVu7QwryBN4jNseQdUKES71ZAOnB6UQQJPZvqk= github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -86,12 +112,16 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= @@ -136,8 +166,11 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.7.1 h1:TlEtJq5GvGqMykEwWzbZWjjztF86swFhsPix1i0bkgA= @@ -153,12 +186,15 @@ github.com/segmentio/kafka-go v0.4.17/go.mod h1:19+Eg7KwrNKy/PFhiIthEPkO8k+ac7/Z github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= -github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= @@ -196,8 +232,9 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg= +golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -213,6 +250,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -230,6 +268,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -264,9 +303,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/internal/analytics.go b/internal/analytics.go index 0be58a4..6a28577 100644 --- a/internal/analytics.go +++ b/internal/analytics.go @@ -1,24 +1,24 @@ -package internal - -// counter only up -// gague up or down -// summaries you dont know the value -// histogram you know the values - -// events total -// events per manager -// events per shardgroup -// events per guild -// events type total -// gateway latency - -// shard status total - -// unavailable guilds - -// cache counts -// cache requests -// cache hits -// cache misses - -// producer events total +package internal + +// counter only up +// gague up or down +// summaries you dont know the value +// histogram you know the values + +// events total +// events per manager +// events per shardgroup +// events per guild +// events type total +// gateway latency + +// shard status total + +// unavailable guilds + +// cache counts +// cache requests +// cache hits +// cache misses + +// producer events total diff --git a/internal/client.go b/internal/client.go index 10147d6..2f3338a 100644 --- a/internal/client.go +++ b/internal/client.go @@ -1,135 +1,135 @@ -package internal - -import ( - "context" - "fmt" - "io" - "io/ioutil" - "net/http" - "strings" - "sync" - "time" - - discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" - "golang.org/x/xerrors" -) - -// Client represents the REST client. -type Client struct { - mu sync.Mutex - - Token string - - HTTP *http.Client - - // We will manually add the API version - APIVersion string - - // Used to safely create URLs and is filled if empty - URLHost string - URLScheme string - UserAgent string - - isBot bool -} - -// NewClient makes a new client. -func NewClient(token string) *Client { - return &Client{ - mu: sync.Mutex{}, - Token: token, - HTTP: http.DefaultClient, - APIVersion: "9", - URLHost: "discord.com", - URLScheme: "https", - UserAgent: "Sandwich/" + VERSION + " (github.com/WelcomerTeam/Sandwich-Daemon)", - } -} - -// Fetch returns the response. Passing any headers will be sent to the request however -// Authorization will be overwrote. -func (c *Client) Fetch(ctx context.Context, method string, url string, - body io.Reader, headers map[string]string) (_body []byte, status int, err error) { - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err != nil { - return - } - - for k, v := range headers { - req.Header.Set(k, v) - } - - res, err := c.HandleRequest(req, false) - if err != nil { - return - } - - defer res.Body.Close() - - _body, err = ioutil.ReadAll(res.Body) - if err != nil { - return nil, res.StatusCode, xerrors.Errorf("failed to read request body: %w", err) - } - - return _body, res.StatusCode, nil -} - -// FetchJSON attempts to convert the response into a JSON structure. Passing any headers -// will be sent to the request however Authorization will be overwrote. -func (c *Client) FetchJSON(ctx context.Context, method string, url string, body io.Reader, - headers map[string]string, structure interface{}) (status int, err error) { - responseBody, status, err := c.Fetch(ctx, method, url, body, headers) - if err != nil { - return - } - - err = json.Unmarshal(responseBody, &structure) - if err != nil { - return -1, fmt.Errorf("failed to unmarshal body: %w", err) - } - - return -} - -// HandleRequest makes a request to the Discord API. -func (c *Client) HandleRequest(req *http.Request, retry bool) (res *http.Response, err error) { - c.mu.Lock() - defer c.mu.Unlock() - - // Add the /api and version prefix when we did not include one - if !strings.HasPrefix(req.URL.Path, "/api") { - req.URL.Path = "/api/v" + c.APIVersion + req.URL.Path - } - - req.URL.Host = replaceIfEmpty(req.URL.Host, c.URLHost) - req.URL.Scheme = replaceIfEmpty(req.URL.Scheme, c.URLScheme) - - req.Header.Set("User-Agent", replaceIfEmpty(req.Header.Get("User-Agent"), c.UserAgent)) - - if c.Token != "" { - req.Header.Set("Authorization", replaceIfEmpty(req.Header.Get("Authorization"), c.Token)) - } - - if res, err = c.HTTP.Do(req); err != nil { - return res, fmt.Errorf("failed to do HTTP request: %w", err) - } - - if res.StatusCode == http.StatusTooManyRequests { - resp := discord.TooManyRequests{} - err = json.NewDecoder(res.Body).Decode(&resp) - - if err != nil { - return res, fmt.Errorf("failed to decode body: %w", err) - } - - <-time.After(time.Duration(resp.RetryAfter) * time.Millisecond) - - return c.HandleRequest(req, true) - } - - if res.StatusCode == http.StatusUnauthorized { - return res, ErrInvalidToken - } - - return res, nil -} +package internal + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + "sync" + "time" + + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + "golang.org/x/xerrors" +) + +// Client represents the REST client. +type Client struct { + mu sync.Mutex + + Token string + + HTTP *http.Client + + // We will manually add the API version + APIVersion string + + // Used to safely create URLs and is filled if empty + URLHost string + URLScheme string + UserAgent string + + isBot bool +} + +// NewClient makes a new client. +func NewClient(token string) *Client { + return &Client{ + mu: sync.Mutex{}, + Token: token, + HTTP: http.DefaultClient, + APIVersion: "9", + URLHost: "discord.com", + URLScheme: "https", + UserAgent: "Sandwich/" + VERSION + " (github.com/WelcomerTeam/Sandwich-Daemon)", + } +} + +// Fetch returns the response. Passing any headers will be sent to the request however +// Authorization will be overwrote. +func (c *Client) Fetch(ctx context.Context, method string, url string, + body io.Reader, headers map[string]string) (_body []byte, status int, err error) { + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + res, err := c.HandleRequest(req, false) + if err != nil { + return + } + + defer res.Body.Close() + + _body, err = ioutil.ReadAll(res.Body) + if err != nil { + return nil, res.StatusCode, xerrors.Errorf("failed to read request body: %w", err) + } + + return _body, res.StatusCode, nil +} + +// FetchJSON attempts to convert the response into a JSON structure. Passing any headers +// will be sent to the request however Authorization will be overwrote. +func (c *Client) FetchJSON(ctx context.Context, method string, url string, body io.Reader, + headers map[string]string, structure interface{}) (status int, err error) { + responseBody, status, err := c.Fetch(ctx, method, url, body, headers) + if err != nil { + return + } + + err = json.Unmarshal(responseBody, &structure) + if err != nil { + return -1, fmt.Errorf("failed to unmarshal body: %w", err) + } + + return +} + +// HandleRequest makes a request to the Discord API. +func (c *Client) HandleRequest(req *http.Request, retry bool) (res *http.Response, err error) { + c.mu.Lock() + defer c.mu.Unlock() + + // Add the /api and version prefix when we did not include one + if !strings.HasPrefix(req.URL.Path, "/api") { + req.URL.Path = "/api/v" + c.APIVersion + req.URL.Path + } + + req.URL.Host = replaceIfEmpty(req.URL.Host, c.URLHost) + req.URL.Scheme = replaceIfEmpty(req.URL.Scheme, c.URLScheme) + + req.Header.Set("User-Agent", replaceIfEmpty(req.Header.Get("User-Agent"), c.UserAgent)) + + if c.Token != "" { + req.Header.Set("Authorization", replaceIfEmpty(req.Header.Get("Authorization"), c.Token)) + } + + if res, err = c.HTTP.Do(req); err != nil { + return res, fmt.Errorf("failed to do HTTP request: %w", err) + } + + if res.StatusCode == http.StatusTooManyRequests { + resp := discord.TooManyRequests{} + err = json.NewDecoder(res.Body).Decode(&resp) + + if err != nil { + return res, fmt.Errorf("failed to decode body: %w", err) + } + + <-time.After(time.Duration(resp.RetryAfter) * time.Millisecond) + + return c.HandleRequest(req, true) + } + + if res.StatusCode == http.StatusUnauthorized { + return res, ErrInvalidToken + } + + return res, nil +} diff --git a/internal/errors.go b/internal/errors.go index 68c8f0b..f85be2b 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -1,30 +1,30 @@ -package internal - -import ( - "golang.org/x/xerrors" -) - -// ErrSessionLimitExhausted is returned when the sessions remaining -// is less than the ShardGroup is starting with. -var ErrSessionLimitExhausted = xerrors.New("The session limit has been reached") - -// ErrInvalidToken is returned when an invalid token is used. -var ErrInvalidToken = xerrors.New("Token passed is not valid") - -// ErrReconnect is used to distinguish if the shard simply wants to reconnect. -var ErrReconnect = xerrors.New("Reconnect is required") - -var ( - ErrInvalidManager = xerrors.New("No manager with this name exists") - ErrInvalidShardGroup = xerrors.New("Invalid shard group id specified") - ErrInvalidShard = xerrors.New("Invalid shard id specified") - ErrChunkTimeout = xerrors.New("Timed out on initial member chunks") -) - -var ( - ErrReadConfigurationFailure = xerrors.New("Failed to read configuration") - ErrLoadConfigurationFailure = xerrors.New("Failed to load configuration") - ErrConfigurationValidateIdentify = xerrors.New("Configuration missing valid Identify URI") - ErrConfigurationValidatePrometheus = xerrors.New("Configuration missing valid Prometheus Host") - ErrConfigurationValidateGRPC = xerrors.New("Configuration missing valid GRPC Host") -) +package internal + +import ( + "golang.org/x/xerrors" +) + +// ErrSessionLimitExhausted is returned when the sessions remaining +// is less than the ShardGroup is starting with. +var ErrSessionLimitExhausted = xerrors.New("The session limit has been reached") + +// ErrInvalidToken is returned when an invalid token is used. +var ErrInvalidToken = xerrors.New("Token passed is not valid") + +// ErrReconnect is used to distinguish if the shard simply wants to reconnect. +var ErrReconnect = xerrors.New("Reconnect is required") + +var ( + ErrInvalidManager = xerrors.New("No manager with this name exists") + ErrInvalidShardGroup = xerrors.New("Invalid shard group id specified") + ErrInvalidShard = xerrors.New("Invalid shard id specified") + ErrChunkTimeout = xerrors.New("Timed out on initial member chunks") +) + +var ( + ErrReadConfigurationFailure = xerrors.New("Failed to read configuration") + ErrLoadConfigurationFailure = xerrors.New("Failed to load configuration") + ErrConfigurationValidateIdentify = xerrors.New("Configuration missing valid Identify URI") + ErrConfigurationValidatePrometheus = xerrors.New("Configuration missing valid Prometheus Host") + ErrConfigurationValidateGRPC = xerrors.New("Configuration missing valid GRPC Host") +) diff --git a/internal/grpc.go b/internal/grpc.go index 8499c20..c4e72e7 100644 --- a/internal/grpc.go +++ b/internal/grpc.go @@ -1,5 +1,5 @@ -package internal - -func (sg *Sandwich) NewGatewayServer() { - +package internal + +func (sg *Sandwich) NewGatewayServer() { + } \ No newline at end of file diff --git a/internal/manager.go b/internal/manager.go index 1f7e80e..b7d5fad 100644 --- a/internal/manager.go +++ b/internal/manager.go @@ -1,379 +1,386 @@ -package internal - -import ( - "bytes" - "context" - "crypto/sha256" - "fmt" - "math/rand" - "net/http" - "net/url" - "strconv" - "strings" - "sync" - "time" - - discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" - "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" - "github.com/rs/zerolog" - "github.com/vmihailenco/msgpack" - "go.uber.org/atomic" -) - -const ( - ShardMaxRetries = 5 - ShardCompression = true - ShardLargeThreshold = 100 - ShardMaxHeartbeatFailures = 5 - MessagingMaxClientNameNumber = 9999 - IdentifyRetry = (5 * time.Second) - IdentifyRateLimit = (5 * time.Second) + (500 * time.Millisecond) -) - -// Manager represents a single application. -type Manager struct { - ctx context.Context - cancel func() - - Error atomic.String `json:"error"` - - Sandwich *Sandwich `json:"-"` - Logger zerolog.Logger `json:"-"` - - configurationMu sync.RWMutex - Configuration *ManagerConfiguration `json:"configuration"` - - gatewayMu sync.RWMutex - Gateway discord.GatewayBot `json:"gateway"` - - shardGroupsMu sync.RWMutex - ShardGroups map[int32]*ShardGroup `json:"shard_groups"` - - ProducerClient MQClient `json:"-"` - - Client *Client `json:"-"` - - shardGroupCounter *atomic.Int32 - - eventBlacklistMu sync.RWMutex - eventBlacklist []string - - produceBlacklistMu sync.RWMutex - produceBlacklist []string -} - -// ManagerConfiguration represents the configuration for the manager. -type ManagerConfiguration struct { - // Unique name that will be referenced internally - Identifier string `json:"identifier"` - // Non-unique name that is sent to consumers. - ProducerIdentifier string `json:"producer_identifier"` - - FriendlyName string `json:"friendly_name"` - - Token string `json:"token"` - AutoStart bool `json:"auto_start"` - - // Bot specific configuration - Bot struct { - DefaultPresence *discord.Activity `json:"default_presence"` - Intents int64 `json:"intents"` - ChunkGuildsOnStartup bool `json":chunk_guilds_on_startup"` - } `json:"bot"` - - Caching struct { - CacheUsers bool `json:"cache_users"` - CacheMembers bool `json:"cache_members"` - StoreMutuals bool `json:"store_mutuals"` - } `json:"caching"` - - Events struct { - EventBlacklist []string `json:"event_blacklist"` - ProduceBlacklist []string `json:"produce_blacklist"` - } `json:"events"` - - Messaging struct { - ClientName string `json:"client_name"` - ChannelName string `json:"channel_name"` - UseRandomSuffix bool `json:"use_random_prefix"` - } `json:"messaging"` - - Sharding struct { - AutoSharded bool `json:"auto_sharded"` - ShardCount int `json:"shard_count"` - ShardIDs string `json:"shard_ids"` - } `json:"sharding"` -} - -// NewManager creates a new manager. -func (sg *Sandwich) NewManager(configuration *ManagerConfiguration) (mg *Manager, err error) { - logger := sg.Logger.With().Str("manager", configuration.Identifier).Logger() - logger.Info().Msg("Creating new manager") - - mg = &Manager{ - Sandwich: sg, - Logger: logger, - - configurationMu: sync.RWMutex{}, - Configuration: configuration, - - gatewayMu: sync.RWMutex{}, - Gateway: discord.GatewayBot{}, - - shardGroupsMu: sync.RWMutex{}, - ShardGroups: make(map[int32]*ShardGroup), - - Client: NewClient(configuration.Token), - - shardGroupCounter: atomic.NewInt32(0), - - eventBlacklistMu: sync.RWMutex{}, - eventBlacklist: configuration.Events.EventBlacklist, - - produceBlacklistMu: sync.RWMutex{}, - produceBlacklist: configuration.Events.ProduceBlacklist, - } - - return mg, nil -} - -// Initialize handles the start up process including connecting the message queue client. -func (mg *Manager) Initialize() (err error) { - mg.Gateway, err = mg.GetGateway() - if err != nil { - return err - } - - mg.ProducerClient, err = NewMQClient(mg.Sandwich.Configuration.Producer.Type) - if err != nil { - return err - } - - clientName := mg.Configuration.Messaging.ClientName - if mg.Configuration.Messaging.UseRandomSuffix { - clientName = clientName + "-" + strconv.Itoa(rand.Intn(MessagingMaxClientNameNumber)) - } - - err = mg.ProducerClient.Connect( - mg.ctx, - clientName, - mg.Sandwich.Configuration.Producer.Configuration, - ) - if err != nil { - return nil - } - - return nil -} - -// Open handles retrieving shard counts and scaling. -func (mg *Manager) Open() (err error) { - shardIDs, shardCount := mg.getInitialShardCount() - - sg := mg.Scale(shardIDs, shardCount) - - ready, err := sg.Open() - if err != nil { - go mg.Sandwich.PublishSimpleWebhook("Failed to scale manager", "`"+err.Error()+"`", "Manager: "+mg.Configuration.Identifier, EmbedColourDanger) - - return err - } - - <-ready - - return nil -} - -// GetGateway returns the response from /gateway/bot. -func (mg *Manager) GetGateway() (resp discord.GatewayBot, err error) { - mg.Sandwich.gatewayLimiter.Lock() - _, err = mg.Client.FetchJSON(mg.ctx, "GET", "/gateway/bot", nil, nil, &resp) - - return -} - -// Scale handles the creation of new ShardGroups with a specified shard count and IDs. -func (mg *Manager) Scale(shardIDs []int, shardCount int) (sg *ShardGroup) { - shardGroupID := mg.shardGroupCounter.Add(1) - sg = mg.NewShardGroup(shardGroupID, shardIDs, shardCount) - - mg.shardGroupsMu.Lock() - mg.ShardGroups[shardGroupID] = sg - mg.shardGroupsMu.Unlock() - - return sg -} - -// PublishEvent sends an event to consumers. -func (mg *Manager) PublishEvent(eventType string, eventData interface{}) (err error) { - packet := mg.Sandwich.payloadPool.Get().(*structs.SandwichPayload) - defer mg.Sandwich.payloadPool.Put(packet) - - mg.configurationMu.RLock() - identifier := mg.Configuration.ProducerIdentifier - channel := mg.Configuration.Messaging.ChannelName - mg.configurationMu.RUnlock() - - packet.Type = eventType - packet.Op = discord.GatewayOpDispatch - packet.Data = eventData - - packet.Metadata = structs.SandwichMetadata{ - Version: VERSION, - Identifier: identifier, - } - - // Clear currently unused values - packet.Sequence = 0 - packet.Extra = nil - packet.Trace = nil - - data, err := msgpack.Marshal(packet) - if err != nil { - return err - } - - if mg.ProducerClient == nil { - return - } - - err = mg.ProducerClient.Publish( - mg.ctx, - channel, - data, - ) - - return -} - -// WaitForIdentify blocks until a shard can identify. -func (mg *Manager) WaitForIdentify(shardID int, shardCount int) (err error) { - mg.Sandwich.configurationMu.RLock() - identifyURL := mg.Sandwich.Configuration.Identify.URL - identifyHeaders := mg.Sandwich.Configuration.Identify.Headers - token := mg.Configuration.Token - mg.Sandwich.configurationMu.RUnlock() - - mg.gatewayMu.RLock() - maxConcurrency := mg.Gateway.SessionStartLimit.MaxConcurrency - mg.gatewayMu.RUnlock() - - hash, err := quickHash(sha256.New(), token) - if err != nil { - return err - } - - if identifyURL == "" { - identifyBucketName := fmt.Sprintf( - "identify:%s:%d", - hash, - shardID%mg.Gateway.SessionStartLimit.MaxConcurrency, - ) - - mg.Sandwich.IdentifyBuckets.CreateBucket( - identifyBucketName, 1, IdentifyRateLimit, - ) - - mg.Sandwich.IdentifyBuckets.WaitForBucket(identifyBucketName) - } else { - // Pass arguments to URL - sendURL := strings.Replace(identifyURL, "{shard_id}", strconv.Itoa(shardID), 0) - sendURL = strings.Replace(sendURL, "{shard_count}", strconv.Itoa(shardCount), 0) - sendURL = strings.Replace(sendURL, "{token}", token, 0) - sendURL = strings.Replace(sendURL, "{token_hash}", hash, 0) - sendURL = strings.Replace(sendURL, "{max_concurrency}", strconv.Itoa(maxConcurrency), 0) - - _, sendURLErr := url.Parse(sendURL) - if sendURLErr != nil { - return nil - } - - var body bytes.Buffer - - var identifyResponse structs.IdentifyResponse - - identifyPayload := structs.IdentifyPayload{ - ShardID: shardID, - ShardCount: shardCount, - Token: token, - MaxConcurrency: maxConcurrency, - } - - err = json.NewEncoder(&body).Encode(identifyPayload) - if err != nil { - return err - } - - client := http.DefaultClient - - for { - req, err := http.NewRequestWithContext(mg.ctx, "POST", sendURL, &body) - if err != nil { - return err - } - - for k, v := range identifyHeaders { - req.Header.Set(k, v) - } - - res, err := client.Do(req) - if err != nil { - mg.Logger.Warn().Err(err).Msg("Encountered error whilst identifying") - time.Sleep(IdentifyRetry) - - continue - } - - err = json.NewDecoder(res.Body).Decode(&identifyResponse) - if err != nil { - mg.Logger.Warn().Err(err).Msg("Failed to decode identify response") - time.Sleep(IdentifyRetry) - - continue - } - - res.Body.Close() - - if identifyResponse.Success { - break - } - - time.Sleep(time.Millisecond * time.Duration(identifyResponse.Wait)) - } - } - - return nil -} - -func (mg *Manager) Close() { - mg.Logger.Info().Msg("Closing manager shardgroups") - - mg.shardGroupsMu.RLock() - for _, sg := range mg.ShardGroups { - sg.Close() - } - mg.shardGroupsMu.RUnlock() - - if mg.cancel != nil { - mg.cancel() - } -} - -// getInitialShardCount returns the initial shard count and ids to use. -func (mg *Manager) getInitialShardCount() (shardIDs []int, shardCount int) { - mg.configurationMu.RLock() - defer mg.configurationMu.RUnlock() - - if mg.Configuration.Sharding.AutoSharded { - shardCount = mg.Gateway.Shards - - for i := 0; i < shardCount; i++ { - shardIDs = append(shardIDs, i) - } - } else { - shardCount = mg.Configuration.Sharding.ShardCount - shardIDs = returnRange(mg.Configuration.Sharding.ShardIDs, shardCount) - } - - return -} +package internal + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "math/rand" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" + "github.com/rs/zerolog" + "github.com/vmihailenco/msgpack" + "go.uber.org/atomic" +) + +const ( + ShardMaxRetries = 5 + ShardCompression = true + ShardLargeThreshold = 100 + ShardMaxHeartbeatFailures = 5 + MessagingMaxClientNameNumber = 9999 + + StandardIdentifyLimit = 5 + IdentifyRetry = (StandardIdentifyLimit * time.Second) + IdentifyRateLimit = (StandardIdentifyLimit * time.Second) + (500 * time.Millisecond) +) + +// Manager represents a single application. +type Manager struct { + ctx context.Context + cancel func() + + Error *atomic.String `json:"error"` + + Sandwich *Sandwich `json:"-"` + Logger zerolog.Logger `json:"-"` + + configurationMu sync.RWMutex + Configuration *ManagerConfiguration `json:"configuration"` + + gatewayMu sync.RWMutex + Gateway discord.GatewayBot `json:"gateway"` + + shardGroupsMu sync.RWMutex + ShardGroups map[int32]*ShardGroup `json:"shard_groups"` + + ProducerClient MQClient `json:"-"` + + Client *Client `json:"-"` + + shardGroupCounter *atomic.Int32 + + eventBlacklistMu sync.RWMutex + eventBlacklist []string + + produceBlacklistMu sync.RWMutex + produceBlacklist []string +} + +// ManagerConfiguration represents the configuration for the manager. +type ManagerConfiguration struct { + // Unique name that will be referenced internally + Identifier string `json:"identifier"` + // Non-unique name that is sent to consumers. + ProducerIdentifier string `json:"producer_identifier"` + + FriendlyName string `json:"friendly_name"` + + Token string `json:"token"` + AutoStart bool `json:"auto_start"` + + // Bot specific configuration + Bot struct { + DefaultPresence *discord.UpdateStatus `json:"default_presence"` + Intents int64 `json:"intents"` + ChunkGuildsOnStartup bool `json":chunk_guilds_on_startup"` + } `json:"bot"` + + Caching struct { + CacheUsers bool `json:"cache_users"` + CacheMembers bool `json:"cache_members"` + StoreMutuals bool `json:"store_mutuals"` + } `json:"caching"` + + Events struct { + EventBlacklist []string `json:"event_blacklist"` + ProduceBlacklist []string `json:"produce_blacklist"` + } `json:"events"` + + Messaging struct { + ClientName string `json:"client_name"` + ChannelName string `json:"channel_name"` + UseRandomSuffix bool `json:"use_random_prefix"` + } `json:"messaging"` + + Sharding struct { + AutoSharded bool `json:"auto_sharded"` + ShardCount int `json:"shard_count"` + ShardIDs string `json:"shard_ids"` + } `json:"sharding"` +} + +// NewManager creates a new manager. +func (sg *Sandwich) NewManager(configuration *ManagerConfiguration) (mg *Manager, err error) { + logger := sg.Logger.With().Str("manager", configuration.Identifier).Logger() + logger.Info().Msg("Creating new manager") + + mg = &Manager{ + Sandwich: sg, + Logger: logger, + + configurationMu: sync.RWMutex{}, + Configuration: configuration, + + gatewayMu: sync.RWMutex{}, + Gateway: discord.GatewayBot{}, + + shardGroupsMu: sync.RWMutex{}, + ShardGroups: make(map[int32]*ShardGroup), + + Client: NewClient(configuration.Token), + + shardGroupCounter: atomic.NewInt32(0), + + eventBlacklistMu: sync.RWMutex{}, + eventBlacklist: configuration.Events.EventBlacklist, + + produceBlacklistMu: sync.RWMutex{}, + produceBlacklist: configuration.Events.ProduceBlacklist, + } + + return mg, nil +} + +// Initialize handles the start up process including connecting the message queue client. +func (mg *Manager) Initialize() (err error) { + mg.Gateway, err = mg.GetGateway() + if err != nil { + return err + } + + mg.ProducerClient, err = NewMQClient(mg.Sandwich.Configuration.Producer.Type) + if err != nil { + return err + } + + clientName := mg.Configuration.Messaging.ClientName + if mg.Configuration.Messaging.UseRandomSuffix { + clientName = clientName + "-" + strconv.Itoa(rand.Intn(MessagingMaxClientNameNumber)) + } + + err = mg.ProducerClient.Connect( + mg.ctx, + clientName, + mg.Sandwich.Configuration.Producer.Configuration, + ) + if err != nil { + return nil + } + + return nil +} + +// Open handles retrieving shard counts and scaling. +func (mg *Manager) Open() (err error) { + shardIDs, shardCount := mg.getInitialShardCount() + + sg := mg.Scale(shardIDs, shardCount) + + ready, err := sg.Open() + if err != nil { + go mg.Sandwich.PublishSimpleWebhook( + "Failed to scale manager", + "`"+err.Error()+"`", + "Manager: "+mg.Configuration.Identifier, + EmbedColourDanger, + ) + + return err + } + + <-ready + + return nil +} + +// GetGateway returns the response from /gateway/bot. +func (mg *Manager) GetGateway() (resp discord.GatewayBot, err error) { + mg.Sandwich.gatewayLimiter.Lock() + _, err = mg.Client.FetchJSON(mg.ctx, "GET", "/gateway/bot", nil, nil, &resp) + + return +} + +// Scale handles the creation of new ShardGroups with a specified shard count and IDs. +func (mg *Manager) Scale(shardIDs []int, shardCount int) (sg *ShardGroup) { + shardGroupID := mg.shardGroupCounter.Add(1) + sg = mg.NewShardGroup(shardGroupID, shardIDs, shardCount) + + mg.shardGroupsMu.Lock() + mg.ShardGroups[shardGroupID] = sg + mg.shardGroupsMu.Unlock() + + return sg +} + +// PublishEvent sends an event to consumers. +func (mg *Manager) PublishEvent(eventType string, eventData interface{}) (err error) { + packet := mg.Sandwich.payloadPool.Get().(*structs.SandwichPayload) + defer mg.Sandwich.payloadPool.Put(packet) + + mg.configurationMu.RLock() + identifier := mg.Configuration.ProducerIdentifier + channel := mg.Configuration.Messaging.ChannelName + mg.configurationMu.RUnlock() + + packet.Type = eventType + packet.Op = discord.GatewayOpDispatch + packet.Data = eventData + + packet.Metadata = structs.SandwichMetadata{ + Version: VERSION, + Identifier: identifier, + } + + // Clear currently unused values + packet.Sequence = 0 + packet.Extra = nil + packet.Trace = nil + + data, err := msgpack.Marshal(packet) + if err != nil { + return err + } + + if mg.ProducerClient == nil { + return + } + + err = mg.ProducerClient.Publish( + mg.ctx, + channel, + data, + ) + + return err +} + +// WaitForIdentify blocks until a shard can identify. +func (mg *Manager) WaitForIdentify(shardID int, shardCount int) (err error) { + mg.Sandwich.configurationMu.RLock() + identifyURL := mg.Sandwich.Configuration.Identify.URL + identifyHeaders := mg.Sandwich.Configuration.Identify.Headers + token := mg.Configuration.Token + mg.Sandwich.configurationMu.RUnlock() + + mg.gatewayMu.RLock() + maxConcurrency := mg.Gateway.SessionStartLimit.MaxConcurrency + mg.gatewayMu.RUnlock() + + hash, err := quickHash(sha256.New(), token) + if err != nil { + return err + } + + if identifyURL == "" { + identifyBucketName := fmt.Sprintf( + "identify:%s:%d", + hash, + shardID%mg.Gateway.SessionStartLimit.MaxConcurrency, + ) + + mg.Sandwich.IdentifyBuckets.CreateBucket( + identifyBucketName, 1, IdentifyRateLimit, + ) + + mg.Sandwich.IdentifyBuckets.WaitForBucket(identifyBucketName) + } else { + // Pass arguments to URL + sendURL := strings.Replace(identifyURL, "{shard_id}", strconv.Itoa(shardID), 0) + sendURL = strings.Replace(sendURL, "{shard_count}", strconv.Itoa(shardCount), 0) + sendURL = strings.Replace(sendURL, "{token}", token, 0) + sendURL = strings.Replace(sendURL, "{token_hash}", hash, 0) + sendURL = strings.Replace(sendURL, "{max_concurrency}", strconv.Itoa(maxConcurrency), 0) + + _, sendURLErr := url.Parse(sendURL) + if sendURLErr != nil { + return nil + } + + var body bytes.Buffer + + var identifyResponse structs.IdentifyResponse + + identifyPayload := structs.IdentifyPayload{ + ShardID: shardID, + ShardCount: shardCount, + Token: token, + MaxConcurrency: maxConcurrency, + } + + err = json.NewEncoder(&body).Encode(identifyPayload) + if err != nil { + return err + } + + client := http.DefaultClient + + for { + req, err := http.NewRequestWithContext(mg.ctx, "POST", sendURL, &body) + if err != nil { + return err + } + + for k, v := range identifyHeaders { + req.Header.Set(k, v) + } + + res, err := client.Do(req) + if err != nil { + mg.Logger.Warn().Err(err).Msg("Encountered error whilst identifying") + time.Sleep(IdentifyRetry) + + continue + } + + err = json.NewDecoder(res.Body).Decode(&identifyResponse) + if err != nil { + mg.Logger.Warn().Err(err).Msg("Failed to decode identify response") + time.Sleep(IdentifyRetry) + + continue + } + + res.Body.Close() + + if identifyResponse.Success { + break + } + + time.Sleep(time.Millisecond * time.Duration(identifyResponse.Wait)) + } + } + + return nil +} + +func (mg *Manager) Close() { + mg.Logger.Info().Msg("Closing manager shardgroups") + + mg.shardGroupsMu.RLock() + for _, sg := range mg.ShardGroups { + sg.Close() + } + mg.shardGroupsMu.RUnlock() + + if mg.cancel != nil { + mg.cancel() + } +} + +// getInitialShardCount returns the initial shard count and ids to use. +func (mg *Manager) getInitialShardCount() (shardIDs []int, shardCount int) { + mg.configurationMu.RLock() + defer mg.configurationMu.RUnlock() + + if mg.Configuration.Sharding.AutoSharded { + shardCount = mg.Gateway.Shards + + for i := 0; i < shardCount; i++ { + shardIDs = append(shardIDs, i) + } + } else { + shardCount = mg.Configuration.Sharding.ShardCount + shardIDs = returnRange(mg.Configuration.Sharding.ShardIDs, shardCount) + } + + return +} diff --git a/internal/messaging.go b/internal/messaging.go index dc4f444..d66421c 100644 --- a/internal/messaging.go +++ b/internal/messaging.go @@ -1,151 +1,153 @@ -package internal - -import ( - "bytes" - "context" - - messaging "github.com/WelcomerTeam/Sandwich-Daemon/next/messaging" - "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" - "github.com/andybalholm/brotli" - "github.com/savsgio/gotils" - "github.com/vmihailenco/msgpack" - "golang.org/x/xerrors" -) - -const ( - minPayloadCompressionSize = 1000000 // Apply higher level compression to payloads >1 Mb -) - -type MQClient interface { - String() string - Channel() string - Cluster() string - - Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) - Publish(ctx context.Context, channel string, data []byte) (err error) - // Function to receive a channel with messages - // Function to close -} - -func NewMQClient(mqType string) (MQClient, error) { - switch mqType { - case "stan": - return &messaging.StanMQClient{}, nil - case "kafka": - return &messaging.KafkaMQClient{}, nil - case "redis": - return &messaging.RedisMQClient{}, nil - default: - return nil, xerrors.New("No MQ client named " + mqType) - } -} - -// PublishEvent publishes a SandwichPayload. -func (sh *Shard) PublishEvent(packet *structs.SandwichPayload) (err error) { - sh.Manager.ConfigurationMu.RLock() - defer sh.Manager.ConfigurationMu.RUnlock() - - packet.Metadata = structs.SandwichMetadata{ - Version: VERSION, - Identifier: sh.Manager.Configuration.Identifier, - Shard: [3]int{ - int(sh.ShardGroup.ID), - sh.ShardID, - sh.ShardGroup.ShardCount, - }, - } - - payload, err := msgpack.Marshal(packet) - if err != nil { - return xerrors.Errorf("failed to marshal payload: %w", err) - } - - sh.Logger.Trace().Str("event", gotils.B2S(payload)).Msgf("Processed %s event", packet.Type) - - // Compression testing of large payloads. In the future this *may* be - // added however in its current state it is uncertain. With using a 1mb - // msgpack payload, compression can be brought down to 48kb using brotli - // level 11 however will take around 1.5 seconds. However, it is likely - // level 0 or 6 will be used which produce 95kb in 3ms and 54kb in 20ms - // respectively. It is likely the actual data portion of the payload will - // be compressed so the metadata and the rest of the data can be preserved - // then pass in the metadata it is compressed instead of using magic bytes - // or guessing by consumers. - - // Whilst compression can prove a benefit, having it enabled for all events - // do not provide any benefit and only affect larger payloads which is - // not common apart from GUILD_CREATE events. - - // Sample testing of a GUILD_CREATE event: - - // METHOD | Level | Ms | Resulting Payload Size - // -------|--------------|------|----------------------- - // NONE | | | 1011967 - // BROTLI | 0 (speed) | 3 | 95908 ( 9.5%) - // BROTLI | 6 (default) | 20 | 54545 ( 5.4%) - // BROTLI | 11 (best) | 1245 | 47044 ( 4.6%) - // GZIP | 1 (speed) | 3 | 115799 (11.5%) - // GZIP | -1 (default) | 8 | 82336 ( 8.1%) - // GZIP | 9 (best) | 19 | 78253 ( 7.7%) - - // Compression stats - - // RAW | 1.12Mbit - // BROTLI 0 | 64.33Kbit | 152ms - // BROTLI 6 | 31.04Kbit | 841ms - // BROTLI 9 | 30.71KBit | 695ms - // GZIP 1 | 92.40KBit | 92ms - // GZIP -1 | 64.52KBit | 290ms - // GZIP 9 | 61.01KBit | 720ms - - // This may not be the most efficient way but it was useful for testing many - // payloads. More cohesive benchmarking will take place if this is ever properly - // implemented and may be a 1.0 feature however it is unlikely to be necessary.. - - // Payloads larger than 1MB will default to using Level 6 brotli compression. - // For consistency sake, we also compress smaller payloads on the lowest level - // which should not affect performance too much as they are still fairly fast. - - // a := time.Now() - - compressedPayload := sh.cp.Get().(*bytes.Buffer) - - if len(payload) > minPayloadCompressionSize { - dc := sh.DefaultCompressor.Get().(*brotli.Writer) - dc.Reset(compressedPayload) - - _, err = dc.Write(payload) - if err != nil { - sh.Logger.Warn().Err(err).Msg("Failed to write payload to brotli compressor") - } - - dc.Flush() - sh.DefaultCompressor.Put(dc) - } else { - fc := sh.FastCompressor.Get().(*brotli.Writer) - fc.Reset(compressedPayload) - - _, err = fc.Write(payload) - if err != nil { - sh.Logger.Warn().Err(err).Msg("Failed to write payload to brotli compressor") - } - - fc.Flush() - sh.FastCompressor.Put(fc) - } - - err = sh.Manager.ProducerClient.Publish( - sh.ctx, - sh.Manager.Configuration.Messaging.ChannelName, - compressedPayload.Bytes(), - ) - - compressedPayload.Reset() - sh.cp.Put(compressedPayload) - - if err != nil { - return xerrors.Errorf("publishEvent publish: %w", err) - } - - return nil -} +package internal + +import ( + "bytes" + "context" + + messaging "github.com/WelcomerTeam/Sandwich-Daemon/next/messaging" + "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" + "github.com/andybalholm/brotli" + "github.com/savsgio/gotils" + "github.com/vmihailenco/msgpack" + "golang.org/x/xerrors" +) + +const ( + minPayloadCompressionSize = 1000000 // Apply higher level compression to payloads >1 Mb +) + +type MQClient interface { + String() string + Channel() string + Cluster() string + + Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) + Publish(ctx context.Context, channel string, data []byte) (err error) + // Function to receive a channel with messages + // Function to close +} + +func NewMQClient(mqType string) (MQClient, error) { + switch mqType { + case "stan": + return &messaging.StanMQClient{}, nil + case "kafka": + return &messaging.KafkaMQClient{}, nil + case "redis": + return &messaging.RedisMQClient{}, nil + default: + return nil, xerrors.New("No MQ client named " + mqType) + } +} + +// PublishEvent publishes a SandwichPayload. +func (sh *Shard) PublishEvent(packet *structs.SandwichPayload) (err error) { + sh.Manager.configurationMu.RLock() + identifier := sh.Manager.Configuration.ProducerIdentifier + channelName := sh.Manager.Configuration.Messaging.ChannelName + sh.Manager.configurationMu.RUnlock() + + packet.Metadata = structs.SandwichMetadata{ + Version: VERSION, + Identifier: identifier, + Shard: [3]int{ + int(sh.ShardGroup.ID), + sh.ShardID, + sh.ShardGroup.ShardCount, + }, + } + + payload, err := msgpack.Marshal(packet) + if err != nil { + return xerrors.Errorf("failed to marshal payload: %w", err) + } + + sh.Logger.Trace().Str("event", gotils.B2S(payload)).Msgf("Processed %s event", packet.Type) + + // Compression testing of large payloads. In the future this *may* be + // added however in its current state it is uncertain. With using a 1mb + // msgpack payload, compression can be brought down to 48kb using brotli + // level 11 however will take around 1.5 seconds. However, it is likely + // level 0 or 6 will be used which produce 95kb in 3ms and 54kb in 20ms + // respectively. It is likely the actual data portion of the payload will + // be compressed so the metadata and the rest of the data can be preserved + // then pass in the metadata it is compressed instead of using magic bytes + // or guessing by consumers. + + // Whilst compression can prove a benefit, having it enabled for all events + // do not provide any benefit and only affect larger payloads which is + // not common apart from GUILD_CREATE events. + + // Sample testing of a GUILD_CREATE event: + + // METHOD | Level | Ms | Resulting Payload Size + // -------|--------------|------|----------------------- + // NONE | | | 1011967 + // BROTLI | 0 (speed) | 3 | 95908 ( 9.5%) + // BROTLI | 6 (default) | 20 | 54545 ( 5.4%) + // BROTLI | 11 (best) | 1245 | 47044 ( 4.6%) + // GZIP | 1 (speed) | 3 | 115799 (11.5%) + // GZIP | -1 (default) | 8 | 82336 ( 8.1%) + // GZIP | 9 (best) | 19 | 78253 ( 7.7%) + + // Compression stats + + // RAW | 1.12Mbit + // BROTLI 0 | 64.33Kbit | 152ms + // BROTLI 6 | 31.04Kbit | 841ms + // BROTLI 9 | 30.71KBit | 695ms + // GZIP 1 | 92.40KBit | 92ms + // GZIP -1 | 64.52KBit | 290ms + // GZIP 9 | 61.01KBit | 720ms + + // This may not be the most efficient way but it was useful for testing many + // payloads. More cohesive benchmarking will take place if this is ever properly + // implemented and may be a 1.0 feature however it is unlikely to be necessary.. + + // Payloads larger than 1MB will default to using Level 6 brotli compression. + // For consistency sake, we also compress smaller payloads on the lowest level + // which should not affect performance too much as they are still fairly fast. + + // a := time.Now() + + compressedPayload := sh.Sandwich.bufferPool.Get().(*bytes.Buffer) + + if len(payload) > minPayloadCompressionSize { + dc := sh.Sandwich.defaultCompressorPool.Get().(*brotli.Writer) + dc.Reset(compressedPayload) + + _, err = dc.Write(payload) + if err != nil { + sh.Logger.Warn().Err(err).Msg("Failed to write payload to brotli compressor") + } + + dc.Flush() + sh.Sandwich.defaultCompressorPool.Put(dc) + } else { + fc := sh.Sandwich.fastCompressorPool.Get().(*brotli.Writer) + fc.Reset(compressedPayload) + + _, err = fc.Write(payload) + if err != nil { + sh.Logger.Warn().Err(err).Msg("Failed to write payload to brotli compressor") + } + + fc.Flush() + sh.Sandwich.fastCompressorPool.Put(fc) + } + + err = sh.Manager.ProducerClient.Publish( + sh.ctx, + channelName, + compressedPayload.Bytes(), + ) + + compressedPayload.Reset() + sh.Sandwich.bufferPool.Put(compressedPayload) + + if err != nil { + return xerrors.Errorf("publishEvent publish: %w", err) + } + + return nil +} diff --git a/internal/sandwich.go b/internal/sandwich.go index 98fa692..aa76144 100644 --- a/internal/sandwich.go +++ b/internal/sandwich.go @@ -1,432 +1,440 @@ -package internal - -import ( - "bytes" - "context" - "io" - "io/ioutil" - "net/http" - "os" - "path" - "sync" - "time" - - "github.com/WelcomerTeam/RealRock/bucketstore" - limiter "github.com/WelcomerTeam/RealRock/limiter" - discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" - "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" - "github.com/andybalholm/brotli" - jsoniter "github.com/json-iterator/go" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "go.uber.org/atomic" - "gopkg.in/natefinch/lumberjack.v2" - "gopkg.in/yaml.v3" -) - -const VERSION = "0.0.1" - -var json = jsoniter.ConfigCompatibleWithStandardLibrary - -type Sandwich struct { - sync.Mutex - - ctx context.Context - cancel func() - - Logger zerolog.Logger - StartTime time.Time `json:"start_time"` - - configurationMu sync.RWMutex - Configuration *SandwichConfiguration `json:"configuration"` - - gatewayLimiter limiter.DurationLimiter `json:"-"` - - ProducerClient *MQClient `json:"-"` - - IdentifyBuckets *bucketstore.BucketStore `json:"-"` - - // EventPool contains the global event pool limiter defined on startup flags. - // EventPoolWaiting stores any events that are waiting for a spot. - EventPool *limiter.ConcurrencyLimiter `json:"-"` - EventPoolWaiting atomic.Int64 `json:"-"` - EventPoolLimit int `json:"-"` - - managersMu sync.RWMutex - Managers map[string]*Manager `json:"managers"` - - State *SandwichState `json:"-"` - - Client *Client `json:"-"` - - // SandwichPayload pool - payloadPool sync.Pool - // ReceivedPayload pool - receivedPool sync.Pool - // SentPayload pool - sentPool sync.Pool - // Buffer pool - bufferPool sync.Pool - - // Brotli writers pool - defaultCompressorPool sync.Pool - fastCompressorPool sync.Pool -} - -// SandwichConfiguration represents the configuration file -type SandwichConfiguration struct { - Logging struct { - Level string - FileLoggingEnabled bool - - EncodeAsJSON bool - - Directory string - Filename string - MaxSize int - MaxBackups int - MaxAge int - Compress bool - - MinimalWebhooks bool - } - - State struct { - StoreGuildMembers bool - StoreEmojis bool - - EnableSmaz bool - } - - Identify struct { - // URL allows for variables: - // {shard_id}, {shard_count}, {token} {token_hash}, {max_concurrency} - URL string - - Headers map[string]string - } - - Producer struct { - Type string - Configuration map[string]interface{} - } - - Prometheus struct { - Host string - } - - GRPC struct { - Network string - Host string - } - - Webhooks []string - - Managers []*ManagerConfiguration -} - -// NewSandwich creates the application state and initializes it -func NewSandwich(logger io.Writer, configurationLocation string, eventPoolLimit int) (sg *Sandwich, err error) { - sg = &Sandwich{ - Logger: zerolog.New(logger).With().Timestamp().Logger(), - - configurationMu: sync.RWMutex{}, - Configuration: &SandwichConfiguration{}, - - gatewayLimiter: *limiter.NewDurationLimiter(1, time.Second), - - managersMu: sync.RWMutex{}, - Managers: make(map[string]*Manager), - - EventPool: limiter.NewConcurrencyLimiter(eventPoolLimit), - EventPoolWaiting: *atomic.NewInt64(0), - EventPoolLimit: eventPoolLimit, - - State: NewSandwichState(), - - payloadPool: sync.Pool{ - New: func() interface{} { return new(structs.SandwichPayload) }, - }, - - receivedPool: sync.Pool{ - New: func() interface{} { return new(discord.GatewayPayload) }, - }, - - sentPool: sync.Pool{ - New: func() interface{} { return new(discord.SentPayload) }, - }, - - bufferPool: sync.Pool{ - New: func() interface{} { return new(bytes.Buffer) }, - }, - - defaultCompressorPool: sync.Pool{ - New: func() interface{} { return brotli.NewWriterLevel(nil, brotli.DefaultCompression) }, - }, - - fastCompressorPool: sync.Pool{ - New: func() interface{} { return brotli.NewWriterLevel(nil, brotli.BestSpeed) }, - }, - } - - sg.Lock() - defer sg.Unlock() - - configuration, err := sg.LoadConfiguration(configurationLocation) - if err != nil { - return nil, err - } - - sg.configurationMu.Lock() - defer sg.configurationMu.Unlock() - - sg.Configuration = configuration - - zlLevel, err := zerolog.ParseLevel(sg.Configuration.Logging.Level) - if err != nil { - sg.Logger.Warn().Str("level", sg.Configuration.Logging.Level).Msg("Logging level providied is not valid") - } else { - sg.Logger.Info().Str("level", sg.Configuration.Logging.Level).Msg("Changed logging level") - zerolog.SetGlobalLevel(zlLevel) - } - - // Create file and console logging - - var writers []io.Writer - - writers = append(writers, logger) - - if sg.Configuration.Logging.FileLoggingEnabled { - if err := os.MkdirAll(sg.Configuration.Logging.Directory, 0o744); err != nil { - log.Error().Err(err).Str("path", sg.Configuration.Logging.Directory).Msg("Unable to create log directory") - } else { - lumber := &lumberjack.Logger{ - Filename: path.Join(sg.Configuration.Logging.Directory, sg.Configuration.Logging.Filename), - MaxBackups: sg.Configuration.Logging.MaxBackups, - MaxSize: sg.Configuration.Logging.MaxSize, - MaxAge: sg.Configuration.Logging.MaxAge, - Compress: sg.Configuration.Logging.Compress, - } - - if sg.Configuration.Logging.EncodeAsJSON { - writers = append(writers, lumber) - } else { - writers = append(writers, zerolog.ConsoleWriter{ - Out: lumber, - TimeFormat: time.Stamp, - NoColor: true, - }) - } - } - } - - mw := io.MultiWriter(writers...) - sg.Logger = zerolog.New(mw).With().Timestamp().Logger() - sg.Logger.Info().Msg("Logging configured") - - return sg, nil -} - -// LoadConfiguration handles loading the configuration file -func (sg *Sandwich) LoadConfiguration(path string) (configuration *SandwichConfiguration, err error) { - sg.Logger.Debug().Msg("Loading configuration") - - defer func() { - if err == nil { - sg.Logger.Info().Msg("Configuration loaded") - } - }() - - file, err := ioutil.ReadFile(path) - if err != nil { - return configuration, ErrReadConfigurationFailure - } - - configuration = &SandwichConfiguration{} - - err = yaml.Unmarshal(file, &configuration) - if err != nil { - return configuration, ErrLoadConfigurationFailure - } - - err = sg.ValidateConfiguration(configuration) - if err != nil { - return configuration, err - } - - return configuration, nil -} - -// SaveConfiguration handles saving the configuration file -func (sg *Sandwich) SaveConfiguration(configuration *SandwichConfiguration, path string) (err error) { - sg.Logger.Debug().Msg("Saving configuration") - - defer func() { - if err == nil { - sg.Logger.Info().Msg("Flushed configuration to disk") - } - }() - - data, err := yaml.Marshal(configuration) - if err != nil { - return err - } - - err = ioutil.WriteFile(path, data, 0o600) - if err != nil { - return err - } - - return nil -} - -// ValidateConfiguration ensures certain values in the configuration are passed -func (sg *Sandwich) ValidateConfiguration(configuration *SandwichConfiguration) (err error) { - if configuration.Identify.URL == "" { - return ErrConfigurationValidateIdentify - } - - if configuration.Prometheus.Host == "" { - return ErrConfigurationValidatePrometheus - } - - if configuration.GRPC.Host == "" { - return ErrConfigurationValidateGRPC - } - - return nil -} - -// Open starts up any listeners, configures services and starts up managers. -func (sg *Sandwich) Open() (err error) { - sg.configurationMu.RLock() - defer sg.configurationMu.RUnlock() - - sg.StartTime = time.Now().UTC() - sg.Logger.Info().Msgf("Starting sandwich. Version %s", VERSION) - - go sg.PublishSimpleWebhook("Starting sandwich", "", "Version "+VERSION, EmbedColourSandwich) - - // Setup GRPC - go sg.setupGRPC() - - // Setup Prometheus - go sg.setupPrometheus() - - sg.Logger.Info().Msg("Creating managers") - sg.startManagers() - - return -} - -// Close closes all managers gracefully. -func (sg *Sandwich) Close() (err error) { - sg.Logger.Info().Msg("Closing sandwich") - go sg.PublishSimpleWebhook("Sandwich closing", "", "", EmbedColourSandwich) - - sg.managersMu.RLock() - for _, manager := range sg.Managers { - manager.Close() - } - sg.managersMu.RUnlock() - - if sg.cancel != nil { - sg.cancel() - } - - return nil -} - -func (sg *Sandwich) startManagers() (err error) { - sg.managersMu.Lock() - - for _, managerConfiguration := range sg.Configuration.Managers { - if _, duplicate := sg.Managers[managerConfiguration.Identifier]; duplicate { - sg.Logger.Warn(). - Str("identifier", managerConfiguration.Identifier). - Msg("Manager contains duplicate identifier. Ignoring.") - - go sg.PublishSimpleWebhook("Manager contains duplicate identifier. Ignoring.", managerConfiguration.Identifier, "", EmbedColourWarning) - } - - manager, err := sg.NewManager(managerConfiguration) - if err != nil { - sg.Logger.Error().Err(err).Msg("Failed to create manager") - - continue - } - - sg.Managers[managerConfiguration.Identifier] = manager - - err = manager.Initialize() - if err != nil { - manager.Error.Store(err.Error()) - - manager.Logger.Error().Err(err).Msg("Failed to initialize manager") - go sg.PublishSimpleWebhook("Failed to open manager", "`"+err.Error()+"`", "", EmbedColourDanger) - } - - if managerConfiguration.AutoStart { - go manager.Open() - } - } - - sg.managersMu.Unlock() - - return nil -} - -func (sg *Sandwich) setupGRPC() (err error) { - // sg.configurationMu.RLock() - // network := sg.Configuration.GRPC.Network - // host := sg.Configuration.GRPC.Host - // sg.configurationMu.RUnlock() - - // listener, err := net.Listen(network, host) - // if err != nil { - // sg.Logger.Error().Str("host", host).Err(err).Msg("Failed to bind to host") - - // return - // } - - // var grpcOptions []grpc.ServerOptions - // grpcListener := grpc.NewServer(opts...) - // grpcServer.RegisterGatewayServer(grpcListener, sg.NewGatewayServer()) - - // err = grpcListener.Serve(listener) - // if err != nil { - // sg.Logger.Error().Str("host", host).Err(err).Msg("Failed to serve gRPC server") - - // return - // } - - // sg.Logger.Info().Msgf("Serving gRPC at %s", host) - - return nil -} - -func (sg *Sandwich) setupPrometheus() (err error) { - sg.configurationMu.RLock() - host := sg.Configuration.Prometheus.Host - sg.configurationMu.RUnlock() - - prometheus.MustRegister(prometheus.NewBuildInfoCollector()) - - http.Handle("/metrics", promhttp.HandlerFor( - prometheus.DefaultGatherer, - promhttp.HandlerOpts{ - EnableOpenMetrics: true, - }, - )) - - err = http.ListenAndServe(host, nil) - if err != nil { - sg.Logger.Error().Str("host", host).Err(err).Msg("Failed to serve prometheus server") - } - - sg.Logger.Info().Msgf("Serving prometheus at %s", host) - - return nil -} +package internal + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "sync" + "time" + + "github.com/WelcomerTeam/RealRock/bucketstore" + limiter "github.com/WelcomerTeam/RealRock/limiter" + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" + "github.com/andybalholm/brotli" + jsoniter "github.com/json-iterator/go" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "go.uber.org/atomic" + "gopkg.in/natefinch/lumberjack.v2" + "gopkg.in/yaml.v3" +) + +const VERSION = "0.0.1" + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +const ( + PermissionsDefault = 0o744 + PermissionWrite = 0o600 +) + +type Sandwich struct { + sync.Mutex + + ctx context.Context + cancel func() + + Logger zerolog.Logger + StartTime time.Time `json:"start_time"` + + configurationMu sync.RWMutex + Configuration *SandwichConfiguration `json:"configuration"` + + gatewayLimiter limiter.DurationLimiter `json:"-"` + + ProducerClient *MQClient `json:"-"` + + IdentifyBuckets *bucketstore.BucketStore `json:"-"` + + // EventPool contains the global event pool limiter defined on startup flags. + // EventPoolWaiting stores any events that are waiting for a spot. + EventPool *limiter.ConcurrencyLimiter `json:"-"` + EventPoolWaiting *atomic.Int64 `json:"-"` + EventPoolLimit int `json:"-"` + + managersMu sync.RWMutex + Managers map[string]*Manager `json:"managers"` + + State *SandwichState `json:"-"` + + Client *Client `json:"-"` + + // SandwichPayload pool + payloadPool sync.Pool + // ReceivedPayload pool + receivedPool sync.Pool + // SentPayload pool + sentPool sync.Pool + // Buffer pool + bufferPool sync.Pool + + // Brotli writers pool + defaultCompressorPool sync.Pool + fastCompressorPool sync.Pool +} + +// SandwichConfiguration represents the configuration file. +type SandwichConfiguration struct { + Logging struct { + Level string + FileLoggingEnabled bool + + EncodeAsJSON bool + + Directory string + Filename string + MaxSize int + MaxBackups int + MaxAge int + Compress bool + + MinimalWebhooks bool + } + + State struct { + StoreGuildMembers bool + StoreEmojis bool + + EnableSmaz bool + } + + Identify struct { + // URL allows for variables: + // {shard_id}, {shard_count}, {token} {token_hash}, {max_concurrency} + URL string + + Headers map[string]string + } + + Producer struct { + Type string + Configuration map[string]interface{} + } + + Prometheus struct { + Host string + } + + GRPC struct { + Network string + Host string + } + + Webhooks []string + + Managers []*ManagerConfiguration +} + +// NewSandwich creates the application state and initializes it. +func NewSandwich(logger io.Writer, configurationLocation string, eventPoolLimit int) (sg *Sandwich, err error) { + sg = &Sandwich{ + Logger: zerolog.New(logger).With().Timestamp().Logger(), + + configurationMu: sync.RWMutex{}, + Configuration: &SandwichConfiguration{}, + + gatewayLimiter: *limiter.NewDurationLimiter(1, time.Second), + + managersMu: sync.RWMutex{}, + Managers: make(map[string]*Manager), + + EventPool: limiter.NewConcurrencyLimiter(eventPoolLimit), + EventPoolWaiting: atomic.NewInt64(0), + EventPoolLimit: eventPoolLimit, + + State: NewSandwichState(), + + payloadPool: sync.Pool{ + New: func() interface{} { return new(structs.SandwichPayload) }, + }, + + receivedPool: sync.Pool{ + New: func() interface{} { return new(discord.GatewayPayload) }, + }, + + sentPool: sync.Pool{ + New: func() interface{} { return new(discord.SentPayload) }, + }, + + bufferPool: sync.Pool{ + New: func() interface{} { return new(bytes.Buffer) }, + }, + + defaultCompressorPool: sync.Pool{ + New: func() interface{} { return brotli.NewWriterLevel(nil, brotli.DefaultCompression) }, + }, + + fastCompressorPool: sync.Pool{ + New: func() interface{} { return brotli.NewWriterLevel(nil, brotli.BestSpeed) }, + }, + } + + sg.Lock() + defer sg.Unlock() + + configuration, err := sg.LoadConfiguration(configurationLocation) + if err != nil { + return nil, err + } + + sg.configurationMu.Lock() + defer sg.configurationMu.Unlock() + + sg.Configuration = configuration + + zlLevel, err := zerolog.ParseLevel(sg.Configuration.Logging.Level) + if err != nil { + sg.Logger.Warn().Str("level", sg.Configuration.Logging.Level).Msg("Logging level providied is not valid") + } else { + sg.Logger.Info().Str("level", sg.Configuration.Logging.Level).Msg("Changed logging level") + zerolog.SetGlobalLevel(zlLevel) + } + + // Create file and console logging + + var writers []io.Writer + + writers = append(writers, logger) + + if sg.Configuration.Logging.FileLoggingEnabled { + if err := os.MkdirAll(sg.Configuration.Logging.Directory, PermissionsDefault); err != nil { + log.Error().Err(err).Str("path", sg.Configuration.Logging.Directory).Msg("Unable to create log directory") + } else { + lumber := &lumberjack.Logger{ + Filename: path.Join(sg.Configuration.Logging.Directory, sg.Configuration.Logging.Filename), + MaxBackups: sg.Configuration.Logging.MaxBackups, + MaxSize: sg.Configuration.Logging.MaxSize, + MaxAge: sg.Configuration.Logging.MaxAge, + Compress: sg.Configuration.Logging.Compress, + } + + if sg.Configuration.Logging.EncodeAsJSON { + writers = append(writers, lumber) + } else { + writers = append(writers, zerolog.ConsoleWriter{ + Out: lumber, + TimeFormat: time.Stamp, + NoColor: true, + }) + } + } + } + + mw := io.MultiWriter(writers...) + sg.Logger = zerolog.New(mw).With().Timestamp().Logger() + sg.Logger.Info().Msg("Logging configured") + + return sg, nil +} + +// LoadConfiguration handles loading the configuration file. +func (sg *Sandwich) LoadConfiguration(path string) (configuration *SandwichConfiguration, err error) { + sg.Logger.Debug().Msg("Loading configuration") + + defer func() { + if err == nil { + sg.Logger.Info().Msg("Configuration loaded") + } + }() + + file, err := ioutil.ReadFile(path) + if err != nil { + return configuration, ErrReadConfigurationFailure + } + + configuration = &SandwichConfiguration{} + + err = yaml.Unmarshal(file, &configuration) + if err != nil { + return configuration, ErrLoadConfigurationFailure + } + + err = sg.ValidateConfiguration(configuration) + if err != nil { + return configuration, err + } + + return configuration, nil +} + +// SaveConfiguration handles saving the configuration file. +func (sg *Sandwich) SaveConfiguration(configuration *SandwichConfiguration, path string) (err error) { + sg.Logger.Debug().Msg("Saving configuration") + + defer func() { + if err == nil { + sg.Logger.Info().Msg("Flushed configuration to disk") + } + }() + + data, err := yaml.Marshal(configuration) + if err != nil { + return err + } + + err = ioutil.WriteFile(path, data, PermissionWrite) + if err != nil { + return err + } + + return nil +} + +// ValidateConfiguration ensures certain values in the configuration are passed. +func (sg *Sandwich) ValidateConfiguration(configuration *SandwichConfiguration) (err error) { + if configuration.Identify.URL == "" { + return ErrConfigurationValidateIdentify + } + + if configuration.Prometheus.Host == "" { + return ErrConfigurationValidatePrometheus + } + + if configuration.GRPC.Host == "" { + return ErrConfigurationValidateGRPC + } + + return nil +} + +// Open starts up any listeners, configures services and starts up managers. +func (sg *Sandwich) Open() (err error) { + sg.StartTime = time.Now().UTC() + sg.Logger.Info().Msgf("Starting sandwich. Version %s", VERSION) + + go sg.PublishSimpleWebhook("Starting sandwich", "", "Version "+VERSION, EmbedColourSandwich) + + // Setup GRPC + go sg.setupGRPC() + + // Setup Prometheus + go sg.setupPrometheus() + + sg.Logger.Info().Msg("Creating managers") + sg.startManagers() + + return +} + +// Close closes all managers gracefully. +func (sg *Sandwich) Close() (err error) { + sg.Logger.Info().Msg("Closing sandwich") + + go sg.PublishSimpleWebhook("Sandwich closing", "", "", EmbedColourSandwich) + + sg.managersMu.RLock() + for _, manager := range sg.Managers { + manager.Close() + } + sg.managersMu.RUnlock() + + if sg.cancel != nil { + sg.cancel() + } + + return nil +} + +func (sg *Sandwich) startManagers() (err error) { + sg.managersMu.Lock() + + for _, managerConfiguration := range sg.Configuration.Managers { + if _, duplicate := sg.Managers[managerConfiguration.Identifier]; duplicate { + sg.Logger.Warn(). + Str("identifier", managerConfiguration.Identifier). + Msg("Manager contains duplicate identifier. Ignoring.") + + go sg.PublishSimpleWebhook( + "Manager contains duplicate identifier. Ignoring.", + managerConfiguration.Identifier, + "", + EmbedColourWarning, + ) + } + + manager, err := sg.NewManager(managerConfiguration) + if err != nil { + sg.Logger.Error().Err(err).Msg("Failed to create manager") + + continue + } + + sg.Managers[managerConfiguration.Identifier] = manager + + err = manager.Initialize() + if err != nil { + manager.Error.Store(err.Error()) + + manager.Logger.Error().Err(err).Msg("Failed to initialize manager") + + go sg.PublishSimpleWebhook("Failed to open manager", "`"+err.Error()+"`", "", EmbedColourDanger) + } + + if managerConfiguration.AutoStart { + go manager.Open() + } + } + + sg.managersMu.Unlock() + + return nil +} + +func (sg *Sandwich) setupGRPC() (err error) { + // sg.configurationMu.RLock() + // network := sg.Configuration.GRPC.Network + // host := sg.Configuration.GRPC.Host + // sg.configurationMu.RUnlock() + + // listener, err := net.Listen(network, host) + // if err != nil { + // sg.Logger.Error().Str("host", host).Err(err).Msg("Failed to bind to host") + + // return + // } + + // var grpcOptions []grpc.ServerOptions + // grpcListener := grpc.NewServer(opts...) + // grpcServer.RegisterGatewayServer(grpcListener, sg.NewGatewayServer()) + + // err = grpcListener.Serve(listener) + // if err != nil { + // sg.Logger.Error().Str("host", host).Err(err).Msg("Failed to serve gRPC server") + + // return + // } + + // sg.Logger.Info().Msgf("Serving gRPC at %s", host) + + return nil +} + +func (sg *Sandwich) setupPrometheus() (err error) { + sg.configurationMu.RLock() + host := sg.Configuration.Prometheus.Host + sg.configurationMu.RUnlock() + + prometheus.MustRegister(prometheus.NewGoCollector()) + prometheus.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) + + http.Handle("/metrics", promhttp.HandlerFor( + prometheus.DefaultGatherer, + promhttp.HandlerOpts{}, + )) + + err = http.ListenAndServe(host, nil) + if err != nil { + sg.Logger.Error().Str("host", host).Err(err).Msg("Failed to serve prometheus server") + } + + sg.Logger.Info().Msgf("Serving prometheus at %s", host) + + return nil +} diff --git a/internal/shard.go b/internal/shard.go index 626fadf..e36f83e 100644 --- a/internal/shard.go +++ b/internal/shard.go @@ -1,32 +1,586 @@ -package internal - -// ShardGroup Connect() and opens (in goroutine) first shard -// continue with all others and wait for all are done - - -// NewShard -// Open starts listen to message and error channel and processes events. -// Connect to gateway and setup message channels -// Listen handles reading from websocket, errors and basic reconnection -// Feed reads from the gateway and decompresses messages and push to message channel -// OnEvent handles gateway ops and dispatch -// OnDispatch handles cheking blacklists, handling dispatch and publishing -// Heartbeat maintains Heartbeat -// Reconnect reconnects to gateway -// Close sends a close code - -// Resume -// Identify -// Reconnect - -// SendEvent sends a sentpayload packet -// WriteJSON sends a message to discord respecting ratelimits - -// WaitForReady returns when shard is ready - -// SetStatus - -// ChunkGuild chunks a guild - -// readMessage returns a message or error from channels -// decodeContent unmarshals received payload +package internal + +import ( + "context" + "errors" + "fmt" + "runtime" + "strconv" + "sync" + "time" + + "github.com/WelcomerTeam/RealRock/limiter" + "github.com/WelcomerTeam/RealRock/snowflake" + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + "github.com/WelcomerTeam/czlib" + "github.com/rs/zerolog" + "go.uber.org/atomic" + "golang.org/x/xerrors" + "nhooyr.io/websocket" +) + +const ( + WebsocketReadLimit = 512 << 20 + WebsocketReconnectCloseCode = 4000 + + // Time required before warning about an event taking too long. + DispatchWarningTimeout = 30 * time.Second + + MessageChannelBuffer = 64 + MinPayloadCompressionSize = 1000000 // Applies higher compression to payloads larger than this in bytes + + // Time necessary to abort chunking when no events have been received yet in this time frame. + InitialMemberChunkTimeout = 10 * time.Second + // Time necessary to mark chunking as completed when no more events are received in this time frame. + MemberChunkTimeout = 1 * time.Second + // Time between chunks no longer marked as chunked anymore. + ChunkStatePersistTimeout = 10 * time.Second + + // Number of retries attempted before considering a shard not working. + ShardConnectRetries = 3 + + ShardWSRateLimit = 118 + GatewayLargeThreshold = 100 + + FirstEventTimeout = 5 * time.Second +) + +// Shard represents the shard object. +type Shard struct { + ctx context.Context + cancel func() + + Start time.Time `json:"start"` + RetriesRemaining *atomic.Int32 `json:"-"` + + Logger zerolog.Logger `json:"-"` + + ShardID int `json:"shard_id"` + + Sandwich *Sandwich `json:"-"` + Manager *Manager `json:"-"` + ShardGroup *ShardGroup `json:"-"` + + HeartbeatActive *atomic.Bool `json:"-"` + LastHeartbeatAck *atomic.Time `json:"-"` + LastHeartbeatSent *atomic.Time `json:"-"` + + Heartbeater *time.Ticker `json:"-"` + HeartbeatInterval time.Duration `json:"-"` + + // Duration since last heartbeat Ack beforereconnecting. + HeartbeatFailureInterval time.Duration `json:"-"` + + unavailableMu sync.RWMutex `json:"-"` + Unavailable map[snowflake.ID]bool `json:"-"` + + MessageCh chan discord.GatewayPayload + ErrorCh chan error + + Sequence *atomic.Int64 + SessionID *atomic.String + + wsConn *websocket.Conn + + wsRatelimit *limiter.DurationLimiter + + ready chan void +} + +// NewShard creates a new shard object. +func (sg *ShardGroup) NewShard(shardID int) (sh *Shard) { + logger := sg.Logger.With().Int("shard_id", shardID).Logger() + sh = &Shard{ + RetriesRemaining: atomic.NewInt32(ShardConnectRetries), + + Logger: logger, + + ShardID: shardID, + + Sandwich: sg.Manager.Sandwich, + Manager: sg.Manager, + ShardGroup: sg, + + HeartbeatActive: atomic.NewBool(false), + LastHeartbeatAck: &atomic.Time{}, + LastHeartbeatSent: &atomic.Time{}, + + unavailableMu: sync.RWMutex{}, + Unavailable: make(map[snowflake.ID]bool), + + Sequence: &atomic.Int64{}, + SessionID: &atomic.String{}, + + // We use 118 just to allow heartbeating to not be limited + // by WS but not use it itself. + wsRatelimit: limiter.NewDurationLimiter(ShardWSRateLimit, time.Minute), + + ready: make(chan void, 1), + } + + sh.ctx, sh.cancel = context.WithCancel(sg.Manager.ctx) + + return sh +} + +// Open starts listening to the gateway. +func (sh *Shard) Open() { + sh.Logger.Debug().Msg("Started listening to shard") + + for { + err := sh.Listen() + if xerrors.Is(err, context.Canceled) { + sh.Logger.Debug().Msg("Shard context canceled") + + return + } + + select { + case <-sh.ctx.Done(): + sh.Logger.Debug().Msg("Shard context done") + + return + default: + } + } +} + +// Connect connects to the gateway and handles identifying +func (sh *Shard) Connect() (err error) { + sh.Logger.Debug().Msg("Connecting shard") + + select { + case <-sh.ctx.Done(): + sh.Logger.Trace().Msg("Creating new context") + + sh.ctx, sh.cancel = context.WithCancel(sh.Manager.ctx) + default: + sh.Logger.Trace().Msg("No need for new context") + } + + sh.Manager.gatewayMu.RLock() + gatewayURL := sh.Manager.Gateway.URL + sh.Manager.gatewayMu.RUnlock() + + defer func() { + if err != nil && sh.wsConn != nil { + sh.CloseWS(websocket.StatusNormalClosure) + } + }() + + if sh.wsConn == nil { + var errorCh chan error + + var messageCh chan discord.GatewayPayload + + errorCh, messageCh, err = sh.FeedWebsocket(gatewayURL, nil) + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to dial gateway") + + go sh.Sandwich.PublishSimpleWebhook( + fmt.Sprintf("Failed to dial `%s`", gatewayURL), + "`"+err.Error()+"`", + fmt.Sprintf( + "Manager: %s ShardGroup: %d ShardID: %d/%d", + sh.Manager.Configuration.Identifier, + sh.ShardGroup.ID, + sh.ShardID, + sh.ShardGroup.ShardCount, + ), + EmbedColourDanger, + ) + + return + } + + sh.ErrorCh = errorCh + sh.MessageCh = messageCh + } else { + sh.Logger.Info().Msg("Reusing websocket connection") + } + + sh.Logger.Trace().Msg("Reading from gateway") + + // Read a message from Gateway, this should be Hello + msg, err := sh.readMessage() + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to read message") + + return + } + + helloResponse := discord.Hello{} + + err = sh.decodeContent(msg, &helloResponse) + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to decode HELLO event") + + return + } + + now := time.Now().UTC() + + sh.Start = now + sh.LastHeartbeatAck.Store(now) + sh.LastHeartbeatSent.Store(now) + + sh.HeartbeatInterval = helloResponse.HeartbeatInterval * time.Millisecond + sh.HeartbeatFailureInterval = sh.HeartbeatInterval * ShardMaxHeartbeatFailures + sh.Heartbeater = time.NewTicker(sh.HeartbeatInterval) + + if !sh.HeartbeatActive.Load() { + go sh.Heartbeat() + } + + sequence := sh.Sequence.Load() + sessionID := sh.SessionID.Load() + + sh.Logger.Debug(). + Dur("interval", sh.HeartbeatInterval). + Int64("sequence", sequence). + Msg("Received HELLO event") + + if sessionID == "" || sequence == 0 { + err = sh.Identify() + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to identify") + + go sh.Sandwich.PublishSimpleWebhook( + "Failed to Identify", + "`"+err.Error()+"`", + fmt.Sprintf( + "Manager: %s ShardGroup: %d ShardID: %d/%d", + sh.Manager.Configuration.Identifier, + sh.ShardGroup.ID, + sh.ShardID, + sh.ShardGroup.ShardCount, + ), + EmbedColourDanger, + ) + + return + } + } else { + err = sh.Resume() + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to resume") + + go sh.Sandwich.PublishSimpleWebhook( + "Failed to Resume", + "`"+err.Error()+"`", + fmt.Sprintf( + "Manager: %s ShardGroup: %d ShardID: %d/%d", + sh.Manager.Configuration.Identifier, + sh.ShardGroup.ID, + sh.ShardID, + sh.ShardGroup.ShardCount, + ), + EmbedColourDanger, + ) + + return + } + + // We can assume the bot is now connected to discord. + } + + t := time.NewTicker(FirstEventTimeout) + + // We wait until we either receive a first event, error + // or we hit our FirstEventTimeout. We do nothing when + // hitting the FirstEventtimeout. + + sh.Logger.Trace().Msg("Waiting for first event") + + select { + case err = <-sh.ErrorCh: + sh.Logger.Error().Err(err).Msg("Encountered error whilst connecting") + + go sh.Sandwich.PublishSimpleWebhook( + "Encountered error during connection", + "`"+err.Error()+"`", + fmt.Sprintf( + "Manager: %s ShardGroup: %d ShardID: %d/%d", + sh.Manager.Configuration.Identifier, + sh.ShardGroup.ID, + sh.ShardID, + sh.ShardGroup.ShardCount, + ), + EmbedColourDanger, + ) + + return err + case msg = <-sh.MessageCh: + sh.Logger.Debug().Msgf("Received first event %d %s", msg.Op, msg.Type) + + sh.MessageCh <- msg + case <-t.C: + } + + return err +} + +// Listen to gateway and process accordingly. +func (sh *Shard) Listen() (err error) { + wsConn := sh.wsConn + + for { + select { + case <-sh.ctx.Done(): + return + default: + } + + msg, err := sh.readMessage() + if err != nil { + if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) { + break + } + + sh.Logger.Error().Err(err).Msg("Error reading from gateway") + + var closeError *websocket.CloseError + + if errors.As(err, &closeError) { + // If possible, we will check the close error to determine if we can continue + switch closeError.Code { + case discord.CloseNotAuthenticated, // Not authenticated + discord.CloseInvalidShard, // Invalid shard + discord.CloseShardingRequired, // Sharding required + discord.CloseInvalidAPIVersion, // Invalid API version + discord.CloseInvalidIntents, // Invalid Intent(s) + discord.CloseDisallowedIntents: // Disallowed intent(s) + sh.Logger.Error().Int("code", int(closeError.Code)).Msg("Shard received closure code") + + go sh.Sandwich.PublishSimpleWebhook( + "Shard received closure code", + "`"+strconv.Itoa(int(closeError.Code))+"` - `"+err.Error()+"`", + fmt.Sprintf( + "Manager: %s ShardGroup: %d ShardID: %d/%d", + sh.Manager.Configuration.Identifier, + sh.ShardGroup.ID, + sh.ShardID, + sh.ShardGroup.ShardCount, + ), + EmbedColourDanger, + ) + + sh.ShardGroup.Error.Store(err.Error()) + + return err + default: + sh.Logger.Warn().Msgf("Websocket was closed with code %d", closeError.Code) + } + } + + if wsConn == sh.wsConn { + // We have likely closed so we should attempt to reconnect + sh.Logger.Warn().Msg("We have encountered an error whilst in the same connection, reconnecting...") + err = sh.Reconnect(websocket.StatusNormalClosure) + + if err != nil { + return err + } + + return nil + } + + wsConn = sh.wsConn + } + + sh.OnEvent(msg) + + // In the event we have reconnected, the wsConn could have changed, + // we will use the new wsConn if this is the case + if sh.wsConn != wsConn { + sh.Logger.Debug().Msg("New wsConn was assigned to shard") + wsConn = sh.wsConn + } + } + + return err +} + +// FeedWebsocket reads websocket events and feeds them through a channel. +func (sh *Shard) FeedWebsocket(u string, + opts *websocket.DialOptions) (errorCh chan error, messageCh chan discord.GatewayPayload, err error) { + messageCh = make(chan discord.GatewayPayload, MessageChannelBuffer) + errorCh = make(chan error, 1) + + conn, _, err := websocket.Dial(sh.ctx, u, opts) + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to dial websocket") + + return errorCh, messageCh, xerrors.Errorf("failed to connect to websocket: %w", err) + } + + conn.SetReadLimit(WebsocketReadLimit) + sh.wsConn = conn + + go func() { + for { + _, buf, err := conn.Read(sh.ctx) + + select { + case <-sh.ctx.Done(): + return + default: + } + + if err != nil { + errorCh <- err + + return + } + + buf, err = czlib.Decompress(buf) + if err != nil { + errorCh <- err + + return + } + + msg := sh.Sandwich.receivedPool.Get().(discord.GatewayPayload) + + err = json.Unmarshal(buf, &msg) + if err != nil { + sh.Logger.Error().Err(err).Msg("Failed to unmarshal message") + + continue + } + + // TODO Add Analytics + + messageCh <- msg + } + }() + + return errorCh, messageCh, nil +} + +// Identify sends the identify packet to discord. +func (sh *Shard) Identify() (err error) { + sh.Manager.gatewayMu.Lock() + sh.Manager.Gateway.SessionStartLimit.Remaining-- + sh.Manager.gatewayMu.Unlock() + + sh.Manager.WaitForIdentify(sh.ShardID, sh.ShardGroup.ShardCount) + sh.Logger.Debug().Msg("Wait for identify completed") + + sh.Logger.Debug().Msg("Sending identify") + + err = sh.SendEvent(discord.GatewayOpIdentify, discord.Identify{ + Token: sh.Manager.Configuration.Token, + Properties: &discord.IdentifyProperties{ + OS: runtime.GOOS, + Browser: "Sandwich " + VERSION, + Device: "Sandwich " + VERSION, + }, + Compress: true, + LargeThreshold: GatewayLargeThreshold, + Shard: [2]int{sh.ShardID, sh.ShardGroup.ShardCount}, + Presence: sh.Manager.Configuration.Bot.DefaultPresence, + Intents: sh.Manager.Configuration.Bot.Intents, + }) + + return err +} + +// SendEvent sends an event to discord. +func (sh *Shard) SendEvent(op discord.GatewayOp, data interface{}) (err error) { + packet := sh.Sandwich.sentPool.Get().(*discord.SentPayload) + defer sh.Sandwich.sentPool.Put(packet) + + packet.Op = op + packet.Data = data + + err = sh.WriteJSON(op, packet) + if err != nil { + return xerrors.Errorf("sendEvent writeJson: %w", err) + } + + return +} + +// WriteJSON writes json data to the websocket. +func (sh *Shard) WriteJSON(op discord.GatewayOp, i interface{}) (err error) { + res, err := json.Marshal(i) + if err != nil { + return err + } + + if op != discord.GatewayOpHeartbeat { + sh.wsRatelimit.Lock() + } + + err = sh.wsConn.Write(sh.ctx, websocket.MessageText, res) + + return err +} + +// decodeContent converts the stored msg into the passed interface. +func (sh *Shard) decodeContent(msg discord.GatewayPayload, out interface{}) (err error) { + err = json.Unmarshal(msg.Data, &out) + + return +} + +// readMessage fills the shard msg buffer from a websocket message. +func (sh *Shard) readMessage() (msg discord.GatewayPayload, err error) { + // Prioritize errors + select { + case err = <-sh.ErrorCh: + return msg, err + default: + } + + select { + case err = <-sh.ErrorCh: + return msg, err + case msg = <-sh.MessageCh: + msg.AddTrace("read", time.Now().UTC()) + + return msg, nil + } +} + +// CloseWS closes the websocket. This will always return 0 as the error is suppressed. +func (sh *Shard) CloseWS(statusCode websocket.StatusCode) (err error) { + if sh.wsConn != nil { + sh.Logger.Debug().Int("code", int(statusCode)).Msg("Closing websocket connection") + + err = sh.wsConn.Close(statusCode, "") + if err != nil && !xerrors.Is(err, context.Canceled) { + sh.Logger.Warn().Err(err).Msg("Failed to close websocket connection") + } + + sh.wsConn = nil + } + + return nil +} + +// Connect to gateway and setup message channels +// Listen handles reading from websocket, errors and basic reconnection +// Feed reads from the gateway and decompresses messages and push to message channel +// OnEvent handles gateway ops and dispatch +// OnDispatch handles cheking blacklists, handling dispatch and publishing +// Heartbeat maintains Heartbeat +// Reconnect reconnects to gateway +// Close sends a close code + +// Resume +// Identify +// Reconnect + +// SendEvent sends a sentpayload packet +// WriteJSON sends a message to discord respecting ratelimits + +// WaitForReady returns when shard is ready + +// SetStatus + +// ChunkGuild chunks a guild + +// readMessage returns a message or error from channels +// decodeContent unmarshals received payload diff --git a/internal/shardgroup.go b/internal/shardgroup.go index fa9cf61..6b0876e 100644 --- a/internal/shardgroup.go +++ b/internal/shardgroup.go @@ -1,120 +1,217 @@ -package internal - -import ( - "sync" - "time" - - snowflake "github.com/WelcomerTeam/RealRock/snowflake" - "github.com/rs/zerolog" - "go.uber.org/atomic" - "nhooyr.io/websocket" -) - -// ShardGroup represents a group of shards -type ShardGroup struct { - Error atomic.String `json:"error"` - - Manager *Manager `json:"-"` - Logger zerolog.Logger `json:"-"` - - Start time.Time `json:"start"` - - WaitingFor atomic.Int32 `json:"-"` - - ID int32 `json:"id"` - - ShardCount int `json:"shard_count"` - ShardIDs []int `json:"shard_ids"` - - shardsMu sync.RWMutex `json:"-"` - Shards map[int]*Shard `json:"shards"` - - ReadyWait *sync.WaitGroup `json:"-"` - - // MemberChunksCallback is used to signal when a guild is chunking. - memberChunksCallbackMu sync.RWMutex `json:"-"` - MemberChunksCallback map[snowflake.ID]*sync.WaitGroup `json:"-"` - - // MemberChunksComplete is used to signal if a guild has recently - // been chunked. It is up to the guild task to remove this bool - // a few seconds after finishing chunking. - memberChunksCompleteMu sync.RWMutex `json:"-"` - MemberChunksComplete map[snowflake.ID]*atomic.Bool `json:"-"` - - // MemberChunkCallbacks is used to signal when any MEMBER_CHUNK - // events are received for the specific guild. - memberChunkCallbacksMu sync.RWMutex `json:"-"` - MemberChunkCallbacks map[snowflake.ID]chan bool `json:"-"` - - // Used to override when events can be processed. - // Used to orchestrate scaling of shardgroups. - floodgate *atomic.Bool -} - -// NewShardGroup creates a new shardgroup. -func (mg *Manager) NewShardGroup(shardGroupID int32, shardIDs []int, shardCount int) (sg *ShardGroup) { - sg = &ShardGroup{ - Error: atomic.String{}, - - Manager: mg, - Logger: mg.Logger, - - Start: time.Now().UTC(), - - WaitingFor: atomic.Int32{}, - - ID: shardGroupID, - - ShardCount: shardCount, - ShardIDs: shardIDs, - - shardsMu: sync.RWMutex{}, - Shards: make(map[int]*Shard), - - memberChunksCallbackMu: sync.RWMutex{}, - MemberChunksCallback: make(map[snowflake.ID]*sync.WaitGroup), - - memberChunksCompleteMu: sync.RWMutex{}, - MemberChunksComplete: make(map[snowflake.ID]*atomic.Bool), - - memberChunkCallbacksMu: sync.RWMutex{}, - MemberChunkCallbacks: make(map[snowflake.ID]chan bool), - - floodgate: &atomic.Bool{}, - } - - return sg -} - -// Open handles the startup of a shard group. -// On startup of a shard group, the first shard is connected and ran to confirm token and such are valid. -// If an issue occurs starting the first shard, open will return an error. Other shards will then connect -// concurrently and will attempt to reconnect on error. -// Once the shardgroup has fully finished connecting and are ready, then floodgate will be enabled allowing -// their events to be handled. -func (sg *ShardGroup) Open() (ready chan bool, err error) { - // TODO - - // Create Shards - // Try connect first Shard. Retry on error and set sg.Error after too many retries. - // Open first shard in goroutine - // Create new goroutine for each shard that attempts to constantly retry then shard.Open after and Done a waitgroup - // Wait for all shards to connect and open. - // Create new goroutine that calls shard.WaitforReady - // Enable floodgate and close ready. - - return -} - -// Close closes all shards in a shardgroup. -func (sg *ShardGroup) Close() { - sg.Logger.Info().Msgf("Closing shardgroup %d", sg.ID) - - sg.shardsMu.RLock() - for _, sh := range sg.Shards { - sh.Close(websocket.StatusNormalClosure) - } - sg.shardsMu.RUnlock() -} - -// SetStatus +package internal + +import ( + "sync" + "time" + + snowflake "github.com/WelcomerTeam/RealRock/snowflake" + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + "github.com/rs/zerolog" + "go.uber.org/atomic" + "golang.org/x/net/context" + "golang.org/x/xerrors" + "nhooyr.io/websocket" +) + +// ShardGroup represents a group of shards +type ShardGroup struct { + Error *atomic.String `json:"error"` + + Manager *Manager `json:"-"` + Logger zerolog.Logger `json:"-"` + + Start time.Time `json:"start"` + + WaitingFor *atomic.Int32 `json:"-"` + + userMu sync.RWMutex `json:"-"` + User *discord.User `json:"user"` + + ID int32 `json:"id"` + + ShardCount int `json:"shard_count"` + ShardIDs []int `json:"shard_ids"` + + shardsMu sync.RWMutex `json:"-"` + Shards map[int]*Shard `json:"shards"` + + ReadyWait *sync.WaitGroup `json:"-"` + + // MemberChunksCallback is used to signal when a guild is chunking. + memberChunksCallbackMu sync.RWMutex `json:"-"` + MemberChunksCallback map[snowflake.ID]*sync.WaitGroup `json:"-"` + + // MemberChunksComplete is used to signal if a guild has recently + // been chunked. It is up to the guild task to remove this bool + // a few seconds after finishing chunking. + memberChunksCompleteMu sync.RWMutex `json:"-"` + MemberChunksComplete map[snowflake.ID]*atomic.Bool `json:"-"` + + // MemberChunkCallbacks is used to signal when any MEMBER_CHUNK + // events are received for the specific guild. + memberChunkCallbacksMu sync.RWMutex `json:"-"` + MemberChunkCallbacks map[snowflake.ID]chan bool `json:"-"` + + // Used to override when events can be processed. + // Used to orchestrate scaling of shardgroups. + floodgate *atomic.Bool +} + +// NewShardGroup creates a new shardgroup. +func (mg *Manager) NewShardGroup(shardGroupID int32, shardIDs []int, shardCount int) (sg *ShardGroup) { + sg = &ShardGroup{ + Error: &atomic.String{}, + + Manager: mg, + Logger: mg.Logger, + + Start: time.Now().UTC(), + + WaitingFor: &atomic.Int32{}, + + ID: shardGroupID, + + ShardCount: shardCount, + ShardIDs: shardIDs, + + shardsMu: sync.RWMutex{}, + Shards: make(map[int]*Shard), + + memberChunksCallbackMu: sync.RWMutex{}, + MemberChunksCallback: make(map[snowflake.ID]*sync.WaitGroup), + + memberChunksCompleteMu: sync.RWMutex{}, + MemberChunksComplete: make(map[snowflake.ID]*atomic.Bool), + + memberChunkCallbacksMu: sync.RWMutex{}, + MemberChunkCallbacks: make(map[snowflake.ID]chan bool), + + floodgate: &atomic.Bool{}, + } + + return sg +} + +// Open handles the startup of a shard group. +// On startup of a shard group, the first shard is connected and ran to confirm token and such are valid. +// If an issue occurs starting the first shard, open will return an error. Other shards will then connect +// concurrently and will attempt to reconnect on error. +// Once the shardgroup has fully finished connecting and are ready, then floodgate will be enabled allowing +// their events to be handled. +func (sg *ShardGroup) Open() (ready chan bool, err error) { + sg.Start = time.Now().UTC() + + ready = make(chan bool, 1) + + sg.Logger.Info(). + Int("shard_count", sg.ShardCount). + Int("shard_ids", len(sg.ShardIDs)). + Msg("Starting shardgroup") + + sg.shardsMu.Lock() + for _, shardID := range sg.ShardIDs { + shard := sg.NewShard(shardID) + sg.Shards[shardID] = shard + } + sg.shardsMu.Unlock() + + initialShard := sg.Shards[sg.ShardIDs[0]] + + for { + err = initialShard.Connect() + + if err != nil && !xerrors.Is(err, context.Canceled) { + retriesRemaining := initialShard.RetriesRemaining.Load() + + if retriesRemaining > 0 { + sg.Logger.Error().Err(err). + Int32("retries_remaining", retriesRemaining). + Msg("Failed to connect shard. Retrying...") + } else { + sg.Logger.Error().Err(err). + Msg("Failed to connect shard. Cannot continue.") + + sg.Error.Store(err.Error()) + + sg.Close() + + return + } + + initialShard.RetriesRemaining.Sub(1) + } else { + break + } + } + + go initialShard.Open() + + connectGroup := sync.WaitGroup{} + + for _, shardID := range sg.ShardIDs[1:] { + connectGroup.Add(1) + + go func(shardID int) { + sg.shardsMu.RLock() + shard := sg.Shards[shardID] + sg.shardsMu.RUnlock() + + for { + err = shard.Connect() + if err != nil && !xerrors.Is(err, context.Canceled) { + sg.Logger.Warn().Err(err). + Int("shard_id", shardID). + Msgf("Failed to connect shard. Retrying.") + } else { + go shard.Open() + + break + } + + connectGroup.Done() + } + }(shardID) + } + + connectGroup.Wait() + sg.Logger.Info().Msg("All shards have connected") + + go func(sg *ShardGroup) { + sg.shardsMu.RLock() + for _, shardID := range sg.ShardIDs { + shard := sg.Shards[shardID] + sg.WaitingFor.Store(int32(shardID)) + shard.WaitForReady() + } + sg.shardsMu.RUnlock() + + sg.Logger.Info().Msg("All shards are now ready") + + sg.Manager.shardGroupsMu.RLock() + for sgID, _sg := range sg.Manager.ShardGroups { + if sgID != sg.ID { + _sg.floodgate.Store(false) + _sg.Close() + } + } + sg.Manager.shardGroupsMu.RUnlock() + + sg.floodgate.Store(true) + close(ready) + }(sg) + + return err +} + +// Close closes all shards in a shardgroup. +func (sg *ShardGroup) Close() { + sg.Logger.Info().Msgf("Closing shardgroup %d", sg.ID) + + sg.shardsMu.RLock() + for _, sh := range sg.Shards { + sh.Close(websocket.StatusNormalClosure) + } + sg.shardsMu.RUnlock() +} + +// SetStatus diff --git a/internal/state.go b/internal/state.go index 22193ae..9a15a4f 100644 --- a/internal/state.go +++ b/internal/state.go @@ -1,82 +1,82 @@ -package internal - -import ( - "sync" - - snowflake "github.com/WelcomerTeam/RealRock/snowflake" - discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" - structs "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" - "golang.org/x/xerrors" -) - -var NoEventHandler = xerrors.New("No registered handler for event") - -var stateHandlers = make(map[string]func(ctx *StateCtx, msg discord.GatewayPayload) (result structs.StateResult, ok bool, err error)) - -type StateCtx struct { - Sg *Sandwich - Mg *Manager - Sh *Shard - - Vars map[string]interface{} -} - -// SandwichState stores the collective state of all ShardGroups -// accross all Managers. -type SandwichState struct { - guildsMu sync.RWMutex - Guilds map[snowflake.ID]*discord.StateGuild - - guildMembersMu sync.RWMutex - GuildMembers map[snowflake.ID]*discord.StateGuildMembers - - channelsMu sync.RWMutex - Channels map[snowflake.ID]*discord.StateChannel - - rolesMu sync.RWMutex - Roles map[snowflake.ID]*discord.StateRole - - emojisMu sync.RWMutex - Emojis map[snowflake.ID]*discord.StateEmoji - - usersMu sync.RWMutex - Users map[snowflake.ID]*discord.StateUser -} - -func NewSandwichState() (st *SandwichState) { - st = &SandwichState{ - guildsMu: sync.RWMutex{}, - Guilds: make(map[snowflake.ID]*discord.StateGuild), - - guildMembersMu: sync.RWMutex{}, - GuildMembers: make(map[snowflake.ID]*discord.StateGuildMembers), - - channelsMu: sync.RWMutex{}, - Channels: make(map[snowflake.ID]*discord.StateChannel), - - rolesMu: sync.RWMutex{}, - Roles: make(map[snowflake.ID]*discord.StateRole), - - emojisMu: sync.RWMutex{}, - Emojis: make(map[snowflake.ID]*discord.StateEmoji), - - usersMu: sync.RWMutex{}, - Users: make(map[snowflake.ID]*discord.StateUser), - } - - return st -} - -func registerState(eventType string, handler func(ctx *StateCtx, msg discord.GatewayPayload) (result structs.StateResult, ok bool, err error)) { - stateHandlers[eventType] = handler -} - -// StateDispatch handles selecting the proper state handler and executing it. -func StateDispatch(ctx *StateCtx, - event discord.GatewayPayload) (result structs.StateResult, ok bool, err error) { - if f, ok := stateHandlers[event.Type]; ok { - return f(ctx, event) - } - - return result, false, NoEventHandler -} +package internal + +import ( + "sync" + + snowflake "github.com/WelcomerTeam/RealRock/snowflake" + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + structs "github.com/WelcomerTeam/Sandwich-Daemon/next/structs" + "golang.org/x/xerrors" +) + +var NoEventHandler = xerrors.New("No registered handler for event") + +var stateHandlers = make(map[string]func(ctx *StateCtx, msg discord.GatewayPayload) (result structs.StateResult, ok bool, err error)) + +type StateCtx struct { + Sg *Sandwich + Mg *Manager + Sh *Shard + + Vars map[string]interface{} +} + +// SandwichState stores the collective state of all ShardGroups +// across all Managers. +type SandwichState struct { + guildsMu sync.RWMutex + Guilds map[snowflake.ID]*discord.StateGuild + + guildMembersMu sync.RWMutex + GuildMembers map[snowflake.ID]*discord.StateGuildMembers + + channelsMu sync.RWMutex + Channels map[snowflake.ID]*discord.StateChannel + + rolesMu sync.RWMutex + Roles map[snowflake.ID]*discord.StateRole + + emojisMu sync.RWMutex + Emojis map[snowflake.ID]*discord.StateEmoji + + usersMu sync.RWMutex + Users map[snowflake.ID]*discord.StateUser +} + +func NewSandwichState() (st *SandwichState) { + st = &SandwichState{ + guildsMu: sync.RWMutex{}, + Guilds: make(map[snowflake.ID]*discord.StateGuild), + + guildMembersMu: sync.RWMutex{}, + GuildMembers: make(map[snowflake.ID]*discord.StateGuildMembers), + + channelsMu: sync.RWMutex{}, + Channels: make(map[snowflake.ID]*discord.StateChannel), + + rolesMu: sync.RWMutex{}, + Roles: make(map[snowflake.ID]*discord.StateRole), + + emojisMu: sync.RWMutex{}, + Emojis: make(map[snowflake.ID]*discord.StateEmoji), + + usersMu: sync.RWMutex{}, + Users: make(map[snowflake.ID]*discord.StateUser), + } + + return st +} + +func registerState(eventType string, handler func(ctx *StateCtx, msg discord.GatewayPayload) (result structs.StateResult, ok bool, err error)) { + stateHandlers[eventType] = handler +} + +// StateDispatch handles selecting the proper state handler and executing it. +func StateDispatch(ctx *StateCtx, + event discord.GatewayPayload) (result structs.StateResult, ok bool, err error) { + if f, ok := stateHandlers[event.Type]; ok { + return f(ctx, event) + } + + return result, false, NoEventHandler +} diff --git a/internal/utils.go b/internal/utils.go index 3c33045..dbedc88 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -1,75 +1,67 @@ -package internal - -import ( - "encoding/hex" - "hash" - "strconv" - "strings" - "time" - - "github.com/WelcomerTeam/RealRock/snowflake" -) - -const ( - daySeconds = 86400 - hourSeconds = 3600 - minuteSeconds = 60 - discordSnowflakeEpoch = 1420070400000 -) - -// We change the default Epoch of the snowflake to match discord's. -func init() { - snowflake.Epoch = discordSnowflakeEpoch -} - -type void struct{} - -func replaceIfEmpty(v string, s string) string { - if v == "" { - return s - } - - return v -} - -func returnError(err error) string { - if err != nil { - return err.Error() - } - - return "" -} - -// quickHash returns hash from method and input. -func quickHash(hashMethod hash.Hash, text string) (result string, err error) { - hashMethod.Reset() - - if _, err := hashMethod.Write([]byte(text)); err != nil { - return "", err - } - - return hex.EncodeToString(hashMethod.Sum(nil)), nil -} - -// returnRange converts a string like 0-4,6-7 to [0,1,2,3,4,6,7]. -func returnRange(_range string, max int) (result []int) { - for _, split := range strings.Split(_range, ",") { - ranges := strings.Split(split, "-") - if low, err := strconv.Atoi(ranges[0]); err == nil { - if hi, err := strconv.Atoi(ranges[len(ranges)-1]); err == nil { - for i := low; i < hi+1; i++ { - if 0 <= i && i < max { - result = append(result, i) - } - } - } - } - } - - return result -} - -// webhookTime returns a formatted time.Time as a time accepted by webhooks. -func webhookTime(_time time.Time) string { - return _time.Format("2006-01-02T15:04:05Z") -} +package internal + +import ( + "encoding/hex" + "hash" + "strconv" + "strings" + "time" +) + +const ( + daySeconds = 86400 + hourSeconds = 3600 + minuteSeconds = 60 +) + +type void struct{} + +func replaceIfEmpty(v string, s string) string { + if v == "" { + return s + } + + return v +} + +func returnError(err error) string { + if err != nil { + return err.Error() + } + + return "" +} + +// quickHash returns hash from method and input. +func quickHash(hashMethod hash.Hash, text string) (result string, err error) { + hashMethod.Reset() + + if _, err := hashMethod.Write([]byte(text)); err != nil { + return "", err + } + + return hex.EncodeToString(hashMethod.Sum(nil)), nil +} + +// returnRange converts a string like 0-4,6-7 to [0,1,2,3,4,6,7]. +func returnRange(_range string, max int) (result []int) { + for _, split := range strings.Split(_range, ",") { + ranges := strings.Split(split, "-") + if low, err := strconv.Atoi(ranges[0]); err == nil { + if hi, err := strconv.Atoi(ranges[len(ranges)-1]); err == nil { + for i := low; i < hi+1; i++ { + if 0 <= i && i < max { + result = append(result, i) + } + } + } + } + } + + return result +} + +// webhookTime returns a formatted time.Time as a time accepted by webhooks. +func webhookTime(_time time.Time) string { + return _time.Format("2006-01-02T15:04:05Z") +} diff --git a/internal/webhook.go b/internal/webhook.go index 9d5d452..f567a1c 100644 --- a/internal/webhook.go +++ b/internal/webhook.go @@ -1,68 +1,67 @@ -package internal - -import ( - "bytes" - "context" - "net/url" - "strings" - "time" - - discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" - "golang.org/x/xerrors" -) - -// Embed colours for webhooks. -const ( - EmbedColourSandwich = 16701571 - EmbedColourWarning = 16760839 - EmbedColourDanger = 14431557 -) - -// PublishSimpleWebhook is a helper function for creating quicker webhook messages. -func (sg *Sandwich) PublishSimpleWebhook(title string, description string, footer string, colour int) { - sg.PublishWebhook(discord.WebhookMessage{ - Embeds: []*discord.Embed{ - { - Title: title, - Description: description, - Color: colour, - Timestamp: webhookTime(time.Now().UTC()), - Footer: &discord.EmbedFooter{ - Text: footer, - }, - }, - }, - }) -} - -// PublishWebhook sends a webhook message to all added webhooks in the configuration. -func (sg *Sandwich) PublishWebhook(message discord.WebhookMessage) { - for _, webhook := range sg.Configuration.Webhooks { - _, err := sg.SendWebhook(webhook, message) - if err != nil && !xerrors.Is(err, context.Canceled) { - sg.Logger.Warn().Err(err).Str("url", webhook).Msg("Failed to send webhook") - } - } -} - -func (sg *Sandwich) SendWebhook(webhookUrl string, - message discord.WebhookMessage) (status int, err error) { - - webhookUrl = strings.TrimSpace(webhookUrl) - - _, err = url.Parse(webhookUrl) - if err != nil { - return -1, xerrors.Errorf("failed to parse webhook URL: %w", err) - } - - res, err := json.Marshal(message) - if err != nil { - return -1, xerrors.Errorf("failed to marshal webhook message: %w", err) - } - - _, status, err = sg.Client.Fetch(sg.ctx, "POST", webhookUrl, bytes.NewBuffer(res), map[string]string{ - "Content-Type": "application/json", - }) - - return status, err -} +package internal + +import ( + "bytes" + "context" + "net/url" + "strings" + "time" + + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" + "golang.org/x/xerrors" +) + +// Embed colours for webhooks. +const ( + EmbedColourSandwich = 16701571 + EmbedColourWarning = 16760839 + EmbedColourDanger = 14431557 +) + +// PublishSimpleWebhook is a helper function for creating quicker webhook messages. +func (sg *Sandwich) PublishSimpleWebhook(title string, description string, footer string, colour int) { + sg.PublishWebhook(discord.WebhookMessage{ + Embeds: []*discord.Embed{ + { + Title: title, + Description: description, + Color: colour, + Timestamp: webhookTime(time.Now().UTC()), + Footer: &discord.EmbedFooter{ + Text: footer, + }, + }, + }, + }) +} + +// PublishWebhook sends a webhook message to all added webhooks in the configuration. +func (sg *Sandwich) PublishWebhook(message discord.WebhookMessage) { + for _, webhook := range sg.Configuration.Webhooks { + _, err := sg.SendWebhook(webhook, message) + if err != nil && !xerrors.Is(err, context.Canceled) { + sg.Logger.Warn().Err(err).Str("url", webhook).Msg("Failed to send webhook") + } + } +} + +func (sg *Sandwich) SendWebhook(webhookUrl string, message discord.WebhookMessage) (status int, err error) { + + webhookUrl = strings.TrimSpace(webhookUrl) + + _, err = url.Parse(webhookUrl) + if err != nil { + return -1, xerrors.Errorf("failed to parse webhook URL: %w", err) + } + + res, err := json.Marshal(message) + if err != nil { + return -1, xerrors.Errorf("failed to marshal webhook message: %w", err) + } + + _, status, err = sg.Client.Fetch(sg.ctx, "POST", webhookUrl, bytes.NewBuffer(res), map[string]string{ + "Content-Type": "application/json", + }) + + return status, err +} diff --git a/messaging/kafka.go b/messaging/kafka.go index 29f632d..2ffe66d 100644 --- a/messaging/kafka.go +++ b/messaging/kafka.go @@ -1,100 +1,100 @@ -package mqclients - -import ( - "context" - "strconv" - - "github.com/segmentio/kafka-go" - "golang.org/x/xerrors" -) - -func init() { - MQClients = append(MQClients, "kafka") -} - -type KafkaMQClient struct { - KafkaClient *kafka.Writer - - channel string - cluster string -} - -func parseKafkaBalancer(balancer string) kafka.Balancer { - switch balancer { - case "crc32": - return &kafka.CRC32Balancer{} - case "hash": - return &kafka.Hash{} - case "murmur2": - return &kafka.Murmur2Balancer{} - case "roundrobin": - return &kafka.RoundRobin{} - case "leastbytes": - return &kafka.LeastBytes{} - default: - return nil - } -} - -func (kafkaMQ *KafkaMQClient) String() string { - return "kafka" -} - -func (kafkaMQ *KafkaMQClient) Channel() string { - return kafkaMQ.channel -} - -func (kafkaMQ *KafkaMQClient) Cluster() string { - return kafkaMQ.cluster -} - -func (kafkaMQ *KafkaMQClient) Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) { - var ok bool - - var address string - - if address, ok = GetEntry(args, "Address").(string); !ok { - return xerrors.New("kafkaMQ connect: string type assertion failed for Address") - } - - var topic string - - if topic, ok = GetEntry(args, "Topic").(string); !ok { - return xerrors.New("kafkaMQ connect: string type assertion failed for Topic") - } - - var balancer kafka.Balancer - - if balancerStr, ok := GetEntry(args, "Balancer").(string); ok { - balancer = parseKafkaBalancer(balancerStr) - } else { - return xerrors.New("kafkaMQ connect: string type assertion failed for Balancer") - } - - var async bool - - if asyncStr, ok := GetEntry(args, "Async").(string); ok { - async, _ = strconv.ParseBool(asyncStr) - } else { - async = false - } - - kafkaMQ.KafkaClient = &kafka.Writer{ - Addr: kafka.TCP(address), - Topic: topic, - Balancer: balancer, - Async: async, - } - - return nil -} - -func (kafkaMQ *KafkaMQClient) Publish(ctx context.Context, channelName string, data []byte) (err error) { - return kafkaMQ.KafkaClient.WriteMessages( - ctx, - kafka.Message{ - Topic: channelName, - Value: data, - }, - ) -} +package mqclients + +import ( + "context" + "strconv" + + "github.com/segmentio/kafka-go" + "golang.org/x/xerrors" +) + +func init() { + MQClients = append(MQClients, "kafka") +} + +type KafkaMQClient struct { + KafkaClient *kafka.Writer + + channel string + cluster string +} + +func parseKafkaBalancer(balancer string) kafka.Balancer { + switch balancer { + case "crc32": + return &kafka.CRC32Balancer{} + case "hash": + return &kafka.Hash{} + case "murmur2": + return &kafka.Murmur2Balancer{} + case "roundrobin": + return &kafka.RoundRobin{} + case "leastbytes": + return &kafka.LeastBytes{} + default: + return nil + } +} + +func (kafkaMQ *KafkaMQClient) String() string { + return "kafka" +} + +func (kafkaMQ *KafkaMQClient) Channel() string { + return kafkaMQ.channel +} + +func (kafkaMQ *KafkaMQClient) Cluster() string { + return kafkaMQ.cluster +} + +func (kafkaMQ *KafkaMQClient) Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) { + var ok bool + + var address string + + if address, ok = GetEntry(args, "Address").(string); !ok { + return xerrors.New("kafkaMQ connect: string type assertion failed for Address") + } + + var topic string + + if topic, ok = GetEntry(args, "Topic").(string); !ok { + return xerrors.New("kafkaMQ connect: string type assertion failed for Topic") + } + + var balancer kafka.Balancer + + if balancerStr, ok := GetEntry(args, "Balancer").(string); ok { + balancer = parseKafkaBalancer(balancerStr) + } else { + return xerrors.New("kafkaMQ connect: string type assertion failed for Balancer") + } + + var async bool + + if asyncStr, ok := GetEntry(args, "Async").(string); ok { + async, _ = strconv.ParseBool(asyncStr) + } else { + async = false + } + + kafkaMQ.KafkaClient = &kafka.Writer{ + Addr: kafka.TCP(address), + Topic: topic, + Balancer: balancer, + Async: async, + } + + return nil +} + +func (kafkaMQ *KafkaMQClient) Publish(ctx context.Context, channelName string, data []byte) (err error) { + return kafkaMQ.KafkaClient.WriteMessages( + ctx, + kafka.Message{ + Topic: channelName, + Value: data, + }, + ) +} diff --git a/messaging/redis.go b/messaging/redis.go index 51a5bfa..372df75 100644 --- a/messaging/redis.go +++ b/messaging/redis.go @@ -1,78 +1,78 @@ -package mqclients - -import ( - "context" - "strconv" - - "github.com/go-redis/redis/v8" - "golang.org/x/xerrors" -) - -func init() { - MQClients = append(MQClients, "redis") -} - -type RedisMQClient struct { - redisClient *redis.Client - - channel string - cluster string -} - -func (redisMQ *RedisMQClient) String() string { - return "redis" -} - -func (redisMQ *RedisMQClient) Channel() string { - return redisMQ.channel -} - -func (redisMQ *RedisMQClient) Cluster() string { - return redisMQ.cluster -} - -func (redisMQ *RedisMQClient) Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) { - var ok bool - - var address string - - if address, ok = GetEntry(args, "Address").(string); !ok { - return xerrors.New("redisMQ connect: string type assertion failed for Address") - } - - var password string - - if password, ok = GetEntry(args, "Password").(string); !ok { - return xerrors.New("redisMQ connect: string type assertion failed for Password") - } - - var db int - - if dbStr, ok := GetEntry(args, "DB").(string); !ok { - db, err = strconv.Atoi(dbStr) - if err != nil { - return xerrors.Errorf("redisMQ connect db atoi: %w", err) - } - } - - redisMQ.redisClient = redis.NewClient(&redis.Options{ - Addr: address, - Password: password, - DB: db, - }) - - err = redisMQ.redisClient.Ping(ctx).Err() - if err != nil { - return xerrors.Errorf("redisMQ connect ping: %w", err) - } - - return nil -} - -func (redisMQ *RedisMQClient) Publish(ctx context.Context, channelName string, data []byte) (err error) { - return redisMQ.redisClient.Publish( - ctx, - channelName, - data, - ).Err() -} +package mqclients + +import ( + "context" + "strconv" + + "github.com/go-redis/redis/v8" + "golang.org/x/xerrors" +) + +func init() { + MQClients = append(MQClients, "redis") +} + +type RedisMQClient struct { + redisClient *redis.Client + + channel string + cluster string +} + +func (redisMQ *RedisMQClient) String() string { + return "redis" +} + +func (redisMQ *RedisMQClient) Channel() string { + return redisMQ.channel +} + +func (redisMQ *RedisMQClient) Cluster() string { + return redisMQ.cluster +} + +func (redisMQ *RedisMQClient) Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) { + var ok bool + + var address string + + if address, ok = GetEntry(args, "Address").(string); !ok { + return xerrors.New("redisMQ connect: string type assertion failed for Address") + } + + var password string + + if password, ok = GetEntry(args, "Password").(string); !ok { + return xerrors.New("redisMQ connect: string type assertion failed for Password") + } + + var db int + + if dbStr, ok := GetEntry(args, "DB").(string); !ok { + db, err = strconv.Atoi(dbStr) + if err != nil { + return xerrors.Errorf("redisMQ connect db atoi: %w", err) + } + } + + redisMQ.redisClient = redis.NewClient(&redis.Options{ + Addr: address, + Password: password, + DB: db, + }) + + err = redisMQ.redisClient.Ping(ctx).Err() + if err != nil { + return xerrors.Errorf("redisMQ connect ping: %w", err) + } + + return nil +} + +func (redisMQ *RedisMQClient) Publish(ctx context.Context, channelName string, data []byte) (err error) { + return redisMQ.redisClient.Publish( + ctx, + channelName, + data, + ).Err() +} diff --git a/messaging/stan.go b/messaging/stan.go index b833d36..734d1df 100644 --- a/messaging/stan.go +++ b/messaging/stan.go @@ -1,118 +1,118 @@ -package mqclients - -import ( - "context" - "strconv" - - "github.com/nats-io/nats.go" - "github.com/nats-io/stan.go" - "golang.org/x/xerrors" -) - -func init() { - MQClients = append(MQClients, "stan") -} - -type StanMQClient struct { - NatsClient *nats.Conn `json:"-"` - StanClient stan.Conn `json:"-"` - - async bool - - channel string - cluster string -} - -func (stanMQ *StanMQClient) String() string { - return "stan" -} - -func (stanMQ *StanMQClient) Channel() string { - return stanMQ.channel -} - -func (stanMQ *StanMQClient) Cluster() string { - return stanMQ.cluster -} - -func (stanMQ *StanMQClient) Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) { - var ok bool - - var address string - - if address, ok = GetEntry(args, "Address").(string); !ok { - return xerrors.New("stanMQ connect: string type assertion failed for Address") - } - - var cluster string - - if cluster, ok = GetEntry(args, "Cluster").(string); !ok { - return xerrors.New("stanMQ connect: string type assertion failed for Cluster") - } - - var channel string - - if channel, ok = GetEntry(args, "Channel").(string); !ok { - return xerrors.New("stanMQ connect: string type assertion failed for Channel") - } - - stanMQ.cluster = cluster - stanMQ.channel = channel - - var useNatsConnection bool - - if useNatsConnectionStr, ok := GetEntry(args, "UseNATSConnection").(string); ok { - if useNatsConnection, err = strconv.ParseBool(useNatsConnectionStr); err != nil { - useNatsConnection = true - } - } else { - useNatsConnection = true - } - - if asyncStr, ok := GetEntry(args, "Async").(string); ok { - stanMQ.async, _ = strconv.ParseBool(asyncStr) - } else { - stanMQ.async = false - } - - var option stan.Option - - if useNatsConnection { - stanMQ.NatsClient, err = nats.Connect(address) - if err != nil { - return xerrors.Errorf("stanMQ connect nats: %w", err) - } - - option = stan.NatsConn(stanMQ.NatsClient) - } else { - option = stan.NatsURL(address) - } - - stanMQ.StanClient, err = stan.Connect( - cluster, - clientName, - option, - ) - if err != nil { - return xerrors.Errorf("stanMQ connect stan: %w", err) - } - - return nil -} - -func (stanMQ *StanMQClient) Publish(ctx context.Context, channelName string, data []byte) (err error) { - if stanMQ.async { - _, err = stanMQ.StanClient.PublishAsync( - channelName, - data, - nil, - ) - - return - } - - return stanMQ.StanClient.Publish( - channelName, - data, - ) -} +package mqclients + +import ( + "context" + "strconv" + + "github.com/nats-io/nats.go" + "github.com/nats-io/stan.go" + "golang.org/x/xerrors" +) + +func init() { + MQClients = append(MQClients, "stan") +} + +type StanMQClient struct { + NatsClient *nats.Conn `json:"-"` + StanClient stan.Conn `json:"-"` + + async bool + + channel string + cluster string +} + +func (stanMQ *StanMQClient) String() string { + return "stan" +} + +func (stanMQ *StanMQClient) Channel() string { + return stanMQ.channel +} + +func (stanMQ *StanMQClient) Cluster() string { + return stanMQ.cluster +} + +func (stanMQ *StanMQClient) Connect(ctx context.Context, clientName string, args map[string]interface{}) (err error) { + var ok bool + + var address string + + if address, ok = GetEntry(args, "Address").(string); !ok { + return xerrors.New("stanMQ connect: string type assertion failed for Address") + } + + var cluster string + + if cluster, ok = GetEntry(args, "Cluster").(string); !ok { + return xerrors.New("stanMQ connect: string type assertion failed for Cluster") + } + + var channel string + + if channel, ok = GetEntry(args, "Channel").(string); !ok { + return xerrors.New("stanMQ connect: string type assertion failed for Channel") + } + + stanMQ.cluster = cluster + stanMQ.channel = channel + + var useNatsConnection bool + + if useNatsConnectionStr, ok := GetEntry(args, "UseNATSConnection").(string); ok { + if useNatsConnection, err = strconv.ParseBool(useNatsConnectionStr); err != nil { + useNatsConnection = true + } + } else { + useNatsConnection = true + } + + if asyncStr, ok := GetEntry(args, "Async").(string); ok { + stanMQ.async, _ = strconv.ParseBool(asyncStr) + } else { + stanMQ.async = false + } + + var option stan.Option + + if useNatsConnection { + stanMQ.NatsClient, err = nats.Connect(address) + if err != nil { + return xerrors.Errorf("stanMQ connect nats: %w", err) + } + + option = stan.NatsConn(stanMQ.NatsClient) + } else { + option = stan.NatsURL(address) + } + + stanMQ.StanClient, err = stan.Connect( + cluster, + clientName, + option, + ) + if err != nil { + return xerrors.Errorf("stanMQ connect stan: %w", err) + } + + return nil +} + +func (stanMQ *StanMQClient) Publish(ctx context.Context, channelName string, data []byte) (err error) { + if stanMQ.async { + _, err = stanMQ.StanClient.PublishAsync( + channelName, + data, + nil, + ) + + return + } + + return stanMQ.StanClient.Publish( + channelName, + data, + ) +} diff --git a/messaging/utils.go b/messaging/utils.go index b074dae..30e56eb 100644 --- a/messaging/utils.go +++ b/messaging/utils.go @@ -1,18 +1,18 @@ -package mqclients - -import "strings" - -// MQClients lists all current mqclients we have available. -var MQClients = []string{} - -// Returns first match from a map and handles keys as non case sensitive. -func GetEntry(m map[string]interface{}, key string) interface{} { - key = strings.ToLower(key) - for i, k := range m { - if strings.ToLower(i) == key { - return k - } - } - - return nil -} +package mqclients + +import "strings" + +// MQClients lists all current mqclients we have available. +var MQClients = []string{} + +// Returns first match from a map and handles keys as non case sensitive. +func GetEntry(m map[string]interface{}, key string) interface{} { + key = strings.ToLower(key) + for i, k := range m { + if strings.ToLower(i) == key { + return k + } + } + + return nil +} diff --git a/structs/http.go b/structs/http.go index d6fe29d..2315525 100644 --- a/structs/http.go +++ b/structs/http.go @@ -1,19 +1,19 @@ -package structs - -// IdentifyPayload represents the payload for external identifying -type IdentifyPayload struct { - ShardID int `json:"shard_id"` - ShardCount int `json:"shard_count"` - Token string `json:"token"` - MaxConcurrency int `json:"max_concurrency"` -} - -// IdentifyResponse represents the response to external identifying -type IdentifyResponse struct { - Success bool `json:"success"` - - // If Success is false and this is passed, - // a value of 5000 represents waiting 5 seconds. - Wait int64 `json:"wait"` - Message string `json:"message"` -} +package structs + +// IdentifyPayload represents the payload for external identifying +type IdentifyPayload struct { + ShardID int `json:"shard_id"` + ShardCount int `json:"shard_count"` + Token string `json:"token"` + MaxConcurrency int `json:"max_concurrency"` +} + +// IdentifyResponse represents the response to external identifying +type IdentifyResponse struct { + Success bool `json:"success"` + + // If Success is false and this is passed, + // a value of 5000 represents waiting 5 seconds. + Wait int64 `json:"wait"` + Message string `json:"message"` +} diff --git a/structs/messaging.go b/structs/messaging.go index a25b63d..d3735e5 100644 --- a/structs/messaging.go +++ b/structs/messaging.go @@ -1,24 +1,24 @@ -package structs - -import ( - discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" -) - -// SandwichMetadata represents the identification information that consumers will use. -type SandwichMetadata struct { - Version string - Identifier string - // ShardGroup ID, Shard ID, Shard Count - Shard [3]int -} - -// SandwichPayload represents the data that is sent to consumers. -type SandwichPayload struct { - discord.GatewayPayload - - Data interface{} - - Extra map[string]interface{} - Metadata SandwichMetadata - Trace map[string]int -} +package structs + +import ( + discord "github.com/WelcomerTeam/Sandwich-Daemon/next/discord/structs" +) + +// SandwichMetadata represents the identification information that consumers will use. +type SandwichMetadata struct { + Version string + Identifier string + // ShardGroup ID, Shard ID, Shard Count + Shard [3]int +} + +// SandwichPayload represents the data that is sent to consumers. +type SandwichPayload struct { + discord.GatewayPayload + + Data interface{} + + Extra map[string]interface{} + Metadata SandwichMetadata + Trace map[string]int +} diff --git a/structs/state.go b/structs/state.go index 2f7830c..2f2dd8d 100644 --- a/structs/state.go +++ b/structs/state.go @@ -1,8 +1,8 @@ -package structs - -// StateResult represents the data a state handler would return which would be converted to -// a sandwich payload. -type StateResult struct { - Data interface{} - Extra map[string]interface{} -} +package structs + +// StateResult represents the data a state handler would return which would be converted to +// a sandwich payload. +type StateResult struct { + Data interface{} + Extra map[string]interface{} +} diff --git a/structs/status.go b/structs/status.go index c025054..a309d16 100644 --- a/structs/status.go +++ b/structs/status.go @@ -1 +1 @@ -package structs +package structs diff --git a/structure.txt b/structure.txt index 2425617..b9244fb 100644 --- a/structure.txt +++ b/structure.txt @@ -1,10 +1,10 @@ -Internal - Any generic Sandwich Daemon utilities -Discord - Interfacing with the discord client (structs, data parsing) -Gateway - ... gateway -Messaging - Messaging Clients -State - Interfacing with the Sandwich Daemon state (structs, data passing) -Structs - Generic Sandwich Daemon structures - -Grpc - Sandwich GRPC -Http - Backend +Internal - Any generic Sandwich Daemon utilities +Discord - Interfacing with the discord client (structs, data parsing) +Gateway - ... gateway +Messaging - Messaging Clients +State - Interfacing with the Sandwich Daemon state (structs, data passing) +Structs - Generic Sandwich Daemon structures + +Grpc - Sandwich GRPC +Http - Backend Web - Frontend Web application \ No newline at end of file