From dcd6955049de5207769560302db2140bd3c16cab Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Wed, 12 Nov 2025 12:19:04 +0100 Subject: [PATCH 01/36] create scaffolding for embedg-service --- embedg-service/common/id.go | 38 ++ embedg-service/config/config.go | 98 +++++ embedg-service/config/default.toml | 8 + embedg-service/config/model.go | 53 +++ embedg-service/db/postgres/client.go | 50 +++ embedg-service/db/postgres/migrater.go | 105 +++++ .../001_create_users_table.down.sql | 1 + .../migrations/001_create_users_table.up.sql | 7 + .../002_create_session_table.down.sql | 1 + .../002_create_session_table.up.sql | 8 + .../003_create_saved_messages_table.down.sql | 1 + .../003_create_saved_messages_table.up.sql | 9 + ..._create_message_action_sets_table.down.sql | 1 + ...04_create_message_action_sets_table.up.sql | 6 + .../005_create_entitlements_table.down.sql | 1 + .../005_create_entitlements_table.up.sql | 10 + .../006_create_shared_messages_table.down.sql | 1 + .../006_create_shared_messages_table.up.sql | 6 + .../007_create_custom_bots_table.down.sql | 1 + .../007_create_custom_bots_table.up.sql | 13 + .../008_create_custom_commands_table.down.sql | 1 + .../008_create_custom_commands_table.up.sql | 12 + ...09_add_derived_permissions_column.down.sql | 3 + .../009_add_derived_permissions_column.up.sql | 3 + .../migrations/010_create_db_indexes.up.sql | 7 + .../011_create_images_table.down.sql | 1 + .../migrations/011_create_images_table.up.sql | 10 + .../012_add_custom_bots_gateway.down.sql | 1 + .../012_add_custom_bots_gateway.up.sql | 1 + ...3_create_scheduled_messages_table.down.sql | 1 + ...013_create_scheduled_messages_table.up.sql | 18 + .../014_add_scheduled_messages_tz.down.sql | 1 + .../014_add_scheduled_messages_tz.up.sql | 1 + .../015_create_embed_links_table.down.sql | 1 + .../015_create_embed_links_table.up.sql | 25 ++ .../016_create_kv_entries_table.down.sql | 1 + .../016_create_kv_entries_table.up.sql | 11 + .../017_add_entitlements_consumed.down.sql | 1 + .../017_add_entitlements_consumed.up.sql | 1 + ...dd_scheduled_messages_thread_name.down.sql | 1 + ..._add_scheduled_messages_thread_name.up.sql | 1 + .../db/postgres/pgmodel/custom_bots.sql.go | 325 ++++++++++++++ .../postgres/pgmodel/custom_commands.sql.go | 268 ++++++++++++ embedg-service/db/postgres/pgmodel/db.go | 32 ++ .../db/postgres/pgmodel/embed_links.sql.go | 132 ++++++ .../db/postgres/pgmodel/entitlements.sql.go | 251 +++++++++++ .../db/postgres/pgmodel/images.sql.go | 72 ++++ .../db/postgres/pgmodel/kv_entries.sql.go | 201 +++++++++ .../pgmodel/message_action_sets.sql.go | 110 +++++ embedg-service/db/postgres/pgmodel/models.go | 160 +++++++ .../db/postgres/pgmodel/saved_messages.sql.go | 235 ++++++++++ .../pgmodel/scheduled_messages.sql.go | 403 ++++++++++++++++++ .../db/postgres/pgmodel/sessions.sql.go | 104 +++++ .../postgres/pgmodel/shared_messages.sql.go | 65 +++ .../db/postgres/pgmodel/users.sql.go | 67 +++ .../db/postgres/queries/custom_bots.sql | 28 ++ .../db/postgres/queries/custom_commands.sql | 23 + .../db/postgres/queries/embed_links.sql | 37 ++ .../db/postgres/queries/entitlements.sql | 53 +++ embedg-service/db/postgres/queries/images.sql | 5 + .../db/postgres/queries/kv_entries.sql | 54 +++ .../postgres/queries/message_action_sets.sql | 11 + .../db/postgres/queries/saved_messages.sql | 23 + .../postgres/queries/scheduled_messages.sql | 59 +++ .../db/postgres/queries/sessions.sql | 11 + .../db/postgres/queries/shared_messages.sql | 8 + embedg-service/db/postgres/queries/users.sql | 8 + embedg-service/db/s3/db_backups.go | 31 ++ embedg-service/db/s3/files.go | 86 ++++ embedg-service/db/s3/store.go | 70 +++ embedg-service/go.mod | 68 +++ embedg-service/go.sum | 200 +++++++++ embedg-service/logging/logging.go | 61 +++ embedg-service/main.go | 1 + embedg-service/model/custom_bot.go | 28 ++ embedg-service/model/custom_command.go | 24 ++ embedg-service/model/embed_link.go | 26 ++ embedg-service/model/entitlement.go | 21 + embedg-service/model/image.go | 14 + embedg-service/model/kv.go | 17 + embedg-service/model/message_action_set.go | 18 + embedg-service/model/saved_message.go | 19 + embedg-service/model/scheduled_message.go | 29 ++ embedg-service/model/session.go | 16 + embedg-service/model/shared_message.go | 10 + embedg-service/model/user.go | 14 + embedg-service/sqlc.yaml | 11 + embedg-service/store/base.go | 6 + embedg-service/store/custom_bot.go | 37 ++ embedg-service/store/custom_command.go | 20 + embedg-service/store/embed_link.go | 12 + embedg-service/store/entitlement.go | 17 + embedg-service/store/image.go | 12 + embedg-service/store/kv.go | 28 ++ embedg-service/store/message_action_set.go | 15 + embedg-service/store/saved_message.go | 19 + embedg-service/store/scheduled_message.go | 20 + embedg-service/store/session.go | 14 + embedg-service/store/shared_message.go | 14 + embedg-service/store/user.go | 14 + go.work | 2 + go.work.sum | 361 +++++++++++++++- 102 files changed, 4602 insertions(+), 18 deletions(-) create mode 100644 embedg-service/common/id.go create mode 100644 embedg-service/config/config.go create mode 100644 embedg-service/config/default.toml create mode 100644 embedg-service/config/model.go create mode 100644 embedg-service/db/postgres/client.go create mode 100644 embedg-service/db/postgres/migrater.go create mode 100644 embedg-service/db/postgres/migrations/001_create_users_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/001_create_users_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/002_create_session_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/002_create_session_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/003_create_saved_messages_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/003_create_saved_messages_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/004_create_message_action_sets_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/004_create_message_action_sets_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/005_create_entitlements_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/005_create_entitlements_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/006_create_shared_messages_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/006_create_shared_messages_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/007_create_custom_bots_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/007_create_custom_bots_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/008_create_custom_commands_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/008_create_custom_commands_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/009_add_derived_permissions_column.down.sql create mode 100644 embedg-service/db/postgres/migrations/009_add_derived_permissions_column.up.sql create mode 100644 embedg-service/db/postgres/migrations/010_create_db_indexes.up.sql create mode 100644 embedg-service/db/postgres/migrations/011_create_images_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/011_create_images_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.down.sql create mode 100644 embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.up.sql create mode 100644 embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.down.sql create mode 100644 embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.up.sql create mode 100644 embedg-service/db/postgres/migrations/015_create_embed_links_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/015_create_embed_links_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/016_create_kv_entries_table.down.sql create mode 100644 embedg-service/db/postgres/migrations/016_create_kv_entries_table.up.sql create mode 100644 embedg-service/db/postgres/migrations/017_add_entitlements_consumed.down.sql create mode 100644 embedg-service/db/postgres/migrations/017_add_entitlements_consumed.up.sql create mode 100644 embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.down.sql create mode 100644 embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.up.sql create mode 100644 embedg-service/db/postgres/pgmodel/custom_bots.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/custom_commands.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/db.go create mode 100644 embedg-service/db/postgres/pgmodel/embed_links.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/entitlements.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/images.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/kv_entries.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/message_action_sets.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/models.go create mode 100644 embedg-service/db/postgres/pgmodel/saved_messages.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/scheduled_messages.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/sessions.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/shared_messages.sql.go create mode 100644 embedg-service/db/postgres/pgmodel/users.sql.go create mode 100644 embedg-service/db/postgres/queries/custom_bots.sql create mode 100644 embedg-service/db/postgres/queries/custom_commands.sql create mode 100644 embedg-service/db/postgres/queries/embed_links.sql create mode 100644 embedg-service/db/postgres/queries/entitlements.sql create mode 100644 embedg-service/db/postgres/queries/images.sql create mode 100644 embedg-service/db/postgres/queries/kv_entries.sql create mode 100644 embedg-service/db/postgres/queries/message_action_sets.sql create mode 100644 embedg-service/db/postgres/queries/saved_messages.sql create mode 100644 embedg-service/db/postgres/queries/scheduled_messages.sql create mode 100644 embedg-service/db/postgres/queries/sessions.sql create mode 100644 embedg-service/db/postgres/queries/shared_messages.sql create mode 100644 embedg-service/db/postgres/queries/users.sql create mode 100644 embedg-service/db/s3/db_backups.go create mode 100644 embedg-service/db/s3/files.go create mode 100644 embedg-service/db/s3/store.go create mode 100644 embedg-service/go.mod create mode 100644 embedg-service/go.sum create mode 100644 embedg-service/logging/logging.go create mode 100644 embedg-service/main.go create mode 100644 embedg-service/model/custom_bot.go create mode 100644 embedg-service/model/custom_command.go create mode 100644 embedg-service/model/embed_link.go create mode 100644 embedg-service/model/entitlement.go create mode 100644 embedg-service/model/image.go create mode 100644 embedg-service/model/kv.go create mode 100644 embedg-service/model/message_action_set.go create mode 100644 embedg-service/model/saved_message.go create mode 100644 embedg-service/model/scheduled_message.go create mode 100644 embedg-service/model/session.go create mode 100644 embedg-service/model/shared_message.go create mode 100644 embedg-service/model/user.go create mode 100644 embedg-service/sqlc.yaml create mode 100644 embedg-service/store/base.go create mode 100644 embedg-service/store/custom_bot.go create mode 100644 embedg-service/store/custom_command.go create mode 100644 embedg-service/store/embed_link.go create mode 100644 embedg-service/store/entitlement.go create mode 100644 embedg-service/store/image.go create mode 100644 embedg-service/store/kv.go create mode 100644 embedg-service/store/message_action_set.go create mode 100644 embedg-service/store/saved_message.go create mode 100644 embedg-service/store/scheduled_message.go create mode 100644 embedg-service/store/session.go create mode 100644 embedg-service/store/shared_message.go create mode 100644 embedg-service/store/user.go diff --git a/embedg-service/common/id.go b/embedg-service/common/id.go new file mode 100644 index 000000000..6e9b91d59 --- /dev/null +++ b/embedg-service/common/id.go @@ -0,0 +1,38 @@ +package common + +import ( + "encoding/json" + "time" + + "github.com/disgoorg/snowflake/v2" +) + +type ID = snowflake.ID + +type NullID struct { + Valid bool + ID ID +} + +func (n NullID) MarshalJSON() ([]byte, error) { + if !n.Valid { + return []byte("null"), nil + } + return json.Marshal(n.ID) +} + +func (n *NullID) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.Valid = false + return nil + } + return json.Unmarshal(data, &n.ID) +} + +func UniqueID() snowflake.ID { + return snowflake.New(time.Now().UTC()) +} + +func ParseID(id string) (ID, error) { + return snowflake.Parse(id) +} diff --git a/embedg-service/config/config.go b/embedg-service/config/config.go new file mode 100644 index 000000000..02582621a --- /dev/null +++ b/embedg-service/config/config.go @@ -0,0 +1,98 @@ +package config + +import ( + _ "embed" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + gotoml "github.com/pelletier/go-toml/v2" + + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/knadh/koanf/v2" +) + +const ConfigFile = "stateway.toml" + +//go:embed default.toml +var defaultConfig []byte + +type Validate interface { + Validate() error +} + +func LoadConfig[T Validate]() (T, error) { + var res T + + k, err := loadBase(".") + if err != nil { + return res, fmt.Errorf("Failed to load base config: %v", err) + } + + if err := k.UnmarshalWithConf("", &res, koanf.UnmarshalConf{Tag: "toml"}); err != nil { + return res, fmt.Errorf("Failed to unmarshal full config: %v", err) + } + + if err := res.Validate(); err != nil { + return res, fmt.Errorf("Failed to validate plugin config: %v", err) + } + + return res, nil +} + +func loadBase(basePath string) (*koanf.Koanf, error) { + k := koanf.New(".") + parser := toml.Parser() + + if err := k.Load(rawbytes.Provider(defaultConfig), parser); err != nil { + return nil, fmt.Errorf("Failed to load default config: %v", err) + } + + configPath := filepath.Join(basePath, ConfigFile) + if err := k.Load(file.Provider(configPath), parser); err != nil { + var pathError *fs.PathError + if !errors.As(err, &pathError) { + return nil, fmt.Errorf("Failed to load config file: %v", err) + } + } + + envProvider := env.Provider("XENEX_", ".", func(s string) string { + return strings.Replace(strings.ToLower( + strings.TrimPrefix(s, "XENEX_")), "__", ".", -1) + }) + if err := k.Load(envProvider, nil); err != nil { + return nil, fmt.Errorf("Failed to load env config: %v", err) + } + + return k, nil +} + +func ConfigExists(basePath string) bool { + configPath := filepath.Join(basePath, ConfigFile) + _, err := os.Stat(configPath) + return err == nil +} + +func WriteConfig(basePath string, conf interface{}) error { + configPath := filepath.Join(basePath, ConfigFile) + + f, err := os.Create(configPath) + if err != nil { + return fmt.Errorf("Failed to create config file: %v", err) + } + + defer f.Close() + + encoder := gotoml.NewEncoder(f) + if err := encoder.Encode(conf); err != nil { + return fmt.Errorf("Failed to encode config: %v", err) + } + + return nil +} diff --git a/embedg-service/config/default.toml b/embedg-service/config/default.toml new file mode 100644 index 000000000..e6084ab9e --- /dev/null +++ b/embedg-service/config/default.toml @@ -0,0 +1,8 @@ +[database.postgres] +host = "127.0.0.1" +port = 5432 +user = "postgres" +db_name = "stateway" + +[broker.nats] +url = "nats://127.0.0.1:4222" diff --git a/embedg-service/config/model.go b/embedg-service/config/model.go new file mode 100644 index 000000000..238ee2dc9 --- /dev/null +++ b/embedg-service/config/model.go @@ -0,0 +1,53 @@ +package config + +import ( + "github.com/go-playground/validator/v10" +) + +type RootConfig struct { + Logging LoggingConfig `toml:"logging"` + Database DatabaseConfig `toml:"database"` + Broker BrokerConfig `toml:"broker"` +} + +func (cfg *RootConfig) Validate() error { + validate := validator.New(validator.WithRequiredStructEnabled()) + return validate.Struct(cfg) +} + +type LoggingConfig struct { + Filename string `toml:"filename"` + MaxSize int `toml:"max_size"` + MaxAge int `toml:"max_age"` + MaxBackups int `toml:"max_backups"` + Debug bool `toml:"debug"` +} + +type DatabaseConfig struct { + Postgres PostgresConfig `toml:"postgres"` + S3 S3Config `toml:"s3"` +} + +type PostgresConfig struct { + Host string `toml:"host" validate:"required"` + Port int `toml:"port" validate:"required"` + DBName string `toml:"db_name" validate:"required"` + User string `toml:"user" validate:"required"` + Password string `toml:"password"` +} + +type S3Config struct { + Endpoint string `toml:"endpoint" validate:"required"` + AccessKeyID string `toml:"access_key_id" validate:"required"` + SecretAccessKey string `toml:"secret_access_key" validate:"required"` + Secure bool `toml:"secure"` + SSECKey string `toml:"ssec_key"` +} + +type BrokerConfig struct { + NATS NATSConfig `toml:"nats"` +} + +type NATSConfig struct { + URL string `toml:"url" validate:"required"` +} diff --git a/embedg-service/db/postgres/client.go b/embedg-service/db/postgres/client.go new file mode 100644 index 000000000..5933d39b4 --- /dev/null +++ b/embedg-service/db/postgres/client.go @@ -0,0 +1,50 @@ +package postgres + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" +) + +type Client struct { + DB *pgxpool.Pool + Q *pgmodel.Queries + connectionDSN string +} + +type ClientConfig struct { + Host string + Port int + DBName string + User string + Password string +} + +func New(ctx context.Context, config ClientConfig) (*Client, error) { + connectionDSN := BuildConnectionDSN(config) + + db, err := pgxpool.New(ctx, connectionDSN) + if err != nil { + return nil, fmt.Errorf("Failed to connect to postgres db: %v", err) + } + + return &Client{ + DB: db, + Q: pgmodel.New(db), + connectionDSN: connectionDSN, + }, nil +} + +func BuildConnectionDSN(cfg ClientConfig) string { + dsn := fmt.Sprintf( + "host=%s port=%d dbname=%s user=%s sslmode=disable connect_timeout=4", + cfg.Host, cfg.Port, cfg.DBName, cfg.User, + ) + + if cfg.Password != "" { + dsn += " password=" + cfg.Password + } + return dsn +} diff --git a/embedg-service/db/postgres/migrater.go b/embedg-service/db/postgres/migrater.go new file mode 100644 index 000000000..3d6597917 --- /dev/null +++ b/embedg-service/db/postgres/migrater.go @@ -0,0 +1,105 @@ +package postgres + +import ( + "database/sql" + "embed" + "fmt" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed migrations/*.sql +var migrationsFS embed.FS + +type Migrater struct { + m *migrate.Migrate + close func() error +} + +func (mig *Migrater) Up() error { + return mig.m.Up() +} + +func (mig *Migrater) Down() error { + return mig.m.Down() +} + +func (mig *Migrater) Version() (uint, bool, error) { + return mig.m.Version() +} + +func (mig *Migrater) To(version uint) error { + return mig.m.Migrate(version) +} + +func (mig *Migrater) Force(version int) error { + return mig.m.Force(version) +} + +func (mig *Migrater) List() ([]string, error) { + dirEntries, err := migrationsFS.ReadDir("migrations") + if err != nil { + return nil, err + } + + migrationFiles := make([]string, 0) + for _, entry := range dirEntries { + migrationFiles = append(migrationFiles, entry.Name()) + } + return migrationFiles, nil +} + +func (mig *Migrater) Close() error { + return mig.close() +} + +func (mig *Migrater) SetLogger(logger migrate.Logger) { + mig.m.Log = logger +} + +func (pgs *Client) GetMigrater() (*Migrater, error) { + d, err := iofs.New(migrationsFS, "migrations") + if err != nil { + return nil, fmt.Errorf("failed to open Postgres migrations iofs: %w", err) + } + + db, err := sql.Open("postgres", pgs.connectionDSN) + if err != nil { + return nil, fmt.Errorf("failed to open postgres db with postgres driver: %w", err) + } + defer db.Close() + + _, err = db.Exec("CREATE SCHEMA IF NOT EXISTS gateway") + if err != nil { + return nil, fmt.Errorf("failed to create gateway schema: %w", err) + } + + driver, err := postgres.WithInstance(db, &postgres.Config{ + SchemaName: "gateway", + }) + if err != nil { + return nil, fmt.Errorf("failed to open postgres migration: %w", err) + } + + m, err := migrate.NewWithInstance( + "iofs", d, + "postgres", driver) + if err != nil { + return nil, fmt.Errorf("failed to create Postgres migrate instance: %w", err) + } + + close := func() error { + err1, err2 := m.Close() + if err1 != nil || err2 != nil { + return fmt.Errorf("source close error: %v, driver close error: %v", err1, err2) + } + return nil + } + + return &Migrater{ + m: m, + close: close, + }, nil +} diff --git a/embedg-service/db/postgres/migrations/001_create_users_table.down.sql b/embedg-service/db/postgres/migrations/001_create_users_table.down.sql new file mode 100644 index 000000000..365a21075 --- /dev/null +++ b/embedg-service/db/postgres/migrations/001_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/001_create_users_table.up.sql b/embedg-service/db/postgres/migrations/001_create_users_table.up.sql new file mode 100644 index 000000000..13f821912 --- /dev/null +++ b/embedg-service/db/postgres/migrations/001_create_users_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + discriminator TEXT NOT NULL, + avatar TEXT, + is_tester BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/embedg-service/db/postgres/migrations/002_create_session_table.down.sql b/embedg-service/db/postgres/migrations/002_create_session_table.down.sql new file mode 100644 index 000000000..3b1518818 --- /dev/null +++ b/embedg-service/db/postgres/migrations/002_create_session_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS sessions; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/002_create_session_table.up.sql b/embedg-service/db/postgres/migrations/002_create_session_table.up.sql new file mode 100644 index 000000000..821dd2a91 --- /dev/null +++ b/embedg-service/db/postgres/migrations/002_create_session_table.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS sessions ( + token_hash TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + guild_ids TEXT[] NOT NULL, + access_token TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/003_create_saved_messages_table.down.sql b/embedg-service/db/postgres/migrations/003_create_saved_messages_table.down.sql new file mode 100644 index 000000000..d2426afac --- /dev/null +++ b/embedg-service/db/postgres/migrations/003_create_saved_messages_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS saved_messages; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/003_create_saved_messages_table.up.sql b/embedg-service/db/postgres/migrations/003_create_saved_messages_table.up.sql new file mode 100644 index 000000000..9aaf38f8c --- /dev/null +++ b/embedg-service/db/postgres/migrations/003_create_saved_messages_table.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS saved_messages ( + id TEXT PRIMARY KEY, + creator_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + guild_id TEXT, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL, + description TEXT, + data JSONB NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/004_create_message_action_sets_table.down.sql b/embedg-service/db/postgres/migrations/004_create_message_action_sets_table.down.sql new file mode 100644 index 000000000..de9e55a5e --- /dev/null +++ b/embedg-service/db/postgres/migrations/004_create_message_action_sets_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS message_action_sets; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/004_create_message_action_sets_table.up.sql b/embedg-service/db/postgres/migrations/004_create_message_action_sets_table.up.sql new file mode 100644 index 000000000..ee39aa1f1 --- /dev/null +++ b/embedg-service/db/postgres/migrations/004_create_message_action_sets_table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS message_action_sets ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + set_id TEXT NOT NULL, + actions JSONB NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/005_create_entitlements_table.down.sql b/embedg-service/db/postgres/migrations/005_create_entitlements_table.down.sql new file mode 100644 index 000000000..2502c6412 --- /dev/null +++ b/embedg-service/db/postgres/migrations/005_create_entitlements_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS entitlements; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/005_create_entitlements_table.up.sql b/embedg-service/db/postgres/migrations/005_create_entitlements_table.up.sql new file mode 100644 index 000000000..405f014df --- /dev/null +++ b/embedg-service/db/postgres/migrations/005_create_entitlements_table.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS entitlements ( + id TEXT PRIMARY KEY, + user_id TEXT, + guild_id TEXT, + updated_at TIMESTAMP NOT NULL, + deleted BOOLEAN NOT NULL, + sku_id TEXT NOT NULL, + starts_at TIMESTAMP, + ends_at TIMESTAMP +); \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/006_create_shared_messages_table.down.sql b/embedg-service/db/postgres/migrations/006_create_shared_messages_table.down.sql new file mode 100644 index 000000000..982c36874 --- /dev/null +++ b/embedg-service/db/postgres/migrations/006_create_shared_messages_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS shared_messages; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/006_create_shared_messages_table.up.sql b/embedg-service/db/postgres/migrations/006_create_shared_messages_table.up.sql new file mode 100644 index 000000000..ab628eb63 --- /dev/null +++ b/embedg-service/db/postgres/migrations/006_create_shared_messages_table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS shared_messages ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + data JSONB NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/007_create_custom_bots_table.down.sql b/embedg-service/db/postgres/migrations/007_create_custom_bots_table.down.sql new file mode 100644 index 000000000..da761ae69 --- /dev/null +++ b/embedg-service/db/postgres/migrations/007_create_custom_bots_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS custom_bots; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/007_create_custom_bots_table.up.sql b/embedg-service/db/postgres/migrations/007_create_custom_bots_table.up.sql new file mode 100644 index 000000000..bfa555d93 --- /dev/null +++ b/embedg-service/db/postgres/migrations/007_create_custom_bots_table.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS custom_bots ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL UNIQUE, + application_id TEXT NOT NULL, + token TEXT NOT NULL, + public_key TEXT NOT NULL, + user_id TEXT NOT NULL, + user_name TEXT NOT NULL, + user_discriminator TEXT NOT NULL, + user_avatar TEXT, + handled_first_interaction BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/008_create_custom_commands_table.down.sql b/embedg-service/db/postgres/migrations/008_create_custom_commands_table.down.sql new file mode 100644 index 000000000..3a406546d --- /dev/null +++ b/embedg-service/db/postgres/migrations/008_create_custom_commands_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS custom_commands; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/008_create_custom_commands_table.up.sql b/embedg-service/db/postgres/migrations/008_create_custom_commands_table.up.sql new file mode 100644 index 000000000..2b5f672cd --- /dev/null +++ b/embedg-service/db/postgres/migrations/008_create_custom_commands_table.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS custom_commands ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT true, + parameters JSONB NOT NULL, + actions JSONB NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + deployed_at TIMESTAMP +); diff --git a/embedg-service/db/postgres/migrations/009_add_derived_permissions_column.down.sql b/embedg-service/db/postgres/migrations/009_add_derived_permissions_column.down.sql new file mode 100644 index 000000000..f43cc1c6e --- /dev/null +++ b/embedg-service/db/postgres/migrations/009_add_derived_permissions_column.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE message_action_sets DROP COLUMN derived_permissions, DROP COLUMN last_used_at, DROP COLUMN ephemeral; + +ALTER TABLE custom_commands DROP COLUMN derived_permissions, DROP COLUMN last_used_at; diff --git a/embedg-service/db/postgres/migrations/009_add_derived_permissions_column.up.sql b/embedg-service/db/postgres/migrations/009_add_derived_permissions_column.up.sql new file mode 100644 index 000000000..848be8607 --- /dev/null +++ b/embedg-service/db/postgres/migrations/009_add_derived_permissions_column.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE message_action_sets ADD COLUMN derived_permissions JSONB, ADD COLUMN last_used_at TIMESTAMP NOT NULL DEFAULT NOW(), ADD COLUMN ephemeral BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE custom_commands ADD COLUMN derived_permissions JSONB, ADD COLUMN last_used_at TIMESTAMP NOT NULL DEFAULT NOW(); diff --git a/embedg-service/db/postgres/migrations/010_create_db_indexes.up.sql b/embedg-service/db/postgres/migrations/010_create_db_indexes.up.sql new file mode 100644 index 000000000..f520bbda6 --- /dev/null +++ b/embedg-service/db/postgres/migrations/010_create_db_indexes.up.sql @@ -0,0 +1,7 @@ +CREATE INDEX ON saved_messages (guild_id); +CREATE INDEX ON saved_messages (creator_id); + +CREATE INDEX ON custom_commands (guild_id); + +CREATE INDEX ON message_action_sets (message_id); +CREATE INDEX ON message_action_sets (set_id); diff --git a/embedg-service/db/postgres/migrations/011_create_images_table.down.sql b/embedg-service/db/postgres/migrations/011_create_images_table.down.sql new file mode 100644 index 000000000..b9f21e659 --- /dev/null +++ b/embedg-service/db/postgres/migrations/011_create_images_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS images; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/011_create_images_table.up.sql b/embedg-service/db/postgres/migrations/011_create_images_table.up.sql new file mode 100644 index 000000000..1d0450230 --- /dev/null +++ b/embedg-service/db/postgres/migrations/011_create_images_table.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS images ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + guild_id TEXT, + file_hash TEXT NOT NULL, + file_name TEXT NOT NULL, + file_size INTEGER NOT NULL, + file_content_type TEXT NOT NULL, + s3_key TEXT NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.down.sql b/embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.down.sql new file mode 100644 index 000000000..f4a8b1733 --- /dev/null +++ b/embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.down.sql @@ -0,0 +1 @@ +ALTER TABLE custom_bots DROP COLUMN token_invalid, DROP COLUMN gateway_status, DROP COLUMN gateway_activity_type, DROP COLUMN gateway_activity_name, DROP COLUMN gateway_activity_state, DROP COLUMN gateway_activity_url; diff --git a/embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.up.sql b/embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.up.sql new file mode 100644 index 000000000..7bf0d3138 --- /dev/null +++ b/embedg-service/db/postgres/migrations/012_add_custom_bots_gateway.up.sql @@ -0,0 +1 @@ +ALTER TABLE custom_bots ADD COLUMN token_invalid BOOLEAN NOT NULL DEFAULT false, ADD COLUMN gateway_status TEXT NOT NULL DEFAULT 'online', ADD COLUMN gateway_activity_type SMALLINT, ADD COLUMN gateway_activity_name TEXT, ADD COLUMN gateway_activity_state TEXT, ADD COLUMN gateway_activity_url TEXT; diff --git a/embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.down.sql b/embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.down.sql new file mode 100644 index 000000000..48f3a8c44 --- /dev/null +++ b/embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS scheduled_messages; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.up.sql b/embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.up.sql new file mode 100644 index 000000000..99ef4aa0f --- /dev/null +++ b/embedg-service/db/postgres/migrations/013_create_scheduled_messages_table.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS scheduled_messages ( + id TEXT PRIMARY KEY, + creator_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + message_id TEXT, + saved_message_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + cron_expression TEXT, -- This may be null if the message is scheduled to only be sent once + only_once BOOLEAN NOT NULL DEFAULT false, -- Whether the message should be sent only once or repeatedly + start_at TIMESTAMP NOT NULL, -- The first time the message was / will be sent + end_at TIMESTAMP, -- The last time the message was / will be sent + next_at TIMESTAMP NOT NULL, -- The next or only time the message should be sent + enabled BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.down.sql b/embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.down.sql new file mode 100644 index 000000000..689cbedbf --- /dev/null +++ b/embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.down.sql @@ -0,0 +1 @@ +ALTER TABLE scheduled_messages DROP COLUMN cron_timezone; diff --git a/embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.up.sql b/embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.up.sql new file mode 100644 index 000000000..40c34dd32 --- /dev/null +++ b/embedg-service/db/postgres/migrations/014_add_scheduled_messages_tz.up.sql @@ -0,0 +1 @@ +ALTER TABLE scheduled_messages ADD COLUMN cron_timezone TEXT; -- The timezone to use for the cron expression diff --git a/embedg-service/db/postgres/migrations/015_create_embed_links_table.down.sql b/embedg-service/db/postgres/migrations/015_create_embed_links_table.down.sql new file mode 100644 index 000000000..e07089779 --- /dev/null +++ b/embedg-service/db/postgres/migrations/015_create_embed_links_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS embed_links; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/015_create_embed_links_table.up.sql b/embedg-service/db/postgres/migrations/015_create_embed_links_table.up.sql new file mode 100644 index 000000000..4c086f0a2 --- /dev/null +++ b/embedg-service/db/postgres/migrations/015_create_embed_links_table.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS embed_links ( + id TEXT PRIMARY KEY, + + url TEXT NOT NULL, + theme_color TEXT, + + -- Additional OpenGraph metadata + og_title TEXT, + og_site_name TEXT, + og_description TEXT, + og_image TEXT, + + -- Additional oEmbed metadata + oe_type TEXT, + oe_author_name TEXT, + oe_author_url TEXT, + oe_provider_name TEXT, + oe_provider_url TEXT, + + -- Additional Twitter metadata + tw_card TEXT, + + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL +); diff --git a/embedg-service/db/postgres/migrations/016_create_kv_entries_table.down.sql b/embedg-service/db/postgres/migrations/016_create_kv_entries_table.down.sql new file mode 100644 index 000000000..45a8d9e67 --- /dev/null +++ b/embedg-service/db/postgres/migrations/016_create_kv_entries_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS kv_entries; diff --git a/embedg-service/db/postgres/migrations/016_create_kv_entries_table.up.sql b/embedg-service/db/postgres/migrations/016_create_kv_entries_table.up.sql new file mode 100644 index 000000000..b9ec074d2 --- /dev/null +++ b/embedg-service/db/postgres/migrations/016_create_kv_entries_table.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS kv_entries ( + key TEXT NOT NULL, + guild_id TEXT NOT NULL, + value TEXT NOT NULL, + + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + + PRIMARY KEY (key, guild_id) +); diff --git a/embedg-service/db/postgres/migrations/017_add_entitlements_consumed.down.sql b/embedg-service/db/postgres/migrations/017_add_entitlements_consumed.down.sql new file mode 100644 index 000000000..264de7f8d --- /dev/null +++ b/embedg-service/db/postgres/migrations/017_add_entitlements_consumed.down.sql @@ -0,0 +1 @@ +ALTER TABLE entitlements DROP COLUMN consumed, DROP COLUMN consumed_guild_id; \ No newline at end of file diff --git a/embedg-service/db/postgres/migrations/017_add_entitlements_consumed.up.sql b/embedg-service/db/postgres/migrations/017_add_entitlements_consumed.up.sql new file mode 100644 index 000000000..51ebf8a59 --- /dev/null +++ b/embedg-service/db/postgres/migrations/017_add_entitlements_consumed.up.sql @@ -0,0 +1 @@ +ALTER TABLE entitlements ADD COLUMN consumed BOOLEAN NOT NULL DEFAULT FALSE, ADD COLUMN consumed_guild_id TEXT; diff --git a/embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.down.sql b/embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.down.sql new file mode 100644 index 000000000..1798d5aeb --- /dev/null +++ b/embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.down.sql @@ -0,0 +1 @@ +ALTER TABLE scheduled_messages DROP COLUMN thread_name; diff --git a/embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.up.sql b/embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.up.sql new file mode 100644 index 000000000..12f3b2e9d --- /dev/null +++ b/embedg-service/db/postgres/migrations/018_add_scheduled_messages_thread_name.up.sql @@ -0,0 +1 @@ +ALTER TABLE scheduled_messages ADD COLUMN thread_name TEXT; diff --git a/embedg-service/db/postgres/pgmodel/custom_bots.sql.go b/embedg-service/db/postgres/pgmodel/custom_bots.sql.go new file mode 100644 index 000000000..6493bf710 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/custom_bots.sql.go @@ -0,0 +1,325 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: custom_bots.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteCustomBot = `-- name: DeleteCustomBot :one +DELETE FROM custom_bots WHERE guild_id = $1 RETURNING id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url +` + +func (q *Queries) DeleteCustomBot(ctx context.Context, guildID string) (CustomBot, error) { + row := q.db.QueryRow(ctx, deleteCustomBot, guildID) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} + +const getCustomBot = `-- name: GetCustomBot :one +SELECT id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url FROM custom_bots WHERE id = $1 +` + +func (q *Queries) GetCustomBot(ctx context.Context, id string) (CustomBot, error) { + row := q.db.QueryRow(ctx, getCustomBot, id) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} + +const getCustomBotByGuildID = `-- name: GetCustomBotByGuildID :one +SELECT id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url FROM custom_bots WHERE guild_id = $1 +` + +func (q *Queries) GetCustomBotByGuildID(ctx context.Context, guildID string) (CustomBot, error) { + row := q.db.QueryRow(ctx, getCustomBotByGuildID, guildID) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} + +const getCustomBots = `-- name: GetCustomBots :many +SELECT id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url FROM custom_bots +` + +func (q *Queries) GetCustomBots(ctx context.Context) ([]CustomBot, error) { + rows, err := q.db.Query(ctx, getCustomBots) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CustomBot + for rows.Next() { + var i CustomBot + if err := rows.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setCustomBotHandledFirstInteraction = `-- name: SetCustomBotHandledFirstInteraction :exec +UPDATE custom_bots SET handled_first_interaction = true WHERE id = $1 +` + +func (q *Queries) SetCustomBotHandledFirstInteraction(ctx context.Context, id string) error { + _, err := q.db.Exec(ctx, setCustomBotHandledFirstInteraction, id) + return err +} + +const updateCustomBotPresence = `-- name: UpdateCustomBotPresence :one +UPDATE custom_bots SET gateway_status = $2, gateway_activity_type = $3, gateway_activity_name = $4, gateway_activity_state = $5, gateway_activity_url = $6 WHERE guild_id = $1 RETURNING id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url +` + +type UpdateCustomBotPresenceParams struct { + GuildID string + GatewayStatus string + GatewayActivityType pgtype.Int2 + GatewayActivityName pgtype.Text + GatewayActivityState pgtype.Text + GatewayActivityUrl pgtype.Text +} + +func (q *Queries) UpdateCustomBotPresence(ctx context.Context, arg UpdateCustomBotPresenceParams) (CustomBot, error) { + row := q.db.QueryRow(ctx, updateCustomBotPresence, + arg.GuildID, + arg.GatewayStatus, + arg.GatewayActivityType, + arg.GatewayActivityName, + arg.GatewayActivityState, + arg.GatewayActivityUrl, + ) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} + +const updateCustomBotTokenInvalid = `-- name: UpdateCustomBotTokenInvalid :one +UPDATE custom_bots SET token_invalid = $2 WHERE guild_id = $1 RETURNING id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url +` + +type UpdateCustomBotTokenInvalidParams struct { + GuildID string + TokenInvalid bool +} + +func (q *Queries) UpdateCustomBotTokenInvalid(ctx context.Context, arg UpdateCustomBotTokenInvalidParams) (CustomBot, error) { + row := q.db.QueryRow(ctx, updateCustomBotTokenInvalid, arg.GuildID, arg.TokenInvalid) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} + +const updateCustomBotUser = `-- name: UpdateCustomBotUser :one +UPDATE custom_bots SET user_name = $2, user_discriminator = $3, user_avatar = $4 WHERE guild_id = $1 RETURNING id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url +` + +type UpdateCustomBotUserParams struct { + GuildID string + UserName string + UserDiscriminator string + UserAvatar pgtype.Text +} + +func (q *Queries) UpdateCustomBotUser(ctx context.Context, arg UpdateCustomBotUserParams) (CustomBot, error) { + row := q.db.QueryRow(ctx, updateCustomBotUser, + arg.GuildID, + arg.UserName, + arg.UserDiscriminator, + arg.UserAvatar, + ) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} + +const upsertCustomBot = `-- name: UpsertCustomBot :one +INSERT INTO custom_bots (id, guild_id, application_id, user_id, user_name, user_discriminator, user_avatar, token, public_key, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +ON CONFLICT (guild_id) DO UPDATE SET id = $1, application_id = $3, user_id = $4, user_name = $5, user_discriminator = $6, user_avatar = $7, token = $8, public_key = $9, created_at = $10, handled_first_interaction = false, token_invalid = false +RETURNING id, guild_id, application_id, token, public_key, user_id, user_name, user_discriminator, user_avatar, handled_first_interaction, created_at, token_invalid, gateway_status, gateway_activity_type, gateway_activity_name, gateway_activity_state, gateway_activity_url +` + +type UpsertCustomBotParams struct { + ID string + GuildID string + ApplicationID string + UserID string + UserName string + UserDiscriminator string + UserAvatar pgtype.Text + Token string + PublicKey string + CreatedAt pgtype.Timestamp +} + +func (q *Queries) UpsertCustomBot(ctx context.Context, arg UpsertCustomBotParams) (CustomBot, error) { + row := q.db.QueryRow(ctx, upsertCustomBot, + arg.ID, + arg.GuildID, + arg.ApplicationID, + arg.UserID, + arg.UserName, + arg.UserDiscriminator, + arg.UserAvatar, + arg.Token, + arg.PublicKey, + arg.CreatedAt, + ) + var i CustomBot + err := row.Scan( + &i.ID, + &i.GuildID, + &i.ApplicationID, + &i.Token, + &i.PublicKey, + &i.UserID, + &i.UserName, + &i.UserDiscriminator, + &i.UserAvatar, + &i.HandledFirstInteraction, + &i.CreatedAt, + &i.TokenInvalid, + &i.GatewayStatus, + &i.GatewayActivityType, + &i.GatewayActivityName, + &i.GatewayActivityState, + &i.GatewayActivityUrl, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/custom_commands.sql.go b/embedg-service/db/postgres/pgmodel/custom_commands.sql.go new file mode 100644 index 000000000..8b1246b68 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/custom_commands.sql.go @@ -0,0 +1,268 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: custom_commands.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const countCustomCommands = `-- name: CountCustomCommands :one +SELECT COUNT(*) FROM custom_commands WHERE guild_id = $1 +` + +func (q *Queries) CountCustomCommands(ctx context.Context, guildID string) (int64, error) { + row := q.db.QueryRow(ctx, countCustomCommands, guildID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const deleteCustomCommand = `-- name: DeleteCustomCommand :one +DELETE FROM custom_commands WHERE id = $1 AND guild_id = $2 RETURNING id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at +` + +type DeleteCustomCommandParams struct { + ID string + GuildID string +} + +func (q *Queries) DeleteCustomCommand(ctx context.Context, arg DeleteCustomCommandParams) (CustomCommand, error) { + row := q.db.QueryRow(ctx, deleteCustomCommand, arg.ID, arg.GuildID) + var i CustomCommand + err := row.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ) + return i, err +} + +const getCustomCommand = `-- name: GetCustomCommand :one +SELECT id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at FROM custom_commands WHERE id = $1 AND guild_id = $2 +` + +type GetCustomCommandParams struct { + ID string + GuildID string +} + +func (q *Queries) GetCustomCommand(ctx context.Context, arg GetCustomCommandParams) (CustomCommand, error) { + row := q.db.QueryRow(ctx, getCustomCommand, arg.ID, arg.GuildID) + var i CustomCommand + err := row.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ) + return i, err +} + +const getCustomCommandByName = `-- name: GetCustomCommandByName :one +SELECT id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at FROM custom_commands WHERE name = $1 AND guild_id = $2 +` + +type GetCustomCommandByNameParams struct { + Name string + GuildID string +} + +func (q *Queries) GetCustomCommandByName(ctx context.Context, arg GetCustomCommandByNameParams) (CustomCommand, error) { + row := q.db.QueryRow(ctx, getCustomCommandByName, arg.Name, arg.GuildID) + var i CustomCommand + err := row.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ) + return i, err +} + +const getCustomCommands = `-- name: GetCustomCommands :many +SELECT id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at FROM custom_commands WHERE guild_id = $1 +` + +func (q *Queries) GetCustomCommands(ctx context.Context, guildID string) ([]CustomCommand, error) { + rows, err := q.db.Query(ctx, getCustomCommands, guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CustomCommand + for rows.Next() { + var i CustomCommand + if err := rows.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertCustomCommand = `-- name: InsertCustomCommand :one +INSERT INTO custom_commands (id, guild_id, name, description, parameters, actions, derived_permissions, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at +` + +type InsertCustomCommandParams struct { + ID string + GuildID string + Name string + Description string + Parameters []byte + Actions []byte + DerivedPermissions []byte + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) InsertCustomCommand(ctx context.Context, arg InsertCustomCommandParams) (CustomCommand, error) { + row := q.db.QueryRow(ctx, insertCustomCommand, + arg.ID, + arg.GuildID, + arg.Name, + arg.Description, + arg.Parameters, + arg.Actions, + arg.DerivedPermissions, + arg.CreatedAt, + arg.UpdatedAt, + ) + var i CustomCommand + err := row.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ) + return i, err +} + +const setCustomCommandsDeployedAt = `-- name: SetCustomCommandsDeployedAt :one +UPDATE custom_commands SET deployed_at = $2 WHERE guild_id = $1 RETURNING id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at +` + +type SetCustomCommandsDeployedAtParams struct { + GuildID string + DeployedAt pgtype.Timestamp +} + +func (q *Queries) SetCustomCommandsDeployedAt(ctx context.Context, arg SetCustomCommandsDeployedAtParams) (CustomCommand, error) { + row := q.db.QueryRow(ctx, setCustomCommandsDeployedAt, arg.GuildID, arg.DeployedAt) + var i CustomCommand + err := row.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ) + return i, err +} + +const updateCustomCommand = `-- name: UpdateCustomCommand :one +UPDATE custom_commands SET name = $3, description = $4, enabled = $5, actions = $6, parameters = $7, derived_permissions = $8, updated_at = $9 WHERE id = $1 AND guild_id = $2 RETURNING id, guild_id, name, description, enabled, parameters, actions, created_at, updated_at, deployed_at, derived_permissions, last_used_at +` + +type UpdateCustomCommandParams struct { + ID string + GuildID string + Name string + Description string + Enabled bool + Actions []byte + Parameters []byte + DerivedPermissions []byte + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) UpdateCustomCommand(ctx context.Context, arg UpdateCustomCommandParams) (CustomCommand, error) { + row := q.db.QueryRow(ctx, updateCustomCommand, + arg.ID, + arg.GuildID, + arg.Name, + arg.Description, + arg.Enabled, + arg.Actions, + arg.Parameters, + arg.DerivedPermissions, + arg.UpdatedAt, + ) + var i CustomCommand + err := row.Scan( + &i.ID, + &i.GuildID, + &i.Name, + &i.Description, + &i.Enabled, + &i.Parameters, + &i.Actions, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeployedAt, + &i.DerivedPermissions, + &i.LastUsedAt, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/db.go b/embedg-service/db/postgres/pgmodel/db.go new file mode 100644 index 000000000..5791df857 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/embedg-service/db/postgres/pgmodel/embed_links.sql.go b/embedg-service/db/postgres/pgmodel/embed_links.sql.go new file mode 100644 index 000000000..aec409e4b --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/embed_links.sql.go @@ -0,0 +1,132 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: embed_links.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getEmbedLink = `-- name: GetEmbedLink :one +SELECT id, url, theme_color, og_title, og_site_name, og_description, og_image, oe_type, oe_author_name, oe_author_url, oe_provider_name, oe_provider_url, tw_card, expires_at, created_at FROM embed_links WHERE id = $1 +` + +func (q *Queries) GetEmbedLink(ctx context.Context, id string) (EmbedLink, error) { + row := q.db.QueryRow(ctx, getEmbedLink, id) + var i EmbedLink + err := row.Scan( + &i.ID, + &i.Url, + &i.ThemeColor, + &i.OgTitle, + &i.OgSiteName, + &i.OgDescription, + &i.OgImage, + &i.OeType, + &i.OeAuthorName, + &i.OeAuthorUrl, + &i.OeProviderName, + &i.OeProviderUrl, + &i.TwCard, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} + +const insertEmbedLink = `-- name: InsertEmbedLink :one +INSERT INTO embed_links ( + id, + url, + theme_color, + og_title, + og_site_name, + og_description, + og_image, + oe_type, + oe_author_name, + oe_author_url, + oe_provider_name, + oe_provider_url, + tw_card, + expires_at, + created_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 +) RETURNING id, url, theme_color, og_title, og_site_name, og_description, og_image, oe_type, oe_author_name, oe_author_url, oe_provider_name, oe_provider_url, tw_card, expires_at, created_at +` + +type InsertEmbedLinkParams struct { + ID string + Url string + ThemeColor pgtype.Text + OgTitle pgtype.Text + OgSiteName pgtype.Text + OgDescription pgtype.Text + OgImage pgtype.Text + OeType pgtype.Text + OeAuthorName pgtype.Text + OeAuthorUrl pgtype.Text + OeProviderName pgtype.Text + OeProviderUrl pgtype.Text + TwCard pgtype.Text + ExpiresAt pgtype.Timestamp + CreatedAt pgtype.Timestamp +} + +func (q *Queries) InsertEmbedLink(ctx context.Context, arg InsertEmbedLinkParams) (EmbedLink, error) { + row := q.db.QueryRow(ctx, insertEmbedLink, + arg.ID, + arg.Url, + arg.ThemeColor, + arg.OgTitle, + arg.OgSiteName, + arg.OgDescription, + arg.OgImage, + arg.OeType, + arg.OeAuthorName, + arg.OeAuthorUrl, + arg.OeProviderName, + arg.OeProviderUrl, + arg.TwCard, + arg.ExpiresAt, + arg.CreatedAt, + ) + var i EmbedLink + err := row.Scan( + &i.ID, + &i.Url, + &i.ThemeColor, + &i.OgTitle, + &i.OgSiteName, + &i.OgDescription, + &i.OgImage, + &i.OeType, + &i.OeAuthorName, + &i.OeAuthorUrl, + &i.OeProviderName, + &i.OeProviderUrl, + &i.TwCard, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/entitlements.sql.go b/embedg-service/db/postgres/pgmodel/entitlements.sql.go new file mode 100644 index 000000000..0cbbe01bd --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/entitlements.sql.go @@ -0,0 +1,251 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: entitlements.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getActiveEntitlementsForGuild = `-- name: GetActiveEntitlementsForGuild :many +SELECT id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id FROM entitlements +WHERE deleted = false + AND (starts_at IS NULL OR starts_at < NOW()) + AND (ends_at IS NULL OR ends_at > NOW()) + AND (guild_id = $1 OR consumed_guild_id = $1) +` + +func (q *Queries) GetActiveEntitlementsForGuild(ctx context.Context, guildID pgtype.Text) ([]Entitlement, error) { + rows, err := q.db.Query(ctx, getActiveEntitlementsForGuild, guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Entitlement + for rows.Next() { + var i Entitlement + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.UpdatedAt, + &i.Deleted, + &i.SkuID, + &i.StartsAt, + &i.EndsAt, + &i.Consumed, + &i.ConsumedGuildID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getActiveEntitlementsForUser = `-- name: GetActiveEntitlementsForUser :many +SELECT id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id FROM entitlements +WHERE deleted = false + AND (starts_at IS NULL OR starts_at < NOW()) + AND (ends_at IS NULL OR ends_at > NOW()) + AND user_id = $1 +` + +func (q *Queries) GetActiveEntitlementsForUser(ctx context.Context, userID pgtype.Text) ([]Entitlement, error) { + rows, err := q.db.Query(ctx, getActiveEntitlementsForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Entitlement + for rows.Next() { + var i Entitlement + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.UpdatedAt, + &i.Deleted, + &i.SkuID, + &i.StartsAt, + &i.EndsAt, + &i.Consumed, + &i.ConsumedGuildID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getEntitlement = `-- name: GetEntitlement :one +SELECT id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id FROM entitlements WHERE id = $1 AND user_id = $2 +` + +type GetEntitlementParams struct { + ID string + UserID pgtype.Text +} + +func (q *Queries) GetEntitlement(ctx context.Context, arg GetEntitlementParams) (Entitlement, error) { + row := q.db.QueryRow(ctx, getEntitlement, arg.ID, arg.UserID) + var i Entitlement + err := row.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.UpdatedAt, + &i.Deleted, + &i.SkuID, + &i.StartsAt, + &i.EndsAt, + &i.Consumed, + &i.ConsumedGuildID, + ) + return i, err +} + +const getEntitlements = `-- name: GetEntitlements :many +SELECT id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id FROM entitlements +` + +func (q *Queries) GetEntitlements(ctx context.Context) ([]Entitlement, error) { + rows, err := q.db.Query(ctx, getEntitlements) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Entitlement + for rows.Next() { + var i Entitlement + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.UpdatedAt, + &i.Deleted, + &i.SkuID, + &i.StartsAt, + &i.EndsAt, + &i.Consumed, + &i.ConsumedGuildID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateEntitlementConsumedGuildID = `-- name: UpdateEntitlementConsumedGuildID :one +UPDATE entitlements SET consumed = true, consumed_guild_id = $2 WHERE id = $1 RETURNING id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id +` + +type UpdateEntitlementConsumedGuildIDParams struct { + ID string + ConsumedGuildID pgtype.Text +} + +func (q *Queries) UpdateEntitlementConsumedGuildID(ctx context.Context, arg UpdateEntitlementConsumedGuildIDParams) (Entitlement, error) { + row := q.db.QueryRow(ctx, updateEntitlementConsumedGuildID, arg.ID, arg.ConsumedGuildID) + var i Entitlement + err := row.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.UpdatedAt, + &i.Deleted, + &i.SkuID, + &i.StartsAt, + &i.EndsAt, + &i.Consumed, + &i.ConsumedGuildID, + ) + return i, err +} + +const upsertEntitlement = `-- name: UpsertEntitlement :one +INSERT INTO entitlements ( + id, + user_id, + guild_id, + updated_at, + deleted, + sku_id, + starts_at, + ends_at, + consumed +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9 +) +ON CONFLICT (id) +DO UPDATE SET + deleted = $5, + starts_at = $7, + ends_at = $8, + updated_at = $4, + consumed = $9 +RETURNING id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id +` + +type UpsertEntitlementParams struct { + ID string + UserID pgtype.Text + GuildID pgtype.Text + UpdatedAt pgtype.Timestamp + Deleted bool + SkuID string + StartsAt pgtype.Timestamp + EndsAt pgtype.Timestamp + Consumed bool +} + +func (q *Queries) UpsertEntitlement(ctx context.Context, arg UpsertEntitlementParams) (Entitlement, error) { + row := q.db.QueryRow(ctx, upsertEntitlement, + arg.ID, + arg.UserID, + arg.GuildID, + arg.UpdatedAt, + arg.Deleted, + arg.SkuID, + arg.StartsAt, + arg.EndsAt, + arg.Consumed, + ) + var i Entitlement + err := row.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.UpdatedAt, + &i.Deleted, + &i.SkuID, + &i.StartsAt, + &i.EndsAt, + &i.Consumed, + &i.ConsumedGuildID, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/images.sql.go b/embedg-service/db/postgres/pgmodel/images.sql.go new file mode 100644 index 000000000..e461f8a92 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/images.sql.go @@ -0,0 +1,72 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: images.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getImage = `-- name: GetImage :one +SELECT id, user_id, guild_id, file_hash, file_name, file_size, file_content_type, s3_key FROM images WHERE id = $1 +` + +func (q *Queries) GetImage(ctx context.Context, id string) (Image, error) { + row := q.db.QueryRow(ctx, getImage, id) + var i Image + err := row.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.FileHash, + &i.FileName, + &i.FileSize, + &i.FileContentType, + &i.S3Key, + ) + return i, err +} + +const insertImage = `-- name: InsertImage :one +INSERT INTO images (id, guild_id, user_id, file_hash, file_name, file_content_type, file_size, s3_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, user_id, guild_id, file_hash, file_name, file_size, file_content_type, s3_key +` + +type InsertImageParams struct { + ID string + GuildID pgtype.Text + UserID string + FileHash string + FileName string + FileContentType string + FileSize int32 + S3Key string +} + +func (q *Queries) InsertImage(ctx context.Context, arg InsertImageParams) (Image, error) { + row := q.db.QueryRow(ctx, insertImage, + arg.ID, + arg.GuildID, + arg.UserID, + arg.FileHash, + arg.FileName, + arg.FileContentType, + arg.FileSize, + arg.S3Key, + ) + var i Image + err := row.Scan( + &i.ID, + &i.UserID, + &i.GuildID, + &i.FileHash, + &i.FileName, + &i.FileSize, + &i.FileContentType, + &i.S3Key, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/kv_entries.sql.go b/embedg-service/db/postgres/pgmodel/kv_entries.sql.go new file mode 100644 index 000000000..0a08a58a6 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/kv_entries.sql.go @@ -0,0 +1,201 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: kv_entries.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const countKVEntries = `-- name: CountKVEntries :one +SELECT COUNT(*) FROM kv_entries WHERE guild_id = $1 +` + +func (q *Queries) CountKVEntries(ctx context.Context, guildID string) (int64, error) { + row := q.db.QueryRow(ctx, countKVEntries, guildID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const deleteKVEntry = `-- name: DeleteKVEntry :one +DELETE FROM kv_entries WHERE key = $1 AND guild_id = $2 RETURNING key, guild_id, value, expires_at, created_at, updated_at +` + +type DeleteKVEntryParams struct { + Key string + GuildID string +} + +func (q *Queries) DeleteKVEntry(ctx context.Context, arg DeleteKVEntryParams) (KvEntry, error) { + row := q.db.QueryRow(ctx, deleteKVEntry, arg.Key, arg.GuildID) + var i KvEntry + err := row.Scan( + &i.Key, + &i.GuildID, + &i.Value, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getKVEntry = `-- name: GetKVEntry :one +SELECT key, guild_id, value, expires_at, created_at, updated_at FROM kv_entries WHERE key = $1 AND guild_id = $2 +` + +type GetKVEntryParams struct { + Key string + GuildID string +} + +func (q *Queries) GetKVEntry(ctx context.Context, arg GetKVEntryParams) (KvEntry, error) { + row := q.db.QueryRow(ctx, getKVEntry, arg.Key, arg.GuildID) + var i KvEntry + err := row.Scan( + &i.Key, + &i.GuildID, + &i.Value, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const increaseKVEntry = `-- name: IncreaseKVEntry :one +INSERT INTO kv_entries ( + key, + guild_id, + value, + expires_at, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) ON CONFLICT (key, guild_id) +DO UPDATE SET + value = kv_entries.value::int + EXCLUDED.value::int, + expires_at = EXCLUDED.expires_at, + updated_at = EXCLUDED.updated_at +RETURNING key, guild_id, value, expires_at, created_at, updated_at +` + +type IncreaseKVEntryParams struct { + Key string + GuildID string + Value string + ExpiresAt pgtype.Timestamp + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) IncreaseKVEntry(ctx context.Context, arg IncreaseKVEntryParams) (KvEntry, error) { + row := q.db.QueryRow(ctx, increaseKVEntry, + arg.Key, + arg.GuildID, + arg.Value, + arg.ExpiresAt, + arg.CreatedAt, + arg.UpdatedAt, + ) + var i KvEntry + err := row.Scan( + &i.Key, + &i.GuildID, + &i.Value, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const searchKVEntries = `-- name: SearchKVEntries :many +SELECT key, guild_id, value, expires_at, created_at, updated_at FROM kv_entries WHERE key LIKE $1 AND guild_id = $2 +` + +type SearchKVEntriesParams struct { + Key string + GuildID string +} + +func (q *Queries) SearchKVEntries(ctx context.Context, arg SearchKVEntriesParams) ([]KvEntry, error) { + rows, err := q.db.Query(ctx, searchKVEntries, arg.Key, arg.GuildID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []KvEntry + for rows.Next() { + var i KvEntry + if err := rows.Scan( + &i.Key, + &i.GuildID, + &i.Value, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setKVEntry = `-- name: SetKVEntry :exec +INSERT INTO kv_entries ( + key, + guild_id, + value, + expires_at, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) ON CONFLICT (key, guild_id) +DO UPDATE SET + value = EXCLUDED.value, + expires_at = EXCLUDED.expires_at, + updated_at = EXCLUDED.updated_at +` + +type SetKVEntryParams struct { + Key string + GuildID string + Value string + ExpiresAt pgtype.Timestamp + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) SetKVEntry(ctx context.Context, arg SetKVEntryParams) error { + _, err := q.db.Exec(ctx, setKVEntry, + arg.Key, + arg.GuildID, + arg.Value, + arg.ExpiresAt, + arg.CreatedAt, + arg.UpdatedAt, + ) + return err +} diff --git a/embedg-service/db/postgres/pgmodel/message_action_sets.sql.go b/embedg-service/db/postgres/pgmodel/message_action_sets.sql.go new file mode 100644 index 000000000..25e2a0896 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/message_action_sets.sql.go @@ -0,0 +1,110 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: message_action_sets.sql + +package pgmodel + +import ( + "context" +) + +const deleteMessageActionSetsForMessage = `-- name: DeleteMessageActionSetsForMessage :exec +DELETE FROM message_action_sets WHERE message_id = $1 +` + +func (q *Queries) DeleteMessageActionSetsForMessage(ctx context.Context, messageID string) error { + _, err := q.db.Exec(ctx, deleteMessageActionSetsForMessage, messageID) + return err +} + +const getMessageActionSet = `-- name: GetMessageActionSet :one +SELECT id, message_id, set_id, actions, derived_permissions, last_used_at, ephemeral FROM message_action_sets WHERE message_id = $1 AND set_id = $2 +` + +type GetMessageActionSetParams struct { + MessageID string + SetID string +} + +func (q *Queries) GetMessageActionSet(ctx context.Context, arg GetMessageActionSetParams) (MessageActionSet, error) { + row := q.db.QueryRow(ctx, getMessageActionSet, arg.MessageID, arg.SetID) + var i MessageActionSet + err := row.Scan( + &i.ID, + &i.MessageID, + &i.SetID, + &i.Actions, + &i.DerivedPermissions, + &i.LastUsedAt, + &i.Ephemeral, + ) + return i, err +} + +const getMessageActionSets = `-- name: GetMessageActionSets :many +SELECT id, message_id, set_id, actions, derived_permissions, last_used_at, ephemeral FROM message_action_sets WHERE message_id = $1 +` + +func (q *Queries) GetMessageActionSets(ctx context.Context, messageID string) ([]MessageActionSet, error) { + rows, err := q.db.Query(ctx, getMessageActionSets, messageID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MessageActionSet + for rows.Next() { + var i MessageActionSet + if err := rows.Scan( + &i.ID, + &i.MessageID, + &i.SetID, + &i.Actions, + &i.DerivedPermissions, + &i.LastUsedAt, + &i.Ephemeral, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertMessageActionSet = `-- name: InsertMessageActionSet :one +INSERT INTO message_action_sets (id, message_id, set_id, actions, derived_permissions, ephemeral) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, message_id, set_id, actions, derived_permissions, last_used_at, ephemeral +` + +type InsertMessageActionSetParams struct { + ID string + MessageID string + SetID string + Actions []byte + DerivedPermissions []byte + Ephemeral bool +} + +func (q *Queries) InsertMessageActionSet(ctx context.Context, arg InsertMessageActionSetParams) (MessageActionSet, error) { + row := q.db.QueryRow(ctx, insertMessageActionSet, + arg.ID, + arg.MessageID, + arg.SetID, + arg.Actions, + arg.DerivedPermissions, + arg.Ephemeral, + ) + var i MessageActionSet + err := row.Scan( + &i.ID, + &i.MessageID, + &i.SetID, + &i.Actions, + &i.DerivedPermissions, + &i.LastUsedAt, + &i.Ephemeral, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/models.go b/embedg-service/db/postgres/pgmodel/models.go new file mode 100644 index 000000000..ad6031f78 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/models.go @@ -0,0 +1,160 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package pgmodel + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type CustomBot struct { + ID string + GuildID string + ApplicationID string + Token string + PublicKey string + UserID string + UserName string + UserDiscriminator string + UserAvatar pgtype.Text + HandledFirstInteraction bool + CreatedAt pgtype.Timestamp + TokenInvalid bool + GatewayStatus string + GatewayActivityType pgtype.Int2 + GatewayActivityName pgtype.Text + GatewayActivityState pgtype.Text + GatewayActivityUrl pgtype.Text +} + +type CustomCommand struct { + ID string + GuildID string + Name string + Description string + Enabled bool + Parameters []byte + Actions []byte + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + DeployedAt pgtype.Timestamp + DerivedPermissions []byte + LastUsedAt pgtype.Timestamp +} + +type EmbedLink struct { + ID string + Url string + ThemeColor pgtype.Text + OgTitle pgtype.Text + OgSiteName pgtype.Text + OgDescription pgtype.Text + OgImage pgtype.Text + OeType pgtype.Text + OeAuthorName pgtype.Text + OeAuthorUrl pgtype.Text + OeProviderName pgtype.Text + OeProviderUrl pgtype.Text + TwCard pgtype.Text + ExpiresAt pgtype.Timestamp + CreatedAt pgtype.Timestamp +} + +type Entitlement struct { + ID string + UserID pgtype.Text + GuildID pgtype.Text + UpdatedAt pgtype.Timestamp + Deleted bool + SkuID string + StartsAt pgtype.Timestamp + EndsAt pgtype.Timestamp + Consumed bool + ConsumedGuildID pgtype.Text +} + +type Image struct { + ID string + UserID string + GuildID pgtype.Text + FileHash string + FileName string + FileSize int32 + FileContentType string + S3Key string +} + +type KvEntry struct { + Key string + GuildID string + Value string + ExpiresAt pgtype.Timestamp + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +type MessageActionSet struct { + ID string + MessageID string + SetID string + Actions []byte + DerivedPermissions []byte + LastUsedAt pgtype.Timestamp + Ephemeral bool +} + +type SavedMessage struct { + ID string + CreatorID string + GuildID pgtype.Text + UpdatedAt pgtype.Timestamp + Name string + Description pgtype.Text + Data []byte +} + +type ScheduledMessage struct { + ID string + CreatorID string + GuildID string + ChannelID string + MessageID pgtype.Text + SavedMessageID string + Name string + Description pgtype.Text + CronExpression pgtype.Text + OnlyOnce bool + StartAt pgtype.Timestamp + EndAt pgtype.Timestamp + NextAt pgtype.Timestamp + Enabled bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + CronTimezone pgtype.Text + ThreadName pgtype.Text +} + +type Session struct { + TokenHash string + UserID string + GuildIds []string + AccessToken string + CreatedAt pgtype.Timestamp + ExpiresAt pgtype.Timestamp +} + +type SharedMessage struct { + ID string + CreatedAt pgtype.Timestamp + ExpiresAt pgtype.Timestamp + Data []byte +} + +type User struct { + ID string + Name string + Discriminator string + Avatar pgtype.Text + IsTester bool +} diff --git a/embedg-service/db/postgres/pgmodel/saved_messages.sql.go b/embedg-service/db/postgres/pgmodel/saved_messages.sql.go new file mode 100644 index 000000000..b73dec4be --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/saved_messages.sql.go @@ -0,0 +1,235 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: saved_messages.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteSavedMessageForCreator = `-- name: DeleteSavedMessageForCreator :exec +DELETE FROM saved_messages WHERE id = $1 AND creator_id = $2 +` + +type DeleteSavedMessageForCreatorParams struct { + ID string + CreatorID string +} + +func (q *Queries) DeleteSavedMessageForCreator(ctx context.Context, arg DeleteSavedMessageForCreatorParams) error { + _, err := q.db.Exec(ctx, deleteSavedMessageForCreator, arg.ID, arg.CreatorID) + return err +} + +const deleteSavedMessageForGuild = `-- name: DeleteSavedMessageForGuild :exec +DELETE FROM saved_messages WHERE id = $1 AND guild_id = $2 +` + +type DeleteSavedMessageForGuildParams struct { + ID string + GuildID pgtype.Text +} + +func (q *Queries) DeleteSavedMessageForGuild(ctx context.Context, arg DeleteSavedMessageForGuildParams) error { + _, err := q.db.Exec(ctx, deleteSavedMessageForGuild, arg.ID, arg.GuildID) + return err +} + +const getSavedMessageForGuild = `-- name: GetSavedMessageForGuild :one +SELECT id, creator_id, guild_id, updated_at, name, description, data FROM saved_messages WHERE guild_id = $1 AND id = $2 +` + +type GetSavedMessageForGuildParams struct { + GuildID pgtype.Text + ID string +} + +func (q *Queries) GetSavedMessageForGuild(ctx context.Context, arg GetSavedMessageForGuildParams) (SavedMessage, error) { + row := q.db.QueryRow(ctx, getSavedMessageForGuild, arg.GuildID, arg.ID) + var i SavedMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.UpdatedAt, + &i.Name, + &i.Description, + &i.Data, + ) + return i, err +} + +const getSavedMessagesForCreator = `-- name: GetSavedMessagesForCreator :many +SELECT id, creator_id, guild_id, updated_at, name, description, data FROM saved_messages WHERE creator_id = $1 AND guild_id IS NULL ORDER BY updated_at DESC +` + +func (q *Queries) GetSavedMessagesForCreator(ctx context.Context, creatorID string) ([]SavedMessage, error) { + rows, err := q.db.Query(ctx, getSavedMessagesForCreator, creatorID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SavedMessage + for rows.Next() { + var i SavedMessage + if err := rows.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.UpdatedAt, + &i.Name, + &i.Description, + &i.Data, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getSavedMessagesForGuild = `-- name: GetSavedMessagesForGuild :many +SELECT id, creator_id, guild_id, updated_at, name, description, data FROM saved_messages WHERE guild_id = $1 ORDER BY updated_at DESC +` + +func (q *Queries) GetSavedMessagesForGuild(ctx context.Context, guildID pgtype.Text) ([]SavedMessage, error) { + rows, err := q.db.Query(ctx, getSavedMessagesForGuild, guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SavedMessage + for rows.Next() { + var i SavedMessage + if err := rows.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.UpdatedAt, + &i.Name, + &i.Description, + &i.Data, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertSavedMessage = `-- name: InsertSavedMessage :one +INSERT INTO saved_messages (id, creator_id, guild_id, updated_at, name, description, data) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, creator_id, guild_id, updated_at, name, description, data +` + +type InsertSavedMessageParams struct { + ID string + CreatorID string + GuildID pgtype.Text + UpdatedAt pgtype.Timestamp + Name string + Description pgtype.Text + Data []byte +} + +func (q *Queries) InsertSavedMessage(ctx context.Context, arg InsertSavedMessageParams) (SavedMessage, error) { + row := q.db.QueryRow(ctx, insertSavedMessage, + arg.ID, + arg.CreatorID, + arg.GuildID, + arg.UpdatedAt, + arg.Name, + arg.Description, + arg.Data, + ) + var i SavedMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.UpdatedAt, + &i.Name, + &i.Description, + &i.Data, + ) + return i, err +} + +const updateSavedMessageForCreator = `-- name: UpdateSavedMessageForCreator :one +UPDATE saved_messages SET updated_at = $3, name = $4, description = $5, data = $6 WHERE id = $1 AND creator_id = $2 RETURNING id, creator_id, guild_id, updated_at, name, description, data +` + +type UpdateSavedMessageForCreatorParams struct { + ID string + CreatorID string + UpdatedAt pgtype.Timestamp + Name string + Description pgtype.Text + Data []byte +} + +func (q *Queries) UpdateSavedMessageForCreator(ctx context.Context, arg UpdateSavedMessageForCreatorParams) (SavedMessage, error) { + row := q.db.QueryRow(ctx, updateSavedMessageForCreator, + arg.ID, + arg.CreatorID, + arg.UpdatedAt, + arg.Name, + arg.Description, + arg.Data, + ) + var i SavedMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.UpdatedAt, + &i.Name, + &i.Description, + &i.Data, + ) + return i, err +} + +const updateSavedMessageForGuild = `-- name: UpdateSavedMessageForGuild :one +UPDATE saved_messages SET updated_at = $3, name = $4, description = $5, data = $6 WHERE id = $1 AND guild_id = $2 RETURNING id, creator_id, guild_id, updated_at, name, description, data +` + +type UpdateSavedMessageForGuildParams struct { + ID string + GuildID pgtype.Text + UpdatedAt pgtype.Timestamp + Name string + Description pgtype.Text + Data []byte +} + +func (q *Queries) UpdateSavedMessageForGuild(ctx context.Context, arg UpdateSavedMessageForGuildParams) (SavedMessage, error) { + row := q.db.QueryRow(ctx, updateSavedMessageForGuild, + arg.ID, + arg.GuildID, + arg.UpdatedAt, + arg.Name, + arg.Description, + arg.Data, + ) + var i SavedMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.UpdatedAt, + &i.Name, + &i.Description, + &i.Data, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/scheduled_messages.sql.go b/embedg-service/db/postgres/pgmodel/scheduled_messages.sql.go new file mode 100644 index 000000000..f6b9e75e3 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/scheduled_messages.sql.go @@ -0,0 +1,403 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: scheduled_messages.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteScheduledMessage = `-- name: DeleteScheduledMessage :exec +DELETE FROM scheduled_messages WHERE id = $1 AND guild_id = $2 +` + +type DeleteScheduledMessageParams struct { + ID string + GuildID string +} + +func (q *Queries) DeleteScheduledMessage(ctx context.Context, arg DeleteScheduledMessageParams) error { + _, err := q.db.Exec(ctx, deleteScheduledMessage, arg.ID, arg.GuildID) + return err +} + +const getDueScheduledMessages = `-- name: GetDueScheduledMessages :many +SELECT id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name FROM scheduled_messages WHERE next_at <= $1 AND (end_at IS NULL OR end_at >= $1) AND enabled = true +` + +func (q *Queries) GetDueScheduledMessages(ctx context.Context, nextAt pgtype.Timestamp) ([]ScheduledMessage, error) { + rows, err := q.db.Query(ctx, getDueScheduledMessages, nextAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ScheduledMessage + for rows.Next() { + var i ScheduledMessage + if err := rows.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getScheduledMessage = `-- name: GetScheduledMessage :one +SELECT id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name FROM scheduled_messages WHERE id = $1 AND guild_id = $2 +` + +type GetScheduledMessageParams struct { + ID string + GuildID string +} + +func (q *Queries) GetScheduledMessage(ctx context.Context, arg GetScheduledMessageParams) (ScheduledMessage, error) { + row := q.db.QueryRow(ctx, getScheduledMessage, arg.ID, arg.GuildID) + var i ScheduledMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ) + return i, err +} + +const getScheduledMessages = `-- name: GetScheduledMessages :many +SELECT id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name FROM scheduled_messages WHERE guild_id = $1 ORDER BY updated_at DESC +` + +func (q *Queries) GetScheduledMessages(ctx context.Context, guildID string) ([]ScheduledMessage, error) { + rows, err := q.db.Query(ctx, getScheduledMessages, guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ScheduledMessage + for rows.Next() { + var i ScheduledMessage + if err := rows.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertScheduledMessage = `-- name: InsertScheduledMessage :one +INSERT INTO scheduled_messages ( + id, + creator_id, + guild_id, + channel_id, + message_id, + thread_name, + saved_message_id, + name, + description, + cron_expression, + cron_timezone, + start_at, + end_at, + next_at, + only_once, + enabled, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 +) RETURNING id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name +` + +type InsertScheduledMessageParams struct { + ID string + CreatorID string + GuildID string + ChannelID string + MessageID pgtype.Text + ThreadName pgtype.Text + SavedMessageID string + Name string + Description pgtype.Text + CronExpression pgtype.Text + CronTimezone pgtype.Text + StartAt pgtype.Timestamp + EndAt pgtype.Timestamp + NextAt pgtype.Timestamp + OnlyOnce bool + Enabled bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) InsertScheduledMessage(ctx context.Context, arg InsertScheduledMessageParams) (ScheduledMessage, error) { + row := q.db.QueryRow(ctx, insertScheduledMessage, + arg.ID, + arg.CreatorID, + arg.GuildID, + arg.ChannelID, + arg.MessageID, + arg.ThreadName, + arg.SavedMessageID, + arg.Name, + arg.Description, + arg.CronExpression, + arg.CronTimezone, + arg.StartAt, + arg.EndAt, + arg.NextAt, + arg.OnlyOnce, + arg.Enabled, + arg.CreatedAt, + arg.UpdatedAt, + ) + var i ScheduledMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ) + return i, err +} + +const updateScheduledMessage = `-- name: UpdateScheduledMessage :one +UPDATE scheduled_messages SET + channel_id = $3, + message_id = $4, + thread_name = $5, + saved_message_id = $6, + name = $7, + description = $8, + cron_expression = $9, + next_at = $10, + start_at = $11, + end_at = $12, + only_once = $13, + enabled = $14, + updated_at = $15, + cron_timezone = $16 +WHERE id = $1 AND guild_id = $2 RETURNING id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name +` + +type UpdateScheduledMessageParams struct { + ID string + GuildID string + ChannelID string + MessageID pgtype.Text + ThreadName pgtype.Text + SavedMessageID string + Name string + Description pgtype.Text + CronExpression pgtype.Text + NextAt pgtype.Timestamp + StartAt pgtype.Timestamp + EndAt pgtype.Timestamp + OnlyOnce bool + Enabled bool + UpdatedAt pgtype.Timestamp + CronTimezone pgtype.Text +} + +func (q *Queries) UpdateScheduledMessage(ctx context.Context, arg UpdateScheduledMessageParams) (ScheduledMessage, error) { + row := q.db.QueryRow(ctx, updateScheduledMessage, + arg.ID, + arg.GuildID, + arg.ChannelID, + arg.MessageID, + arg.ThreadName, + arg.SavedMessageID, + arg.Name, + arg.Description, + arg.CronExpression, + arg.NextAt, + arg.StartAt, + arg.EndAt, + arg.OnlyOnce, + arg.Enabled, + arg.UpdatedAt, + arg.CronTimezone, + ) + var i ScheduledMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ) + return i, err +} + +const updateScheduledMessageEnabled = `-- name: UpdateScheduledMessageEnabled :one +UPDATE scheduled_messages SET enabled = $3, updated_at = $4 WHERE id = $1 AND guild_id = $2 RETURNING id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name +` + +type UpdateScheduledMessageEnabledParams struct { + ID string + GuildID string + Enabled bool + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) UpdateScheduledMessageEnabled(ctx context.Context, arg UpdateScheduledMessageEnabledParams) (ScheduledMessage, error) { + row := q.db.QueryRow(ctx, updateScheduledMessageEnabled, + arg.ID, + arg.GuildID, + arg.Enabled, + arg.UpdatedAt, + ) + var i ScheduledMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ) + return i, err +} + +const updateScheduledMessageNextAt = `-- name: UpdateScheduledMessageNextAt :one +UPDATE scheduled_messages SET next_at = $3, updated_at = $4 WHERE id = $1 AND guild_id = $2 RETURNING id, creator_id, guild_id, channel_id, message_id, saved_message_id, name, description, cron_expression, only_once, start_at, end_at, next_at, enabled, created_at, updated_at, cron_timezone, thread_name +` + +type UpdateScheduledMessageNextAtParams struct { + ID string + GuildID string + NextAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +func (q *Queries) UpdateScheduledMessageNextAt(ctx context.Context, arg UpdateScheduledMessageNextAtParams) (ScheduledMessage, error) { + row := q.db.QueryRow(ctx, updateScheduledMessageNextAt, + arg.ID, + arg.GuildID, + arg.NextAt, + arg.UpdatedAt, + ) + var i ScheduledMessage + err := row.Scan( + &i.ID, + &i.CreatorID, + &i.GuildID, + &i.ChannelID, + &i.MessageID, + &i.SavedMessageID, + &i.Name, + &i.Description, + &i.CronExpression, + &i.OnlyOnce, + &i.StartAt, + &i.EndAt, + &i.NextAt, + &i.Enabled, + &i.CreatedAt, + &i.UpdatedAt, + &i.CronTimezone, + &i.ThreadName, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/sessions.sql.go b/embedg-service/db/postgres/pgmodel/sessions.sql.go new file mode 100644 index 000000000..4f4e28710 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/sessions.sql.go @@ -0,0 +1,104 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: sessions.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteSession = `-- name: DeleteSession :exec +DELETE FROM sessions WHERE token_hash = $1 +` + +func (q *Queries) DeleteSession(ctx context.Context, tokenHash string) error { + _, err := q.db.Exec(ctx, deleteSession, tokenHash) + return err +} + +const getSession = `-- name: GetSession :one +SELECT token_hash, user_id, guild_ids, access_token, created_at, expires_at FROM sessions WHERE token_hash = $1 +` + +func (q *Queries) GetSession(ctx context.Context, tokenHash string) (Session, error) { + row := q.db.QueryRow(ctx, getSession, tokenHash) + var i Session + err := row.Scan( + &i.TokenHash, + &i.UserID, + &i.GuildIds, + &i.AccessToken, + &i.CreatedAt, + &i.ExpiresAt, + ) + return i, err +} + +const getSessionsForUser = `-- name: GetSessionsForUser :many +SELECT token_hash, user_id, guild_ids, access_token, created_at, expires_at FROM sessions WHERE user_id = $1 +` + +func (q *Queries) GetSessionsForUser(ctx context.Context, userID string) ([]Session, error) { + rows, err := q.db.Query(ctx, getSessionsForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Session + for rows.Next() { + var i Session + if err := rows.Scan( + &i.TokenHash, + &i.UserID, + &i.GuildIds, + &i.AccessToken, + &i.CreatedAt, + &i.ExpiresAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertSession = `-- name: InsertSession :one +INSERT INTO sessions (token_hash, user_id, guild_ids, access_token, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING token_hash, user_id, guild_ids, access_token, created_at, expires_at +` + +type InsertSessionParams struct { + TokenHash string + UserID string + GuildIds []string + AccessToken string + CreatedAt pgtype.Timestamp + ExpiresAt pgtype.Timestamp +} + +func (q *Queries) InsertSession(ctx context.Context, arg InsertSessionParams) (Session, error) { + row := q.db.QueryRow(ctx, insertSession, + arg.TokenHash, + arg.UserID, + arg.GuildIds, + arg.AccessToken, + arg.CreatedAt, + arg.ExpiresAt, + ) + var i Session + err := row.Scan( + &i.TokenHash, + &i.UserID, + &i.GuildIds, + &i.AccessToken, + &i.CreatedAt, + &i.ExpiresAt, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/shared_messages.sql.go b/embedg-service/db/postgres/pgmodel/shared_messages.sql.go new file mode 100644 index 000000000..a11722496 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/shared_messages.sql.go @@ -0,0 +1,65 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: shared_messages.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteExpiredSharedMessages = `-- name: DeleteExpiredSharedMessages :exec +DELETE FROM shared_messages WHERE expires_at < $1 +` + +func (q *Queries) DeleteExpiredSharedMessages(ctx context.Context, expiresAt pgtype.Timestamp) error { + _, err := q.db.Exec(ctx, deleteExpiredSharedMessages, expiresAt) + return err +} + +const getSharedMessage = `-- name: GetSharedMessage :one +SELECT id, created_at, expires_at, data FROM shared_messages WHERE id = $1 +` + +func (q *Queries) GetSharedMessage(ctx context.Context, id string) (SharedMessage, error) { + row := q.db.QueryRow(ctx, getSharedMessage, id) + var i SharedMessage + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.ExpiresAt, + &i.Data, + ) + return i, err +} + +const insertSharedMessage = `-- name: InsertSharedMessage :one +INSERT INTO shared_messages (id, created_at, expires_at, data) VALUES ($1, $2, $3, $4) RETURNING id, created_at, expires_at, data +` + +type InsertSharedMessageParams struct { + ID string + CreatedAt pgtype.Timestamp + ExpiresAt pgtype.Timestamp + Data []byte +} + +func (q *Queries) InsertSharedMessage(ctx context.Context, arg InsertSharedMessageParams) (SharedMessage, error) { + row := q.db.QueryRow(ctx, insertSharedMessage, + arg.ID, + arg.CreatedAt, + arg.ExpiresAt, + arg.Data, + ) + var i SharedMessage + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.ExpiresAt, + &i.Data, + ) + return i, err +} diff --git a/embedg-service/db/postgres/pgmodel/users.sql.go b/embedg-service/db/postgres/pgmodel/users.sql.go new file mode 100644 index 000000000..20faad8c5 --- /dev/null +++ b/embedg-service/db/postgres/pgmodel/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: users.sql + +package pgmodel + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users WHERE id = $1 +` + +func (q *Queries) DeleteUser(ctx context.Context, id string) error { + _, err := q.db.Exec(ctx, deleteUser, id) + return err +} + +const getUser = `-- name: GetUser :one +SELECT id, name, discriminator, avatar, is_tester FROM users WHERE id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, id string) (User, error) { + row := q.db.QueryRow(ctx, getUser, id) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.Discriminator, + &i.Avatar, + &i.IsTester, + ) + return i, err +} + +const upsertUser = `-- name: UpsertUser :one +INSERT INTO users (id, name, discriminator, avatar) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET name = $2, discriminator = $3, avatar = $4 RETURNING id, name, discriminator, avatar, is_tester +` + +type UpsertUserParams struct { + ID string + Name string + Discriminator string + Avatar pgtype.Text +} + +func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, error) { + row := q.db.QueryRow(ctx, upsertUser, + arg.ID, + arg.Name, + arg.Discriminator, + arg.Avatar, + ) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.Discriminator, + &i.Avatar, + &i.IsTester, + ) + return i, err +} diff --git a/embedg-service/db/postgres/queries/custom_bots.sql b/embedg-service/db/postgres/queries/custom_bots.sql new file mode 100644 index 000000000..e5aae9eb4 --- /dev/null +++ b/embedg-service/db/postgres/queries/custom_bots.sql @@ -0,0 +1,28 @@ +-- name: UpsertCustomBot :one +INSERT INTO custom_bots (id, guild_id, application_id, user_id, user_name, user_discriminator, user_avatar, token, public_key, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +ON CONFLICT (guild_id) DO UPDATE SET id = $1, application_id = $3, user_id = $4, user_name = $5, user_discriminator = $6, user_avatar = $7, token = $8, public_key = $9, created_at = $10, handled_first_interaction = false, token_invalid = false +RETURNING *; + +-- name: UpdateCustomBotPresence :one +UPDATE custom_bots SET gateway_status = $2, gateway_activity_type = $3, gateway_activity_name = $4, gateway_activity_state = $5, gateway_activity_url = $6 WHERE guild_id = $1 RETURNING *; + +-- name: UpdateCustomBotUser :one +UPDATE custom_bots SET user_name = $2, user_discriminator = $3, user_avatar = $4 WHERE guild_id = $1 RETURNING *; + +-- name: UpdateCustomBotTokenInvalid :one +UPDATE custom_bots SET token_invalid = $2 WHERE guild_id = $1 RETURNING *; + +-- name: DeleteCustomBot :one +DELETE FROM custom_bots WHERE guild_id = $1 RETURNING *; + +-- name: GetCustomBot :one +SELECT * FROM custom_bots WHERE id = $1; + +-- name: GetCustomBotByGuildID :one +SELECT * FROM custom_bots WHERE guild_id = $1; + +-- name: SetCustomBotHandledFirstInteraction :exec +UPDATE custom_bots SET handled_first_interaction = true WHERE id = $1; + +-- name: GetCustomBots :many +SELECT * FROM custom_bots; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/custom_commands.sql b/embedg-service/db/postgres/queries/custom_commands.sql new file mode 100644 index 000000000..eb9d7a79d --- /dev/null +++ b/embedg-service/db/postgres/queries/custom_commands.sql @@ -0,0 +1,23 @@ +-- name: GetCustomCommands :many +SELECT * FROM custom_commands WHERE guild_id = $1; + +-- name: GetCustomCommand :one +SELECT * FROM custom_commands WHERE id = $1 AND guild_id = $2; + +-- name: GetCustomCommandByName :one +SELECT * FROM custom_commands WHERE name = $1 AND guild_id = $2; + +-- name: CountCustomCommands :one +SELECT COUNT(*) FROM custom_commands WHERE guild_id = $1; + +-- name: InsertCustomCommand :one +INSERT INTO custom_commands (id, guild_id, name, description, parameters, actions, derived_permissions, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; + +-- name: UpdateCustomCommand :one +UPDATE custom_commands SET name = $3, description = $4, enabled = $5, actions = $6, parameters = $7, derived_permissions = $8, updated_at = $9 WHERE id = $1 AND guild_id = $2 RETURNING *; + +-- name: DeleteCustomCommand :one +DELETE FROM custom_commands WHERE id = $1 AND guild_id = $2 RETURNING *; + +-- name: SetCustomCommandsDeployedAt :one +UPDATE custom_commands SET deployed_at = $2 WHERE guild_id = $1 RETURNING *; diff --git a/embedg-service/db/postgres/queries/embed_links.sql b/embedg-service/db/postgres/queries/embed_links.sql new file mode 100644 index 000000000..ca68d3713 --- /dev/null +++ b/embedg-service/db/postgres/queries/embed_links.sql @@ -0,0 +1,37 @@ +-- name: InsertEmbedLink :one +INSERT INTO embed_links ( + id, + url, + theme_color, + og_title, + og_site_name, + og_description, + og_image, + oe_type, + oe_author_name, + oe_author_url, + oe_provider_name, + oe_provider_url, + tw_card, + expires_at, + created_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 +) RETURNING *; + +-- name: GetEmbedLink :one +SELECT * FROM embed_links WHERE id = $1; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/entitlements.sql b/embedg-service/db/postgres/queries/entitlements.sql new file mode 100644 index 000000000..3e75cc4c3 --- /dev/null +++ b/embedg-service/db/postgres/queries/entitlements.sql @@ -0,0 +1,53 @@ +-- name: GetActiveEntitlementsForGuild :many +SELECT * FROM entitlements +WHERE deleted = false + AND (starts_at IS NULL OR starts_at < NOW()) + AND (ends_at IS NULL OR ends_at > NOW()) + AND (guild_id = $1 OR consumed_guild_id = $1); + +-- name: GetActiveEntitlementsForUser :many +SELECT * FROM entitlements +WHERE deleted = false + AND (starts_at IS NULL OR starts_at < NOW()) + AND (ends_at IS NULL OR ends_at > NOW()) + AND user_id = $1; + +-- name: GetEntitlements :many +SELECT * FROM entitlements; + +-- name: GetEntitlement :one +SELECT * FROM entitlements WHERE id = $1 AND user_id = $2; + +-- name: UpdateEntitlementConsumedGuildID :one +UPDATE entitlements SET consumed = true, consumed_guild_id = $2 WHERE id = $1 RETURNING *; + +-- name: UpsertEntitlement :one +INSERT INTO entitlements ( + id, + user_id, + guild_id, + updated_at, + deleted, + sku_id, + starts_at, + ends_at, + consumed +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9 +) +ON CONFLICT (id) +DO UPDATE SET + deleted = $5, + starts_at = $7, + ends_at = $8, + updated_at = $4, + consumed = $9 +RETURNING *; diff --git a/embedg-service/db/postgres/queries/images.sql b/embedg-service/db/postgres/queries/images.sql new file mode 100644 index 000000000..92eb9d1b6 --- /dev/null +++ b/embedg-service/db/postgres/queries/images.sql @@ -0,0 +1,5 @@ +-- name: InsertImage :one +INSERT INTO images (id, guild_id, user_id, file_hash, file_name, file_content_type, file_size, s3_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; + +-- name: GetImage :one +SELECT * FROM images WHERE id = $1; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/kv_entries.sql b/embedg-service/db/postgres/queries/kv_entries.sql new file mode 100644 index 000000000..85c13e875 --- /dev/null +++ b/embedg-service/db/postgres/queries/kv_entries.sql @@ -0,0 +1,54 @@ +-- name: GetKVEntry :one +SELECT * FROM kv_entries WHERE key = $1 AND guild_id = $2; + +-- name: SetKVEntry :exec +INSERT INTO kv_entries ( + key, + guild_id, + value, + expires_at, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) ON CONFLICT (key, guild_id) +DO UPDATE SET + value = EXCLUDED.value, + expires_at = EXCLUDED.expires_at, + updated_at = EXCLUDED.updated_at; + +-- name: IncreaseKVEntry :one +INSERT INTO kv_entries ( + key, + guild_id, + value, + expires_at, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) ON CONFLICT (key, guild_id) +DO UPDATE SET + value = kv_entries.value::int + EXCLUDED.value::int, + expires_at = EXCLUDED.expires_at, + updated_at = EXCLUDED.updated_at +RETURNING *; + +-- name: DeleteKVEntry :one +DELETE FROM kv_entries WHERE key = $1 AND guild_id = $2 RETURNING *; + +-- name: SearchKVEntries :many +SELECT * FROM kv_entries WHERE key LIKE $1 AND guild_id = $2; + +-- name: CountKVEntries :one +SELECT COUNT(*) FROM kv_entries WHERE guild_id = $1; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/message_action_sets.sql b/embedg-service/db/postgres/queries/message_action_sets.sql new file mode 100644 index 000000000..7fe47233e --- /dev/null +++ b/embedg-service/db/postgres/queries/message_action_sets.sql @@ -0,0 +1,11 @@ +-- name: InsertMessageActionSet :one +INSERT INTO message_action_sets (id, message_id, set_id, actions, derived_permissions, ephemeral) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; + +-- name: GetMessageActionSet :one +SELECT * FROM message_action_sets WHERE message_id = $1 AND set_id = $2; + +-- name: GetMessageActionSets :many +SELECT * FROM message_action_sets WHERE message_id = $1; + +-- name: DeleteMessageActionSetsForMessage :exec +DELETE FROM message_action_sets WHERE message_id = $1; diff --git a/embedg-service/db/postgres/queries/saved_messages.sql b/embedg-service/db/postgres/queries/saved_messages.sql new file mode 100644 index 000000000..6ed05ee57 --- /dev/null +++ b/embedg-service/db/postgres/queries/saved_messages.sql @@ -0,0 +1,23 @@ +-- name: InsertSavedMessage :one +INSERT INTO saved_messages (id, creator_id, guild_id, updated_at, name, description, data) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; + +-- name: UpdateSavedMessageForCreator :one +UPDATE saved_messages SET updated_at = $3, name = $4, description = $5, data = $6 WHERE id = $1 AND creator_id = $2 RETURNING *; + +-- name: UpdateSavedMessageForGuild :one +UPDATE saved_messages SET updated_at = $3, name = $4, description = $5, data = $6 WHERE id = $1 AND guild_id = $2 RETURNING *; + +-- name: DeleteSavedMessageForCreator :exec +DELETE FROM saved_messages WHERE id = $1 AND creator_id = $2; + +-- name: DeleteSavedMessageForGuild :exec +DELETE FROM saved_messages WHERE id = $1 AND guild_id = $2; + +-- name: GetSavedMessagesForCreator :many +SELECT * FROM saved_messages WHERE creator_id = $1 AND guild_id IS NULL ORDER BY updated_at DESC; + +-- name: GetSavedMessagesForGuild :many +SELECT * FROM saved_messages WHERE guild_id = $1 ORDER BY updated_at DESC; + +-- name: GetSavedMessageForGuild :one +SELECT * FROM saved_messages WHERE guild_id = $1 AND id = $2; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/scheduled_messages.sql b/embedg-service/db/postgres/queries/scheduled_messages.sql new file mode 100644 index 000000000..6f4103c2f --- /dev/null +++ b/embedg-service/db/postgres/queries/scheduled_messages.sql @@ -0,0 +1,59 @@ +-- name: GetDueScheduledMessages :many +SELECT * FROM scheduled_messages WHERE next_at <= $1 AND (end_at IS NULL OR end_at >= $1) AND enabled = true; + +-- name: GetScheduledMessages :many +SELECT * FROM scheduled_messages WHERE guild_id = $1 ORDER BY updated_at DESC; + +-- name: GetScheduledMessage :one +SELECT * FROM scheduled_messages WHERE id = $1 AND guild_id = $2; + +-- name: DeleteScheduledMessage :exec +DELETE FROM scheduled_messages WHERE id = $1 AND guild_id = $2; + +-- name: InsertScheduledMessage :one +INSERT INTO scheduled_messages ( + id, + creator_id, + guild_id, + channel_id, + message_id, + thread_name, + saved_message_id, + name, + description, + cron_expression, + cron_timezone, + start_at, + end_at, + next_at, + only_once, + enabled, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 +) RETURNING *; + +-- name: UpdateScheduledMessage :one +UPDATE scheduled_messages SET + channel_id = $3, + message_id = $4, + thread_name = $5, + saved_message_id = $6, + name = $7, + description = $8, + cron_expression = $9, + next_at = $10, + start_at = $11, + end_at = $12, + only_once = $13, + enabled = $14, + updated_at = $15, + cron_timezone = $16 +WHERE id = $1 AND guild_id = $2 RETURNING *; + +-- name: UpdateScheduledMessageNextAt :one +UPDATE scheduled_messages SET next_at = $3, updated_at = $4 WHERE id = $1 AND guild_id = $2 RETURNING *; + +-- name: UpdateScheduledMessageEnabled :one +UPDATE scheduled_messages SET enabled = $3, updated_at = $4 WHERE id = $1 AND guild_id = $2 RETURNING *; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/sessions.sql b/embedg-service/db/postgres/queries/sessions.sql new file mode 100644 index 000000000..f4b852795 --- /dev/null +++ b/embedg-service/db/postgres/queries/sessions.sql @@ -0,0 +1,11 @@ +-- name: InsertSession :one +INSERT INTO sessions (token_hash, user_id, guild_ids, access_token, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; + +-- name: GetSession :one +SELECT * FROM sessions WHERE token_hash = $1; + +-- name: DeleteSession :exec +DELETE FROM sessions WHERE token_hash = $1; + +-- name: GetSessionsForUser :many +SELECT * FROM sessions WHERE user_id = $1; \ No newline at end of file diff --git a/embedg-service/db/postgres/queries/shared_messages.sql b/embedg-service/db/postgres/queries/shared_messages.sql new file mode 100644 index 000000000..4e93a2ad3 --- /dev/null +++ b/embedg-service/db/postgres/queries/shared_messages.sql @@ -0,0 +1,8 @@ +-- name: InsertSharedMessage :one +INSERT INTO shared_messages (id, created_at, expires_at, data) VALUES ($1, $2, $3, $4) RETURNING *; + +-- name: GetSharedMessage :one +SELECT * FROM shared_messages WHERE id = $1; + +-- name: DeleteExpiredSharedMessages :exec +DELETE FROM shared_messages WHERE expires_at < $1; diff --git a/embedg-service/db/postgres/queries/users.sql b/embedg-service/db/postgres/queries/users.sql new file mode 100644 index 000000000..282e735da --- /dev/null +++ b/embedg-service/db/postgres/queries/users.sql @@ -0,0 +1,8 @@ +-- name: UpsertUser :one +INSERT INTO users (id, name, discriminator, avatar) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET name = $2, discriminator = $3, avatar = $4 RETURNING *; + +-- name: GetUser :one +SELECT * FROM users WHERE id = $1; + +-- name: DeleteUser :exec +DELETE FROM users WHERE id = $1; \ No newline at end of file diff --git a/embedg-service/db/s3/db_backups.go b/embedg-service/db/s3/db_backups.go new file mode 100644 index 000000000..0e7e3c59b --- /dev/null +++ b/embedg-service/db/s3/db_backups.go @@ -0,0 +1,31 @@ +package s3 + +import ( + "context" + "fmt" + "io" + + "github.com/minio/minio-go/v7" +) + +const dbBackupBucket = "embedg-db-backups" + +func (c *BlobStore) StoreDBBackup( + ctx context.Context, + database string, + key string, + size int64, + reader io.Reader, +) error { + objectName := fmt.Sprintf("%s/%s.tar.gz", database, key) + + _, err := c.client.PutObject(ctx, dbBackupBucket, objectName, reader, size, minio.PutObjectOptions{ + ContentType: "application/tar+gzip", + ServerSideEncryption: c.encryption, + }) + if err != nil { + return fmt.Errorf("failed to store db backup: %w", err) + } + + return nil +} diff --git a/embedg-service/db/s3/files.go b/embedg-service/db/s3/files.go new file mode 100644 index 000000000..3bc019783 --- /dev/null +++ b/embedg-service/db/s3/files.go @@ -0,0 +1,86 @@ +package s3 + +import ( + "bytes" + "context" + "io" + "strings" + + "github.com/minio/minio-go/v7" +) + +const imagesBucketName = "embedg-files" + +func (s *BlobStore) UploadFile(ctx context.Context, image *Image) error { + reader := bytes.NewReader(image.Body) + _, err := s.client.PutObject(ctx, imagesBucketName, image.FileName, reader, int64(len(image.Body)), minio.PutObjectOptions{ + ContentType: image.ContentType, + ServerSideEncryption: s.encryption, + }) + if err != nil { + return err + } + + return err +} + +func (s *BlobStore) UploadFileIfNotExists(ctx context.Context, image *Image) error { + reader := bytes.NewReader(image.Body) + + exists, err := s.client.StatObject(ctx, imagesBucketName, image.FileName, minio.StatObjectOptions{ + ServerSideEncryption: s.encryption, + }) + // TODO: refactor to not use error string + if err != nil && err.Error() != "The specified key does not exist." { + return err + } + + if exists.Size > 0 { + return nil + } + + _, err = s.client.PutObject(ctx, imagesBucketName, image.FileName, reader, int64(len(image.Body)), minio.PutObjectOptions{ + ContentType: image.ContentType, + ServerSideEncryption: s.encryption, + }) + return err +} + +func (s *BlobStore) DownloadFile(ctx context.Context, fileName string) (*Image, error) { + object, err := s.client.GetObject(ctx, imagesBucketName, fileName, minio.GetObjectOptions{ + ServerSideEncryption: s.encryption, + }) + if err != nil { + if strings.Contains(err.Error(), "key does not exist") { + return nil, nil + } + + return nil, err + } + + data, err := io.ReadAll(object) + if err != nil { + if strings.Contains(err.Error(), "key does not exist") { + return nil, nil + } + + return nil, err + } + + info, err := object.Stat() + if err != nil { + return nil, err + } + + return &Image{ + FileName: fileName, + ContentType: info.ContentType, + Body: data, + }, err +} + +type Image struct { + FileName string + ContentType string + Body []byte +} diff --git a/embedg-service/db/s3/store.go b/embedg-service/db/s3/store.go new file mode 100644 index 000000000..9a21ced34 --- /dev/null +++ b/embedg-service/db/s3/store.go @@ -0,0 +1,70 @@ +package s3 + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/minio/minio-go/v7/pkg/encrypt" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +var requiredBuckets = []string{ + imagesBucketName, + dbBackupBucket, +} + +type BlobStore struct { + client *minio.Client + encryption encrypt.ServerSide +} + +func New() (*BlobStore, error) { + client, err := minio.New(viper.GetString("s3.endpoint"), &minio.Options{ + Creds: credentials.NewStaticV4(viper.GetString("s3.access_key_id"), viper.GetString("s3.secret_access_key"), ""), + Secure: viper.GetBool("s3.secure"), + }) + if err != nil { + return nil, err + } + + for _, bucket := range requiredBuckets { + exists, err := client.BucketExists(context.Background(), bucket) + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + log.Warn().Msgf("Failed to check if bucket %s exists, is S3 correctly configured?", bucket) + continue + } + return nil, fmt.Errorf("Failed to check if bucket %s exists: %w", bucket, err) + } + + if !exists { + err = client.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{}) + if err != nil { + return nil, fmt.Errorf("Failed to create bucket %s: %w", bucket, err) + } + } + } + + var encryption encrypt.ServerSide + if viper.GetString("s3.ssec_key") != "" { + key, err := hex.DecodeString(viper.GetString("s3.ssec_key")) + if err != nil { + return nil, fmt.Errorf("failed to decode S3 encryption key: %w", err) + } + + encryption, err = encrypt.NewSSEC(key) + if err != nil { + return nil, fmt.Errorf("failed to create S3 encryption: %w", err) + } + } + + return &BlobStore{ + client: client, + encryption: encryption, + }, nil +} diff --git a/embedg-service/go.mod b/embedg-service/go.mod new file mode 100644 index 000000000..4c6259d5f --- /dev/null +++ b/embedg-service/go.mod @@ -0,0 +1,68 @@ +module github.com/merlinfuchs/embed-generator/embedg-service + +go 1.25.0 + +require ( + endobit.io/clog v0.6.0 + github.com/cyrusaf/ctxlog v1.3.3 + github.com/disgoorg/snowflake/v2 v2.0.3 + github.com/go-playground/validator/v10 v10.28.0 + github.com/golang-migrate/migrate/v4 v4.19.0 + github.com/jackc/pgx/v5 v5.7.6 + github.com/knadh/koanf/parsers/toml v0.1.0 + github.com/knadh/koanf/providers/env v1.1.0 + github.com/knadh/koanf/providers/file v1.2.0 + github.com/knadh/koanf/providers/rawbytes v1.0.0 + github.com/knadh/koanf/v2 v2.3.0 + github.com/minio/minio-go/v7 v7.0.97 + github.com/pelletier/go-toml/v2 v2.2.4 + github.com/rs/zerolog v1.34.0 + github.com/spf13/viper v1.21.0 + gopkg.in/guregu/null.v4 v4.0.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/minio/crc64nvme v1.1.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/embedg-service/go.sum b/embedg-service/go.sum new file mode 100644 index 000000000..6997999e0 --- /dev/null +++ b/embedg-service/go.sum @@ -0,0 +1,200 @@ +endobit.io/clog v0.6.0 h1:/fnhDMzqF1N1Bnr/MQtPd0Zm48Hks9tdZRLcvzL4D5k= +endobit.io/clog v0.6.0/go.mod h1:A/hC3XbGdhoVY3fq+c/6PJfsb7q0pUvQXy3xk1bEVD8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cyrusaf/ctxlog v1.3.3 h1:0wjHcOpqj/ouAjKQDGFXpwJfFxK0Dh+gZZdmRnBZbg4= +github.com/cyrusaf/ctxlog v1.3.3/go.mod h1:uYxERwb2tWRzkPzJUObIRzmhS/yd1QnL+3R9F3IkoXI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= +github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= +github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc= +github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= +github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= +github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/zsJyc/8qaSnzcdNI= +github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo= +github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= +github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= +github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= +github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/embedg-service/logging/logging.go b/embedg-service/logging/logging.go new file mode 100644 index 000000000..d959fa1f3 --- /dev/null +++ b/embedg-service/logging/logging.go @@ -0,0 +1,61 @@ +package logging + +import ( + "io" + "os" + + "log/slog" + + "endobit.io/clog" + "github.com/cyrusaf/ctxlog" + "gopkg.in/natefinch/lumberjack.v2" +) + +type LoggerConfig struct { + Filename string + MaxSize int + MaxAge int + MaxBackups int + Debug bool +} + +func getLogWriter(cfg LoggerConfig) io.Writer { + logWriters := make([]io.Writer, 0) + logWriters = append(logWriters, os.Stdout) + + if cfg.Filename != "" { + lj := lumberjack.Logger{ + Filename: cfg.Filename, + MaxSize: cfg.MaxSize, + MaxAge: cfg.MaxAge, + MaxBackups: cfg.MaxBackups, + } + logWriters = append(logWriters, &lj) + } + writer := io.MultiWriter(logWriters...) + return writer +} + +func SetupLogger(cfg LoggerConfig) *slog.Logger { + writer := getLogWriter(cfg) + + level := slog.LevelInfo + if cfg.Debug { + level = slog.LevelDebug + } + + handler := ctxlog.NewHandler(clog.HandlerOptions{ + Level: level, + }.NewHandler(writer)) + + logger := slog.New(handler) + hostname, err := os.Hostname() + if err != nil { + logger.With("error", err).Error("failed to get hostname") + hostname = "" + } + logger = logger.With(slog.String("host", hostname)) + + slog.SetDefault(logger) + return logger +} diff --git a/embedg-service/main.go b/embedg-service/main.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/embedg-service/main.go @@ -0,0 +1 @@ +package main diff --git a/embedg-service/model/custom_bot.go b/embedg-service/model/custom_bot.go new file mode 100644 index 000000000..2deea36ff --- /dev/null +++ b/embedg-service/model/custom_bot.go @@ -0,0 +1,28 @@ +package model + +import ( + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type CustomBot struct { + ID string + GuildID common.ID + ApplicationID common.ID + Token string + PublicKey string + UserID common.ID + UserName string + UserDiscriminator string + UserAvatar null.String + HandledFirstInteraction bool + CreatedAt time.Time + TokenInvalid bool + GatewayStatus string + GatewayActivityType null.Int + GatewayActivityName null.String + GatewayActivityState null.String + GatewayActivityUrl null.String +} diff --git a/embedg-service/model/custom_command.go b/embedg-service/model/custom_command.go new file mode 100644 index 000000000..4bc3ef6da --- /dev/null +++ b/embedg-service/model/custom_command.go @@ -0,0 +1,24 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type CustomCommand struct { + ID string + GuildID common.ID + Name string + Description string + Enabled bool + Parameters json.RawMessage + Actions json.RawMessage + CreatedAt time.Time + UpdatedAt time.Time + DeployedAt null.Time + DerivedPermissions json.RawMessage + LastUsedAt null.Time +} diff --git a/embedg-service/model/embed_link.go b/embedg-service/model/embed_link.go new file mode 100644 index 000000000..354d877be --- /dev/null +++ b/embedg-service/model/embed_link.go @@ -0,0 +1,26 @@ +package model + +import ( + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type EmbedLink struct { + ID common.ID + Url string + ThemeColor null.String + OgTitle null.String + OgSiteName null.String + OgDescription null.String + OgImage null.String + OeType null.String + OeAuthorName null.String + OeAuthorUrl null.String + OeProviderName null.String + OeProviderUrl null.String + TwCard null.String + ExpiresAt null.Time + CreatedAt time.Time +} diff --git a/embedg-service/model/entitlement.go b/embedg-service/model/entitlement.go new file mode 100644 index 000000000..416e307f9 --- /dev/null +++ b/embedg-service/model/entitlement.go @@ -0,0 +1,21 @@ +package model + +import ( + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type Entitlement struct { + ID string + UserID common.NullID + GuildID common.NullID + UpdatedAt time.Time + Deleted bool + SkuID string + StartsAt null.Time + EndsAt null.Time + Consumed bool + ConsumedGuildID null.String +} diff --git a/embedg-service/model/image.go b/embedg-service/model/image.go new file mode 100644 index 000000000..6b68e9022 --- /dev/null +++ b/embedg-service/model/image.go @@ -0,0 +1,14 @@ +package model + +import "github.com/merlinfuchs/embed-generator/embedg-service/common" + +type Image struct { + ID string + UserID common.ID + GuildID common.NullID + FileHash string + FileName string + FileSize int + FileContentType string + S3Key string +} diff --git a/embedg-service/model/kv.go b/embedg-service/model/kv.go new file mode 100644 index 000000000..7701075f2 --- /dev/null +++ b/embedg-service/model/kv.go @@ -0,0 +1,17 @@ +package model + +import ( + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type KVEntry struct { + Key string + GuildID common.ID + Value string + ExpiresAt null.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/embedg-service/model/message_action_set.go b/embedg-service/model/message_action_set.go new file mode 100644 index 000000000..fc39823da --- /dev/null +++ b/embedg-service/model/message_action_set.go @@ -0,0 +1,18 @@ +package model + +import ( + "encoding/json" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type MessageActionSet struct { + ID string + MessageID common.ID + SetID string + Actions json.RawMessage + DerivedPermissions json.RawMessage + LastUsedAt null.Time + Ephemeral bool +} diff --git a/embedg-service/model/saved_message.go b/embedg-service/model/saved_message.go new file mode 100644 index 000000000..41eb72625 --- /dev/null +++ b/embedg-service/model/saved_message.go @@ -0,0 +1,19 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type SavedMessage struct { + ID string + CreatorID common.ID + GuildID common.NullID + UpdatedAt time.Time + Name string + Description null.String + Data json.RawMessage +} diff --git a/embedg-service/model/scheduled_message.go b/embedg-service/model/scheduled_message.go new file mode 100644 index 000000000..a130e2574 --- /dev/null +++ b/embedg-service/model/scheduled_message.go @@ -0,0 +1,29 @@ +package model + +import ( + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type ScheduledMessage struct { + ID string + CreatorID common.ID + GuildID common.ID + ChannelID common.ID + MessageID common.NullID + SavedMessageID string + Name string + Description null.String + CronExpression null.String + OnlyOnce bool + StartAt time.Time + EndAt null.Time + NextAt time.Time + Enabled bool + CreatedAt time.Time + UpdatedAt time.Time + CronTimezone null.String + ThreadName null.String +} diff --git a/embedg-service/model/session.go b/embedg-service/model/session.go new file mode 100644 index 000000000..2e4d59f11 --- /dev/null +++ b/embedg-service/model/session.go @@ -0,0 +1,16 @@ +package model + +import ( + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" +) + +type Session struct { + TokenHash string + UserID common.ID + GuildIds []common.ID + AccessToken string + CreatedAt time.Time + ExpiresAt time.Time +} diff --git a/embedg-service/model/shared_message.go b/embedg-service/model/shared_message.go new file mode 100644 index 000000000..e73aa4948 --- /dev/null +++ b/embedg-service/model/shared_message.go @@ -0,0 +1,10 @@ +package model + +import "time" + +type SharedMessage struct { + ID string + CreatedAt time.Time + ExpiresAt time.Time + Data []byte +} diff --git a/embedg-service/model/user.go b/embedg-service/model/user.go new file mode 100644 index 000000000..f115254d2 --- /dev/null +++ b/embedg-service/model/user.go @@ -0,0 +1,14 @@ +package model + +import ( + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "gopkg.in/guregu/null.v4" +) + +type User struct { + ID common.ID + Name string + Discriminator string + Avatar null.String + IsTester bool +} diff --git a/embedg-service/sqlc.yaml b/embedg-service/sqlc.yaml new file mode 100644 index 000000000..833a3dcc9 --- /dev/null +++ b/embedg-service/sqlc.yaml @@ -0,0 +1,11 @@ +version: 2 +sql: + - engine: "postgresql" + schema: + - "db/postgres/migrations" + queries: "db/postgres/queries" + gen: + go: + package: "pgmodel" + sql_package: "pgx/v5" + out: "db/postgres/pgmodel" diff --git a/embedg-service/store/base.go b/embedg-service/store/base.go new file mode 100644 index 000000000..e879153ce --- /dev/null +++ b/embedg-service/store/base.go @@ -0,0 +1,6 @@ +package store + +import "errors" + +var ErrNotFound = errors.New("not found") +var ErrAlreadyExists = errors.New("already exists") diff --git a/embedg-service/store/custom_bot.go b/embedg-service/store/custom_bot.go new file mode 100644 index 000000000..a39b4eb9a --- /dev/null +++ b/embedg-service/store/custom_bot.go @@ -0,0 +1,37 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "gopkg.in/guregu/null.v4" +) + +type UpdateCustomBotUserParams struct { + GuildID common.ID + UserName string + UserDiscriminator string + UserAvatar null.String +} + +type UpdateCustomBotPresenceParams struct { + GuildID common.ID + GatewayStatus string + GatewayActivityType null.Int + GatewayActivityName null.String + GatewayActivityState null.String + GatewayActivityUrl null.String +} + +type CustomBotStore interface { + UpsertCustomBot(ctx context.Context, customBot model.CustomBot) (*model.CustomBot, error) + UpdateCustomBotPresence(ctx context.Context, params UpdateCustomBotPresenceParams) (*model.CustomBot, error) + UpdateCustomBotUser(ctx context.Context, params UpdateCustomBotUserParams) (*model.CustomBot, error) + UpdateCustomBotTokenInvalid(ctx context.Context, guildID common.ID) (*model.CustomBot, error) + DeleteCustomBot(ctx context.Context, guildID common.ID) (*model.CustomBot, error) + GetCustomBot(ctx context.Context, id string) (*model.CustomBot, error) + GetCustomBotByGuildID(ctx context.Context, guildID common.ID) (*model.CustomBot, error) + SetCustomBotHandledFirstInteraction(ctx context.Context, id string) error + GetCustomBots(ctx context.Context) ([]*model.CustomBot, error) +} diff --git a/embedg-service/store/custom_command.go b/embedg-service/store/custom_command.go new file mode 100644 index 000000000..ff4f639c8 --- /dev/null +++ b/embedg-service/store/custom_command.go @@ -0,0 +1,20 @@ +package store + +import ( + "context" + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type CustomCommandStore interface { + GetCustomCommands(ctx context.Context, guildID common.ID) ([]*model.CustomCommand, error) + GetCustomCommand(ctx context.Context, guildID common.ID, id string) (*model.CustomCommand, error) + GetCustomCommandByName(ctx context.Context, guildID common.ID, name string) (*model.CustomCommand, error) + CountCustomCommands(ctx context.Context, guildID common.ID) (int64, error) + CreateCustomCommand(ctx context.Context, customCommand model.CustomCommand) (*model.CustomCommand, error) + UpdateCustomCommand(ctx context.Context, customCommand model.CustomCommand) (*model.CustomCommand, error) + DeleteCustomCommand(ctx context.Context, guildID common.ID, id string) (*model.CustomCommand, error) + SetCustomCommandsDeployedAt(ctx context.Context, guildID common.ID, deployedAt time.Time) (*model.CustomCommand, error) +} diff --git a/embedg-service/store/embed_link.go b/embedg-service/store/embed_link.go new file mode 100644 index 000000000..50b2b69fa --- /dev/null +++ b/embedg-service/store/embed_link.go @@ -0,0 +1,12 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type EmbedLinkStore interface { + CreateEmbedLink(ctx context.Context, embedLink model.EmbedLink) (*model.EmbedLink, error) + GetEmbedLink(ctx context.Context, id string) (*model.EmbedLink, error) +} diff --git a/embedg-service/store/entitlement.go b/embedg-service/store/entitlement.go new file mode 100644 index 000000000..82267fb4f --- /dev/null +++ b/embedg-service/store/entitlement.go @@ -0,0 +1,17 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type EntitlementStore interface { + GetActiveEntitlementsForGuild(ctx context.Context, guildID common.ID) ([]*model.Entitlement, error) + GetActiveEntitlementsForUser(ctx context.Context, userID common.ID) ([]*model.Entitlement, error) + GetEntitlements(ctx context.Context) ([]*model.Entitlement, error) + GetEntitlement(ctx context.Context, id common.ID) (*model.Entitlement, error) + UpdateEntitlementConsumedGuildID(ctx context.Context, id common.ID, consumedGuildID common.NullID) (*model.Entitlement, error) + UpsertEntitlement(ctx context.Context, entitlement model.Entitlement) (*model.Entitlement, error) +} diff --git a/embedg-service/store/image.go b/embedg-service/store/image.go new file mode 100644 index 000000000..64cc8aa1b --- /dev/null +++ b/embedg-service/store/image.go @@ -0,0 +1,12 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type ImageStore interface { + CreateImage(ctx context.Context, img model.Image) error + GetImage(ctx context.Context, id string) (*model.Image, error) +} diff --git a/embedg-service/store/kv.go b/embedg-service/store/kv.go new file mode 100644 index 000000000..42a67a3ca --- /dev/null +++ b/embedg-service/store/kv.go @@ -0,0 +1,28 @@ +package store + +import ( + "context" + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "gopkg.in/guregu/null.v4" +) + +type KVEntryIncreaseParams struct { + Key string + GuildID common.ID + Delta int + ExpiresAt null.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type KVEntryStore interface { + GetKVEntry(ctx context.Context, guildID common.ID, key string) (*model.KVEntry, error) + SetKVEntry(ctx context.Context, entry model.KVEntry) error + IncreaseKVEntry(ctx context.Context, params KVEntryIncreaseParams) (*model.KVEntry, error) + DeleteKVEntry(ctx context.Context, guildID common.ID, key string) (*model.KVEntry, error) + SearchKVEntries(ctx context.Context, guildID common.ID, pattern string) ([]*model.KVEntry, error) + CountKVEntries(ctx context.Context, guildID common.ID) (int64, error) +} diff --git a/embedg-service/store/message_action_set.go b/embedg-service/store/message_action_set.go new file mode 100644 index 000000000..03fdcef7d --- /dev/null +++ b/embedg-service/store/message_action_set.go @@ -0,0 +1,15 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type MessageActionSetStore interface { + CreateMessageActionSet(ctx context.Context, messageActionSet model.MessageActionSet) (*model.MessageActionSet, error) + GetMessageActionSet(ctx context.Context, messageID common.ID, actionSetID string) (*model.MessageActionSet, error) + GetMessageActionSets(ctx context.Context, messageID common.ID) ([]*model.MessageActionSet, error) + DeleteMessageActionSetsForMessage(ctx context.Context, messageID common.ID) error +} diff --git a/embedg-service/store/saved_message.go b/embedg-service/store/saved_message.go new file mode 100644 index 000000000..606e2af17 --- /dev/null +++ b/embedg-service/store/saved_message.go @@ -0,0 +1,19 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type SavedMessageStore interface { + CreateSavedMessage(ctx context.Context, msg model.SavedMessage) error + UpdateSavedMessageForCreator(ctx context.Context, msg model.SavedMessage) error + UpdateSavedMessageForGuild(ctx context.Context, msg model.SavedMessage) error + DeleteSavedMessageForCreator(ctx context.Context, msg model.SavedMessage) error + DeleteSavedMessageForGuild(ctx context.Context, msg model.SavedMessage) error + GetSavedMessagesForCreator(ctx context.Context, creatorID common.ID) ([]model.SavedMessage, error) + GetSavedMessagesForGuild(ctx context.Context, guildID common.ID) ([]model.SavedMessage, error) + GetSavedMessageForGuild(ctx context.Context, guildID common.ID, id string) (*model.SavedMessage, error) +} diff --git a/embedg-service/store/scheduled_message.go b/embedg-service/store/scheduled_message.go new file mode 100644 index 000000000..937e2ac76 --- /dev/null +++ b/embedg-service/store/scheduled_message.go @@ -0,0 +1,20 @@ +package store + +import ( + "context" + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type ScheduledMessageStore interface { + GetDueScheduledMessages(ctx context.Context, now time.Time) ([]model.ScheduledMessage, error) + GetScheduledMessages(ctx context.Context, guildID common.ID) ([]model.ScheduledMessage, error) + GetScheduledMessage(ctx context.Context, id common.ID) (*model.ScheduledMessage, error) + DeleteScheduledMessage(ctx context.Context, id common.ID) error + CreateScheduledMessage(ctx context.Context, msg model.ScheduledMessage) error + UpdateScheduledMessage(ctx context.Context, msg model.ScheduledMessage) error + UpdateScheduledMessageNextAt(ctx context.Context, id common.ID, nextAt time.Time) error + UpdateScheduledMessageEnabled(ctx context.Context, id common.ID, enabled bool) error +} diff --git a/embedg-service/store/session.go b/embedg-service/store/session.go new file mode 100644 index 000000000..860dd8b78 --- /dev/null +++ b/embedg-service/store/session.go @@ -0,0 +1,14 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type SessionStore interface { + CreateSession(ctx context.Context, session model.Session) error + GetSession(ctx context.Context, tokenHash string) (*model.Session, error) + DeleteSession(ctx context.Context, tokenHash string) error + GetSessionsForUser(ctx context.Context, userID string) ([]model.Session, error) +} diff --git a/embedg-service/store/shared_message.go b/embedg-service/store/shared_message.go new file mode 100644 index 000000000..601661703 --- /dev/null +++ b/embedg-service/store/shared_message.go @@ -0,0 +1,14 @@ +package store + +import ( + "context" + "time" + + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type SharedMessageStore interface { + CreateSharedMessage(ctx context.Context, msg model.SharedMessage) error + GetSharedMessage(ctx context.Context, id string) (*model.SharedMessage, error) + DeleteExpiredSharedMessages(ctx context.Context, now time.Time) error +} diff --git a/embedg-service/store/user.go b/embedg-service/store/user.go new file mode 100644 index 000000000..a40ef0691 --- /dev/null +++ b/embedg-service/store/user.go @@ -0,0 +1,14 @@ +package store + +import ( + "context" + + "github.com/disgoorg/snowflake/v2" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type UserStore interface { + UpsertUser(ctx context.Context, user model.User) error + GetUser(ctx context.Context, userID snowflake.ID) (*model.User, error) + DeleteUser(ctx context.Context, userID snowflake.ID) error +} diff --git a/go.work b/go.work index a74ddc75a..299cbf313 100644 --- a/go.work +++ b/go.work @@ -2,6 +2,8 @@ go 1.25.0 use ./embedg-server +use ./embedg-service + use ./embedg-app use ./embedg-site diff --git a/go.work.sum b/go.work.sum index 7c8dbb112..faf1aedb0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,28 +1,157 @@ bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512 h1:SRsZGA7aFnCZETmov57jwPrWuTmaZK6+4R4v5FUe1/c= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0= +cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q= +cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM= +cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0= +cloud.google.com/go/apigateway v1.6.5/go.mod h1:6wCwvYRckRQogyDDltpANi3zsCDl6kWi0b4Je+w2UiI= +cloud.google.com/go/apigeeconnect v1.6.5/go.mod h1:MEKm3AiT7s11PqTfKE3KZluZA9O91FNysvd3E6SJ6Ow= +cloud.google.com/go/apigeeregistry v0.8.3/go.mod h1:aInOWnqF4yMQx8kTjDqHNXjZGh/mxeNlAf52YqtASUs= +cloud.google.com/go/appengine v1.8.5/go.mod h1:uHBgNoGLTS5di7BvU25NFDuKa82v0qQLjyMJLuPQrVo= +cloud.google.com/go/area120 v0.8.5/go.mod h1:BcoFCbDLZjsfe4EkCnEq1LKvHSK0Ew/zk5UFu6GMyA0= +cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot72EMfrkvL9g9aRjnnM= +cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4= +cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkHxwIpNM9vtmhvt+6wqk= +cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y= +cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY= +cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc= +cloud.google.com/go/beyondcorp v1.0.4/go.mod h1:Gx8/Rk2MxrvWfn4WIhHIG1NV7IBfg14pTKv1+EArVcc= cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= +cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc= +cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUkvKjCUcpSE= +cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ= +cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0kuqkKZIXdvlZRO7z0VtM= +cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9b2mApQeHc= +cloud.google.com/go/cloudbuild v1.15.1/go.mod h1:gIofXZSu+XD2Uy+qkOrGKEx45zd7s28u/k8f99qKals= +cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFotwdE79DieY= +cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlRi/KZQLQjDsaY= cloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk= cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI= +cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA= +cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE= +cloud.google.com/go/datacatalog v1.19.3/go.mod h1:ra8V3UAsciBpJKQ+z9Whkxzxv7jmQg1hfODr3N3YPJ4= +cloud.google.com/go/dataflow v0.9.5/go.mod h1:udl6oi8pfUHnL0z6UN9Lf9chGqzDMVqcYTcZ1aPnCZQ= +cloud.google.com/go/dataform v0.9.2/go.mod h1:S8cQUwPNWXo7m/g3DhWHsLBoufRNn9EgFrMgne2j7cI= +cloud.google.com/go/datafusion v1.7.5/go.mod h1:bYH53Oa5UiqahfbNK9YuYKteeD4RbQSNMx7JF7peGHc= +cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20mcPxDBm36lps+s= +cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/PxBf+ZymA2U= +cloud.google.com/go/dataproc/v2 v2.4.0/go.mod h1:3B1Ht2aRB8VZIteGxQS/iNSJGzt9+CA0WGnDVMEm7Z4= +cloud.google.com/go/dataqna v0.8.5/go.mod h1:vgihg1mz6n7pb5q2YJF7KlXve6tCglInd6XO0JGOlWM= cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= +cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= +cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGGvSNdn4p1sRZo= +cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5ym7MN/50= +cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6kMV3EW3oz/fe0= +cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3UfU/h/w= +cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv6hHEY5br4KzY= +cloud.google.com/go/domains v0.9.5/go.mod h1:dBzlxgepazdFhvG7u23XMhmMKBjrkoUNaw0A8AQB55Y= +cloud.google.com/go/edgecontainer v1.1.5/go.mod h1:rgcjrba3DEDEQAidT4yuzaKWTbkTI5zAMu3yy6ZWS0M= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.6.6/go.mod h1:XbqHJGaiH0v2UvtuucfOzFXN+rpL/aU5BCZLn4DYl1Q= +cloud.google.com/go/eventarc v1.13.4/go.mod h1:zV5sFVoAa9orc/52Q+OuYUG9xL2IIZTbbuTHC6JSY8s= +cloud.google.com/go/filestore v1.8.1/go.mod h1:MbN9KcaM47DRTIuLfQhJEsjaocVebNtNQhSLhKCF5GM= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/functions v1.16.0/go.mod h1:nbNpfAG7SG7Duw/o1iZ6ohvL7mc6MapWQVpqtM29n8k= +cloud.google.com/go/gkebackup v1.3.5/go.mod h1:KJ77KkNN7Wm1LdMopOelV6OodM01pMuK2/5Zt1t4Tvc= +cloud.google.com/go/gkeconnect v0.8.5/go.mod h1:LC/rS7+CuJ5fgIbXv8tCD/mdfnlAadTaUufgOkmijuk= +cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/fMNHJ0wA= +cloud.google.com/go/gkemulticloud v1.1.1/go.mod h1:C+a4vcHlWeEIf45IB5FFR5XGjTeYhF83+AYIpTy4i2Q= +cloud.google.com/go/gsuiteaddons v1.6.5/go.mod h1:Lo4P2IvO8uZ9W+RaC6s1JVxo42vgy+TX5a6hfBZ0ubs= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/iap v1.9.4/go.mod h1:vO4mSq0xNf/Pu6E5paORLASBwEmphXEjgCFg7aeNu1w= +cloud.google.com/go/ids v1.4.5/go.mod h1:p0ZnyzjMWxww6d2DvMGnFwCsSxDJM666Iir1bK1UuBo= +cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0YsSsqs= +cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI= +cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnAJrTRXU8+f8= +cloud.google.com/go/lifesciences v0.9.5/go.mod h1:OdBm0n7C0Osh5yZB7j9BXyrMnTRGBJIZonUMxo5CzPw= +cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI= +cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI= +cloud.google.com/go/mediatranslation v0.8.5/go.mod h1:y7kTHYIPCIfgyLbKncgqouXJtLsU+26hZhHEEy80fSs= +cloud.google.com/go/memcache v1.10.5/go.mod h1:/FcblbNd0FdMsx4natdj+2GWzTq+cjZvMa1I+9QsuMA= +cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH6gz3KvJ9BAE= +cloud.google.com/go/monitoring v1.18.0/go.mod h1:c92vVBCeq/OB4Ioyo+NbN2U7tlg5ZH41PZcdvfc+Lcg= +cloud.google.com/go/networkconnectivity v1.14.4/go.mod h1:PU12q++/IMnDJAB+3r+tJtuCXCfwfN+C6Niyj6ji1Po= +cloud.google.com/go/networkmanagement v1.9.4/go.mod h1:daWJAl0KTFytFL7ar33I6R/oNBH8eEOX/rBNHrC/8TA= +cloud.google.com/go/networksecurity v0.9.5/go.mod h1:KNkjH/RsylSGyyZ8wXpue8xpCEK+bTtvof8SBfIhMG8= +cloud.google.com/go/notebooks v1.11.3/go.mod h1:0wQyI2dQC3AZyQqWnRsp+yA+kY4gC7ZIVP4Qg3AQcgo= +cloud.google.com/go/optimization v1.6.3/go.mod h1:8ve3svp3W6NFcAEFr4SfJxrldzhUl4VMUJmhrqVKtYA= +cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8= +cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I= +cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8= +cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/KPn9gmhws= +cloud.google.com/go/phishingprotection v0.8.5/go.mod h1:g1smd68F7mF1hgQPuYn3z8HDbNre8L6Z0b7XMYFmX7I= +cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk= +cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk= cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= +cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE= +cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU= +cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr0g59NRNREz4cx7F58HAsQ= +cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtudMhMIG4ni8nr+0= +cloud.google.com/go/redis v1.14.2/go.mod h1:g0Lu7RRRz46ENdFKQ2EcQZBAJ2PtJHJLuiiRuEXwyQw= +cloud.google.com/go/resourcemanager v1.9.5/go.mod h1:hep6KjelHA+ToEjOfO3garMKi/CLYwTqeAw7YiEI9x8= +cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I= +cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+01J9dYWzE= +cloud.google.com/go/run v1.3.4/go.mod h1:FGieuZvQ3tj1e9GnzXqrMABSuir38AJg5xhiYq+SF3o= +cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE= +cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4= +cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra6c7MPnldtc= +cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9+7jbc+LvPjXhpwcU= +cloud.google.com/go/servicedirectory v1.11.4/go.mod h1:Bz2T9t+/Ehg6x+Y7Ycq5xiShYLD96NfEsWNHyitj1qM= +cloud.google.com/go/shell v1.7.5/go.mod h1:hL2++7F47/IfpfTO53KYf1EC+F56k3ThfNEXd4zcuiE= cloud.google.com/go/spanner v1.28.0 h1:1ZukQlok9wZyZUBFm++xpleudtviOPO8gvGAF2ydxWQ= +cloud.google.com/go/spanner v1.56.0/go.mod h1:DndqtUKQAt3VLuV2Le+9Y3WTnq5cNKrnLb/Piqcj+h0= +cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52FfMU1aZrIA= cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs= +cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ0g8tseQ= +cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M= +cloud.google.com/go/tpu v1.6.5/go.mod h1:P9DFOEBIBhuEcZhXi+wPoVy/cji+0ICFi4TtTkMHSSs= +cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M= +cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk= +cloud.google.com/go/video v1.20.4/go.mod h1:LyUVjyW+Bwj7dh3UJnUGZfyqjEto9DnrvTe1f/+QrW0= +cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxvi12HlW7Em0lJO14FC3I= +cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU= +cloud.google.com/go/vmmigration v1.7.5/go.mod h1:pkvO6huVnVWzkFioxSghZxIGcsstDvYiVCxQ9ZH3eYI= +cloud.google.com/go/vmwareengine v1.1.1/go.mod h1:nMpdsIVkUrSaX8UvmnBhzVzG7PPvNYc5BszcvIVudYs= +cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcpIWz6P/ziig= +cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U= +cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/Ijvuoa3FCyS1zBa1rAVQ= +cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= @@ -31,11 +160,14 @@ github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8K github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= @@ -51,8 +183,11 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjj github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alexflint/go-filemutex v1.1.0 h1:IAWuUuRYL2hETx5b8vCgwnD+xSdlsTQY6s2JjBsqLdg= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30 h1:HGREIyk0QRPt70R69Gm1JFHDgoiyYpCyuGE8E9k/nf0= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= @@ -61,19 +196,39 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/aws/aws-sdk-go v1.17.7 h1:/4+rDPe0W95KBmNGYCG+NUvdL8ssPYBMxL+aSCg6nIA= +github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.9.2 h1:dUFQcMNZMLON4BOe273pl0filK9RqyQMhCK/6xssL6s= +github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8/go.mod h1:JTnlBSot91steJeti4ryyu/tLd4Sk84O5W22L7O2EQU= github.com/aws/aws-sdk-go-v2/config v1.8.3 h1:o5583X4qUfuRrOGOgmOcDgvr5gJVSu57NK08cWAhIDk= +github.com/aws/aws-sdk-go-v2/config v1.17.7/go.mod h1:dN2gja/QXxFF15hQreyrqYhLBaQo1d9ZKe/v/uplQoI= github.com/aws/aws-sdk-go-v2/credentials v1.4.3 h1:LTdD5QhK073MpElh9umLLP97wxphkgVC/OjQaEbBwZA= +github.com/aws/aws-sdk-go-v2/credentials v1.12.20/go.mod h1:UKY5HyIux08bbNA7Blv4PcXQ8cTkGh7ghHMFklaviR4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 h1:9tfxW/icbSu98C2pcNynm5jmDwU3/741F11688B6QnU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17/go.mod h1:yIkQcCDYNsZfXpd5UX2Cy+sWA1jPgIhGTw9cOBzfVnQ= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4 h1:TnU1cY51027j/MQeFy7DIgk1UuzJY+wLFYqXceY/fiE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33/go.mod h1:84XgODVR8uRhmOnUkKGUZKqIMxmjmLOR8Uyp7G/TPwc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 h1:leSJ6vCqtPpTmBIgE7044B1wql1E4n//McF+mEgNrYg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24/go.mod h1:jULHjqqjDlbyTa7pfM7WICATnOv+iOhjletM3N0Xbu8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14/go.mod h1:AyGgqiKv9ECM6IZeNQtdT8NnMvUb3/2wokeq2Fgryto= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0 h1:gceOysEWNNwLd6cki65IMBZ4WAM0MwgBQq2n7kejoT8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9/go.mod h1:a9j48l6yL5XINLHLcOKInjdvknN+vWqPBxqeIDw7ktw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18/go.mod h1:NS55eQ4YixUJPTC+INxi2/jCqe1y2Uw3rnh9wEOVJxY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 h1:r7jel2aa4d9Duys7wEmWqDd5ebpC9w6Kxu6wIjjp18E= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2 h1:RnZjLgtCGLsF2xYYksy0yrx6xPvKG9BYv29VfK4p/J8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17/go.mod h1:YqMdV+gEKCQ59NrB7rzrJdALeBIsYiVi8Inj3+KcqHI= github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1 h1:z+P3r4LrwdudLKBoEVWxIORrk4sVg4/iqpG3+CS53AY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11/go.mod h1:fmgDANqTUCxciViKl9hb/zD5LFbvPINFRgWhDbR+vZo= github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 h1:pZwkxZbspdqRGzddDB92bkZBoB7lg85sMRE7OqdB3V0= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.23/go.mod h1:/w0eg9IhFGjGyyncHIQrXtU8wvNsTJOP0R6PPj0wf80= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.5/go.mod h1:csZuQY65DAdFBt1oIjO5hhBR49kQqop4+lcuCjf2arA= github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 h1:ol2Y5DWqnJeKqNd8th7JWzBtqu63xpOfs1Is+n1t8/4= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.19/go.mod h1:h4J3oPZQbxLhzGnk+j9dfYHi5qIOVJ5kczZd658/ydM= github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc= +github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= @@ -100,8 +255,10 @@ github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo= github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= @@ -113,7 +270,9 @@ github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= @@ -128,10 +287,12 @@ github.com/containerd/fifo v1.0.0 h1:6PirWBr9/L7GDamKr+XM0IeUFXu5mf3M/BPpH9gaLBU github.com/containerd/go-cni v1.1.3 h1:t0MQwrtM96SH71Md8tH0uKrVE9v+jxkDTbvFSm3B9VE= github.com/containerd/go-runc v1.0.0 h1:oU+lLv1ULm5taqgV/CJivypVODI4SUz1znWjv3nNYS0= github.com/containerd/imgcrypt v1.1.3 h1:69UKRsA3Q/lAwo2eDzWshdjimqhmprrWXfNtBeO0fBc= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nri v0.1.0 h1:6QioHRlThlKh2RkRTR4kIT3PKAcrLo3gIWnjkM4dQmQ= github.com/containerd/stargz-snapshotter/estargz v0.4.1 h1:5e7heayhB7CcgdTkqfZqrNaNv15gABwr3Q2jBTbLlt4= github.com/containerd/ttrpc v1.1.0 h1:GbtyLRxb0gOLR0TYQWt3O6B0NvT8tMdorEHqIQo/lWI= github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/containerd/zfs v1.0.0 h1:cXLJbx+4Jj7rNsTiqVfm6i+RNLx6FFA2fMmDlEf+Wm8= github.com/containernetworking/cni v1.0.1 h1:9OIL/sZmMYDBe+G8svzILAlulUpaDTUjeAbtH/JNLBo= github.com/containernetworking/plugins v1.0.1 h1:wwCfYbTCj5FC0EJgyzyjTXmqysOiJE9r712Z+2KVZAk= @@ -146,12 +307,14 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc= github.com/d2g/dhcp4client v1.0.0 h1:suYBsYZIkSlUMEz4TAYCczKf62IA2UWC+O8+KtdOhCo= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5 h1:+CpLbZIeUn94m02LdEKPcgErLJ347NUwxPKs5u8ieiY= github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 h1:itqmmf1PFpC4n5JW+j4BU7X4MTfVurhYRTjODoPb2Y8= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -167,6 +330,8 @@ github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rY github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 h1:2HQmlpI3yI9deH18Q6xiSOIjXD4sLI55Y/gfpa8/558= github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= @@ -176,13 +341,16 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QL github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/dpatrie/urbandictionary v0.0.0-20151214192647-3b38cbf4cb81 h1:PFUFa2tSuf8Ji9+1NcPA6nooMGA/6o+VTLNweqhZXwo= github.com/dpatrie/urbandictionary v0.0.0-20151214192647-3b38cbf4cb81/go.mod h1:XI+Vghhgy+ZsXWIT/O1n2uG/u5b0GJlPbyxYyJZZ4EQ= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= github.com/ei14/calc v0.0.0-20220307072502-adbe43bdf801 h1:c12O2oSZIZ70jcdjZlMcjxaC/d9LpPfJrnMWpasFfRE= github.com/ei14/calc v0.0.0-20220307072502-adbe43bdf801/go.mod h1:6NJNLNXVmKw1eea3XvpI286ab0HMbRqpye7CwPGZk4k= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYAymNJek76W3mgw= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= @@ -201,6 +369,7 @@ github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzj github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= @@ -226,7 +395,6 @@ github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07 h1:OTlfMvwR1rLyf9goVmXfuS5AJn80+Vmj4rTf4n46SOs= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= @@ -237,10 +405,6 @@ github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wab github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= @@ -264,39 +428,56 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 h1:N/MD/sr6o61X+iZBAT2qEUF023s4KbA8RWfKzl0L6MQ= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.4.0 h1:zgVt4UpGxcqVOw97aRGxT4svlcmdK35fynLNctY32zI= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.5.1 h1:/+mFTs4AlwsJ/mJe8NDtKb7BxLtbZFpcn8vDsneEkwQ= github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= +github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/safebrowsing v0.0.0-20190624211811-bbf0d20d26b3 h1:4SV2fLwScO6iAgUKNqXwIrz9Fq2ykQxbSV4ObXtNCWY= github.com/google/safebrowsing v0.0.0-20190624211811-bbf0d20d26b3/go.mod h1:hT4r/grkURkgVSWJaWd6PyS4xfAb+vb34DyMDYiOGa8= github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -306,11 +487,14 @@ github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YAR github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hashicorp/consul/api v1.18.0 h1:R7PPNzTCeN6VuQNDwwhZWJvzCtGSrNpJqfb22h3yH9g= github.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4= @@ -327,6 +511,7 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3 github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -339,6 +524,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -347,18 +533,40 @@ github.com/j-keck/arping v1.0.2 h1:hlLhuXgQkzIJTZuhMigvG/CuSkaspeaD9hRDk2zuiMI= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 h1:WAvSpGf7MsFuzAtK4Vk7R4EVe+liW4x83r4oWu0WHKw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3/v2 v2.0.7 h1:6Pwi1b3QdY65cuv6SyVO0FgPd5J3Bl7wf/nQQjinHMA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= @@ -391,21 +599,24 @@ github.com/karrick/godirwalk v1.10.3 h1:lOpSw2vJP0y5eLBW906QwKsUK/fe/QDyoqM5rnnu github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 h1:jUp75lepDg0phMUJBCmvaeFDldD2N3S1lBuPwUTszio= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.5.3 h1:zSGLzsUew8RT+ZKPHc3jnf8XLaVyHzTcAFBzHtCNR20= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k= @@ -413,8 +624,8 @@ github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMAS github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2 h1:g+4J5sZg6osfvEfkRZxJ1em0VT95/UOZgi/l7zi1/oE= @@ -424,28 +635,32 @@ github.com/merlinfuchs/discordgo v0.0.0-20230424012904-69f6c46a340c h1:YXNIyWpGt github.com/merlinfuchs/discordgo v0.0.0-20230424012904-69f6c46a340c/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw= github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8= +github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA= github.com/miolini/datacounter v1.0.3/go.mod h1:C45dc2hBumHjDpEU64IqPwR6TDyPVpzOqqRTN7zmBUA= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/51gbeLHfKGNfgEQQIWrlbdaOsidbQ= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY= github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -480,12 +695,17 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdi v1.0.12 h1:RZb9NG62cw/RW0rHAduVRo+98R8o/G1krcg2ns7DakQ= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= +github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 h1:0XM1XL/OFFJjXsYXlG30spTkV/E9+gmd5GD1w2HE8xM= @@ -493,6 +713,7 @@ github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626 github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= @@ -501,8 +722,11 @@ github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= @@ -536,6 +760,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/snowflakedb/gosnowflake v1.6.3 h1:EJDdDi74YbYt1ty164ge3fMZ0eVZ6KA7b1zmAa/wnRo= +github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= @@ -549,6 +774,8 @@ github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ai github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck= github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= @@ -603,6 +830,7 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f h1:mvXjJIHRZyhNuGassLTcXTwjiWq7NmjdavZsUnmFybQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= @@ -611,6 +839,8 @@ github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= @@ -626,81 +856,158 @@ go.etcd.io/etcd/client/v3 v3.5.6/go.mod h1:f6GRinRMCsFVv9Ht42EyY7nfsVGwrNO0WEoS2 go.etcd.io/etcd/pkg/v3 v3.5.0 h1:ntrg6vvKRW26JRmHTE0iNlDgYK6JX3hg/4cD62X0ixk= go.etcd.io/etcd/raft/v3 v3.5.0 h1:kw2TmO3yFTgE+F0mdKkG7xMxkit2duBDa2Hu6D/HMlw= go.etcd.io/etcd/server/v3 v3.5.0 h1:jk8D/lwGEDlQU9kZXUFMSANkE22Sg5+mW27ip8xcF9E= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 h1:Ky1MObd188aGbgb5OgNnwGuEEwI9MVIcc7rBW6zk5Ak= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 h1:Q3C9yzW6I9jqEc8sawxzxZmY48fs9u220KXq6d5s3XU= go.opentelemetry.io/otel v1.3.0 h1:APxLf0eiBwLl+SOXiJJCVYzA1OOJNyAoV8C5RNRyy7Y= go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0 h1:VQbUHoJqytHHSJ1OZodPH9tvZZSVzUHjPHpkO85sT6k= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g= go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8= go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY= go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M= golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.9.0 h1:3sEo36Uopv1/SA/dMFFaxXoL5XyikJ9Sf2Vll/k6+2E= google.golang.org/api v0.131.0 h1:AcgWS2edQ4chVEt/SxgDKubVu/9/idCJy00tBGuGB4M= google.golang.org/api v0.131.0/go.mod h1:7vtkbKv2REjJbxmHSkBTBQ5LUGvPdAqjjvt84XAfhpA= +google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -723,9 +1030,15 @@ k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c h1:jvamsI1tn9V0S8jicyX82qaFC0H/NKxv2e5mbqsgR80= k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/b v1.0.0 h1:vpvqeyp17ddcQWF29Czawql4lDdABCDRbXRAS4+aF2o= modernc.org/cc/v3 v3.32.4 h1:1ScT6MCQRWwvwVdERhGPsPq0f55J1/pFEOCiqM7zc78= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/ccgo/v3 v3.9.2 h1:mOLFgduk60HFuPmxSix3AluTEh7zhozkby+e1VDo/ro= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/db v1.0.0 h1:2c6NdCfaLnshSvY7OU09cyAY0gYXUZj4lmg5ItHyucg= modernc.org/file v1.0.0 h1:9/PdvjVxd5+LcWUQIfapAWRGOkDLK90rloa8s/au06A= modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= @@ -733,17 +1046,29 @@ modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/internal v1.0.0 h1:XMDsFDcBDsibbBnHB2xzljZ+B1yrOVLEFkKL2u15Glw= modernc.org/libc v1.9.5 h1:zv111ldxmP7DJ5mOIqzRbza7ZDl3kh4ncKfASB2jIYY= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/lldb v1.0.0 h1:6vjDJxQEfhlOLwl4bhpwIz00uyFK4EmSYcbwqwbynsc= modernc.org/mathutil v1.2.2 h1:+yFk8hBprV+4c0U9GjFtL+dV3N8hOJ8JCituQcMShFY= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/ql v1.0.0 h1:bIQ/trWNVjQPlinI6jdOQsi195SIturGo3mp5hsDqVU= modernc.org/sortutil v1.1.0 h1:oP3U4uM+NT/qBQcbg/K2iqAX0Nx7B1b6YZtq3Gk/PjM= modernc.org/sqlite v1.10.6 h1:iNDTQbULcm0IJAqrzCm2JcCqxaKRS94rJ5/clBMRmc8= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/strutil v1.1.0 h1:+1/yCzZxY2pZwwrsbH+4T7BQMoLQ9QiBshRC9eicYsc= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.5.2 h1:sYNjGr4zK6cDH74USl8wVJRrvDX6UOLpG0j4lFvR0W0= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/z v1.0.1 h1:WyIDpEpAIx4Hel6q/Pcgj/VhaQV5XPJ2I6ryIYbjnpc= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= modernc.org/zappy v1.0.0 h1:dPVaP+3ueIUv4guk8PuZ2wiUGcJ1WUVvIheeSSTD0yk= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= From 365bf4f648f179a6a2872efb0d8021c49ad826a2 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Wed, 12 Nov 2025 19:04:48 +0100 Subject: [PATCH 02/36] implement all postgres stores --- embedg-service/common/id.go | 5 + .../db/postgres/store_custom_bot.go | 161 +++++++++++++++++ .../db/postgres/store_custom_command.go | 169 ++++++++++++++++++ .../db/postgres/store_embed_link.go | 71 ++++++++ .../db/postgres/store_entitlement.go | 125 +++++++++++++ embedg-service/db/postgres/store_image.go | 61 +++++++ embedg-service/db/postgres/store_kv.go | 110 ++++++++++++ .../db/postgres/store_message_action_set.go | 88 +++++++++ .../db/postgres/store_saved_message.go | 133 ++++++++++++++ .../db/postgres/store_scheduled_message.go | 160 +++++++++++++++++ embedg-service/db/postgres/store_session.go | 80 +++++++++ .../db/postgres/store_shared_message.go | 50 ++++++ embedg-service/db/postgres/store_user.go | 53 ++++++ embedg-service/model/plan.go | 75 ++++++++ embedg-service/store/custom_bot.go | 2 +- embedg-service/store/custom_command.go | 2 +- embedg-service/store/entitlement.go | 8 +- embedg-service/store/kv.go | 2 +- embedg-service/store/message_action_set.go | 2 +- embedg-service/store/plan.go | 14 ++ embedg-service/store/scheduled_message.go | 8 +- 21 files changed, 1367 insertions(+), 12 deletions(-) create mode 100644 embedg-service/db/postgres/store_custom_bot.go create mode 100644 embedg-service/db/postgres/store_custom_command.go create mode 100644 embedg-service/db/postgres/store_embed_link.go create mode 100644 embedg-service/db/postgres/store_entitlement.go create mode 100644 embedg-service/db/postgres/store_image.go create mode 100644 embedg-service/db/postgres/store_kv.go create mode 100644 embedg-service/db/postgres/store_message_action_set.go create mode 100644 embedg-service/db/postgres/store_saved_message.go create mode 100644 embedg-service/db/postgres/store_scheduled_message.go create mode 100644 embedg-service/db/postgres/store_session.go create mode 100644 embedg-service/db/postgres/store_shared_message.go create mode 100644 embedg-service/db/postgres/store_user.go create mode 100644 embedg-service/model/plan.go create mode 100644 embedg-service/store/plan.go diff --git a/embedg-service/common/id.go b/embedg-service/common/id.go index 6e9b91d59..cde2dac35 100644 --- a/embedg-service/common/id.go +++ b/embedg-service/common/id.go @@ -36,3 +36,8 @@ func UniqueID() snowflake.ID { func ParseID(id string) (ID, error) { return snowflake.Parse(id) } + +func DefinitelyID(id string) ID { + res, _ := snowflake.Parse(id) + return res +} diff --git a/embedg-service/db/postgres/store_custom_bot.go b/embedg-service/db/postgres/store_custom_bot.go new file mode 100644 index 000000000..5560ea3dd --- /dev/null +++ b/embedg-service/db/postgres/store_custom_bot.go @@ -0,0 +1,161 @@ +package postgres + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.CustomBotStore = (*Client)(nil) + +func (c *Client) UpsertCustomBot(ctx context.Context, customBot model.CustomBot) (*model.CustomBot, error) { + row, err := c.Q.UpsertCustomBot(ctx, pgmodel.UpsertCustomBotParams{ + ID: customBot.ID, + GuildID: customBot.GuildID.String(), + ApplicationID: customBot.ApplicationID.String(), + UserID: customBot.UserID.String(), + UserName: customBot.UserName, + UserDiscriminator: customBot.UserDiscriminator, + UserAvatar: pgtype.Text{String: customBot.UserAvatar.String, Valid: customBot.UserAvatar.Valid}, + Token: customBot.Token, + PublicKey: customBot.PublicKey, + CreatedAt: pgtype.Timestamp{Time: customBot.CreatedAt, Valid: true}, + }) + if err != nil { + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) UpdateCustomBotPresence(ctx context.Context, params store.UpdateCustomBotPresenceParams) (*model.CustomBot, error) { + row, err := c.Q.UpdateCustomBotPresence(ctx, pgmodel.UpdateCustomBotPresenceParams{ + GuildID: params.GuildID.String(), + GatewayStatus: params.GatewayStatus, + GatewayActivityType: pgtype.Int2{Int16: int16(params.GatewayActivityType.Int64), Valid: params.GatewayActivityType.Valid}, + GatewayActivityName: pgtype.Text{String: params.GatewayActivityName.String, Valid: params.GatewayActivityName.Valid}, + GatewayActivityState: pgtype.Text{String: params.GatewayActivityState.String, Valid: params.GatewayActivityState.Valid}, + GatewayActivityUrl: pgtype.Text{String: params.GatewayActivityUrl.String, Valid: params.GatewayActivityUrl.Valid}, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) UpdateCustomBotUser(ctx context.Context, params store.UpdateCustomBotUserParams) (*model.CustomBot, error) { + row, err := c.Q.UpdateCustomBotUser(ctx, pgmodel.UpdateCustomBotUserParams{ + GuildID: params.GuildID.String(), + UserName: params.UserName, + UserDiscriminator: params.UserDiscriminator, + UserAvatar: pgtype.Text{String: params.UserAvatar.String, Valid: params.UserAvatar.Valid}, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) UpdateCustomBotTokenInvalid(ctx context.Context, guildID common.ID) (*model.CustomBot, error) { + row, err := c.Q.UpdateCustomBotTokenInvalid(ctx, pgmodel.UpdateCustomBotTokenInvalidParams{ + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) DeleteCustomBot(ctx context.Context, guildID common.ID) (*model.CustomBot, error) { + row, err := c.Q.DeleteCustomBot(ctx, guildID.String()) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) GetCustomBot(ctx context.Context, id string) (*model.CustomBot, error) { + row, err := c.Q.GetCustomBot(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) GetCustomBotByGuildID(ctx context.Context, guildID common.ID) (*model.CustomBot, error) { + row, err := c.Q.GetCustomBotByGuildID(ctx, guildID.String()) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomBot(row), nil +} + +func (c *Client) SetCustomBotHandledFirstInteraction(ctx context.Context, id string) error { + err := c.Q.SetCustomBotHandledFirstInteraction(ctx, id) + if err != nil { + return err + } + return nil +} + +func (c *Client) GetCustomBots(ctx context.Context) ([]model.CustomBot, error) { + rows, err := c.Q.GetCustomBots(ctx) + if err != nil { + return nil, err + } + return rowsToCustomBots(rows), nil +} + +func rowsToCustomBots(rows []pgmodel.CustomBot) []model.CustomBot { + bots := make([]model.CustomBot, len(rows)) + for i, row := range rows { + bots[i] = *rowToCustomBot(row) + } + return bots +} + +func rowToCustomBot(row pgmodel.CustomBot) *model.CustomBot { + return &model.CustomBot{ + ID: row.ID, + GuildID: common.DefinitelyID(row.GuildID), + ApplicationID: common.DefinitelyID(row.ApplicationID), + Token: row.Token, + PublicKey: row.PublicKey, + UserID: common.DefinitelyID(row.UserID), + UserName: row.UserName, + UserDiscriminator: row.UserDiscriminator, + UserAvatar: null.NewString(row.UserAvatar.String, row.UserAvatar.Valid), + HandledFirstInteraction: row.HandledFirstInteraction, + CreatedAt: row.CreatedAt.Time, + TokenInvalid: row.TokenInvalid, + GatewayStatus: row.GatewayStatus, + GatewayActivityType: null.NewInt(int64(row.GatewayActivityType.Int16), row.GatewayActivityType.Valid), + GatewayActivityName: null.NewString(row.GatewayActivityName.String, row.GatewayActivityName.Valid), + GatewayActivityState: null.NewString(row.GatewayActivityState.String, row.GatewayActivityState.Valid), + GatewayActivityUrl: null.NewString(row.GatewayActivityUrl.String, row.GatewayActivityUrl.Valid), + } +} diff --git a/embedg-service/db/postgres/store_custom_command.go b/embedg-service/db/postgres/store_custom_command.go new file mode 100644 index 000000000..f4ef54b75 --- /dev/null +++ b/embedg-service/db/postgres/store_custom_command.go @@ -0,0 +1,169 @@ +package postgres + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.CustomCommandStore = (*Client)(nil) + +func (c *Client) GetCustomCommands(ctx context.Context, guildID common.ID) ([]model.CustomCommand, error) { + rows, err := c.Q.GetCustomCommands(ctx, guildID.String()) + if err != nil { + return nil, err + } + return rowsToCustomCommands(rows), nil +} + +func (c *Client) GetCustomCommand(ctx context.Context, guildID common.ID, id string) (*model.CustomCommand, error) { + row, err := c.Q.GetCustomCommand(ctx, pgmodel.GetCustomCommandParams{ + ID: id, + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomCommand(row), nil +} + +func (c *Client) GetCustomCommandByName(ctx context.Context, guildID common.ID, name string) (*model.CustomCommand, error) { + row, err := c.Q.GetCustomCommandByName(ctx, pgmodel.GetCustomCommandByNameParams{ + Name: name, + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomCommand(row), nil +} + +func (c *Client) CountCustomCommands(ctx context.Context, guildID common.ID) (int64, error) { + count, err := c.Q.CountCustomCommands(ctx, guildID.String()) + if err != nil { + return 0, err + } + return count, nil +} + +func (c *Client) CreateCustomCommand(ctx context.Context, customCommand model.CustomCommand) (*model.CustomCommand, error) { + row, err := c.Q.InsertCustomCommand(ctx, pgmodel.InsertCustomCommandParams{ + ID: customCommand.ID, + GuildID: customCommand.GuildID.String(), + Name: customCommand.Name, + Description: customCommand.Description, + Parameters: customCommand.Parameters, + Actions: customCommand.Actions, + DerivedPermissions: customCommand.DerivedPermissions, + CreatedAt: pgtype.Timestamp{Time: customCommand.CreatedAt, Valid: true}, + UpdatedAt: pgtype.Timestamp{Time: customCommand.UpdatedAt, Valid: true}, + }) + if err != nil { + return nil, err + } + return rowToCustomCommand(row), nil +} + +func (c *Client) UpdateCustomCommand(ctx context.Context, customCommand model.CustomCommand) (*model.CustomCommand, error) { + row, err := c.Q.UpdateCustomCommand(ctx, pgmodel.UpdateCustomCommandParams{ + ID: customCommand.ID, + GuildID: customCommand.GuildID.String(), + Name: customCommand.Name, + Description: customCommand.Description, + Enabled: customCommand.Enabled, + Actions: customCommand.Actions, + Parameters: customCommand.Parameters, + DerivedPermissions: customCommand.DerivedPermissions, + UpdatedAt: pgtype.Timestamp{Time: customCommand.UpdatedAt, Valid: true}, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomCommand(row), nil +} + +func (c *Client) DeleteCustomCommand(ctx context.Context, guildID common.ID, id string) (*model.CustomCommand, error) { + row, err := c.Q.DeleteCustomCommand(ctx, pgmodel.DeleteCustomCommandParams{ + ID: id, + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomCommand(row), nil +} + +func (c *Client) SetCustomCommandsDeployedAt(ctx context.Context, guildID common.ID, deployedAt time.Time) (*model.CustomCommand, error) { + row, err := c.Q.SetCustomCommandsDeployedAt(ctx, pgmodel.SetCustomCommandsDeployedAtParams{ + GuildID: guildID.String(), + DeployedAt: pgtype.Timestamp{Time: deployedAt, Valid: true}, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToCustomCommand(row), nil +} + +func rowsToCustomCommands(rows []pgmodel.CustomCommand) []model.CustomCommand { + commands := make([]model.CustomCommand, len(rows)) + for i, row := range rows { + commands[i] = *rowToCustomCommand(row) + } + return commands +} + +func rowToCustomCommand(row pgmodel.CustomCommand) *model.CustomCommand { + var parameters json.RawMessage + if row.Parameters != nil { + parameters = json.RawMessage(row.Parameters) + } + + var actions json.RawMessage + if row.Actions != nil { + actions = json.RawMessage(row.Actions) + } + + var derivedPermissions json.RawMessage + if row.DerivedPermissions != nil { + derivedPermissions = json.RawMessage(row.DerivedPermissions) + } + + return &model.CustomCommand{ + ID: row.ID, + GuildID: common.DefinitelyID(row.GuildID), + Name: row.Name, + Description: row.Description, + Enabled: row.Enabled, + Parameters: parameters, + Actions: actions, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + DeployedAt: null.NewTime(row.DeployedAt.Time, row.DeployedAt.Valid), + DerivedPermissions: derivedPermissions, + LastUsedAt: null.NewTime(row.LastUsedAt.Time, row.LastUsedAt.Valid), + } +} diff --git a/embedg-service/db/postgres/store_embed_link.go b/embedg-service/db/postgres/store_embed_link.go new file mode 100644 index 000000000..ef8ea747c --- /dev/null +++ b/embedg-service/db/postgres/store_embed_link.go @@ -0,0 +1,71 @@ +package postgres + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.EmbedLinkStore = (*Client)(nil) + +func (c *Client) CreateEmbedLink(ctx context.Context, embedLink model.EmbedLink) (*model.EmbedLink, error) { + row, err := c.Q.InsertEmbedLink(ctx, pgmodel.InsertEmbedLinkParams{ + ID: embedLink.ID.String(), + Url: embedLink.Url, + ThemeColor: pgtype.Text{String: embedLink.ThemeColor.String, Valid: embedLink.ThemeColor.Valid}, + OgTitle: pgtype.Text{String: embedLink.OgTitle.String, Valid: embedLink.OgTitle.Valid}, + OgSiteName: pgtype.Text{String: embedLink.OgSiteName.String, Valid: embedLink.OgSiteName.Valid}, + OgDescription: pgtype.Text{String: embedLink.OgDescription.String, Valid: embedLink.OgDescription.Valid}, + OgImage: pgtype.Text{String: embedLink.OgImage.String, Valid: embedLink.OgImage.Valid}, + OeType: pgtype.Text{String: embedLink.OeType.String, Valid: embedLink.OeType.Valid}, + OeAuthorName: pgtype.Text{String: embedLink.OeAuthorName.String, Valid: embedLink.OeAuthorName.Valid}, + OeAuthorUrl: pgtype.Text{String: embedLink.OeAuthorUrl.String, Valid: embedLink.OeAuthorUrl.Valid}, + OeProviderName: pgtype.Text{String: embedLink.OeProviderName.String, Valid: embedLink.OeProviderName.Valid}, + OeProviderUrl: pgtype.Text{String: embedLink.OeProviderUrl.String, Valid: embedLink.OeProviderUrl.Valid}, + TwCard: pgtype.Text{String: embedLink.TwCard.String, Valid: embedLink.TwCard.Valid}, + ExpiresAt: pgtype.Timestamp{Time: embedLink.ExpiresAt.Time, Valid: embedLink.ExpiresAt.Valid}, + CreatedAt: pgtype.Timestamp{Time: embedLink.CreatedAt, Valid: true}, + }) + if err != nil { + return nil, err + } + return rowToEmbedLink(row), nil +} + +func (c *Client) GetEmbedLink(ctx context.Context, id string) (*model.EmbedLink, error) { + row, err := c.Q.GetEmbedLink(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToEmbedLink(row), nil +} + +func rowToEmbedLink(row pgmodel.EmbedLink) *model.EmbedLink { + return &model.EmbedLink{ + ID: common.DefinitelyID(row.ID), + Url: row.Url, + ThemeColor: null.NewString(row.ThemeColor.String, row.ThemeColor.Valid), + OgTitle: null.NewString(row.OgTitle.String, row.OgTitle.Valid), + OgSiteName: null.NewString(row.OgSiteName.String, row.OgSiteName.Valid), + OgDescription: null.NewString(row.OgDescription.String, row.OgDescription.Valid), + OgImage: null.NewString(row.OgImage.String, row.OgImage.Valid), + OeType: null.NewString(row.OeType.String, row.OeType.Valid), + OeAuthorName: null.NewString(row.OeAuthorName.String, row.OeAuthorName.Valid), + OeAuthorUrl: null.NewString(row.OeAuthorUrl.String, row.OeAuthorUrl.Valid), + OeProviderName: null.NewString(row.OeProviderName.String, row.OeProviderName.Valid), + OeProviderUrl: null.NewString(row.OeProviderUrl.String, row.OeProviderUrl.Valid), + TwCard: null.NewString(row.TwCard.String, row.TwCard.Valid), + ExpiresAt: null.NewTime(row.ExpiresAt.Time, row.ExpiresAt.Valid), + CreatedAt: row.CreatedAt.Time, + } +} diff --git a/embedg-service/db/postgres/store_entitlement.go b/embedg-service/db/postgres/store_entitlement.go new file mode 100644 index 000000000..61edc3865 --- /dev/null +++ b/embedg-service/db/postgres/store_entitlement.go @@ -0,0 +1,125 @@ +package postgres + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.EntitlementStore = (*Client)(nil) + +func (c *Client) GetActiveEntitlementsForGuild(ctx context.Context, guildID common.ID) ([]model.Entitlement, error) { + rows, err := c.Q.GetActiveEntitlementsForGuild(ctx, pgtype.Text{String: guildID.String(), Valid: true}) + if err != nil { + return nil, err + } + return rowsToEntitlements(rows), nil +} + +func (c *Client) GetActiveEntitlementsForUser(ctx context.Context, userID common.ID) ([]model.Entitlement, error) { + rows, err := c.Q.GetActiveEntitlementsForUser(ctx, pgtype.Text{String: userID.String(), Valid: true}) + if err != nil { + return nil, err + } + return rowsToEntitlements(rows), nil +} + +func (c *Client) GetEntitlements(ctx context.Context) ([]model.Entitlement, error) { + rows, err := c.Q.GetEntitlements(ctx) + if err != nil { + return nil, err + } + return rowsToEntitlements(rows), nil +} + +func (c *Client) GetEntitlement(ctx context.Context, id common.ID, userID common.ID) (*model.Entitlement, error) { + row, err := c.Q.GetEntitlement(ctx, pgmodel.GetEntitlementParams{ + ID: id.String(), + UserID: pgtype.Text{String: userID.String(), Valid: true}, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToEntitlement(row), nil +} + +func (c *Client) UpdateEntitlementConsumedGuildID(ctx context.Context, id common.ID, consumedGuildID common.NullID) (*model.Entitlement, error) { + row, err := c.Q.UpdateEntitlementConsumedGuildID(ctx, pgmodel.UpdateEntitlementConsumedGuildIDParams{ + ID: id.String(), + ConsumedGuildID: pgtype.Text{String: consumedGuildID.ID.String(), Valid: consumedGuildID.Valid}, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToEntitlement(row), nil +} + +func (c *Client) UpsertEntitlement(ctx context.Context, entitlement model.Entitlement) (*model.Entitlement, error) { + row, err := c.Q.UpsertEntitlement(ctx, pgmodel.UpsertEntitlementParams{ + ID: entitlement.ID, + UserID: pgtype.Text{String: entitlement.UserID.ID.String(), Valid: entitlement.UserID.Valid}, + GuildID: pgtype.Text{String: entitlement.GuildID.ID.String(), Valid: entitlement.GuildID.Valid}, + UpdatedAt: pgtype.Timestamp{Time: entitlement.UpdatedAt, Valid: true}, + Deleted: entitlement.Deleted, + SkuID: entitlement.SkuID, + StartsAt: pgtype.Timestamp{Time: entitlement.StartsAt.Time, Valid: entitlement.StartsAt.Valid}, + EndsAt: pgtype.Timestamp{Time: entitlement.EndsAt.Time, Valid: entitlement.EndsAt.Valid}, + Consumed: entitlement.Consumed, + }) + if err != nil { + return nil, err + } + return rowToEntitlement(row), nil +} + +func rowsToEntitlements(rows []pgmodel.Entitlement) []model.Entitlement { + entitlements := make([]model.Entitlement, len(rows)) + for i, row := range rows { + entitlements[i] = *rowToEntitlement(row) + } + return entitlements +} + +func rowToEntitlement(row pgmodel.Entitlement) *model.Entitlement { + var userID common.NullID + if row.UserID.Valid { + userID = common.NullID{ + Valid: true, + ID: common.DefinitelyID(row.UserID.String), + } + } + + var guildID common.NullID + if row.GuildID.Valid { + guildID = common.NullID{ + Valid: true, + ID: common.DefinitelyID(row.GuildID.String), + } + } + + return &model.Entitlement{ + ID: row.ID, + UserID: userID, + GuildID: guildID, + UpdatedAt: row.UpdatedAt.Time, + Deleted: row.Deleted, + SkuID: row.SkuID, + StartsAt: null.NewTime(row.StartsAt.Time, row.StartsAt.Valid), + EndsAt: null.NewTime(row.EndsAt.Time, row.EndsAt.Valid), + Consumed: row.Consumed, + ConsumedGuildID: null.NewString(row.ConsumedGuildID.String, row.ConsumedGuildID.Valid), + } +} diff --git a/embedg-service/db/postgres/store_image.go b/embedg-service/db/postgres/store_image.go new file mode 100644 index 000000000..1d403d0b8 --- /dev/null +++ b/embedg-service/db/postgres/store_image.go @@ -0,0 +1,61 @@ +package postgres + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" +) + +var _ store.ImageStore = (*Client)(nil) + +func (c *Client) CreateImage(ctx context.Context, img model.Image) error { + _, err := c.Q.InsertImage(ctx, pgmodel.InsertImageParams{ + ID: img.ID, + GuildID: pgtype.Text{String: img.GuildID.ID.String(), Valid: img.GuildID.Valid}, + UserID: img.UserID.String(), + FileHash: img.FileHash, + FileName: img.FileName, + FileContentType: img.FileContentType, + FileSize: int32(img.FileSize), + S3Key: img.S3Key, + }) + return err +} + +func (c *Client) GetImage(ctx context.Context, id string) (*model.Image, error) { + row, err := c.Q.GetImage(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToImage(row), nil +} + +func rowToImage(row pgmodel.Image) *model.Image { + var guildID common.NullID + if row.GuildID.Valid { + guildID = common.NullID{ + Valid: true, + ID: common.DefinitelyID(row.GuildID.String), + } + } + + return &model.Image{ + ID: row.ID, + UserID: common.DefinitelyID(row.UserID), + GuildID: guildID, + FileHash: row.FileHash, + FileName: row.FileName, + FileSize: int(row.FileSize), + FileContentType: row.FileContentType, + S3Key: row.S3Key, + } +} diff --git a/embedg-service/db/postgres/store_kv.go b/embedg-service/db/postgres/store_kv.go new file mode 100644 index 000000000..7e2d73e66 --- /dev/null +++ b/embedg-service/db/postgres/store_kv.go @@ -0,0 +1,110 @@ +package postgres + +import ( + "context" + "errors" + "strconv" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.KVEntryStore = (*Client)(nil) + +func (c *Client) GetKVEntry(ctx context.Context, guildID common.ID, key string) (*model.KVEntry, error) { + row, err := c.Q.GetKVEntry(ctx, pgmodel.GetKVEntryParams{ + Key: key, + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToKVEntry(row), nil +} + +func (c *Client) SetKVEntry(ctx context.Context, entry model.KVEntry) error { + err := c.Q.SetKVEntry(ctx, pgmodel.SetKVEntryParams{ + Key: entry.Key, + GuildID: entry.GuildID.String(), + Value: entry.Value, + ExpiresAt: pgtype.Timestamp{Time: entry.ExpiresAt.Time, Valid: entry.ExpiresAt.Valid}, + CreatedAt: pgtype.Timestamp{Time: entry.CreatedAt, Valid: true}, + UpdatedAt: pgtype.Timestamp{Time: entry.UpdatedAt, Valid: true}, + }) + return err +} + +func (c *Client) IncreaseKVEntry(ctx context.Context, params store.KVEntryIncreaseParams) (*model.KVEntry, error) { + row, err := c.Q.IncreaseKVEntry(ctx, pgmodel.IncreaseKVEntryParams{ + Key: params.Key, + GuildID: params.GuildID.String(), + Value: strconv.Itoa(params.Delta), + ExpiresAt: pgtype.Timestamp{Time: params.ExpiresAt.Time, Valid: params.ExpiresAt.Valid}, + CreatedAt: pgtype.Timestamp{Time: params.CreatedAt, Valid: true}, + UpdatedAt: pgtype.Timestamp{Time: params.UpdatedAt, Valid: true}, + }) + if err != nil { + return nil, err + } + return rowToKVEntry(row), nil +} + +func (c *Client) DeleteKVEntry(ctx context.Context, guildID common.ID, key string) (*model.KVEntry, error) { + row, err := c.Q.DeleteKVEntry(ctx, pgmodel.DeleteKVEntryParams{ + Key: key, + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToKVEntry(row), nil +} + +func (c *Client) SearchKVEntries(ctx context.Context, guildID common.ID, pattern string) ([]model.KVEntry, error) { + rows, err := c.Q.SearchKVEntries(ctx, pgmodel.SearchKVEntriesParams{ + Key: pattern, + GuildID: guildID.String(), + }) + if err != nil { + return nil, err + } + return rowsToKVEntries(rows), nil +} + +func (c *Client) CountKVEntries(ctx context.Context, guildID common.ID) (int64, error) { + count, err := c.Q.CountKVEntries(ctx, guildID.String()) + if err != nil { + return 0, err + } + return count, nil +} + +func rowsToKVEntries(rows []pgmodel.KvEntry) []model.KVEntry { + entries := make([]model.KVEntry, len(rows)) + for i, row := range rows { + entries[i] = *rowToKVEntry(row) + } + return entries +} + +func rowToKVEntry(row pgmodel.KvEntry) *model.KVEntry { + return &model.KVEntry{ + Key: row.Key, + GuildID: common.DefinitelyID(row.GuildID), + Value: row.Value, + ExpiresAt: null.NewTime(row.ExpiresAt.Time, row.ExpiresAt.Valid), + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } +} diff --git a/embedg-service/db/postgres/store_message_action_set.go b/embedg-service/db/postgres/store_message_action_set.go new file mode 100644 index 000000000..70fcd5ec6 --- /dev/null +++ b/embedg-service/db/postgres/store_message_action_set.go @@ -0,0 +1,88 @@ +package postgres + +import ( + "context" + "encoding/json" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.MessageActionSetStore = (*Client)(nil) + +func (c *Client) CreateMessageActionSet(ctx context.Context, messageActionSet model.MessageActionSet) (*model.MessageActionSet, error) { + row, err := c.Q.InsertMessageActionSet(ctx, pgmodel.InsertMessageActionSetParams{ + ID: messageActionSet.ID, + MessageID: messageActionSet.MessageID.String(), + SetID: messageActionSet.SetID, + Actions: messageActionSet.Actions, + DerivedPermissions: messageActionSet.DerivedPermissions, + Ephemeral: messageActionSet.Ephemeral, + }) + if err != nil { + return nil, err + } + return rowToMessageActionSet(row), nil +} + +func (c *Client) GetMessageActionSet(ctx context.Context, messageID common.ID, actionSetID string) (*model.MessageActionSet, error) { + row, err := c.Q.GetMessageActionSet(ctx, pgmodel.GetMessageActionSetParams{ + MessageID: messageID.String(), + SetID: actionSetID, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToMessageActionSet(row), nil +} + +func (c *Client) GetMessageActionSets(ctx context.Context, messageID common.ID) ([]model.MessageActionSet, error) { + rows, err := c.Q.GetMessageActionSets(ctx, messageID.String()) + if err != nil { + return nil, err + } + return rowsToMessageActionSets(rows), nil +} + +func (c *Client) DeleteMessageActionSetsForMessage(ctx context.Context, messageID common.ID) error { + err := c.Q.DeleteMessageActionSetsForMessage(ctx, messageID.String()) + return err +} + +func rowsToMessageActionSets(rows []pgmodel.MessageActionSet) []model.MessageActionSet { + sets := make([]model.MessageActionSet, len(rows)) + for i, row := range rows { + sets[i] = *rowToMessageActionSet(row) + } + return sets +} + +func rowToMessageActionSet(row pgmodel.MessageActionSet) *model.MessageActionSet { + var actions json.RawMessage + if row.Actions != nil { + actions = json.RawMessage(row.Actions) + } + + var derivedPermissions json.RawMessage + if row.DerivedPermissions != nil { + derivedPermissions = json.RawMessage(row.DerivedPermissions) + } + + return &model.MessageActionSet{ + ID: row.ID, + MessageID: common.DefinitelyID(row.MessageID), + SetID: row.SetID, + Actions: actions, + DerivedPermissions: derivedPermissions, + LastUsedAt: null.NewTime(row.LastUsedAt.Time, row.LastUsedAt.Valid), + Ephemeral: row.Ephemeral, + } +} diff --git a/embedg-service/db/postgres/store_saved_message.go b/embedg-service/db/postgres/store_saved_message.go new file mode 100644 index 000000000..de8160a53 --- /dev/null +++ b/embedg-service/db/postgres/store_saved_message.go @@ -0,0 +1,133 @@ +package postgres + +import ( + "context" + "encoding/json" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.SavedMessageStore = (*Client)(nil) + +func (c *Client) CreateSavedMessage(ctx context.Context, msg model.SavedMessage) error { + _, err := c.Q.InsertSavedMessage(ctx, pgmodel.InsertSavedMessageParams{ + ID: msg.ID, + CreatorID: msg.CreatorID.String(), + GuildID: pgtype.Text{String: msg.GuildID.ID.String(), Valid: msg.GuildID.Valid}, + UpdatedAt: pgtype.Timestamp{Time: msg.UpdatedAt, Valid: true}, + Name: msg.Name, + Description: pgtype.Text{String: msg.Description.String, Valid: msg.Description.Valid}, + Data: msg.Data, + }) + return err +} + +func (c *Client) UpdateSavedMessageForCreator(ctx context.Context, msg model.SavedMessage) error { + _, err := c.Q.UpdateSavedMessageForCreator(ctx, pgmodel.UpdateSavedMessageForCreatorParams{ + ID: msg.ID, + CreatorID: msg.CreatorID.String(), + UpdatedAt: pgtype.Timestamp{Time: msg.UpdatedAt, Valid: true}, + Name: msg.Name, + Description: pgtype.Text{String: msg.Description.String, Valid: msg.Description.Valid}, + Data: msg.Data, + }) + return err +} + +func (c *Client) UpdateSavedMessageForGuild(ctx context.Context, msg model.SavedMessage) error { + _, err := c.Q.UpdateSavedMessageForGuild(ctx, pgmodel.UpdateSavedMessageForGuildParams{ + ID: msg.ID, + GuildID: pgtype.Text{String: msg.GuildID.ID.String(), Valid: msg.GuildID.Valid}, + UpdatedAt: pgtype.Timestamp{Time: msg.UpdatedAt, Valid: true}, + Name: msg.Name, + Description: pgtype.Text{String: msg.Description.String, Valid: msg.Description.Valid}, + Data: msg.Data, + }) + return err +} + +func (c *Client) DeleteSavedMessageForCreator(ctx context.Context, msg model.SavedMessage) error { + err := c.Q.DeleteSavedMessageForCreator(ctx, pgmodel.DeleteSavedMessageForCreatorParams{ + ID: msg.ID, + CreatorID: msg.CreatorID.String(), + }) + return err +} + +func (c *Client) DeleteSavedMessageForGuild(ctx context.Context, msg model.SavedMessage) error { + err := c.Q.DeleteSavedMessageForGuild(ctx, pgmodel.DeleteSavedMessageForGuildParams{ + ID: msg.ID, + GuildID: pgtype.Text{String: msg.GuildID.ID.String(), Valid: msg.GuildID.Valid}, + }) + return err +} + +func (c *Client) GetSavedMessagesForCreator(ctx context.Context, creatorID common.ID) ([]model.SavedMessage, error) { + rows, err := c.Q.GetSavedMessagesForCreator(ctx, creatorID.String()) + if err != nil { + return nil, err + } + return rowsToSavedMessages(rows), nil +} + +func (c *Client) GetSavedMessagesForGuild(ctx context.Context, guildID common.ID) ([]model.SavedMessage, error) { + rows, err := c.Q.GetSavedMessagesForGuild(ctx, pgtype.Text{String: guildID.String(), Valid: true}) + if err != nil { + return nil, err + } + return rowsToSavedMessages(rows), nil +} + +func (c *Client) GetSavedMessageForGuild(ctx context.Context, guildID common.ID, id string) (*model.SavedMessage, error) { + row, err := c.Q.GetSavedMessageForGuild(ctx, pgmodel.GetSavedMessageForGuildParams{ + GuildID: pgtype.Text{String: guildID.String(), Valid: true}, + ID: id, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToSavedMessage(row), nil +} + +func rowsToSavedMessages(rows []pgmodel.SavedMessage) []model.SavedMessage { + messages := make([]model.SavedMessage, len(rows)) + for i, row := range rows { + messages[i] = *rowToSavedMessage(row) + } + return messages +} + +func rowToSavedMessage(row pgmodel.SavedMessage) *model.SavedMessage { + var guildID common.NullID + if row.GuildID.Valid { + guildID = common.NullID{ + Valid: true, + ID: common.DefinitelyID(row.GuildID.String), + } + } + + var data json.RawMessage + if row.Data != nil { + data = json.RawMessage(row.Data) + } + + return &model.SavedMessage{ + ID: row.ID, + CreatorID: common.DefinitelyID(row.CreatorID), + GuildID: guildID, + UpdatedAt: row.UpdatedAt.Time, + Name: row.Name, + Description: null.NewString(row.Description.String, row.Description.Valid), + Data: data, + } +} diff --git a/embedg-service/db/postgres/store_scheduled_message.go b/embedg-service/db/postgres/store_scheduled_message.go new file mode 100644 index 000000000..7984cbe11 --- /dev/null +++ b/embedg-service/db/postgres/store_scheduled_message.go @@ -0,0 +1,160 @@ +package postgres + +import ( + "context" + "errors" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.ScheduledMessageStore = (*Client)(nil) + +func (c *Client) GetDueScheduledMessages(ctx context.Context, now time.Time) ([]model.ScheduledMessage, error) { + rows, err := c.Q.GetDueScheduledMessages(ctx, pgtype.Timestamp{Time: now, Valid: true}) + if err != nil { + return nil, err + } + return rowsToScheduledMessages(rows), nil +} + +func (c *Client) GetScheduledMessages(ctx context.Context, guildID common.ID) ([]model.ScheduledMessage, error) { + rows, err := c.Q.GetScheduledMessages(ctx, guildID.String()) + if err != nil { + return nil, err + } + return rowsToScheduledMessages(rows), nil +} + +func (c *Client) GetScheduledMessage(ctx context.Context, id common.ID, guildID common.ID) (*model.ScheduledMessage, error) { + row, err := c.Q.GetScheduledMessage(ctx, pgmodel.GetScheduledMessageParams{ + ID: id.String(), + GuildID: guildID.String(), + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToScheduledMessage(row), nil +} + +func (c *Client) DeleteScheduledMessage(ctx context.Context, id common.ID, guildID common.ID) error { + err := c.Q.DeleteScheduledMessage(ctx, pgmodel.DeleteScheduledMessageParams{ + ID: id.String(), + GuildID: guildID.String(), + }) + return err +} + +func (c *Client) CreateScheduledMessage(ctx context.Context, msg model.ScheduledMessage) error { + _, err := c.Q.InsertScheduledMessage(ctx, pgmodel.InsertScheduledMessageParams{ + ID: msg.ID, + CreatorID: msg.CreatorID.String(), + GuildID: msg.GuildID.String(), + ChannelID: msg.ChannelID.String(), + MessageID: pgtype.Text{String: msg.MessageID.ID.String(), Valid: msg.MessageID.Valid}, + ThreadName: pgtype.Text{String: msg.ThreadName.String, Valid: msg.ThreadName.Valid}, + SavedMessageID: msg.SavedMessageID, + Name: msg.Name, + Description: pgtype.Text{String: msg.Description.String, Valid: msg.Description.Valid}, + CronExpression: pgtype.Text{String: msg.CronExpression.String, Valid: msg.CronExpression.Valid}, + CronTimezone: pgtype.Text{String: msg.CronTimezone.String, Valid: msg.CronTimezone.Valid}, + StartAt: pgtype.Timestamp{Time: msg.StartAt, Valid: true}, + EndAt: pgtype.Timestamp{Time: msg.EndAt.Time, Valid: msg.EndAt.Valid}, + NextAt: pgtype.Timestamp{Time: msg.NextAt, Valid: true}, + OnlyOnce: msg.OnlyOnce, + Enabled: msg.Enabled, + CreatedAt: pgtype.Timestamp{Time: msg.CreatedAt, Valid: true}, + UpdatedAt: pgtype.Timestamp{Time: msg.UpdatedAt, Valid: true}, + }) + return err +} + +func (c *Client) UpdateScheduledMessage(ctx context.Context, msg model.ScheduledMessage) error { + _, err := c.Q.UpdateScheduledMessage(ctx, pgmodel.UpdateScheduledMessageParams{ + ID: msg.ID, + GuildID: msg.GuildID.String(), + ChannelID: msg.ChannelID.String(), + MessageID: pgtype.Text{String: msg.MessageID.ID.String(), Valid: msg.MessageID.Valid}, + ThreadName: pgtype.Text{String: msg.ThreadName.String, Valid: msg.ThreadName.Valid}, + SavedMessageID: msg.SavedMessageID, + Name: msg.Name, + Description: pgtype.Text{String: msg.Description.String, Valid: msg.Description.Valid}, + CronExpression: pgtype.Text{String: msg.CronExpression.String, Valid: msg.CronExpression.Valid}, + NextAt: pgtype.Timestamp{Time: msg.NextAt, Valid: true}, + StartAt: pgtype.Timestamp{Time: msg.StartAt, Valid: true}, + EndAt: pgtype.Timestamp{Time: msg.EndAt.Time, Valid: msg.EndAt.Valid}, + OnlyOnce: msg.OnlyOnce, + Enabled: msg.Enabled, + UpdatedAt: pgtype.Timestamp{Time: msg.UpdatedAt, Valid: true}, + CronTimezone: pgtype.Text{String: msg.CronTimezone.String, Valid: msg.CronTimezone.Valid}, + }) + return err +} + +func (c *Client) UpdateScheduledMessageNextAt(ctx context.Context, id common.ID, guildID common.ID, nextAt time.Time) error { + _, err := c.Q.UpdateScheduledMessageNextAt(ctx, pgmodel.UpdateScheduledMessageNextAtParams{ + ID: id.String(), + GuildID: guildID.String(), + NextAt: pgtype.Timestamp{Time: nextAt, Valid: true}, + UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + }) + return err +} + +func (c *Client) UpdateScheduledMessageEnabled(ctx context.Context, id common.ID, guildID common.ID, enabled bool) error { + _, err := c.Q.UpdateScheduledMessageEnabled(ctx, pgmodel.UpdateScheduledMessageEnabledParams{ + ID: id.String(), + GuildID: guildID.String(), + Enabled: enabled, + UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + }) + return err +} + +func rowsToScheduledMessages(rows []pgmodel.ScheduledMessage) []model.ScheduledMessage { + messages := make([]model.ScheduledMessage, len(rows)) + for i, row := range rows { + messages[i] = *rowToScheduledMessage(row) + } + return messages +} + +func rowToScheduledMessage(row pgmodel.ScheduledMessage) *model.ScheduledMessage { + var messageID common.NullID + if row.MessageID.Valid { + messageID = common.NullID{ + Valid: true, + ID: common.DefinitelyID(row.MessageID.String), + } + } + + return &model.ScheduledMessage{ + ID: row.ID, + CreatorID: common.DefinitelyID(row.CreatorID), + GuildID: common.DefinitelyID(row.GuildID), + ChannelID: common.DefinitelyID(row.ChannelID), + MessageID: messageID, + SavedMessageID: row.SavedMessageID, + Name: row.Name, + Description: null.NewString(row.Description.String, row.Description.Valid), + CronExpression: null.NewString(row.CronExpression.String, row.CronExpression.Valid), + OnlyOnce: row.OnlyOnce, + StartAt: row.StartAt.Time, + EndAt: null.NewTime(row.EndAt.Time, row.EndAt.Valid), + NextAt: row.NextAt.Time, + Enabled: row.Enabled, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + CronTimezone: null.NewString(row.CronTimezone.String, row.CronTimezone.Valid), + ThreadName: null.NewString(row.ThreadName.String, row.ThreadName.Valid), + } +} diff --git a/embedg-service/db/postgres/store_session.go b/embedg-service/db/postgres/store_session.go new file mode 100644 index 000000000..1ae64c702 --- /dev/null +++ b/embedg-service/db/postgres/store_session.go @@ -0,0 +1,80 @@ +package postgres + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" +) + +var _ store.SessionStore = (*Client)(nil) + +func (c *Client) CreateSession(ctx context.Context, session model.Session) error { + guildIds := make([]string, len(session.GuildIds)) + for i, id := range session.GuildIds { + guildIds[i] = id.String() + } + + _, err := c.Q.InsertSession(ctx, pgmodel.InsertSessionParams{ + TokenHash: session.TokenHash, + UserID: session.UserID.String(), + GuildIds: guildIds, + AccessToken: session.AccessToken, + CreatedAt: pgtype.Timestamp{Time: session.CreatedAt, Valid: true}, + ExpiresAt: pgtype.Timestamp{Time: session.ExpiresAt, Valid: true}, + }) + return err +} + +func (c *Client) GetSession(ctx context.Context, tokenHash string) (*model.Session, error) { + row, err := c.Q.GetSession(ctx, tokenHash) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToSession(row), nil +} + +func (c *Client) DeleteSession(ctx context.Context, tokenHash string) error { + err := c.Q.DeleteSession(ctx, tokenHash) + return err +} + +func (c *Client) GetSessionsForUser(ctx context.Context, userID string) ([]model.Session, error) { + rows, err := c.Q.GetSessionsForUser(ctx, userID) + if err != nil { + return nil, err + } + return rowsToSessions(rows), nil +} + +func rowsToSessions(rows []pgmodel.Session) []model.Session { + sessions := make([]model.Session, len(rows)) + for i, row := range rows { + sessions[i] = *rowToSession(row) + } + return sessions +} + +func rowToSession(row pgmodel.Session) *model.Session { + guildIds := make([]common.ID, len(row.GuildIds)) + for i, id := range row.GuildIds { + guildIds[i] = common.DefinitelyID(id) + } + + return &model.Session{ + TokenHash: row.TokenHash, + UserID: common.DefinitelyID(row.UserID), + GuildIds: guildIds, + AccessToken: row.AccessToken, + CreatedAt: row.CreatedAt.Time, + ExpiresAt: row.ExpiresAt.Time, + } +} diff --git a/embedg-service/db/postgres/store_shared_message.go b/embedg-service/db/postgres/store_shared_message.go new file mode 100644 index 000000000..bca4a3204 --- /dev/null +++ b/embedg-service/db/postgres/store_shared_message.go @@ -0,0 +1,50 @@ +package postgres + +import ( + "context" + "errors" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" +) + +var _ store.SharedMessageStore = (*Client)(nil) + +func (c *Client) CreateSharedMessage(ctx context.Context, msg model.SharedMessage) error { + _, err := c.Q.InsertSharedMessage(ctx, pgmodel.InsertSharedMessageParams{ + ID: msg.ID, + CreatedAt: pgtype.Timestamp{Time: msg.CreatedAt, Valid: true}, + ExpiresAt: pgtype.Timestamp{Time: msg.ExpiresAt, Valid: true}, + Data: msg.Data, + }) + return err +} + +func (c *Client) GetSharedMessage(ctx context.Context, id string) (*model.SharedMessage, error) { + row, err := c.Q.GetSharedMessage(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToSharedMessage(row), nil +} + +func (c *Client) DeleteExpiredSharedMessages(ctx context.Context, now time.Time) error { + err := c.Q.DeleteExpiredSharedMessages(ctx, pgtype.Timestamp{Time: now, Valid: true}) + return err +} + +func rowToSharedMessage(row pgmodel.SharedMessage) *model.SharedMessage { + return &model.SharedMessage{ + ID: row.ID, + CreatedAt: row.CreatedAt.Time, + ExpiresAt: row.ExpiresAt.Time, + Data: row.Data, + } +} diff --git a/embedg-service/db/postgres/store_user.go b/embedg-service/db/postgres/store_user.go new file mode 100644 index 000000000..7f8b4a2b4 --- /dev/null +++ b/embedg-service/db/postgres/store_user.go @@ -0,0 +1,53 @@ +package postgres + +import ( + "context" + "errors" + + "github.com/disgoorg/snowflake/v2" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres/pgmodel" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "gopkg.in/guregu/null.v4" +) + +var _ store.UserStore = (*Client)(nil) + +func (c *Client) UpsertUser(ctx context.Context, user model.User) error { + _, err := c.Q.UpsertUser(ctx, pgmodel.UpsertUserParams{ + ID: user.ID.String(), + Name: user.Name, + Discriminator: user.Discriminator, + Avatar: pgtype.Text{String: user.Avatar.String, Valid: user.Avatar.Valid}, + }) + return err +} + +func (c *Client) GetUser(ctx context.Context, userID snowflake.ID) (*model.User, error) { + row, err := c.Q.GetUser(ctx, userID.String()) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, err + } + return rowToUser(row), nil +} + +func (c *Client) DeleteUser(ctx context.Context, userID snowflake.ID) error { + err := c.Q.DeleteUser(ctx, userID.String()) + return err +} + +func rowToUser(row pgmodel.User) *model.User { + return &model.User{ + ID: common.DefinitelyID(row.ID), + Name: row.Name, + Discriminator: row.Discriminator, + Avatar: null.NewString(row.Avatar.String, row.Avatar.Valid), + IsTester: row.IsTester, + } +} diff --git a/embedg-service/model/plan.go b/embedg-service/model/plan.go new file mode 100644 index 000000000..5a6444279 --- /dev/null +++ b/embedg-service/model/plan.go @@ -0,0 +1,75 @@ +package model + +type Plan struct { + ID string `mapstructure:"id"` + SKUID string `mapstructure:"sku_id"` + Default bool `mapstructure:"default"` + Features PlanFeatures `mapstructure:"features"` + Consumable bool `mapstructure:"consumable"` +} + +type PlanFeatures struct { + MaxSavedMessages int `mapstructure:"max_saved_messages"` + MaxActionsPerComponent int `mapstructure:"max_actions_per_component"` + AdvancedActionTypes bool `mapstructure:"advanced_action_types"` + AIAssistant bool `mapstructure:"ai_assistant"` + CustomBot bool `mapstructure:"custom_bot"` + ComponentsV2 bool `mapstructure:"components_v2"` + ComponentTypes []int `mapstructure:"component_types"` + MaxCustomCommands int `mapstructure:"max_custom_commands"` + IsPremium bool `mapstructure:"is_premium"` + MaxImageUploadSize int `mapstructure:"max_image_upload_size"` + MaxScheduledMessages int `mapstructure:"max_scheduled_messages"` + PeriodicScheduledMessages bool `mapstructure:"periodic_scheduled_messages"` + MaxTemplateOps int `mapstructure:"max_template_ops"` + MaxKVKeys int `mapstructure:"max_kv_keys"` +} + +func (f *PlanFeatures) Merge(b PlanFeatures) { + if b.MaxSavedMessages > f.MaxSavedMessages { + f.MaxSavedMessages = b.MaxSavedMessages + } + if b.MaxActionsPerComponent > f.MaxActionsPerComponent { + f.MaxActionsPerComponent = b.MaxActionsPerComponent + } + if b.MaxCustomCommands > f.MaxCustomCommands { + f.MaxCustomCommands = b.MaxCustomCommands + } + if b.MaxImageUploadSize > f.MaxImageUploadSize { + f.MaxImageUploadSize = b.MaxImageUploadSize + } + if b.MaxScheduledMessages > f.MaxScheduledMessages { + f.MaxScheduledMessages = b.MaxScheduledMessages + } + if b.MaxTemplateOps > f.MaxTemplateOps { + f.MaxTemplateOps = b.MaxTemplateOps + } + if b.MaxKVKeys > f.MaxKVKeys { + f.MaxKVKeys = b.MaxKVKeys + } + + f.AdvancedActionTypes = f.AdvancedActionTypes || b.AdvancedActionTypes + f.AIAssistant = f.AIAssistant || b.AIAssistant + f.IsPremium = f.IsPremium || b.IsPremium + f.CustomBot = f.CustomBot || b.CustomBot + f.ComponentsV2 = f.ComponentsV2 || b.ComponentsV2 + f.ComponentTypes = mergeIntSlices(f.ComponentTypes, b.ComponentTypes) + f.PeriodicScheduledMessages = f.PeriodicScheduledMessages || b.PeriodicScheduledMessages +} + +func mergeIntSlices(a, b []int) []int { + m := make(map[int]bool, len(a)+len(b)) + for _, v := range a { + m[v] = true + } + for _, v := range b { + m[v] = true + } + + res := make([]int, 0, len(m)) + for k := range m { + res = append(res, k) + } + + return res +} diff --git a/embedg-service/store/custom_bot.go b/embedg-service/store/custom_bot.go index a39b4eb9a..df7de2c28 100644 --- a/embedg-service/store/custom_bot.go +++ b/embedg-service/store/custom_bot.go @@ -33,5 +33,5 @@ type CustomBotStore interface { GetCustomBot(ctx context.Context, id string) (*model.CustomBot, error) GetCustomBotByGuildID(ctx context.Context, guildID common.ID) (*model.CustomBot, error) SetCustomBotHandledFirstInteraction(ctx context.Context, id string) error - GetCustomBots(ctx context.Context) ([]*model.CustomBot, error) + GetCustomBots(ctx context.Context) ([]model.CustomBot, error) } diff --git a/embedg-service/store/custom_command.go b/embedg-service/store/custom_command.go index ff4f639c8..3a6e699af 100644 --- a/embedg-service/store/custom_command.go +++ b/embedg-service/store/custom_command.go @@ -9,7 +9,7 @@ import ( ) type CustomCommandStore interface { - GetCustomCommands(ctx context.Context, guildID common.ID) ([]*model.CustomCommand, error) + GetCustomCommands(ctx context.Context, guildID common.ID) ([]model.CustomCommand, error) GetCustomCommand(ctx context.Context, guildID common.ID, id string) (*model.CustomCommand, error) GetCustomCommandByName(ctx context.Context, guildID common.ID, name string) (*model.CustomCommand, error) CountCustomCommands(ctx context.Context, guildID common.ID) (int64, error) diff --git a/embedg-service/store/entitlement.go b/embedg-service/store/entitlement.go index 82267fb4f..b31cfb384 100644 --- a/embedg-service/store/entitlement.go +++ b/embedg-service/store/entitlement.go @@ -8,10 +8,10 @@ import ( ) type EntitlementStore interface { - GetActiveEntitlementsForGuild(ctx context.Context, guildID common.ID) ([]*model.Entitlement, error) - GetActiveEntitlementsForUser(ctx context.Context, userID common.ID) ([]*model.Entitlement, error) - GetEntitlements(ctx context.Context) ([]*model.Entitlement, error) - GetEntitlement(ctx context.Context, id common.ID) (*model.Entitlement, error) + GetActiveEntitlementsForGuild(ctx context.Context, guildID common.ID) ([]model.Entitlement, error) + GetActiveEntitlementsForUser(ctx context.Context, userID common.ID) ([]model.Entitlement, error) + GetEntitlements(ctx context.Context) ([]model.Entitlement, error) + GetEntitlement(ctx context.Context, id common.ID, userID common.ID) (*model.Entitlement, error) UpdateEntitlementConsumedGuildID(ctx context.Context, id common.ID, consumedGuildID common.NullID) (*model.Entitlement, error) UpsertEntitlement(ctx context.Context, entitlement model.Entitlement) (*model.Entitlement, error) } diff --git a/embedg-service/store/kv.go b/embedg-service/store/kv.go index 42a67a3ca..273c6a5e8 100644 --- a/embedg-service/store/kv.go +++ b/embedg-service/store/kv.go @@ -23,6 +23,6 @@ type KVEntryStore interface { SetKVEntry(ctx context.Context, entry model.KVEntry) error IncreaseKVEntry(ctx context.Context, params KVEntryIncreaseParams) (*model.KVEntry, error) DeleteKVEntry(ctx context.Context, guildID common.ID, key string) (*model.KVEntry, error) - SearchKVEntries(ctx context.Context, guildID common.ID, pattern string) ([]*model.KVEntry, error) + SearchKVEntries(ctx context.Context, guildID common.ID, pattern string) ([]model.KVEntry, error) CountKVEntries(ctx context.Context, guildID common.ID) (int64, error) } diff --git a/embedg-service/store/message_action_set.go b/embedg-service/store/message_action_set.go index 03fdcef7d..4c1ceded4 100644 --- a/embedg-service/store/message_action_set.go +++ b/embedg-service/store/message_action_set.go @@ -10,6 +10,6 @@ import ( type MessageActionSetStore interface { CreateMessageActionSet(ctx context.Context, messageActionSet model.MessageActionSet) (*model.MessageActionSet, error) GetMessageActionSet(ctx context.Context, messageID common.ID, actionSetID string) (*model.MessageActionSet, error) - GetMessageActionSets(ctx context.Context, messageID common.ID) ([]*model.MessageActionSet, error) + GetMessageActionSets(ctx context.Context, messageID common.ID) ([]model.MessageActionSet, error) DeleteMessageActionSetsForMessage(ctx context.Context, messageID common.ID) error } diff --git a/embedg-service/store/plan.go b/embedg-service/store/plan.go new file mode 100644 index 000000000..6ba1dfbc7 --- /dev/null +++ b/embedg-service/store/plan.go @@ -0,0 +1,14 @@ +package store + +import ( + "context" + + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +type PlanStore interface { + GetPlanByID(id string) *model.Plan + GetPlanBySKUID(skuID string) *model.Plan + GetPlanFeaturesForGuild(ctx context.Context, guildID string) (model.PlanFeatures, error) + GetPlanFeaturesForUser(ctx context.Context, userID string) (model.PlanFeatures, error) +} diff --git a/embedg-service/store/scheduled_message.go b/embedg-service/store/scheduled_message.go index 937e2ac76..1420179e8 100644 --- a/embedg-service/store/scheduled_message.go +++ b/embedg-service/store/scheduled_message.go @@ -11,10 +11,10 @@ import ( type ScheduledMessageStore interface { GetDueScheduledMessages(ctx context.Context, now time.Time) ([]model.ScheduledMessage, error) GetScheduledMessages(ctx context.Context, guildID common.ID) ([]model.ScheduledMessage, error) - GetScheduledMessage(ctx context.Context, id common.ID) (*model.ScheduledMessage, error) - DeleteScheduledMessage(ctx context.Context, id common.ID) error + GetScheduledMessage(ctx context.Context, id common.ID, guildID common.ID) (*model.ScheduledMessage, error) + DeleteScheduledMessage(ctx context.Context, id common.ID, guildID common.ID) error CreateScheduledMessage(ctx context.Context, msg model.ScheduledMessage) error UpdateScheduledMessage(ctx context.Context, msg model.ScheduledMessage) error - UpdateScheduledMessageNextAt(ctx context.Context, id common.ID, nextAt time.Time) error - UpdateScheduledMessageEnabled(ctx context.Context, id common.ID, enabled bool) error + UpdateScheduledMessageNextAt(ctx context.Context, id common.ID, guildID common.ID, nextAt time.Time) error + UpdateScheduledMessageEnabled(ctx context.Context, id common.ID, guildID common.ID, enabled bool) error } From e461a22674828b05e499c2836e53031a7352e908 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Fri, 14 Nov 2025 11:12:32 +0100 Subject: [PATCH 03/36] hook up stateway to disgo --- embedg-server/bot/listeners.go | 1 + embedg-server/go.mod | 2 +- embedg-server/go.sum | 2 + embedg-service/.gitignore | 1 + embedg-service/cmd/admin.go | 48 ++ embedg-service/cmd/helpers.go | 39 ++ embedg-service/cmd/root.go | 51 ++ embedg-service/common/discord.go | 37 ++ embedg-service/config/config.go | 2 +- embedg-service/config/default.toml | 8 + embedg-service/config/model.go | 17 +- .../db/postgres/pgmodel/entitlements.sql.go | 24 + .../db/postgres/queries/entitlements.sql | 3 + .../db/postgres/store_entitlement.go | 18 +- embedg-service/embedg/commands.go | 541 ++++++++++++++++++ embedg-service/embedg/embedg.go | 96 ++++ embedg-service/embedg/helpers.go | 32 ++ embedg-service/embedg/listeners.go | 23 + embedg-service/entry/admin/commands.go | 23 + embedg-service/entry/server/server.go | 41 ++ embedg-service/go.mod | 14 + embedg-service/go.sum | 40 ++ embedg-service/main.go | 6 + .../manager/premium/entitlements.go | 89 +++ embedg-service/manager/premium/manager.go | 72 +++ embedg-service/manager/premium/plan.go | 65 +++ embedg-service/manager/premium/roles.go | 62 ++ embedg-service/model/entitlement.go | 2 +- embedg-service/model/plan.go | 40 +- embedg-service/store/entitlement.go | 1 + go.work.sum | 34 +- 31 files changed, 1393 insertions(+), 41 deletions(-) create mode 100644 embedg-service/.gitignore create mode 100644 embedg-service/cmd/admin.go create mode 100644 embedg-service/cmd/helpers.go create mode 100644 embedg-service/cmd/root.go create mode 100644 embedg-service/common/discord.go create mode 100644 embedg-service/embedg/commands.go create mode 100644 embedg-service/embedg/embedg.go create mode 100644 embedg-service/embedg/helpers.go create mode 100644 embedg-service/embedg/listeners.go create mode 100644 embedg-service/entry/admin/commands.go create mode 100644 embedg-service/entry/server/server.go create mode 100644 embedg-service/manager/premium/entitlements.go create mode 100644 embedg-service/manager/premium/manager.go create mode 100644 embedg-service/manager/premium/plan.go create mode 100644 embedg-service/manager/premium/roles.go diff --git a/embedg-server/bot/listeners.go b/embedg-server/bot/listeners.go index 7237b2e86..db0b54082 100644 --- a/embedg-server/bot/listeners.go +++ b/embedg-server/bot/listeners.go @@ -62,6 +62,7 @@ func (b *Bot) onInteractionCreate(_ *discordgo.Session, i *discordgo.Interaction } func (b *Bot) onRawEvent(_ *discordgo.Session, e *discordgo.Event) { + // TODO: discordgo.Event is no longer dispatched when using Stateway, so we need to handle entitlements differently. if e.Type == "ENTITLEMENT_CREATE" || e.Type == "ENTITLEMENT_UPDATE" || e.Type == "ENTITLEMENT_DELETE" { entitlement := &Entitlement{} err := json.Unmarshal(e.RawData, entitlement) diff --git a/embedg-server/go.mod b/embedg-server/go.mod index 5ed03fd3e..4e94db768 100644 --- a/embedg-server/go.mod +++ b/embedg-server/go.mod @@ -53,7 +53,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251109101747-4829dfd63e8d // indirect + github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155245-58bf88272926 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/embedg-server/go.sum b/embedg-server/go.sum index 16b0c26c6..d001cf77c 100644 --- a/embedg-server/go.sum +++ b/embedg-server/go.sum @@ -907,6 +907,8 @@ github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251108220848-8195b521776b github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251108220848-8195b521776b/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251109101747-4829dfd63e8d h1:m1nl+T0JorfW/GvhKgjcYyARv5h9bbL5lUrDBy2I9LU= github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251109101747-4829dfd63e8d/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155245-58bf88272926 h1:cHYOssZzAWarRvmp3b9Q9iU31xeDBqunAa5U7NdNomw= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155245-58bf88272926/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= diff --git a/embedg-service/.gitignore b/embedg-service/.gitignore new file mode 100644 index 000000000..e4f3ba96d --- /dev/null +++ b/embedg-service/.gitignore @@ -0,0 +1 @@ +embedg.toml diff --git a/embedg-service/cmd/admin.go b/embedg-service/cmd/admin.go new file mode 100644 index 000000000..bbe3a80b5 --- /dev/null +++ b/embedg-service/cmd/admin.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "fmt" + "os/signal" + "syscall" + + "github.com/merlinfuchs/embed-generator/embedg-service/entry/admin" + "github.com/urfave/cli/v2" +) + +var adminCMD = cli.Command{ + Name: "admin", + Usage: "Manage admin tasks.", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging.", + }, + }, + Subcommands: []*cli.Command{ + { + Name: "commands", + Usage: "Manage commands.", + Subcommands: []*cli.Command{ + { + Name: "sync", + Usage: "Sync commands.", + Action: func(c *cli.Context) error { + ctx, cancel := signal.NotifyContext(c.Context, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + env, err := setupEnv(ctx, c.Bool("debug")) + if err != nil { + return fmt.Errorf("failed to setup environment: %w", err) + } + + err = admin.SyncCommands(ctx, env.pg, env.cfg) + if err != nil { + return fmt.Errorf("failed to delete gateway stream: %w", err) + } + return nil + }, + }, + }, + }, + }, +} diff --git a/embedg-service/cmd/helpers.go b/embedg-service/cmd/helpers.go new file mode 100644 index 000000000..5b7495d36 --- /dev/null +++ b/embedg-service/cmd/helpers.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/merlinfuchs/embed-generator/embedg-service/config" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres" + "github.com/merlinfuchs/embed-generator/embedg-service/logging" +) + +type env struct { + pg *postgres.Client + cfg *config.RootConfig +} + +func setupEnv(ctx context.Context, debug bool) (*env, error) { + cfg, err := config.LoadConfig[*config.RootConfig]() + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + + loggingConfig := logging.LoggerConfig(cfg.Logging) + if debug { + loggingConfig.Debug = true + } + + logging.SetupLogger(loggingConfig) + + pg, err := postgres.New(ctx, postgres.ClientConfig(cfg.Database.Postgres)) + if err != nil { + return nil, fmt.Errorf("failed to create postgres client: %w", err) + } + + return &env{ + pg: pg, + cfg: cfg, + }, nil +} diff --git a/embedg-service/cmd/root.go b/embedg-service/cmd/root.go new file mode 100644 index 000000000..640f9defa --- /dev/null +++ b/embedg-service/cmd/root.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/merlinfuchs/embed-generator/embedg-service/entry/server" + "github.com/urfave/cli/v2" +) + +var CLI = cli.App{ + Name: "stateway-gateway", + Description: "Stateway Gateway CLI", + Commands: []*cli.Command{ + { + Name: "server", + Usage: "Start the Stateway Cache Server.", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging.", + }, + }, + Action: func(c *cli.Context) error { + ctx, cancel := signal.NotifyContext(c.Context, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + env, err := setupEnv(ctx, c.Bool("debug")) + if err != nil { + return fmt.Errorf("failed to setup environment: %w", err) + } + + err = server.Run(ctx, env.pg, env.cfg) + if err != nil { + return fmt.Errorf("failed to run cache server: %w", err) + } + return nil + }, + }, + &adminCMD, + }, +} + +func Execute() { + if err := CLI.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/embedg-service/common/discord.go b/embedg-service/common/discord.go new file mode 100644 index 000000000..6ff960ace --- /dev/null +++ b/embedg-service/common/discord.go @@ -0,0 +1,37 @@ +package common + +import ( + "errors" + + "github.com/disgoorg/disgo/rest" +) + +func IsDiscordRestErrorCode(err error, codes ...int) bool { + var httpErr *rest.Error + if errors.As(err, &httpErr) { + for _, code := range codes { + if int(httpErr.Code) == code { + return true + } + } + } + + return false +} + +func IsDiscordRestStatusCode(err error, statusCodes ...int) bool { + var httpErr *rest.Error + if errors.As(err, &httpErr) { + if httpErr.Response == nil { + return false + } + + for _, statusCode := range statusCodes { + if httpErr.Response.StatusCode == statusCode { + return true + } + } + } + + return false +} diff --git a/embedg-service/config/config.go b/embedg-service/config/config.go index 02582621a..e6c5b1be3 100644 --- a/embedg-service/config/config.go +++ b/embedg-service/config/config.go @@ -18,7 +18,7 @@ import ( "github.com/knadh/koanf/v2" ) -const ConfigFile = "stateway.toml" +const ConfigFile = "embedg.toml" //go:embed default.toml var defaultConfig []byte diff --git a/embedg-service/config/default.toml b/embedg-service/config/default.toml index e6084ab9e..20cdf9931 100644 --- a/embedg-service/config/default.toml +++ b/embedg-service/config/default.toml @@ -4,5 +4,13 @@ port = 5432 user = "postgres" db_name = "stateway" +[database.s3] +endpoint = "localhost:9000" +access_key_id = "nook" +secret_access_key = "1234567890" + +[broker] +gateway_count = 1 + [broker.nats] url = "nats://127.0.0.1:4222" diff --git a/embedg-service/config/model.go b/embedg-service/config/model.go index 238ee2dc9..a0b1cfa35 100644 --- a/embedg-service/config/model.go +++ b/embedg-service/config/model.go @@ -2,9 +2,13 @@ package config import ( "github.com/go-playground/validator/v10" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" ) type RootConfig struct { + Discord DiscordConfig `toml:"discord"` + Premium PremiumConfig `toml:"premium"` Logging LoggingConfig `toml:"logging"` Database DatabaseConfig `toml:"database"` Broker BrokerConfig `toml:"broker"` @@ -15,6 +19,16 @@ func (cfg *RootConfig) Validate() error { return validate.Struct(cfg) } +type DiscordConfig struct { + Token string `toml:"token" validate:"required"` +} + +type PremiumConfig struct { + BeneficialGuildID common.ID `toml:"beneficial_guild_id" ` + BeneficialRoleID common.ID `toml:"beneficial_role_id"` + Plans []model.Plan `toml:"plans"` +} + type LoggingConfig struct { Filename string `toml:"filename"` MaxSize int `toml:"max_size"` @@ -45,7 +59,8 @@ type S3Config struct { } type BrokerConfig struct { - NATS NATSConfig `toml:"nats"` + NATS NATSConfig `toml:"nats"` + GatewayCount int `toml:"gateway_count"` } type NATSConfig struct { diff --git a/embedg-service/db/postgres/pgmodel/entitlements.sql.go b/embedg-service/db/postgres/pgmodel/entitlements.sql.go index 0cbbe01bd..13947c6e7 100644 --- a/embedg-service/db/postgres/pgmodel/entitlements.sql.go +++ b/embedg-service/db/postgres/pgmodel/entitlements.sql.go @@ -89,6 +89,30 @@ func (q *Queries) GetActiveEntitlementsForUser(ctx context.Context, userID pgtyp return items, nil } +const getEntitledUserIDs = `-- name: GetEntitledUserIDs :many +SELECT DISTINCT user_id FROM entitlements +` + +func (q *Queries) GetEntitledUserIDs(ctx context.Context) ([]pgtype.Text, error) { + rows, err := q.db.Query(ctx, getEntitledUserIDs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []pgtype.Text + for rows.Next() { + var user_id pgtype.Text + if err := rows.Scan(&user_id); err != nil { + return nil, err + } + items = append(items, user_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getEntitlement = `-- name: GetEntitlement :one SELECT id, user_id, guild_id, updated_at, deleted, sku_id, starts_at, ends_at, consumed, consumed_guild_id FROM entitlements WHERE id = $1 AND user_id = $2 ` diff --git a/embedg-service/db/postgres/queries/entitlements.sql b/embedg-service/db/postgres/queries/entitlements.sql index 3e75cc4c3..602380a56 100644 --- a/embedg-service/db/postgres/queries/entitlements.sql +++ b/embedg-service/db/postgres/queries/entitlements.sql @@ -51,3 +51,6 @@ DO UPDATE SET updated_at = $4, consumed = $9 RETURNING *; + +-- name: GetEntitledUserIDs :many +SELECT DISTINCT user_id FROM entitlements; diff --git a/embedg-service/db/postgres/store_entitlement.go b/embedg-service/db/postgres/store_entitlement.go index 61edc3865..d82763877 100644 --- a/embedg-service/db/postgres/store_entitlement.go +++ b/embedg-service/db/postgres/store_entitlement.go @@ -74,7 +74,7 @@ func (c *Client) UpsertEntitlement(ctx context.Context, entitlement model.Entitl GuildID: pgtype.Text{String: entitlement.GuildID.ID.String(), Valid: entitlement.GuildID.Valid}, UpdatedAt: pgtype.Timestamp{Time: entitlement.UpdatedAt, Valid: true}, Deleted: entitlement.Deleted, - SkuID: entitlement.SkuID, + SkuID: entitlement.SkuID.String(), StartsAt: pgtype.Timestamp{Time: entitlement.StartsAt.Time, Valid: entitlement.StartsAt.Valid}, EndsAt: pgtype.Timestamp{Time: entitlement.EndsAt.Time, Valid: entitlement.EndsAt.Valid}, Consumed: entitlement.Consumed, @@ -85,6 +85,20 @@ func (c *Client) UpsertEntitlement(ctx context.Context, entitlement model.Entitl return rowToEntitlement(row), nil } +func (c *Client) GetEntitledUserIDs(ctx context.Context) ([]common.ID, error) { + rows, err := c.Q.GetEntitledUserIDs(ctx) + if err != nil { + return nil, err + } + userIDs := make([]common.ID, 0, len(rows)) + for _, row := range rows { + if row.Valid { + userIDs = append(userIDs, common.DefinitelyID(row.String)) + } + } + return userIDs, nil +} + func rowsToEntitlements(rows []pgmodel.Entitlement) []model.Entitlement { entitlements := make([]model.Entitlement, len(rows)) for i, row := range rows { @@ -116,7 +130,7 @@ func rowToEntitlement(row pgmodel.Entitlement) *model.Entitlement { GuildID: guildID, UpdatedAt: row.UpdatedAt.Time, Deleted: row.Deleted, - SkuID: row.SkuID, + SkuID: common.DefinitelyID(row.SkuID), StartsAt: null.NewTime(row.StartsAt.Time, row.StartsAt.Valid), EndsAt: null.NewTime(row.EndsAt.Time, row.EndsAt.Valid), Consumed: row.Consumed, diff --git a/embedg-service/embedg/commands.go b/embedg-service/embedg/commands.go new file mode 100644 index 000000000..9d4976ca5 --- /dev/null +++ b/embedg-service/embedg/commands.go @@ -0,0 +1,541 @@ +package embedg + +import ( + "context" + "fmt" + "regexp" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/handler" + "github.com/disgoorg/disgo/handler/middleware" + "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/omit" + "github.com/disgoorg/snowflake/v2" + "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +var commands = []discord.ApplicationCommandCreate{ + discord.SlashCommandCreate{ + Name: "help", + Description: "Show help", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "invite", + Description: "Invite the Embed Generator bot to your server", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "website", + Description: "Open the Embed Generator website", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "format", + Description: "Get the API format for mentions, channels, roles, & custom emojis", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionSubCommand{ + Name: "text", + Description: "Get the API format for a text with multiple mentions, channels, & custom emojis", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "text", + Description: "The text that you want to format (usually containing mentions or custom emojis)", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "user", + Description: "Get the API format for mentioning a user", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionUser{ + Name: "user", + Description: "The user you want to mention", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "channel", + Description: "Get the API format for mentioning a channel", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionChannel{ + Name: "channel", + Description: "The channel you want to mention", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "role", + Description: "Get the API format for mentioning a role", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionRole{ + Name: "role", + Description: "The role you want to mention", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "emoji", + Description: "Get the API format for a standard or custom emoji", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "emoji", + Description: "The standard or custom emoji you want to use", + Required: true, + }, + }, + }, + }, + }, + discord.SlashCommandCreate{ + Name: "image", + Description: "Get the image URL for different entities", + Contexts: []discord.InteractionContextType{ + discord.InteractionContextTypeGuild, + }, + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionSubCommand{ + Name: "avatar", + Description: "Get the avatar URL for a user", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionUser{ + Name: "user", + Description: "The user you want to get the avatar for", + Required: true, + }, + discord.ApplicationCommandOptionBool{ + Name: "static", + Description: "Whether animated avatars should be converted to static images", + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "icon", + Description: "Get the icon URL for this server", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionBool{ + Name: "static", + Description: "Whether animated icons should be converted to static images", + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "emoji", + Description: "Get the image URL for a custom or standard emoji", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "emoji", + Description: "The standard or custom emoji you want the image URL for", + Required: true, + }, + discord.ApplicationCommandOptionBool{ + Name: "static", + Description: "Whether animated emojis should be converted to static images", + }, + }, + }, + }, + }, + discord.SlashCommandCreate{ + + Name: "message", + Description: "Get JSON for or restore a message on Embed Generator", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionSubCommand{ + Name: "restore", + Description: "Restore a message on Embed Generator", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "message_id_or_url", + Description: "ID or URL of the message you want to restore", + Required: true, + }, + }, + }, + discord.ApplicationCommandOptionSubCommand{ + Name: "dump", + Description: "Get the JSON code for a message", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "message_id_or_url", + Description: "ID or URL of the message you want to restore", + Required: true, + }, + }, + }, + }, + }, + discord.MessageCommandCreate{ + Name: "Restore Message", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.MessageCommandCreate{ + Name: "Dump Message", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.UserCommandCreate{ + Name: "Avatar Url", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.UserCommandCreate{ + Name: "Format Mention", + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + discord.ApplicationIntegrationTypeUserInstall, + }, + }, + discord.SlashCommandCreate{ + Name: "embed", + Description: "Create an embed message", + DefaultMemberPermissions: omit.NewPtr(discord.PermissionManageWebhooks), + Contexts: []discord.InteractionContextType{ + discord.InteractionContextTypeGuild, + }, + IntegrationTypes: []discord.ApplicationIntegrationType{ + discord.ApplicationIntegrationTypeGuildInstall, + }, + }, +} + +func (g *EmbedGenerator) SyncCommands(ctx context.Context) error { + if err := handler.SyncCommands(g.Client(), commands, []common.ID{}, rest.WithCtx(ctx)); err != nil { + return fmt.Errorf("error while syncing commands: %w", err) + } + return nil +} + +func (g *EmbedGenerator) interactionMux() *handler.Mux { + mx := handler.New() + mx.Use(middleware.Logger) + mx.Command("/invite", g.handleHelpCommand) + mx.Command("/website", g.handleHelpCommand) + mx.Command("/help", g.handleHelpCommand) + + mx.Route("/format", func(r handler.Router) { + r.Command("/text", g.handleFormatTextCommand) + r.Command("/user", g.handleFormatUserCommand) + r.Command("/channel", g.handleFormatChannelCommand) + r.Command("/role", g.handleFormatRoleCommand) + r.Command("/emoji", g.handleFormatEmojiCommand) + }) + + mx.Route("/image", func(r handler.Router) { + r.Command("/avatar", g.handleImageAvatarCommand) + r.Command("/icon", g.handleImageIconCommand) + r.Command("/emoji", g.handleImageEmojiCommand) + }) + + mx.Route("/message", func(r handler.Router) { + r.Command("/restore", g.handleMessageRestoreCommand) + r.Command("/dump", g.handleMessageDumpCommand) + }) + + mx.Command("/Restore Message", g.handleMessageRestoreContextCommand) + mx.Command("/Dump Message", g.handleMessageDumpContextCommand) + + mx.Command("/Avatar Url", g.handleUserAvatarURLContextCommand) + mx.Command("/Format Mention", g.handleUserFormatMentionContextCommand) + + mx.Command("/embed", g.handleEmbedCommand) + return mx +} + +func (g *EmbedGenerator) handleHelpCommand(e *handler.CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "**The best way to generate rich embed messages for your Discord Server!**\n\nhttps://www.youtube.com/watch?v=DnFP0MRJPIg", + Components: []discord.LayoutComponent{ + discord.ActionRowComponent{ + Components: []discord.InteractiveComponent{ + discord.ButtonComponent{ + Style: discord.ButtonStyleLink, + Label: "Website", + URL: "https://message.style", + }, + discord.ButtonComponent{ + Style: discord.ButtonStyleLink, + Label: "Invite Bot", + URL: g.BotInviteURL(), + }, + discord.ButtonComponent{ + Style: discord.ButtonStyleLink, + Label: "Discord Server", + URL: viper.GetString("links.discord"), + }, + }, + }, + }, + }) +} + +func (g *EmbedGenerator) handleFormatTextCommand(e *handler.CommandEvent) error { + value := e.SlashCommandInteractionData().String("text") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for the provided text: ```%s```", value), + }) +} + +func (g *EmbedGenerator) handleFormatUserCommand(e *handler.CommandEvent) error { + user := e.SlashCommandInteractionData().User("user") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for %s: ```<@%s>```", user.Mention(), user.ID), + }) +} + +func (g *EmbedGenerator) handleFormatChannelCommand(e *handler.CommandEvent) error { + channel := e.SlashCommandInteractionData().Channel("channel") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for <#%s>: ```<#%s>```", channel.ID, channel.ID), + }) +} + +func (g *EmbedGenerator) handleFormatRoleCommand(e *handler.CommandEvent) error { + role := e.SlashCommandInteractionData().Role("role") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for %s: ```<@&%s>```", role.Mention(), role.ID), + }) +} + +func (g *EmbedGenerator) handleFormatEmojiCommand(e *handler.CommandEvent) error { + emoji := e.SlashCommandInteractionData().String("emoji") + + return e.CreateMessage(discord.MessageCreate{ + Content: fmt.Sprintf("API format for %s: ```%s```", emoji, emoji), + }) +} + +func (g *EmbedGenerator) handleImageAvatarCommand(e *handler.CommandEvent) error { + user := e.SlashCommandInteractionData().User("user") + static := e.SlashCommandInteractionData().Bool("static") + + opts := []discord.CDNOpt{ + discord.WithSize(1024), + } + if static { + opts = append(opts, discord.WithFormat(discord.FileFormatPNG)) + } + + avatarURL := user.EffectiveAvatarURL(opts...) + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: avatarURL, + Image: &discord.EmbedResource{ + URL: avatarURL, + }, + }, + }, + }) +} + +func (g *EmbedGenerator) handleImageIconCommand(e *handler.CommandEvent) error { + static := e.SlashCommandInteractionData().Bool("static") + + guild, ok := e.Guild() + if !ok { + log.Error().Int64("guild_id", int64(*e.GuildID())).Msg("Guild for image command is not in cache") + return e.CreateMessage(discord.MessageCreate{ + Content: "Server is not in cache, please report this!", + }) + } + + opts := []discord.CDNOpt{ + discord.WithSize(1024), + } + if static { + opts = append(opts, discord.WithFormat(discord.FileFormatPNG)) + } + + iconURL := guild.IconURL(opts...) + if iconURL == nil { + return e.CreateMessage(discord.MessageCreate{ + Content: "This server doesn't have an icon.", + }) + } + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: *iconURL, + Image: &discord.EmbedResource{ + URL: *iconURL, + }, + }, + }, + }) +} + +var emojiRegex = regexp.MustCompile(`<(a?):.+?:(\d{18})>`) +var unicodeEmojiRegex = regexp.MustCompile(`[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{1F1E0}-\x{1F1FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]`) + +func (g *EmbedGenerator) handleImageEmojiCommand(e *handler.CommandEvent) error { + rawEmoji := e.SlashCommandInteractionData().String("emoji") + static := e.SlashCommandInteractionData().Bool("static") + + // Check if it's a unicode emoji + if unicodeEmojiRegex.MatchString(rawEmoji) { + emojiURL := emojiImageURL(rawEmoji, false) + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: emojiURL, + Image: &discord.EmbedResource{ + URL: emojiURL, + }, + }, + }, + }) + } + + // Parse Discord emoji + matches := emojiRegex.FindStringSubmatch(rawEmoji) + if len(matches) < 2 { + return e.CreateMessage(discord.MessageCreate{ + Content: "Invalid emoji format. Please use a custom Discord emoji like `<:name:id>` or ``.", + }) + } + + emojiID := matches[2] + isAnimated := matches[1] == "a" + + // Build the URL + extension := "gif" + if static || !isAnimated { + extension = "png" + } + + emojiURL := fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.%s", emojiID, extension) + + return e.CreateMessage(discord.MessageCreate{ + Embeds: []discord.Embed{ + { + Description: emojiURL, + Image: &discord.EmbedResource{ + URL: emojiURL, + }, + }, + }, + }) +} + +var messageURLRegex = regexp.MustCompile(`https?://(?:canary\\.|ptb\\.)?discord\\.com/channels/[0-9]+/([0-9]+)/([0-9]+)`) +var messageIDRegex = regexp.MustCompile(`^[0-9]+$`) + +func (g *EmbedGenerator) handleMessageRestoreCommand(e *handler.CommandEvent) error { + messageIDOrURL := e.SlashCommandInteractionData().String("message_id_or_url") + + channelID := e.Channel().ID() + var messageID common.ID + + match := messageURLRegex.FindStringSubmatch(messageIDOrURL) + if match != nil { + channelID, _ = snowflake.Parse(match[1]) + messageID, _ = snowflake.Parse(match[2]) + + channel, ok := g.Client().Caches.Channel(channelID) + if !ok { + return e.CreateMessage(discord.MessageCreate{ + Content: "The message belongs to a channel that the bot doesn't have access to.", + }) + } + + if channel.GuildID() != *e.GuildID() { + return e.CreateMessage(discord.MessageCreate{ + Content: "The channel doesn't belong to this server.", + }) + } + } + + message, err := g.Client().Rest.GetMessage(channelID, messageID, rest.WithCtx(e.Ctx)) + if err != nil { + if common.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMessage) { + return e.CreateMessage(discord.MessageCreate{ + Content: "Message not found.", + }) + } + + return e.CreateMessage(discord.MessageCreate{ + Content: "Failed to get message.", + }) + } + + fmt.Println(message) + + // TODO: Unparse components + + /* _, err = json.MarshalIndent(actions.MessageWithActions{ + Username: message.Author.Username, + // AvatarURL: message.Author.AvatarURL("1024"), + Content: message.Content, + // Embeds: message.Embeds, + }, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal message dump: %w", err) + } */ + + return nil +} + +func (g *EmbedGenerator) handleMessageRestoreContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleMessageDumpCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleMessageDumpContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleUserAvatarURLContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleUserFormatMentionContextCommand(e *handler.CommandEvent) error { + return nil +} + +func (g *EmbedGenerator) handleEmbedCommand(e *handler.CommandEvent) error { + // TODO: Implement with components + return nil +} diff --git a/embedg-service/embedg/embedg.go b/embedg-service/embedg/embedg.go new file mode 100644 index 000000000..c8b2a3990 --- /dev/null +++ b/embedg-service/embedg/embedg.go @@ -0,0 +1,96 @@ +package embedg + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/rest" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "github.com/merlinfuchs/stateway/stateway-lib/broker" + "github.com/merlinfuchs/stateway/stateway-lib/cache" + "github.com/merlinfuchs/stateway/stateway-lib/compat" +) + +type EmbedGeneratorConfig struct { + Token string + BrokerURL string + GatewayCount int +} + +type EmbedGenerator struct { + client *bot.Client + cache cache.Cache + broker broker.Broker + config EmbedGeneratorConfig + + actionSetStore store.MessageActionSetStore +} + +func NewEmbedGenerator( + config EmbedGeneratorConfig, + actionSetStore store.MessageActionSetStore, +) (*EmbedGenerator, error) { + br, err := broker.NewNATSBroker(config.BrokerURL) + if err != nil { + return nil, fmt.Errorf("failed to create NATS broker: %w", err) + } + + gateway := compat.NewDisgoGateway(br, compat.DisgoGatewayConfig{ + GatewayCount: config.GatewayCount, + EventTypes: []string{ + "message.delete", + "interaction.>", + "entitlement.>", + }, + }) + + client, err := disgo.New(config.Token, bot.WithGateway(gateway)) + if err != nil { + return nil, fmt.Errorf("failed to create Discord client: %w", err) + } + + gateway.EventHandlerFunc = client.EventManager.HandleGatewayEvent + + cache := cache.NewCacheClient(br, cache.WithAppID(client.ApplicationID)) + + embedg := &EmbedGenerator{ + client: client, + cache: cache, + broker: br, + config: config, + actionSetStore: actionSetStore, + } + + client.AddEventListeners( + bot.NewListenerFunc(embedg.onMessageDelete), + embedg.interactionMux(), + ) + + return embedg, nil +} + +func (g *EmbedGenerator) Client() *bot.Client { + return g.client +} + +func (g *EmbedGenerator) Rest() rest.Rest { + return g.client.Rest +} + +func (g *EmbedGenerator) Cache() cache.Cache { + return g.cache +} + +func (g *EmbedGenerator) Broker() broker.Broker { + return g.broker +} + +func (g *EmbedGenerator) Open(ctx context.Context) error { + return g.client.OpenGateway(ctx) +} + +func (g *EmbedGenerator) BotInviteURL() string { + return fmt.Sprintf("https://discord.com/oauth2/authorize?client_id=%s&scope=bot%%20applications.commands&permissions=536945664", g.client.ApplicationID) +} diff --git a/embedg-service/embedg/helpers.go b/embedg-service/embedg/helpers.go new file mode 100644 index 000000000..2ec535863 --- /dev/null +++ b/embedg-service/embedg/helpers.go @@ -0,0 +1,32 @@ +package embedg + +import ( + "fmt" + "strings" +) + +func emojiImageURL(emoji string, animated bool) string { + // Convert unicode emoji to Twemoji URL + var codepoints []string + + // Iterate through each rune in the emoji string + for _, r := range emoji { + // Skip zero-width joiners and variation selectors + if r == 0x200D || r == 0xFE0F { + continue + } + // Convert rune to lowercase hex codepoint + codepoints = append(codepoints, fmt.Sprintf("%x", r)) + } + + // Join codepoints with hyphens for multi-codepoint emojis + unicode := strings.Join(codepoints, "-") + + if animated { + // Google Noto animated emoji URL structure + // https://googlefonts.github.io/noto-emoji-animation/ + return fmt.Sprintf("https://fonts.gstatic.com/s/e/notoemoji/latest/%s/512.gif", unicode) + } + + return fmt.Sprintf("https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/72x72/%s.png", unicode) +} diff --git a/embedg-service/embedg/listeners.go b/embedg-service/embedg/listeners.go new file mode 100644 index 000000000..b9d412b1f --- /dev/null +++ b/embedg-service/embedg/listeners.go @@ -0,0 +1,23 @@ +package embedg + +import ( + "context" + "log/slog" + "time" + + "github.com/disgoorg/disgo/events" +) + +func (g *EmbedGenerator) onMessageDelete(event *events.MessageDelete) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := g.actionSetStore.DeleteMessageActionSetsForMessage(ctx, event.MessageID) + if err != nil { + slog.Error( + "Failed to delete message action sets", + slog.String("message_id", event.MessageID.String()), + slog.Any("error", err), + ) + } +} diff --git a/embedg-service/entry/admin/commands.go b/embedg-service/entry/admin/commands.go new file mode 100644 index 000000000..21ac7d348 --- /dev/null +++ b/embedg-service/entry/admin/commands.go @@ -0,0 +1,23 @@ +package admin + +import ( + "context" + "fmt" + + "github.com/merlinfuchs/embed-generator/embedg-service/config" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres" + "github.com/merlinfuchs/embed-generator/embedg-service/embedg" +) + +func SyncCommands(ctx context.Context, pg *postgres.Client, cfg *config.RootConfig) error { + embedg, err := embedg.NewEmbedGenerator(embedg.EmbedGeneratorConfig{ + Token: cfg.Discord.Token, + BrokerURL: cfg.Broker.NATS.URL, + GatewayCount: cfg.Broker.GatewayCount, + }, pg) + if err != nil { + return fmt.Errorf("failed to create embedg: %w", err) + } + + return embedg.SyncCommands(ctx) +} diff --git a/embedg-service/entry/server/server.go b/embedg-service/entry/server/server.go new file mode 100644 index 000000000..a1bb44ec2 --- /dev/null +++ b/embedg-service/entry/server/server.go @@ -0,0 +1,41 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + + "github.com/merlinfuchs/embed-generator/embedg-service/config" + "github.com/merlinfuchs/embed-generator/embedg-service/db/postgres" + "github.com/merlinfuchs/embed-generator/embedg-service/embedg" + "github.com/merlinfuchs/embed-generator/embedg-service/manager/premium" +) + +func Run(ctx context.Context, pg *postgres.Client, cfg *config.RootConfig) error { + embedg, err := embedg.NewEmbedGenerator(embedg.EmbedGeneratorConfig{ + Token: cfg.Discord.Token, + BrokerURL: cfg.Broker.NATS.URL, + GatewayCount: cfg.Broker.GatewayCount, + }, pg) + if err != nil { + return fmt.Errorf("failed to create embedg: %w", err) + } + + premiumManager := premium.NewPremiumManager(premium.Config{ + BeneficialGuildID: cfg.Premium.BeneficialGuildID, + BeneficialRoleID: cfg.Premium.BeneficialRoleID, + Plans: cfg.Premium.Plans, + }, embedg.Rest(), pg) + embedg.Client().AddEventListeners(premiumManager) + go premiumManager.Run(ctx) + + slog.Info("Starting Embed Generator") + + err = embedg.Open(ctx) + if err != nil { + return fmt.Errorf("failed to run embedg: %w", err) + } + + <-ctx.Done() + return nil +} diff --git a/embedg-service/go.mod b/embedg-service/go.mod index 4c6259d5f..249198c0d 100644 --- a/embedg-service/go.mod +++ b/embedg-service/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( endobit.io/clog v0.6.0 github.com/cyrusaf/ctxlog v1.3.3 + github.com/disgoorg/disgo v0.19.0-rc.11 github.com/disgoorg/snowflake/v2 v2.0.3 github.com/go-playground/validator/v10 v10.28.0 github.com/golang-migrate/migrate/v4 v4.19.0 @@ -14,23 +15,30 @@ require ( github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/providers/rawbytes v1.0.0 github.com/knadh/koanf/v2 v2.3.0 + github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113160650-ae2b292f9996 github.com/minio/minio-go/v7 v7.0.97 github.com/pelletier/go-toml/v2 v2.2.4 github.com/rs/zerolog v1.34.0 github.com/spf13/viper v1.21.0 + github.com/urfave/cli/v2 v2.27.7 gopkg.in/guregu/null.v4 v4.0.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/disgoorg/json/v2 v2.0.0 // indirect + github.com/disgoorg/omit v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-ini/ini v1.67.0 // indirect + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -48,16 +56,22 @@ require ( github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tinylib/msgp v1.3.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/net v0.43.0 // indirect diff --git a/embedg-service/go.sum b/embedg-service/go.sum index 6997999e0..a9c9a0213 100644 --- a/embedg-service/go.sum +++ b/embedg-service/go.sum @@ -4,11 +4,15 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyrusaf/ctxlog v1.3.3 h1:0wjHcOpqj/ouAjKQDGFXpwJfFxK0Dh+gZZdmRnBZbg4= github.com/cyrusaf/ctxlog v1.3.3/go.mod h1:uYxERwb2tWRzkPzJUObIRzmhS/yd1QnL+3R9F3IkoXI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -16,6 +20,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/disgoorg/disgo v0.19.0-rc.10 h1:FYflS+wU7TF8u/qFfeytcsHkrqu2rGib7QtdkRmYBzY= +github.com/disgoorg/disgo v0.19.0-rc.10/go.mod h1:JORF6o1leAHEo1bv2SKe0zTuUMhreIj3a7lCF025Te0= +github.com/disgoorg/disgo v0.19.0-rc.11 h1:JKDckk2/LHibvCqgzQChnFkjKXyF9MbvnI0UuH3EmAM= +github.com/disgoorg/disgo v0.19.0-rc.11/go.mod h1:JORF6o1leAHEo1bv2SKe0zTuUMhreIj3a7lCF025Te0= +github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI= +github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI= +github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78= +github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ= github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -42,6 +54,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -61,6 +75,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -106,6 +122,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251112010757-2e2f4e4d085f h1:XaxHslnfQwn8FwD5z0g/t4FJg5nnSuEvI2vY5wup8bE= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251112010757-2e2f4e4d085f/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155245-58bf88272926 h1:cHYOssZzAWarRvmp3b9Q9iU31xeDBqunAa5U7NdNomw= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155245-58bf88272926/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155805-2bd114a648cd h1:/YdqF6aRdLBrY0X1jF2JVm/E3l+VgoYW6kyFLoE+be4= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113155805-2bd114a648cd/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113160650-ae2b292f9996 h1:S/kJ8TbbQNpq/lzCg7J053AL5zJLcGTIMKIY3S5foDk= +github.com/merlinfuchs/stateway/stateway-lib v0.0.0-20251113160650-ae2b292f9996/go.mod h1:lBq4eaCNMEMiMs9HmainZm8nUNiXEqbuj/fiAUN2WHU= github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= @@ -122,6 +146,12 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -142,8 +172,12 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -156,6 +190,7 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -163,6 +198,10 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -195,6 +234,7 @@ gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/embedg-service/main.go b/embedg-service/main.go index 06ab7d0f9..329299552 100644 --- a/embedg-service/main.go +++ b/embedg-service/main.go @@ -1 +1,7 @@ package main + +import "github.com/merlinfuchs/embed-generator/embedg-service/cmd" + +func main() { + cmd.Execute() +} diff --git a/embedg-service/manager/premium/entitlements.go b/embedg-service/manager/premium/entitlements.go new file mode 100644 index 000000000..6bcbfd689 --- /dev/null +++ b/embedg-service/manager/premium/entitlements.go @@ -0,0 +1,89 @@ +package premium + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/rest" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "gopkg.in/guregu/null.v4" +) + +func (m *PremiumManager) handleEntitlement(entitlement discord.Entitlement) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var userID common.NullID + if entitlement.UserID != nil { + userID = common.NullID{ID: *entitlement.UserID, Valid: true} + } + + var guildID common.NullID + if entitlement.GuildID != nil { + guildID = common.NullID{ID: *entitlement.GuildID, Valid: true} + } + + var consumed bool + if entitlement.Consumed != nil { + consumed = *entitlement.Consumed + } + + _, err := m.entitlementStore.UpsertEntitlement(ctx, model.Entitlement{ + ID: entitlement.ID.String(), + UserID: userID, + GuildID: guildID, + UpdatedAt: time.Now().UTC(), + Deleted: entitlement.Deleted, + SkuID: entitlement.SkuID, + StartsAt: null.TimeFromPtr(entitlement.StartsAt), + EndsAt: null.TimeFromPtr(entitlement.EndsAt), + Consumed: consumed, + }) + if err != nil { + slog.Error( + "Failed to upsert entitlement", + slog.String("entitlement_id", entitlement.ID.String()), + slog.Any("error", err), + ) + } +} + +func (m *PremiumManager) SyncEntitlements(ctx context.Context) error { + var after int + for { + entitlements, err := m.rest.GetEntitlements(0, rest.GetEntitlementsParams{ + Limit: 100, + After: after, + }, rest.WithCtx(ctx)) + if err != nil { + return fmt.Errorf("failed to get entitlements: %w", err) + } + + if len(entitlements) == 0 { + break + } + + for _, entitlement := range entitlements { + m.handleEntitlement(entitlement) + after = int(entitlement.ID) + } + } + return nil +} + +func (m *PremiumManager) OnEvent(event bot.Event) { + switch e := event.(type) { + case *events.EntitlementCreate: + m.handleEntitlement(e.Entitlement) + case *events.EntitlementUpdate: + m.handleEntitlement(e.Entitlement) + case *events.EntitlementDelete: + m.handleEntitlement(e.Entitlement) + } +} diff --git a/embedg-service/manager/premium/manager.go b/embedg-service/manager/premium/manager.go new file mode 100644 index 000000000..bd7edfa88 --- /dev/null +++ b/embedg-service/manager/premium/manager.go @@ -0,0 +1,72 @@ +package premium + +import ( + "context" + "log/slog" + "time" + + "github.com/disgoorg/disgo/rest" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/merlinfuchs/embed-generator/embedg-service/store" +) + +type Config struct { + BeneficialGuildID common.ID + BeneficialRoleID common.ID + Plans []model.Plan +} + +type PremiumManager struct { + config Config + rest rest.Rest + entitlementStore store.EntitlementStore + defaultPlanFeatures model.PlanFeatures +} + +func NewPremiumManager( + config Config, + rest rest.Rest, + entitlementStore store.EntitlementStore, +) *PremiumManager { + var defaultPlanFeatures model.PlanFeatures + for _, plan := range config.Plans { + if plan.Default { + defaultPlanFeatures = plan.Features + } + } + + return &PremiumManager{ + config: config, + rest: rest, + entitlementStore: entitlementStore, + defaultPlanFeatures: defaultPlanFeatures, + } +} + +func (m *PremiumManager) Run(ctx context.Context) { + entitlementTicker := time.NewTicker(time.Minute * 5) + defer entitlementTicker.Stop() + + rolesTicker := time.NewTicker(time.Minute * 15) + defer rolesTicker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-entitlementTicker.C: + err := m.SyncEntitlements(ctx) + if err != nil { + slog.Error("Failed to sync entitlements", slog.Any("error", err)) + continue + } + case <-rolesTicker.C: + err := m.assignPremiumRoles(ctx) + if err != nil { + slog.Error("Failed to sync premium roles", slog.Any("error", err)) + continue + } + } + } +} diff --git a/embedg-service/manager/premium/plan.go b/embedg-service/manager/premium/plan.go new file mode 100644 index 000000000..a8e6874a4 --- /dev/null +++ b/embedg-service/manager/premium/plan.go @@ -0,0 +1,65 @@ +package premium + +import ( + "context" + "fmt" + + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" +) + +func (m *PremiumManager) GetPlanByID(id string) *model.Plan { + for _, plan := range m.config.Plans { + if plan.ID == id { + return &plan + } + } + + return nil +} + +func (m *PremiumManager) GetPlanBySKUID(skuID common.ID) *model.Plan { + for _, plan := range m.config.Plans { + if plan.SKUID == skuID { + return &plan + } + } + + return nil +} + +func (m *PremiumManager) GetPlanFeaturesForGuild(ctx context.Context, guildID common.ID) (model.PlanFeatures, error) { + planFeatures := m.defaultPlanFeatures + + entitlements, err := m.entitlementStore.GetActiveEntitlementsForGuild(ctx, guildID) + if err != nil { + return planFeatures, fmt.Errorf("failed to retrieve entitlments for guild: %w", err) + } + + for _, entitlement := range entitlements { + plan := m.GetPlanBySKUID(entitlement.SkuID) + if plan != nil { + planFeatures.Merge(plan.Features) + } + } + + return planFeatures, nil +} + +func (m *PremiumManager) GetPlanFeaturesForUser(ctx context.Context, userID common.ID) (model.PlanFeatures, error) { + planFeatures := m.defaultPlanFeatures + + entitlements, err := m.entitlementStore.GetActiveEntitlementsForUser(ctx, userID) + if err != nil { + return planFeatures, fmt.Errorf("failed to retrieve entitlments for user: %w", err) + } + + for _, entitlement := range entitlements { + plan := m.GetPlanBySKUID(entitlement.SkuID) + if plan != nil { + planFeatures.Merge(plan.Features) + } + } + + return planFeatures, nil +} diff --git a/embedg-service/manager/premium/roles.go b/embedg-service/manager/premium/roles.go new file mode 100644 index 000000000..300d75f7b --- /dev/null +++ b/embedg-service/manager/premium/roles.go @@ -0,0 +1,62 @@ +package premium + +import ( + "context" + "fmt" + "log/slog" + + "github.com/disgoorg/disgo/rest" + "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-server/util" +) + +func (m *PremiumManager) assignPremiumRoles(ctx context.Context) error { + if m.config.BeneficialGuildID == 0 || m.config.BeneficialRoleID == 0 { + return nil + } + + userIDs, err := m.entitlementStore.GetEntitledUserIDs(ctx) + if err != nil { + return fmt.Errorf("Failed to get entitled user IDs: %w", err) + } + + for _, userID := range userIDs { + features, err := m.GetPlanFeaturesForUser(ctx, userID) + if err != nil { + slog.Error("Failed to get plan features for guild", slog.Any("error", err)) + continue + } + + member, err := m.rest.GetMember(m.config.BeneficialGuildID, userID, rest.WithCtx(ctx)) + if err != nil { + if util.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMember) { + continue + } + + slog.Error("Failed to get guild member", slog.Any("error", err)) + continue + } + + hasPremiumRole := false + for _, r := range member.RoleIDs { + if r == m.config.BeneficialRoleID { + hasPremiumRole = true + break + } + } + + if features.IsPremium && !hasPremiumRole { + err = m.rest.AddMemberRole(m.config.BeneficialGuildID, userID, m.config.BeneficialRoleID, rest.WithCtx(ctx)) + if err != nil { + slog.Error("Failed to add premium role", slog.Any("error", err)) + } + } else if !features.IsPremium && hasPremiumRole { + err = m.rest.RemoveMemberRole(m.config.BeneficialGuildID, userID, m.config.BeneficialRoleID, rest.WithCtx(ctx)) + if err != nil { + slog.Error("Failed to remove premium role", slog.Any("error", err)) + } + } + } + + return nil +} diff --git a/embedg-service/model/entitlement.go b/embedg-service/model/entitlement.go index 416e307f9..8eb1fb6c3 100644 --- a/embedg-service/model/entitlement.go +++ b/embedg-service/model/entitlement.go @@ -13,7 +13,7 @@ type Entitlement struct { GuildID common.NullID UpdatedAt time.Time Deleted bool - SkuID string + SkuID common.ID StartsAt null.Time EndsAt null.Time Consumed bool diff --git a/embedg-service/model/plan.go b/embedg-service/model/plan.go index 5a6444279..d311cee71 100644 --- a/embedg-service/model/plan.go +++ b/embedg-service/model/plan.go @@ -1,28 +1,30 @@ package model +import "github.com/merlinfuchs/embed-generator/embedg-service/common" + type Plan struct { - ID string `mapstructure:"id"` - SKUID string `mapstructure:"sku_id"` - Default bool `mapstructure:"default"` - Features PlanFeatures `mapstructure:"features"` - Consumable bool `mapstructure:"consumable"` + ID string `toml:"id"` + SKUID common.ID `toml:"sku_id"` + Default bool `toml:"default"` + Features PlanFeatures `toml:"features"` + Consumable bool `toml:"consumable"` } type PlanFeatures struct { - MaxSavedMessages int `mapstructure:"max_saved_messages"` - MaxActionsPerComponent int `mapstructure:"max_actions_per_component"` - AdvancedActionTypes bool `mapstructure:"advanced_action_types"` - AIAssistant bool `mapstructure:"ai_assistant"` - CustomBot bool `mapstructure:"custom_bot"` - ComponentsV2 bool `mapstructure:"components_v2"` - ComponentTypes []int `mapstructure:"component_types"` - MaxCustomCommands int `mapstructure:"max_custom_commands"` - IsPremium bool `mapstructure:"is_premium"` - MaxImageUploadSize int `mapstructure:"max_image_upload_size"` - MaxScheduledMessages int `mapstructure:"max_scheduled_messages"` - PeriodicScheduledMessages bool `mapstructure:"periodic_scheduled_messages"` - MaxTemplateOps int `mapstructure:"max_template_ops"` - MaxKVKeys int `mapstructure:"max_kv_keys"` + MaxSavedMessages int `toml:"max_saved_messages"` + MaxActionsPerComponent int `toml:"max_actions_per_component"` + AdvancedActionTypes bool `toml:"advanced_action_types"` + AIAssistant bool `toml:"ai_assistant"` + CustomBot bool `toml:"custom_bot"` + ComponentsV2 bool `toml:"components_v2"` + ComponentTypes []int `toml:"component_types"` + MaxCustomCommands int `toml:"max_custom_commands"` + IsPremium bool `toml:"is_premium"` + MaxImageUploadSize int `toml:"max_image_upload_size"` + MaxScheduledMessages int `toml:"max_scheduled_messages"` + PeriodicScheduledMessages bool `toml:"periodic_scheduled_messages"` + MaxTemplateOps int `toml:"max_template_ops"` + MaxKVKeys int `toml:"max_kv_keys"` } func (f *PlanFeatures) Merge(b PlanFeatures) { diff --git a/embedg-service/store/entitlement.go b/embedg-service/store/entitlement.go index b31cfb384..6186dde44 100644 --- a/embedg-service/store/entitlement.go +++ b/embedg-service/store/entitlement.go @@ -14,4 +14,5 @@ type EntitlementStore interface { GetEntitlement(ctx context.Context, id common.ID, userID common.ID) (*model.Entitlement, error) UpdateEntitlementConsumedGuildID(ctx context.Context, id common.ID, consumedGuildID common.NullID) (*model.Entitlement, error) UpsertEntitlement(ctx context.Context, entitlement model.Entitlement) (*model.Entitlement, error) + GetEntitledUserIDs(ctx context.Context) ([]common.ID, error) } diff --git a/go.work.sum b/go.work.sum index faf1aedb0..eec41905d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -162,6 +162,7 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= @@ -258,6 +259,7 @@ github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnd github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo= github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8= @@ -304,6 +306,7 @@ github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= @@ -321,14 +324,6 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= -github.com/disgoorg/disgo v0.19.0-rc.10 h1:FYflS+wU7TF8u/qFfeytcsHkrqu2rGib7QtdkRmYBzY= -github.com/disgoorg/disgo v0.19.0-rc.10/go.mod h1:JORF6o1leAHEo1bv2SKe0zTuUMhreIj3a7lCF025Te0= -github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI= -github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI= -github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78= -github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ= -github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= -github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= @@ -450,12 +445,14 @@ github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.5.1 h1:/+mFTs4AlwsJ/mJe8NDtKb7BxLtbZFpcn8vDsneEkwQ= github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= @@ -493,6 +490,7 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWet github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -670,12 +668,6 @@ github.com/n0madic/twitter-scraper v0.0.0-20230711213008-94503a2bc36c/go.mod h1: github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= -github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= -github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= -github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= -github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncw/swift v1.0.47 h1:4DQRPj35Y41WogBxyhOXlrI37nzGlyEcsforeudyYPQ= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -713,6 +705,7 @@ github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626 github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= @@ -730,13 +723,11 @@ github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmr github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 h1:nlG4Wa5+minh3S9LVFtNoY+GVRiudA2e3EVfcCi3RCA= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1 h1:ZFfeKAhIQiiOrQaI3/znw0gOmYpO28Tcu1YaqMa/jtQ= github.com/sagikazarmark/crypt v0.9.0 h1:fipzMFW34hFUEc4D7fsLQFtE7yElkpgyS2zruedRdZk= github.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2dw6+4vHTMuo= -github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/sclevine/agouti v3.0.0+incompatible h1:8IBJS6PWz3uTlMP3YBIR5f+KAldcGuOeFkFbUWfBgK4= github.com/sclevine/spec v1.2.0 h1:1Jwdf9jSfDl9NVmt8ndHqbTZ7XCCPbh1jI3hkDBHVYA= @@ -863,6 +854,7 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 h1:Ky1MObd188aGbgb5OgNnwGuEEwI9MVIcc7rBW6zk5Ak= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 h1:Q3C9yzW6I9jqEc8sawxzxZmY48fs9u220KXq6d5s3XU= go.opentelemetry.io/otel v1.3.0 h1:APxLf0eiBwLl+SOXiJJCVYzA1OOJNyAoV8C5RNRyy7Y= @@ -876,13 +868,16 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g= go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8= go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY= go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -954,6 +949,7 @@ golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= @@ -963,6 +959,7 @@ golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= @@ -986,17 +983,22 @@ google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxm google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= From 3be6600461e3d91c4d528b5577a6963f0898b736 Mon Sep 17 00:00:00 2001 From: merlinfuchs Date: Fri, 14 Nov 2025 20:08:51 +0100 Subject: [PATCH 04/36] continue service implementation --- embedg-server/actions/data.go | 71 +- embedg-service/access/access.go | 187 +++ embedg-service/access/check.go | 46 + embedg-service/access/helpers.go | 82 ++ embedg-service/actions/data.go | 142 ++ embedg-service/actions/handler/handle.go | 525 +++++++ embedg-service/actions/handler/interaction.go | 114 ++ embedg-service/actions/parser/actions.go | 53 + embedg-service/actions/parser/parse.go | 370 +++++ embedg-service/actions/parser/permissions.go | 190 +++ embedg-service/actions/template/base.go | 267 ++++ embedg-service/actions/template/context.go | 212 +++ embedg-service/actions/template/data.go | 544 ++++++++ embedg-service/actions/template/func.go | 1202 +++++++++++++++++ embedg-service/actions/template/provider.go | 216 +++ embedg-service/actions/template/writer.go | 31 + embedg-service/api/handler/error.go | 47 + embedg-service/api/session/session.go | 15 + .../db/postgres/store_custom_command.go | 75 +- .../db/postgres/store_message_action_set.go | 52 +- .../db/postgres/store_scheduled_message.go | 20 +- embedg-service/embedg/embedg.go | 16 +- embedg-service/entry/server/server.go | 22 + embedg-service/go.mod | 2 +- embedg-service/go.sum | 2 + .../manager/scheduled_message/cron.go | 56 + .../manager/scheduled_message/manager.go | 205 +++ embedg-service/manager/webhook/manager.go | 26 + embedg-service/model/custom_command.go | 5 +- embedg-service/model/message_action_set.go | 7 +- embedg-service/store/plan.go | 7 +- embedg-service/store/scheduled_message.go | 8 +- go.work.sum | 2 + 33 files changed, 4718 insertions(+), 101 deletions(-) create mode 100644 embedg-service/access/access.go create mode 100644 embedg-service/access/check.go create mode 100644 embedg-service/access/helpers.go create mode 100644 embedg-service/actions/data.go create mode 100644 embedg-service/actions/handler/handle.go create mode 100644 embedg-service/actions/handler/interaction.go create mode 100644 embedg-service/actions/parser/actions.go create mode 100644 embedg-service/actions/parser/parse.go create mode 100644 embedg-service/actions/parser/permissions.go create mode 100644 embedg-service/actions/template/base.go create mode 100644 embedg-service/actions/template/context.go create mode 100644 embedg-service/actions/template/data.go create mode 100644 embedg-service/actions/template/func.go create mode 100644 embedg-service/actions/template/provider.go create mode 100644 embedg-service/actions/template/writer.go create mode 100644 embedg-service/api/handler/error.go create mode 100644 embedg-service/api/session/session.go create mode 100644 embedg-service/manager/scheduled_message/cron.go create mode 100644 embedg-service/manager/scheduled_message/manager.go create mode 100644 embedg-service/manager/webhook/manager.go diff --git a/embedg-server/actions/data.go b/embedg-server/actions/data.go index b68ac8971..8d9c1fd99 100644 --- a/embedg-server/actions/data.go +++ b/embedg-server/actions/data.go @@ -3,19 +3,20 @@ package actions import ( "slices" - "github.com/merlinfuchs/discordgo" + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-service/common" ) type MessageWithActions struct { - Content string `json:"content,omitempty"` - Username string `json:"username,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - TTS bool `json:"tts,omitempty"` - Embeds []*discordgo.MessageEmbed `json:"embeds,omitempty"` - AllowedMentions *discordgo.MessageAllowedMentions `json:"allowed_mentions,omitempty"` - Components []ComponentWithActions `json:"components,omitempty"` - Actions map[string]ActionSet `json:"actions,omitempty"` - Flags discordgo.MessageFlags `json:"flags,omitempty"` + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + Embeds []discord.Embed `json:"embeds,omitempty"` + AllowedMentions *discord.AllowedMentions `json:"allowed_mentions,omitempty"` + Components []ComponentWithActions `json:"components,omitempty"` + Actions map[string]ActionSet `json:"actions,omitempty"` + Flags discord.MessageFlags `json:"flags,omitempty"` } func (m MessageWithActions) ComponentsV2Enabled() bool { @@ -23,20 +24,20 @@ func (m MessageWithActions) ComponentsV2Enabled() bool { } type ComponentWithActions struct { - ID int `json:"id,omitempty"` - Type discordgo.ComponentType `json:"type"` - Disabled bool `json:"disabled,omitempty"` - Spoiler bool `json:"spoiler,omitempty"` + ID int `json:"id,omitempty"` + Type discord.ComponentType `json:"type"` + Disabled bool `json:"disabled,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` // Action Row & Section & Container Components []ComponentWithActions `json:"components,omitempty"` // Button - Style discordgo.ButtonStyle `json:"style,omitempty"` - Label string `json:"label,omitempty"` - Emoji *discordgo.ComponentEmoji `json:"emoji,omitempty"` - URL string `json:"url,omitempty"` - ActionSetID string `json:"action_set_id,omitempty"` + Style discord.ButtonStyle `json:"style,omitempty"` + Label string `json:"label,omitempty"` + Emoji *discord.ComponentEmoji `json:"emoji,omitempty"` + URL string `json:"url,omitempty"` + ActionSetID string `json:"action_set_id,omitempty"` // Select Menu Placeholder string `json:"placeholder,omitempty"` @@ -73,11 +74,11 @@ type UnfurledMediaItem struct { } type ComponentSelectOptionWithActions struct { - Label string `json:"label"` - Description string `json:"description"` - Emoji *discordgo.ComponentEmoji `json:"emoji"` - Default bool `json:"default"` - ActionSetID string `json:"action_set_id"` + Label string `json:"label"` + Description string `json:"description"` + Emoji *discord.ComponentEmoji `json:"emoji"` + Default bool `json:"default"` + ActionSetID string `json:"action_set_id"` } type ComponentMediaGalleryItem struct { @@ -117,25 +118,25 @@ type ActionSet struct { } type ActionDerivedPermissions struct { - UserID string `json:"user_id"` - GuildIsOwner bool `json:"guild_is_owner"` - GuildPermissions int64 `json:"guild_permissions"` - ChannelPermissions int64 `json:"channel_permissions"` - AllowedRoleIDs []string `json:"lower_role_ids"` + UserID common.ID `json:"user_id"` + GuildIsOwner bool `json:"guild_is_owner"` + GuildPermissions discord.Permissions `json:"guild_permissions"` + ChannelPermissions discord.Permissions `json:"channel_permissions"` + AllowedRoleIDs []common.ID `json:"lower_role_ids"` } -func (a *ActionDerivedPermissions) HasChannelPermission(permission int64) bool { - return a.GuildIsOwner || (a.GuildPermissions&discordgo.PermissionAdministrator) != 0 || (a.ChannelPermissions&permission) != 0 +func (a *ActionDerivedPermissions) HasChannelPermission(permission discord.Permissions) bool { + return a.GuildIsOwner || (a.GuildPermissions&discord.PermissionAdministrator) != 0 || (a.ChannelPermissions&permission) != 0 } -func (a *ActionDerivedPermissions) HasGuildPermission(permission int64) bool { - return a.GuildIsOwner || (a.GuildPermissions&discordgo.PermissionAdministrator) != 0 || (a.GuildPermissions&permission) != 0 +func (a *ActionDerivedPermissions) HasGuildPermission(permission discord.Permissions) bool { + return a.GuildIsOwner || (a.GuildPermissions&discord.PermissionAdministrator) != 0 || (a.GuildPermissions&permission) != 0 } -func (a *ActionDerivedPermissions) CanManageRole(roleID string) bool { +func (a *ActionDerivedPermissions) CanManageRole(roleID common.ID) bool { if a.GuildIsOwner { return true } - return a.HasGuildPermission(discordgo.PermissionManageRoles) && slices.Contains(a.AllowedRoleIDs, roleID) + return a.HasGuildPermission(discord.PermissionManageRoles) && slices.Contains(a.AllowedRoleIDs, roleID) } diff --git a/embedg-service/access/access.go b/embedg-service/access/access.go new file mode 100644 index 000000000..6815cebe8 --- /dev/null +++ b/embedg-service/access/access.go @@ -0,0 +1,187 @@ +package access + +import ( + "fmt" + + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/merlinfuchs/discordgo" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/spf13/viper" +) + +type AccessManager struct { + caches cache.Caches + rest rest.Rest +} + +func New(caches cache.Caches, rest rest.Rest) *AccessManager { + return &AccessManager{ + caches: caches, + rest: rest, + } +} + +type GuildAccess struct { + HasChannelWithUserAccess bool + HasChannelWithBotAccess bool +} + +type ChannelAccess struct { + UserPermissions discord.Permissions + BotPermissions discord.Permissions +} + +func (c *ChannelAccess) UserAccess() bool { + return c.UserPermissions&(discord.PermissionManageWebhooks|discord.PermissionAdministrator) != 0 +} + +func (c *ChannelAccess) BotAccess() bool { + return c.BotPermissions&(discord.PermissionManageWebhooks|discord.PermissionAdministrator) != 0 +} + +func (m *AccessManager) GetGuildAccessForUser(userID common.ID, guildID common.ID) (GuildAccess, error) { + res := GuildAccess{} + + guild, ok := m.caches.Guild(guildID) + if !ok { + return res, nil + } + + if guild.OwnerID == userID { + res.HasChannelWithUserAccess = true + } + + channels := m.caches.ChannelsForGuild(guildID) + + for channel := range channels { + if !res.HasChannelWithUserAccess { + access := ChannelAccess{} + err := m.SetChannelAccessUserPermissions(&access, userID, channel.ID()) + if err != nil { + return res, err + } + + if access.UserAccess() { + res.HasChannelWithUserAccess = true + } + } + + if !res.HasChannelWithBotAccess { + access := ChannelAccess{} + err := m.SetChannelAccessBotPermissions(&access, channel.ID()) + if err != nil { + return res, err + } + + if access.BotAccess() { + res.HasChannelWithBotAccess = true + } + } + + // We can stop iterating if we already know that the user has access to both + if res.HasChannelWithBotAccess && res.HasChannelWithUserAccess { + break + } + } + + return res, nil +} + +func (m *AccessManager) GetChannelAccessForUser(userID common.ID, channelID common.ID) (ChannelAccess, error) { + res := ChannelAccess{} + + err := m.SetChannelAccessUserPermissions(&res, userID, channelID) + if err != nil { + return res, err + } + + err = m.SetChannelAccessBotPermissions(&res, channelID) + if err != nil { + return res, err + } + + return res, nil +} + +func (m *AccessManager) SetChannelAccessUserPermissions(res *ChannelAccess, userID common.ID, channelID common.ID) (err error) { + res.UserPermissions, err = m.ComputeUserPermissionsForChannel(userID, channelID) + if err != nil { + if common.IsDiscordRestErrorCode(err, discordgo.ErrCodeUnknownMember) { + // The user is not in the server, so we can't compute the permissions + return nil + } + return err + } + + return nil +} + +func (m *AccessManager) SetChannelAccessBotPermissions(res *ChannelAccess, channelID common.ID) error { + botPerms, err := m.ComputeBotPermissionsForChannel(channelID) + if err != nil { + return err + } + if botPerms == 0 { + // The bot doesn't have access to the server so there is no point in checking access for the user + return nil + } + res.BotPermissions = botPerms + + return nil +} + +func (m *AccessManager) ComputeUserPermissionsForChannel(userID common.ID, channelID common.ID) (discord.Permissions, error) { + channel, ok := m.caches.Channel(channelID) + if !ok { + return 0, nil + } + + guild, ok := m.caches.Guild(channel.GuildID()) + if !ok { + return 0, nil + } + + roleIterator := m.caches.Roles(channel.GuildID()) + roles := make([]discord.Role, 0) + for role := range roleIterator { + roles = append(roles, role) + } + + if guild.OwnerID == userID { + // Owner has access to all channels + return discord.PermissionsAll, nil + } + + member, err := m.GetGuildMember(guild.ID, userID) + if err != nil { + return 0, err + } + + perms := memberPermissions(&guild, roles, channel, userID, member.RoleIDs) + return perms, err +} + +func (m *AccessManager) ComputeBotPermissionsForChannel(channelID common.ID) (discord.Permissions, error) { + userID, err := common.ParseID(viper.GetString("discord.client_id")) + if err != nil { + return 0, fmt.Errorf("Failed to parse bot user ID: %w", err) + } + + return m.ComputeUserPermissionsForChannel(userID, channelID) +} + +func (m *AccessManager) GetGuildMember(guildID common.ID, userID common.ID) (*discord.Member, error) { + cached, ok := m.caches.Member(guildID, userID) + if ok { + return &cached, nil + } + + member, err := m.rest.GetMember(guildID, userID) + if err != nil { + return nil, fmt.Errorf("Failed to get guild member: %w", err) + } + + return member, nil +} diff --git a/embedg-service/access/check.go b/embedg-service/access/check.go new file mode 100644 index 000000000..831ce08f6 --- /dev/null +++ b/embedg-service/access/check.go @@ -0,0 +1,46 @@ +package access + +import ( + "github.com/gofiber/fiber/v2" + "github.com/merlinfuchs/embed-generator/embedg-service/api/handler" + "github.com/merlinfuchs/embed-generator/embedg-service/api/session" + "github.com/merlinfuchs/embed-generator/embedg-service/common" +) + +func (m *AccessManager) CheckGuildAccessForRequest(c *fiber.Ctx, guildID common.ID) error { + session := c.Locals("session").(*session.Session) + + access, err := m.GetGuildAccessForUser(session.UserID, guildID) + if err != nil { + return err + } + + if !access.HasChannelWithBotAccess { + return handler.Forbidden("bot_missing_access", "The bot doesn't have access to this guild") + } + + if !access.HasChannelWithUserAccess { + return handler.Forbidden("missing_access", "You don't have access to this guild") + } + + return nil +} + +func (m *AccessManager) CheckChannelAccessForRequest(c *fiber.Ctx, channelID common.ID) error { + session := c.Locals("session").(*session.Session) + + access, err := m.GetChannelAccessForUser(session.UserID, channelID) + if err != nil { + return err + } + + if !access.BotAccess() { + return handler.Forbidden("bot_missing_access", "The bot doesn't have access to this channel") + } + + if !access.UserAccess() { + return handler.Forbidden("missing_access", "You don't have access to this channel") + } + + return nil +} diff --git a/embedg-service/access/helpers.go b/embedg-service/access/helpers.go new file mode 100644 index 000000000..8b43b9600 --- /dev/null +++ b/embedg-service/access/helpers.go @@ -0,0 +1,82 @@ +package access + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-service/common" +) + +func memberPermissions(guild *discord.Guild, roles []discord.Role, channel discord.GuildChannel, userID common.ID, roleIDs []common.ID) (apermissions discord.Permissions) { + if userID == guild.OwnerID { + apermissions = discord.PermissionsAll + return + } + + for _, role := range roles { + if role.ID == guild.ID { + apermissions |= role.Permissions + break + } + } + + for _, role := range roles { + for _, roleID := range roleIDs { + if role.ID == roleID { + apermissions |= role.Permissions + break + } + } + } + + if apermissions&discord.PermissionAdministrator == discord.PermissionAdministrator { + apermissions |= discord.PermissionsAll + return // Administrator bypasses all overrides + } + + if channel == nil { + return + } + + // Apply @everyone overrides from the channel. + for _, overwrite := range channel.PermissionOverwrites() { + if roleOverwrite, ok := overwrite.(discord.RolePermissionOverwrite); ok { + if guild.ID == roleOverwrite.ID() { + apermissions &= ^roleOverwrite.Deny + apermissions |= roleOverwrite.Allow + break + } + } + } + + var denies, allows discord.Permissions + // Member overwrites can override role overrides, so do two passes + for _, overwrite := range channel.PermissionOverwrites() { + if roleOverwrite, ok := overwrite.(discord.RolePermissionOverwrite); ok { + for _, roleID := range roleIDs { + if roleOverwrite.ID() == roleID { + denies |= roleOverwrite.Deny + allows |= roleOverwrite.Allow + break + } + } + } + } + + apermissions &= ^denies + apermissions |= allows + + for _, overwrite := range channel.PermissionOverwrites() { + if memberOverwrite, ok := overwrite.(discord.MemberPermissionOverwrite); ok { + if memberOverwrite.ID() == userID { + apermissions &= ^memberOverwrite.Deny + apermissions |= memberOverwrite.Allow + break + } + } + } + + if apermissions&discord.PermissionAdministrator == discord.PermissionAdministrator { + apermissions |= discord.PermissionsAll + } + + return apermissions +} diff --git a/embedg-service/actions/data.go b/embedg-service/actions/data.go new file mode 100644 index 000000000..8d9c1fd99 --- /dev/null +++ b/embedg-service/actions/data.go @@ -0,0 +1,142 @@ +package actions + +import ( + "slices" + + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-service/common" +) + +type MessageWithActions struct { + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + Embeds []discord.Embed `json:"embeds,omitempty"` + AllowedMentions *discord.AllowedMentions `json:"allowed_mentions,omitempty"` + Components []ComponentWithActions `json:"components,omitempty"` + Actions map[string]ActionSet `json:"actions,omitempty"` + Flags discord.MessageFlags `json:"flags,omitempty"` +} + +func (m MessageWithActions) ComponentsV2Enabled() bool { + return m.Flags&(1<<15) != 0 +} + +type ComponentWithActions struct { + ID int `json:"id,omitempty"` + Type discord.ComponentType `json:"type"` + Disabled bool `json:"disabled,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` + + // Action Row & Section & Container + Components []ComponentWithActions `json:"components,omitempty"` + + // Button + Style discord.ButtonStyle `json:"style,omitempty"` + Label string `json:"label,omitempty"` + Emoji *discord.ComponentEmoji `json:"emoji,omitempty"` + URL string `json:"url,omitempty"` + ActionSetID string `json:"action_set_id,omitempty"` + + // Select Menu + Placeholder string `json:"placeholder,omitempty"` + MinValues *int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Options []ComponentSelectOptionWithActions `json:"options,omitempty"` + + // Section + Accessory *ComponentWithActions `json:"accessory"` + + // Text Display + Content string `json:"content,omitempty"` + + // Thumbnail + Description string `json:"description,omitempty"` + Media *UnfurledMediaItem `json:"media,omitempty"` + + // Media Gallery + Items []ComponentMediaGalleryItem `json:"items,omitempty"` + + // File + File *UnfurledMediaItem `json:"file,omitempty"` + + // Separator + Divider bool `json:"divider,omitempty"` + Spacing int `json:"spacing,omitempty"` + + // Container + AccentColor int `json:"accent_color,omitempty"` +} + +type UnfurledMediaItem struct { + URL string `json:"url"` +} + +type ComponentSelectOptionWithActions struct { + Label string `json:"label"` + Description string `json:"description"` + Emoji *discord.ComponentEmoji `json:"emoji"` + Default bool `json:"default"` + ActionSetID string `json:"action_set_id"` +} + +type ComponentMediaGalleryItem struct { + Media UnfurledMediaItem `json:"media"` + Description string `json:"description,omitempty"` + Spoiler bool `json:"spoiler,omitempty"` +} + +type ActionType int + +const ( + ActionTypeTextResponse ActionType = 1 + ActionTypeToggleRole ActionType = 2 + ActionTypeAddRole ActionType = 3 + ActionTypeRemoveRole ActionType = 4 + ActionTypeSavedMessageResponse ActionType = 5 + ActionTypeTextDM ActionType = 6 + ActionTypeSavedMessageDM ActionType = 7 + ActionTypeTextEdit ActionType = 8 + ActionTypeSavedMessageEdit ActionType = 9 + ActionTypePermissionCheck ActionType = 10 +) + +type Action struct { + Type ActionType `json:"type"` + TargetID string `json:"target_id"` + Text string `json:"text"` + Public bool `json:"public"` + AllowRoleMentions bool `json:"allow_role_mentions"` + DisableDefaultResponse bool `json:"disable_default_response"` + Permissions string `json:"permissions"` + RoleIDs []string `json:"role_ids"` +} + +type ActionSet struct { + Actions []Action `json:"actions"` +} + +type ActionDerivedPermissions struct { + UserID common.ID `json:"user_id"` + GuildIsOwner bool `json:"guild_is_owner"` + GuildPermissions discord.Permissions `json:"guild_permissions"` + ChannelPermissions discord.Permissions `json:"channel_permissions"` + AllowedRoleIDs []common.ID `json:"lower_role_ids"` +} + +func (a *ActionDerivedPermissions) HasChannelPermission(permission discord.Permissions) bool { + return a.GuildIsOwner || (a.GuildPermissions&discord.PermissionAdministrator) != 0 || (a.ChannelPermissions&permission) != 0 +} + +func (a *ActionDerivedPermissions) HasGuildPermission(permission discord.Permissions) bool { + return a.GuildIsOwner || (a.GuildPermissions&discord.PermissionAdministrator) != 0 || (a.GuildPermissions&permission) != 0 +} + +func (a *ActionDerivedPermissions) CanManageRole(roleID common.ID) bool { + if a.GuildIsOwner { + return true + } + + return a.HasGuildPermission(discord.PermissionManageRoles) && slices.Contains(a.AllowedRoleIDs, roleID) +} diff --git a/embedg-service/actions/handler/handle.go b/embedg-service/actions/handler/handle.go new file mode 100644 index 000000000..e2c3b447c --- /dev/null +++ b/embedg-service/actions/handler/handle.go @@ -0,0 +1,525 @@ +package handler + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/snowflake/v2" + "github.com/merlinfuchs/embed-generator/embedg-service/actions" + "github.com/merlinfuchs/embed-generator/embedg-service/actions/parser" + "github.com/merlinfuchs/embed-generator/embedg-service/actions/template" + "github.com/merlinfuchs/embed-generator/embedg-service/store" + "github.com/rs/zerolog/log" +) + +const roleErrorMessage = "Failed to add or remove role.\n\n" + + "Please make sure the role is below the 'Embed Generator' role and that the bot has the manage roles permission." + +type ActionHandler struct { + customCommandStore store.CustomCommandStore + savedMessageStore store.SavedMessageStore + actionSetStore store.MessageActionSetStore + kvEntryStore store.KVEntryStore + parser *parser.ActionParser + planStore store.PlanStore +} + +func New( + customCommandStore store.CustomCommandStore, + savedMessageStore store.SavedMessageStore, + actionSetStore store.MessageActionSetStore, + kvEntryStore store.KVEntryStore, + parser *parser.ActionParser, + planStore store.PlanStore, +) *ActionHandler { + return &ActionHandler{ + customCommandStore: customCommandStore, + savedMessageStore: savedMessageStore, + actionSetStore: actionSetStore, + kvEntryStore: kvEntryStore, + parser: parser, + planStore: planStore, + } +} + +func (m *ActionHandler) HandleActionInteraction(rest rest.Rest, i Interaction) error { + interaction := i.Interaction() + + var actionSet actions.ActionSet + var derivedPerms *actions.ActionDerivedPermissions + + if interaction.Type() == discord.InteractionTypeComponent { + compInteraction := interaction.(discord.ComponentInteraction) + data := compInteraction.Data + + if !strings.HasPrefix(data.CustomID(), "action:") { + return nil + } + + actionSetID := data.CustomID()[7:] + + if strings.HasPrefix(actionSetID, "options:") { + // Handle select menu values + if selectData, ok := data.(discord.StringSelectMenuInteractionData); ok { + actionSetID = selectData.Values[0][7:] + } + } + + col, err := m.actionSetStore.GetMessageActionSet(context.TODO(), compInteraction.Message.ID, actionSetID) + if err != nil { + if err == sql.ErrNoRows { + return nil + } + + log.Error().Err(err).Msg("Failed to get message action set") + return err + } + actionSet = col.Actions + derivedPerms = col.DerivedPermissions + } else if interaction.Type() == discord.InteractionTypeApplicationCommand { + appCommandInteraction := interaction.(discord.ApplicationCommandInteraction) + slashData := appCommandInteraction.SlashCommandInteractionData() + fullName := slashData.CommandName() + for _, opt := range slashData.All() { + if opt.Type == discord.ApplicationCommandOptionTypeSubCommand { + fullName += " " + opt.Name + } else if opt.Type == discord.ApplicationCommandOptionTypeSubCommandGroup { + fullName += " " + opt.Name + // TODO: Handle subcommand group options properly + } + } + + if interaction.GuildID() == nil { + return nil + } + + col, err := m.customCommandStore.GetCustomCommandByName(context.TODO(), *interaction.GuildID(), fullName) + if err != nil { + if err == sql.ErrNoRows { + return nil + } + + log.Error().Err(err).Msg("Failed to get custom command action set") + return err + } + actionSet = col.Actions + derivedPerms = col.DerivedPermissions + } else { + return fmt.Errorf("invalid interaciont type") + } + + // For messages created before the permission context was added we don't run permission checks here + legacyPermissions := derivedPerms == nil + + // DEPRECATED: This has been replaced by templates, it's only here for backwards compatibility + // TODO: Refactor variables to use disgo types + // variables := variables.NewContext( + // variables.NewInteractionVariables(interaction), + // variables.NewGuildVariables(interaction.GuildID().String(), s.State, nil), + // variables.NewChannelVariables(interaction.ChannelID, s.State, nil), + // ) + + features, err := m.planStore.GetPlanFeaturesForGuild(context.TODO(), *interaction.GuildID()) + if err != nil { + return fmt.Errorf("could not get plan features: %w", err) + } + + templates := template.NewContext( + "HANDLE_ACTION", features.MaxTemplateOps, + template.NewInteractionProvider(nil, interaction), // TODO: Fix caches access + template.NewKVProvider(*interaction.GuildID(), m.kvEntryStore, features.MaxKVKeys), + ) + + for _, action := range actionSet.Actions { + switch action.Type { + case actions.ActionTypeTextResponse: + var flags discord.MessageFlags + if !action.Public { + flags = discord.MessageFlagEphemeral + } + + content, ok := executeTemplate(i, templates, action.Text) // TODO: Fix variables.FillString + if !ok { + return nil + } + + allowedMentions := []discord.AllowedMentionType{ + discord.AllowedMentionTypeUsers, + } + if action.AllowRoleMentions { + allowedMentions = append( + allowedMentions, + discord.AllowedMentionTypeRoles, + discord.AllowedMentionTypeEveryone, + ) + } + + i.Respond(discord.MessageCreate{ + Content: content, + Flags: flags, + AllowedMentions: &discord.AllowedMentions{ + Parse: allowedMentions, + }, + }) + case actions.ActionTypeToggleRole: + if !legacyPermissions { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + if !derivedPerms.CanManageRole(roleID) { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("The user that has created this message doesn't have permissions to toggle the role <@&%s>.", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + return nil + } + } + + hasRole := false + if member := interaction.Member(); member != nil { + for _, roleID := range member.RoleIDs { + if roleID.String() == action.TargetID { + hasRole = true + break + } + } + } + + var err error + if member := interaction.Member(); member != nil { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + + if hasRole { + err = rest.RemoveMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Removed role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } + } else { + err = rest.AddMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Added role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } + } + } + if err != nil { + log.Error().Err(err).Msg("Failed to toggle role") + i.Respond(discord.MessageCreate{ + Content: roleErrorMessage, + Flags: discord.MessageFlagEphemeral, + }) + } + case actions.ActionTypeAddRole: + if !legacyPermissions { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + if !derivedPerms.CanManageRole(roleID) { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("The user that has created this message doesn't have permissions to assign the role <@&%s>.", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + return nil + } + } + + if member := interaction.Member(); member != nil { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + + err = rest.AddMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Added role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } else { + log.Error().Err(err).Msg("Failed to add role") + i.Respond(discord.MessageCreate{ + Content: roleErrorMessage, + Flags: discord.MessageFlagEphemeral, + }) + } + } + case actions.ActionTypeRemoveRole: + if !legacyPermissions { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + if !derivedPerms.CanManageRole(roleID) { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("The user that has created this message doesn't have permissions to remove the role <@&%s>.", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + return nil + } + } + + if member := interaction.Member(); member != nil { + roleID, err := snowflake.Parse(action.TargetID) + if err != nil { + log.Error().Err(err).Msg("Failed to parse role ID") + return err + } + + err = rest.RemoveMemberRole(*interaction.GuildID(), member.User.ID, roleID) + if err == nil { + if !action.DisableDefaultResponse { + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Removed role <@&%s>", action.TargetID), + Flags: discord.MessageFlagEphemeral, + }) + } + } else { + log.Error().Err(err).Msg("Failed to remove role") + i.Respond(discord.MessageCreate{ + Content: roleErrorMessage, + Flags: discord.MessageFlagEphemeral, + }) + } + } + case actions.ActionTypeSavedMessageResponse: + if interaction.GuildID() == nil { + continue + } + + msg, err := m.savedMessageStore.GetSavedMessageForGuild(context.TODO(), *interaction.GuildID(), action.TargetID) + if err != nil { + return err + } + + data := &actions.MessageWithActions{} + err = json.Unmarshal(msg.Data, data) + if err != nil { + return err + } + + // TODO: Fix variables system - variables.FillMessage(data) + if !executeTemplateMessage(i, templates, data) { + return nil + } + + var flags discord.MessageFlags + if !action.Public { + flags = discord.MessageFlagEphemeral + } + + var components []discord.LayoutComponent + if !legacyPermissions { + components, err = m.parser.ParseMessageComponents(data.Components, features.ComponentTypes) + if err != nil { + return fmt.Errorf("Invalid actions: %w", err) + } + } + + allowedMentions := []discord.AllowedMentionType{ + discord.AllowedMentionTypeUsers, + } + if action.AllowRoleMentions { + allowedMentions = append( + allowedMentions, + discord.AllowedMentionTypeRoles, + discord.AllowedMentionTypeEveryone, + ) + } + + // We need to get the message id of the response, so it has to be a followup response + if !i.HasResponded() { + i.Respond(discord.MessageCreate{ + Flags: flags, + }) + } + + newMsg := i.Respond(discord.MessageCreate{ + Content: data.Content, + Embeds: data.Embeds, + Components: components, + Flags: flags, + AllowedMentions: &discord.AllowedMentions{ + Parse: allowedMentions, + }, + }) + if newMsg != nil && !legacyPermissions { + err = m.parser.CreateActionsForMessage(context.TODO(), data.Actions, *derivedPerms, newMsg.ID, !action.Public) + if err != nil { + log.Error().Err(err).Msg("failed to create actions for message") + return err + } + } + case actions.ActionTypeTextDM: + // TODO: Fix DM functionality - need to implement with disgo + i.Respond(discord.MessageCreate{ + Content: "DM functionality not yet implemented with disgo", + Flags: discord.MessageFlagEphemeral, + }) + case actions.ActionTypeSavedMessageDM: + // TODO: Fix DM functionality - need to implement with disgo + i.Respond(discord.MessageCreate{ + Content: "DM functionality not yet implemented with disgo", + Flags: discord.MessageFlagEphemeral, + }) + case actions.ActionTypeTextEdit: + content, ok := executeTemplate(i, templates, action.Text) // TODO: Fix variables system + if !ok { + return nil + } + + i.Respond(discord.MessageCreate{ + Content: content, + }) + case actions.ActionTypeSavedMessageEdit: + if interaction.Type() != discord.InteractionTypeComponent || interaction.GuildID() == nil { + continue + } + + msg, err := m.savedMessageStore.GetSavedMessageForGuild(context.TODO(), *interaction.GuildID(), action.TargetID) + if err != nil { + return err + } + + data := &actions.MessageWithActions{} + err = json.Unmarshal(msg.Data, data) + if err != nil { + return err + } + + // TODO: Fix variables system - variables.FillMessage(data) + if !executeTemplateMessage(i, templates, data) { + return nil + } + + var components []discord.LayoutComponent + if !legacyPermissions { + components, err = m.parser.ParseMessageComponents(data.Components, features.ComponentTypes) + if err != nil { + return fmt.Errorf("Invalid actions: %w", err) + } + } + + i.Respond(discord.MessageCreate{ + Content: data.Content, + Embeds: data.Embeds, + Components: components, + }) + + if !legacyPermissions { + if compInteraction, ok := interaction.(discord.ComponentInteraction); ok { + ephemeral := compInteraction.Message.Flags&discord.MessageFlagEphemeral != 0 + err = m.parser.CreateActionsForMessage(context.TODO(), data.Actions, *derivedPerms, compInteraction.Message.ID, ephemeral) + if err != nil { + log.Error().Err(err).Msg("failed to create actions for message") + return err + } + } + } + case actions.ActionTypePermissionCheck: + perms, _ := strconv.ParseInt(action.Permissions, 10, 64) + + if member := interaction.Member(); member != nil && member.Permissions&discord.Permissions(perms) != discord.Permissions(perms) { + responseText := "You don't have the required permissions to use this component or command." + if action.DisableDefaultResponse { + responseText = action.Text + } + + i.Respond(discord.MessageCreate{ + Content: responseText, + Flags: discord.MessageFlagEphemeral, + }) + return nil + } + + responseText := "You don't have the required roles to use this component or command." + if action.DisableDefaultResponse { + responseText = action.Text + } + + if len(action.RoleIDs) != 0 { + if member := interaction.Member(); member != nil { + for _, roleID := range action.RoleIDs { + roleIDSnowflake, err := snowflake.Parse(roleID) + if err != nil { + continue + } + if !slices.Contains(member.RoleIDs, roleIDSnowflake) { + i.Respond(discord.MessageCreate{ + Content: responseText, + Flags: discord.MessageFlagEphemeral, + }) + return nil + } + } + } + } + } + } + + if !i.HasResponded() { + if interaction.Type() == discord.InteractionTypeComponent { + i.Respond(discord.MessageCreate{}) + } else { + i.Respond(discord.MessageCreate{ + Content: "No response", + Flags: discord.MessageFlagEphemeral, + }) + } + } + + return nil +} + +func executeTemplate(i Interaction, templates *template.TemplateContext, text string) (string, bool) { + res, err := templates.ParseAndExecute(text) + if err != nil { + log.Error().Err(err).Msg("Failed to execute template") + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Failed to execute template variables:\n```%s```", err.Error()), + Flags: discord.MessageFlagEphemeral, + }) + return "", false + } + return res, true +} + +func executeTemplateMessage(i Interaction, templates *template.TemplateContext, m *actions.MessageWithActions) bool { + if err := templates.ParseAndExecuteMessage(m); err != nil { + log.Error().Err(err).Msg("Failed to execute template") + i.Respond(discord.MessageCreate{ + Content: fmt.Sprintf("Failed to execute template variables:\n```%s```", err.Error()), + Flags: discord.MessageFlagEphemeral, + }) + return false + } + + return true +} diff --git a/embedg-service/actions/handler/interaction.go b/embedg-service/actions/handler/interaction.go new file mode 100644 index 000000000..f53085533 --- /dev/null +++ b/embedg-service/actions/handler/interaction.go @@ -0,0 +1,114 @@ +package handler + +import ( + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/rs/zerolog/log" +) + +type Interaction interface { + Interaction() discord.Interaction + HasResponded() bool + Respond(data discord.InteractionResponseData, t ...discord.InteractionResponseType) *discord.Message +} + +type GatewayInteraction struct { + Responded bool + Rest rest.Rest + Inner discord.Interaction +} + +func (i *GatewayInteraction) Interaction() discord.Interaction { + return i.Inner +} + +func (i *GatewayInteraction) HasResponded() bool { + return i.Responded +} + +func (i *GatewayInteraction) Respond(data discord.InteractionResponseData, t ...discord.InteractionResponseType) *discord.Message { + var err error + + responseType := discord.InteractionResponseTypeCreateMessage + if len(t) > 0 { + responseType = t[0] + } + + var msg *discord.Message + + if !i.Responded { + err = i.Rest.CreateInteractionResponse( + i.Inner.ID(), + i.Inner.Token(), + discord.InteractionResponse{ + Type: responseType, + Data: data, + }, + ) + } else { + msgData, ok := data.(discord.MessageCreate) + if !ok { + err = fmt.Errorf("can't create followup message, data is not a MessageCreate") + } else { + msg, err = i.Rest.CreateFollowupMessage(i.Inner.ApplicationID(), i.Inner.Token(), msgData) + } + } + + if err != nil { + log.Error().Err(err).Msg("Failed to respond to interaction") + } else { + i.Responded = true + } + + return msg +} + +type RestInteraction struct { + Responded bool + InitialResponse chan *discord.InteractionResponse + Rest rest.Rest + Inner discord.Interaction +} + +func (i *RestInteraction) Interaction() discord.Interaction { + return i.Inner +} + +func (i *RestInteraction) HasResponded() bool { + return i.Responded +} + +func (i *RestInteraction) Respond(data discord.InteractionResponseData, t ...discord.InteractionResponseType) *discord.Message { + var err error + + responseType := discord.InteractionResponseTypeCreateMessage + if len(t) > 0 { + responseType = t[0] + } + + var msg *discord.Message + + if !i.Responded { + i.InitialResponse <- &discord.InteractionResponse{ + Type: responseType, + Data: data, + } + } else { + msgData, ok := data.(discord.MessageCreate) + if !ok { + err = fmt.Errorf("can't create followup message, data is not a MessageCreate") + } else { + msg, err = i.Rest.CreateFollowupMessage(i.Inner.ApplicationID(), i.Inner.Token(), msgData) + } + } + + if err != nil { + log.Error().Err(err).Msg("Failed to respond to interaction") + } else { + i.Responded = true + } + + return msg +} diff --git a/embedg-service/actions/parser/actions.go b/embedg-service/actions/parser/actions.go new file mode 100644 index 000000000..03eb6f993 --- /dev/null +++ b/embedg-service/actions/parser/actions.go @@ -0,0 +1,53 @@ +package parser + +import ( + "context" + "fmt" + + "github.com/merlinfuchs/embed-generator/embedg-server/util" + "github.com/merlinfuchs/embed-generator/embedg-service/actions" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/model" + "github.com/rs/zerolog/log" +) + +func (m *ActionParser) CreateActionsForMessage(ctx context.Context, actionSets map[string]actions.ActionSet, derivedPerms actions.ActionDerivedPermissions, messageID common.ID, ephemeral bool) error { + err := m.actionSetStore.DeleteMessageActionSetsForMessage(ctx, messageID) + if err != nil { + log.Error().Err(err).Msg("Failed to delete message action sets") + } + + for actionSetID, actionSet := range actionSets { + _, err = m.actionSetStore.CreateMessageActionSet(ctx, model.MessageActionSet{ + ID: util.UniqueID(), + MessageID: messageID, + SetID: actionSetID, + Actions: actionSet, + DerivedPermissions: &derivedPerms, + Ephemeral: ephemeral, + }) + if err != nil { + log.Error().Err(err).Msg("Failed to insert message action set") + } + } + return nil +} + +func (m *ActionParser) RetrieveActionsForMessage(ctx context.Context, messageID common.ID) (map[string]actions.ActionSet, error) { + rows, err := m.actionSetStore.GetMessageActionSets(ctx, messageID) + if err != nil { + return nil, fmt.Errorf("Failed to get message action sets: %w", err) + } + + res := make(map[string]actions.ActionSet, len(rows)) + + for _, row := range rows { + res[row.SetID] = row.Actions + } + + return res, nil +} + +func (m *ActionParser) DeleteActionsForMessage(ctx context.Context, messageID common.ID) error { + return m.actionSetStore.DeleteMessageActionSetsForMessage(ctx, messageID) +} diff --git a/embedg-service/actions/parser/parse.go b/embedg-service/actions/parser/parse.go new file mode 100644 index 000000000..e2fe5c568 --- /dev/null +++ b/embedg-service/actions/parser/parse.go @@ -0,0 +1,370 @@ +package parser + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-service/access" + "github.com/merlinfuchs/embed-generator/embedg-service/actions" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/store" +) + +type ActionParser struct { + accessManager *access.AccessManager + actionSetStore store.MessageActionSetStore + savedMessageStore store.SavedMessageStore + caches cache.Caches +} + +func New( + accessManager *access.AccessManager, + actionSetStore store.MessageActionSetStore, + savedMessageStore store.SavedMessageStore, + caches cache.Caches, +) *ActionParser { + return &ActionParser{ + accessManager: accessManager, + actionSetStore: actionSetStore, + savedMessageStore: savedMessageStore, + caches: caches, + } +} + +func (m *ActionParser) ParseMessageComponents(data []actions.ComponentWithActions, allowedComponentTypes []int) ([]discord.LayoutComponent, error) { + components := make([]discord.LayoutComponent, 0, len(data)) + + for _, component := range data { + parsed, err := m.ParseMessageComponent(component, allowedComponentTypes) + if err != nil { + return nil, err + } + + // Convert to LayoutComponent + if layoutComp, ok := parsed.(discord.LayoutComponent); ok { + components = append(components, layoutComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as LayoutComponent", parsed) + } + } + + return components, nil +} + +func (m *ActionParser) ParseMessageComponent(data actions.ComponentWithActions, allowedComponentTypes []int) (discord.Component, error) { + if !slices.Contains(allowedComponentTypes, int(data.Type)) { + return nil, fmt.Errorf("component type %d not allowed, you need to upgrade to a premium plan to use this component", data.Type) + } + + switch data.Type { + case discord.ComponentTypeActionRow: + ar := discord.ActionRowComponent{ + ID: data.ID, + Components: make([]discord.InteractiveComponent, 0, len(data.Components)), + } + + for _, component := range data.Components { + parsed, err := m.ParseMessageComponent(component, allowedComponentTypes) + if err != nil { + return nil, err + } + + if interactiveComp, ok := parsed.(discord.InteractiveComponent); ok { + ar.Components = append(ar.Components, interactiveComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as InteractiveComponent", parsed) + } + } + + return ar, nil + case discord.ComponentTypeButton: + if data.Style == discord.ButtonStyleLink { + return discord.ButtonComponent{ + Label: data.Label, + Style: data.Style, + Disabled: data.Disabled, + URL: data.URL, + Emoji: data.Emoji, + }, nil + } else { + return discord.ButtonComponent{ + CustomID: "action:" + data.ActionSetID, + Label: data.Label, + Style: data.Style, + Disabled: data.Disabled, + Emoji: data.Emoji, + }, nil + } + case discord.ComponentTypeStringSelectMenu: + options := make([]discord.StringSelectMenuOption, len(data.Options)) + for x, option := range data.Options { + options[x] = discord.StringSelectMenuOption{ + Label: option.Label, + Value: "action:" + option.ActionSetID, + Description: option.Description, + Default: option.Default, + Emoji: option.Emoji, + } + } + + return discord.StringSelectMenuComponent{ + CustomID: "action:options:" + common.UniqueID().String(), + Placeholder: data.Placeholder, + MinValues: data.MinValues, + MaxValues: data.MaxValues, + Options: options, + Disabled: data.Disabled, + }, nil + case discord.ComponentTypeSection: + se := discord.SectionComponent{ + Components: make([]discord.SectionSubComponent, 0, len(data.Components)), + } + + for _, component := range data.Components { + parsed, err := m.ParseMessageComponent(component, allowedComponentTypes) + if err != nil { + return nil, err + } + + if sectionSubComp, ok := parsed.(discord.SectionSubComponent); ok { + se.Components = append(se.Components, sectionSubComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as SectionSubComponent", parsed) + } + } + + if data.Accessory != nil { + parsed, err := m.ParseMessageComponent(*data.Accessory, allowedComponentTypes) + if err != nil { + return nil, err + } + if sectionAccessoryComp, ok := parsed.(discord.SectionAccessoryComponent); ok { + se.Accessory = sectionAccessoryComp + } else { + return nil, fmt.Errorf("component type %T cannot be used as SectionAccessoryComponent", parsed) + } + } + + return se, nil + case discord.ComponentTypeTextDisplay: + return discord.TextDisplayComponent{ + Content: data.Content, + }, nil + case discord.ComponentTypeThumbnail: + if data.Media == nil { + return nil, errors.New("media is required for thumbnail component") + } + + return discord.ThumbnailComponent{ + Media: discord.UnfurledMediaItem{URL: data.Media.URL}, + Description: data.Description, + Spoiler: data.Spoiler, + }, nil + case discord.ComponentTypeMediaGallery: + items := make([]discord.MediaGalleryItem, len(data.Items)) + for x, item := range data.Items { + items[x] = discord.MediaGalleryItem{ + Media: discord.UnfurledMediaItem{URL: item.Media.URL}, + Description: item.Description, + Spoiler: item.Spoiler, + } + } + + return discord.MediaGalleryComponent{ + Items: items, + }, nil + case discord.ComponentTypeFile: + if data.File == nil { + return nil, errors.New("file is required for file component") + } + + return discord.FileComponent{ + File: discord.UnfurledMediaItem{URL: data.File.URL}, + Spoiler: data.Spoiler, + }, nil + case discord.ComponentTypeSeparator: + var divider *bool + if data.Divider { + divider = &data.Divider + } + + return discord.SeparatorComponent{ + Divider: divider, + Spacing: discord.SeparatorSpacingSize(data.Spacing), + }, nil + case discord.ComponentTypeContainer: + c := discord.ContainerComponent{ + Components: make([]discord.ContainerSubComponent, 0, len(data.Components)), + AccentColor: data.AccentColor, + Spoiler: data.Spoiler, + } + + for _, component := range data.Components { + parsed, err := m.ParseMessageComponent(component, allowedComponentTypes) + if err != nil { + return nil, err + } + + if containerSubComp, ok := parsed.(discord.ContainerSubComponent); ok { + c.Components = append(c.Components, containerSubComp) + } else { + return nil, fmt.Errorf("component type %T cannot be used as ContainerSubComponent", parsed) + } + } + + return c, nil + default: + return nil, errors.New("invalid component type") + } +} + +func (m *ActionParser) UnparseMessageComponents(data []discord.LayoutComponent) ([]actions.ComponentWithActions, error) { + res := make([]actions.ComponentWithActions, 0, len(data)) + + for _, comp := range data { + parsed, err := m.UnparseMessageComponent(comp) + if err != nil { + return nil, err + } + res = append(res, parsed) + } + + return res, nil +} + +func (m *ActionParser) UnparseMessageComponent(data discord.Component) (actions.ComponentWithActions, error) { + switch c := data.(type) { + case discord.ActionRowComponent: + ar := actions.ComponentWithActions{ + Type: discord.ComponentTypeActionRow, + Components: make([]actions.ComponentWithActions, 0, len(c.Components)), + } + + for _, comp := range c.Components { + parsed, err := m.UnparseMessageComponent(comp) + if err != nil { + return actions.ComponentWithActions{}, err + } + ar.Components = append(ar.Components, parsed) + } + + return ar, nil + case discord.ButtonComponent: + return actions.ComponentWithActions{ + Type: discord.ComponentTypeButton, + Disabled: c.Disabled, + Style: c.Style, + Label: c.Label, + Emoji: c.Emoji, + URL: c.URL, + ActionSetID: strings.TrimPrefix(c.CustomID, "action:"), + }, nil + case discord.StringSelectMenuComponent: + options := make([]actions.ComponentSelectOptionWithActions, 0, len(c.Options)) + for _, option := range c.Options { + options = append(options, actions.ComponentSelectOptionWithActions{ + Label: option.Label, + Description: option.Description, + Emoji: option.Emoji, + Default: option.Default, + ActionSetID: strings.TrimPrefix(option.Value, "action:"), + }) + } + + return actions.ComponentWithActions{ + Type: discord.ComponentTypeStringSelectMenu, + Disabled: c.Disabled, + Placeholder: c.Placeholder, + MinValues: c.MinValues, + MaxValues: c.MaxValues, + Options: options, + }, nil + case discord.SectionComponent: + se := actions.ComponentWithActions{ + Type: discord.ComponentTypeSection, + Components: make([]actions.ComponentWithActions, 0, len(c.Components)), + } + + for _, comp := range c.Components { + parsed, err := m.UnparseMessageComponent(comp) + if err != nil { + return actions.ComponentWithActions{}, err + } + se.Components = append(se.Components, parsed) + } + + if c.Accessory != nil { + parsed, err := m.UnparseMessageComponent(c.Accessory) + if err != nil { + return actions.ComponentWithActions{}, err + } + se.Accessory = &parsed + } + + return se, nil + case discord.TextDisplayComponent: + return actions.ComponentWithActions{ + Type: discord.ComponentTypeTextDisplay, + Content: c.Content, + }, nil + case discord.ThumbnailComponent: + return actions.ComponentWithActions{ + Type: discord.ComponentTypeThumbnail, + Media: &actions.UnfurledMediaItem{URL: c.Media.URL}, + Description: c.Description, + }, nil + case discord.MediaGalleryComponent: + items := make([]actions.ComponentMediaGalleryItem, 0, len(c.Items)) + for _, item := range c.Items { + items = append(items, actions.ComponentMediaGalleryItem{ + Media: actions.UnfurledMediaItem{URL: item.Media.URL}, + Description: item.Description, + Spoiler: item.Spoiler, + }) + } + + return actions.ComponentWithActions{ + Type: discord.ComponentTypeMediaGallery, + Items: items, + }, nil + case discord.FileComponent: + return actions.ComponentWithActions{ + Type: discord.ComponentTypeFile, + File: &actions.UnfurledMediaItem{URL: c.File.URL}, + Spoiler: c.Spoiler, + }, nil + case discord.SeparatorComponent: + var divider bool + if c.Divider != nil { + divider = *c.Divider + } + + return actions.ComponentWithActions{ + Type: discord.ComponentTypeSeparator, + Divider: divider, + Spacing: int(c.Spacing), + }, nil + case discord.ContainerComponent: + components := make([]actions.ComponentWithActions, 0, len(c.Components)) + for _, comp := range c.Components { + parsed, err := m.UnparseMessageComponent(comp) + if err != nil { + return actions.ComponentWithActions{}, err + } + components = append(components, parsed) + } + + return actions.ComponentWithActions{ + Type: discord.ComponentTypeContainer, + Components: components, + AccentColor: c.AccentColor, + Spoiler: c.Spoiler, + }, nil + default: + return actions.ComponentWithActions{}, fmt.Errorf("invalid component type: %T", c) + } +} diff --git a/embedg-service/actions/parser/permissions.go b/embedg-service/actions/parser/permissions.go new file mode 100644 index 000000000..6733d4b89 --- /dev/null +++ b/embedg-service/actions/parser/permissions.go @@ -0,0 +1,190 @@ +package parser + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-service/access" + "github.com/merlinfuchs/embed-generator/embedg-service/actions" + "github.com/merlinfuchs/embed-generator/embedg-service/common" + "github.com/merlinfuchs/embed-generator/embedg-service/store" +) + +func (m *ActionParser) CheckPermissionsForActionSets(actionSets map[string]actions.ActionSet, userID common.ID, guildID common.ID, channelID common.ID) error { + if channelID != 0 { + channel, ok := m.caches.Channel(channelID) + if !ok { + return fmt.Errorf("channel not found in cache") + } + + if channel.GuildID() != guildID { + return fmt.Errorf("Channel %s does not belong to guild %s", channelID, guildID) + } + } + + guild, ok := m.caches.Guild(guildID) + if !ok { + return fmt.Errorf("guild not found in cache") + } + + var channelAccess *access.ChannelAccess + if channelID != 0 { + ca, err := m.accessManager.GetChannelAccessForUser(userID, channelID) + if err != nil { + return err + } + channelAccess = &ca + + if !channelAccess.UserAccess() { + return fmt.Errorf("You have no access to the channel %s", channelID) + } + } + + member, err := m.accessManager.GetGuildMember(guildID, userID) + if err != nil { + return err + } + + memberIsOwner := guild.OwnerID == userID + + highestRolePosition := 0 + var permissions discord.Permissions + + defaultRole, ok := m.caches.Role(guildID, guildID) + if ok { + highestRolePosition = defaultRole.Position + permissions = defaultRole.Permissions + } + + for _, roleID := range member.RoleIDs { + role, ok := m.caches.Role(guildID, roleID) + if ok && role.Position > highestRolePosition { + highestRolePosition = role.Position + permissions |= role.Permissions + } + } + + if channelAccess != nil { + permissions = channelAccess.UserPermissions + } + + var checkActions func(actionSets map[string]actions.ActionSet, nestingLevel int) error + + checkActions = func(actionSets map[string]actions.ActionSet, nestingLevel int) error { + if nestingLevel > 5 { + return fmt.Errorf("You can't nest more than 5 saved messages with actions") + } + + for _, actionSet := range actionSets { + for _, action := range actionSet.Actions { + switch action.Type { + case actions.ActionTypeTextResponse, actions.ActionTypeTextDM, actions.ActionTypeTextEdit: + break + case actions.ActionTypeAddRole, actions.ActionTypeRemoveRole, actions.ActionTypeToggleRole: + if permissions&discord.PermissionManageRoles == 0 { + return fmt.Errorf("You have no permission to manage roles in the channel %s", channelID) + } + + roleID, err := common.ParseID(action.TargetID) + if err != nil { + return fmt.Errorf("Invalid role ID: %s", action.TargetID) + } + + role, ok := m.caches.Role(guildID, roleID) + if !ok { + return fmt.Errorf("Role %s does not exist", action.TargetID) + } + + if !memberIsOwner && role.Position >= highestRolePosition { + return fmt.Errorf("You can not assign the role %s", action.TargetID) + } + break + case actions.ActionTypeSavedMessageResponse, actions.ActionTypeSavedMessageDM, actions.ActionTypeSavedMessageEdit: + msg, err := m.savedMessageStore.GetSavedMessageForGuild(context.TODO(), guildID, action.TargetID) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return fmt.Errorf("Saved message %s does not exist or belongs to a different server", action.TargetID) + } + return err + } + + data := &actions.MessageWithActions{} + err = json.Unmarshal(msg.Data, data) + if err != nil { + return err + } + + return checkActions(data.Actions, nestingLevel+1) + } + } + } + + return nil + } + + return checkActions(actionSets, 0) +} + +func (m *ActionParser) DerivePermissionsForActions(userID common.ID, guildID common.ID, channelID common.ID) (actions.ActionDerivedPermissions, error) { + res := actions.ActionDerivedPermissions{ + UserID: userID, + } + + if channelID != 0 { + channel, ok := m.caches.Channel(channelID) + if !ok { + return res, fmt.Errorf("channel not found in cache") + } + + if channel.GuildID() != guildID { + return res, fmt.Errorf("Channel %s does not belong to guild %s", channelID, guildID) + } + } + + guild, ok := m.caches.Guild(guildID) + if !ok { + return res, fmt.Errorf("guild not found in cache") + } + + res.GuildIsOwner = guild.OwnerID == userID + + if channelID != 0 { + ca, err := m.accessManager.GetChannelAccessForUser(userID, channelID) + if err != nil { + return res, err + } + res.ChannelPermissions = ca.UserPermissions + } + + member, err := m.accessManager.GetGuildMember(guildID, userID) + if err != nil { + return res, err + } + + highestRolePosition := 0 + + defaultRole, ok := m.caches.Role(guildID, guildID) + if ok { + highestRolePosition = defaultRole.Position + res.GuildPermissions = defaultRole.Permissions + } + + for _, roleID := range member.RoleIDs { + role, ok := m.caches.Role(guildID, roleID) + if ok && role.Position > highestRolePosition { + highestRolePosition = role.Position + res.GuildPermissions |= role.Permissions + } + } + + for role := range m.caches.Roles(guildID) { + if role.Position < highestRolePosition { + res.AllowedRoleIDs = append(res.AllowedRoleIDs, role.ID) + } + } + + return res, nil +} diff --git a/embedg-service/actions/template/base.go b/embedg-service/actions/template/base.go new file mode 100644 index 000000000..a5e8a9dfd --- /dev/null +++ b/embedg-service/actions/template/base.go @@ -0,0 +1,267 @@ +package template + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + + "errors" + + "github.com/botlabs-gg/yagpdb/v2/lib/template" +) + +func isContainer(v interface{}) bool { + rv, _ := indirect(reflect.ValueOf(v)) + switch rv.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + return true + default: + return false + } +} + +// Cyclic value detection is modified from encoding/json/encode.go. +const startDetectingCyclesAfter = 250 + +type cyclicValueDetector struct { + ptrLevel uint + ptrSeen map[interface{}]struct{} +} + +func (c *cyclicValueDetector) Check(v reflect.Value) error { + v, _ = indirect(v) + switch v.Kind() { + case reflect.Map: + if c.ptrLevel++; c.ptrLevel > startDetectingCyclesAfter { + ptr := v.Pointer() + if _, ok := c.ptrSeen[ptr]; ok { + return fmt.Errorf("encountered a cycle via %s", v.Type()) + } + c.ptrSeen[ptr] = struct{}{} + } + + it := v.MapRange() + for it.Next() { + if err := c.Check(it.Value()); err != nil { + return err + } + } + c.ptrLevel-- + return nil + case reflect.Array, reflect.Slice: + if c.ptrLevel++; c.ptrLevel > startDetectingCyclesAfter { + ptr := struct { + ptr uintptr + len int + }{v.Pointer(), v.Len()} + if _, ok := c.ptrSeen[ptr]; ok { + return fmt.Errorf("encountered a cycle via %s", v.Type()) + } + c.ptrSeen[ptr] = struct{}{} + } + + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + if err := c.Check(elem); err != nil { + return err + } + } + c.ptrLevel-- + return nil + default: + return nil + } +} + +func detectCyclicValue(v interface{}) error { + c := &cyclicValueDetector{ptrSeen: make(map[interface{}]struct{})} + return c.Check(reflect.ValueOf(v)) +} + +type Dict map[interface{}]interface{} + +func (d Dict) Set(key interface{}, value interface{}) (string, error) { + d[key] = value + if isContainer(value) { + if err := detectCyclicValue(d); err != nil { + return "", template.UncatchableError(err) + } + } + return "", nil +} + +func (d Dict) Get(key interface{}) interface{} { + out, ok := d[key] + if !ok { + switch key.(type) { + case int: + out = d[ToInt64(key)] + case int64: + out = d[tmplToInt(key)] + } + } + return out +} + +func (d Dict) Del(key interface{}) string { + delete(d, key) + return "" +} + +func (d Dict) HasKey(k interface{}) (ok bool) { + _, ok = d[k] + return +} + +func (d Dict) MarshalJSON() ([]byte, error) { + md := make(map[string]interface{}) + for k, v := range d { + krv := reflect.ValueOf(k) + switch krv.Kind() { + case reflect.String: + md[krv.String()] = v + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + md[strconv.FormatInt(krv.Int(), 10)] = v + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + md[strconv.FormatUint(krv.Uint(), 10)] = v + default: + return nil, fmt.Errorf("cannot encode dict with key type %s; only string and integer keys are supported", krv.Type()) + } + } + return json.Marshal(md) +} + +type SDict map[string]interface{} + +func (d SDict) Set(key string, value interface{}) (string, error) { + d[key] = value + if isContainer(value) { + if err := detectCyclicValue(d); err != nil { + return "", template.UncatchableError(err) + } + } + return "", nil +} + +func (d SDict) Get(key string) interface{} { + return d[key] +} + +func (d SDict) Del(key string) string { + delete(d, key) + return "" +} + +func (d SDict) HasKey(k string) (ok bool) { + _, ok = d[k] + return +} + +type Slice []interface{} + +func (s Slice) Append(item interface{}) (interface{}, error) { + if len(s)+1 > 10000 { + return nil, errors.New("resulting slice exceeds slice size limit") + } + + switch v := item.(type) { + case nil: + result := reflect.Append(reflect.ValueOf(&s).Elem(), reflect.Zero(reflect.TypeOf((*interface{})(nil)).Elem())) + return result.Interface(), nil + default: + result := reflect.Append(reflect.ValueOf(&s).Elem(), reflect.ValueOf(v)) + return result.Interface(), nil + } +} + +func (s Slice) Set(index int, item interface{}) (string, error) { + if index >= len(s) { + return "", errors.New("Index out of bounds") + } + + s[index] = item + if isContainer(item) { + if err := detectCyclicValue(s); err != nil { + return "", template.UncatchableError(err) + } + } + return "", nil +} + +func (s Slice) AppendSlice(slice interface{}) (interface{}, error) { + val, _ := indirect(reflect.ValueOf(slice)) + switch val.Kind() { + case reflect.Slice, reflect.Array: + // this is valid + + default: + return nil, errors.New("value passed is not an array or slice") + } + + if len(s)+val.Len() > 10000 { + return nil, errors.New("resulting slice exceeds slice size limit") + } + + result := reflect.ValueOf(&s).Elem() + for i := 0; i < val.Len(); i++ { + switch v := val.Index(i).Interface().(type) { + case nil: + result = reflect.Append(result, reflect.Zero(reflect.TypeOf((*interface{})(nil)).Elem())) + + default: + result = reflect.Append(result, reflect.ValueOf(v)) + } + } + + return result.Interface(), nil +} + +func (s Slice) StringSlice(flag ...bool) interface{} { + strict := false + if len(flag) > 0 { + strict = flag[0] + } + + StringSlice := make([]string, 0, len(s)) + + for _, Sliceval := range s { + switch t := Sliceval.(type) { + case string: + StringSlice = append(StringSlice, t) + + case fmt.Stringer: + if strict { + return nil + } + StringSlice = append(StringSlice, t.String()) + + default: + if strict { + return nil + } + } + } + + return StringSlice +} + +func withOutputLimit(f func(...interface{}) string, limit int) func(...interface{}) (string, error) { + return func(args ...interface{}) (string, error) { + out := f(args...) + if len(out) > limit { + return "", fmt.Errorf("string grew too long: length %d (max %d)", len(out), limit) + } + return out, nil + } +} + +func withOutputLimitF(f func(string, ...interface{}) string, limit int) func(string, ...interface{}) (string, error) { + return func(format string, args ...interface{}) (string, error) { + out := f(format, args...) + if len(out) > limit { + return "", fmt.Errorf("string grew too long: length %d (max %d)", len(out), limit) + } + return out, nil + } +} diff --git a/embedg-service/actions/template/context.go b/embedg-service/actions/template/context.go new file mode 100644 index 000000000..01bb8a718 --- /dev/null +++ b/embedg-service/actions/template/context.go @@ -0,0 +1,212 @@ +package template + +import ( + "bytes" + "fmt" + "io" + "maps" + "strings" + + "github.com/botlabs-gg/yagpdb/v2/lib/template" + "github.com/merlinfuchs/embed-generator/embedg-service/actions" +) + +const DefaultMaxOps = 10000 +const DefaultMaxOutput = 4000 + +const DelimLeft = "{{" +const DelimRight = "}}" + +type TemplateContext struct { + name string + data map[string]interface{} + funcs map[string]interface{} + + MaxOps int + MaxOutput int64 +} + +func NewContext(name string, maxOps int, providers ...ContextProvider) *TemplateContext { + data := make(map[string]interface{}, len(standardDataMap)) + maps.Copy(data, standardDataMap) + + funcs := make(map[string]interface{}, len(standardFuncMap)) + maps.Copy(funcs, standardFuncMap) + + for _, provider := range providers { + provider.ProvideData(data) + provider.ProvideFuncs(funcs) + } + + if maxOps == 0 { + maxOps = DefaultMaxOps + } + + return &TemplateContext{ + name: name, + data: data, + funcs: funcs, + + MaxOps: maxOps, + MaxOutput: DefaultMaxOutput, + } +} + +func (c *TemplateContext) ParseAndExecuteMessage(m *actions.MessageWithActions) error { + var err error + + m.Content, err = c.ParseAndExecute(m.Content) + if err != nil { + return err + } + m.Username, err = c.ParseAndExecute(m.Username) + if err != nil { + return err + } + m.AvatarURL, err = c.ParseAndExecute(m.AvatarURL) + if err != nil { + return err + } + + for _, embed := range m.Embeds { + embed.Title, err = c.ParseAndExecute(embed.Title) + if err != nil { + return err + } + embed.Description, err = c.ParseAndExecute(embed.Description) + if err != nil { + return err + } + embed.URL, err = c.ParseAndExecute(embed.URL) + if err != nil { + return err + } + + if embed.Author != nil { + embed.Author.Name, err = c.ParseAndExecute(embed.Author.Name) + if err != nil { + return err + } + embed.Author.URL, err = c.ParseAndExecute(embed.Author.URL) + if err != nil { + return err + } + embed.Author.IconURL, err = c.ParseAndExecute(embed.Author.IconURL) + if err != nil { + return err + } + } + + if embed.Footer != nil { + embed.Footer.Text, err = c.ParseAndExecute(embed.Footer.Text) + if err != nil { + return err + } + embed.Footer.IconURL, err = c.ParseAndExecute(embed.Footer.IconURL) + if err != nil { + return err + } + } + + if embed.Image != nil { + embed.Image.URL, err = c.ParseAndExecute(embed.Image.URL) + if err != nil { + return err + } + } + + if embed.Thumbnail != nil { + embed.Thumbnail.URL, err = c.ParseAndExecute(embed.Thumbnail.URL) + if err != nil { + return err + } + } + + for _, field := range embed.Fields { + field.Name, err = c.ParseAndExecute(field.Name) + if err != nil { + return err + } + field.Value, err = c.ParseAndExecute(field.Value) + if err != nil { + return err + } + } + } + + for _, row := range m.Components { + for _, component := range row.Components { + component.Label, err = c.ParseAndExecute(component.Label) + if err != nil { + return err + } + + component.URL, err = c.ParseAndExecute(component.URL) + if err != nil { + return err + } + + component.Placeholder, err = c.ParseAndExecute(component.Placeholder) + if err != nil { + return err + } + + for _, option := range component.Options { + option.Label, err = c.ParseAndExecute(option.Label) + if err != nil { + return err + } + + option.Description, err = c.ParseAndExecute(option.Description) + if err != nil { + return err + } + } + } + } + + return nil +} + +func (c *TemplateContext) ParseAndExecute(text string) (string, error) { + if text == "" || !strings.Contains(text, DelimLeft) { + return text, nil + } + + tmpl, err := c.Parse(text) + if err != nil { + return "", err + } + + return c.Execute(tmpl) +} + +func (c *TemplateContext) Parse(text string) (*template.Template, error) { + return template.New(c.name). + Delims(DelimLeft, DelimRight). + Funcs(c.funcs). + Parse(text) +} + +func (c *TemplateContext) Execute(tmpl *template.Template) (string, error) { + tmpl = tmpl.MaxOps(c.MaxOps) + + var buf bytes.Buffer + w := LimitWriter(&buf, c.MaxOutput) + + err := tmpl.Execute(w, c.data) + if err != nil { + if err == io.ErrShortWrite { + err = fmt.Errorf("output exceeded %d characters", c.MaxOutput) + } + return "", err + } + + res := buf.String() + + return res, nil +} + +func (c *TemplateContext) Set(key string, value interface{}) { + c.data[key] = value +} diff --git a/embedg-service/actions/template/data.go b/embedg-service/actions/template/data.go new file mode 100644 index 000000000..cbab19f04 --- /dev/null +++ b/embedg-service/actions/template/data.go @@ -0,0 +1,544 @@ +package template + +import ( + "fmt" + "time" + + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/discord" + "github.com/merlinfuchs/embed-generator/embedg-service/common" +) + +var standardDataMap = map[string]interface{}{} + +type InteractionData struct { + caches cache.Caches + i discord.Interaction +} + +func NewInteractionData(caches cache.Caches, i discord.Interaction) *InteractionData { + return &InteractionData{ + caches: caches, + i: i, + } +} + +func (d *InteractionData) User() interface{} { + if d.i.Member() != nil { + res := NewMemberData(d.caches, *d.i.GuildID(), d.i.Member().Member) + return &res + } + + return NewUserData(d.i.User()) +} + +func (d *InteractionData) Member() *MemberData { + if d.i.Member() == nil { + return nil + } + + return NewMemberData(d.caches, *d.i.GuildID(), d.i.Member().Member) +} + +func (d *InteractionData) Command() *CommandData { + if d.i.Type() != discord.InteractionTypeApplicationCommand { + return nil + } + + cmdInteraction, ok := d.i.(discord.ApplicationCommandInteraction) + if !ok { + return nil + } + + return NewCommandData(d.caches, *d.i.GuildID(), cmdInteraction.Data) +} + +type UserData struct { + u discord.User +} + +func NewUserData(u discord.User) *UserData { + return &UserData{u: u} +} + +func (d *UserData) String() string { + return d.Mention() +} + +func (d *UserData) ID() string { + return d.u.ID.String() +} + +func (d *UserData) Name() string { + if d.u.GlobalName != nil { + return *d.u.GlobalName + } + + return d.u.Username +} + +func (d *UserData) Username() string { + return d.u.Username +} + +func (d *UserData) GlobalName() string { + if d.u.GlobalName != nil { + return *d.u.GlobalName + } + + return "" +} + +func (d *UserData) Discriminator() string { + return d.u.Discriminator +} + +func (d *UserData) Avatar() string { + if d.u.Avatar != nil { + return *d.u.Avatar + } + + return "" +} + +func (d *UserData) Banner() string { + if d.u.Banner != nil { + return *d.u.Banner + } + + return "" +} + +func (d *UserData) Mention() string { + return d.u.Mention() +} + +func (d *UserData) AvatarURL() string { + avatarURL := d.u.AvatarURL(discord.WithSize(512)) + if avatarURL == nil { + return "" + } + + return *avatarURL +} + +func (d *UserData) BannerURL() string { + bannerURL := d.u.BannerURL(discord.WithSize(1024)) + if bannerURL == nil { + return "" + } + + return *bannerURL +} + +type MemberData struct { + UserData + caches cache.Caches + guildID common.ID + m discord.Member +} + +func NewMemberData(caches cache.Caches, guildID common.ID, m discord.Member) *MemberData { + return &MemberData{ + UserData: UserData{m.User}, + caches: caches, + guildID: guildID, + m: m, + } +} + +func (d *MemberData) Nick() string { + if d.m.Nick != nil { + return *d.m.Nick + } + + return "" +} + +func (d *MemberData) Roles() []*RoleData { + res := make([]*RoleData, len(d.m.RoleIDs)) + for i, roleID := range d.m.RoleIDs { + res[i] = NewRoleData(d.caches, d.guildID, roleID, nil) + } + + return res +} + +func (d *MemberData) JoinedAt() time.Time { + if d.m.JoinedAt != nil { + return *d.m.JoinedAt + } + + return time.Time{} +} + +func (d *MemberData) Name() string { + if d.m.Nick != nil { + return *d.m.Nick + } + + return d.UserData.Name() +} + +func (d *MemberData) Avatar() string { + if d.m.Avatar != nil { + return *d.m.Avatar + } + + return d.UserData.Avatar() +} + +func (d *MemberData) AvatarURL() string { + return d.m.EffectiveAvatarURL(discord.WithSize(512)) +} + +type CommandData struct { + caches cache.Caches + guildID common.ID + c discord.ApplicationCommandInteractionData +} + +func NewCommandData(caches cache.Caches, guildID common.ID, c discord.ApplicationCommandInteractionData) *CommandData { + return &CommandData{ + caches: caches, + guildID: guildID, + c: c, + } +} + +func (d *CommandData) String() string { + return d.Mention() +} + +func (d *CommandData) ID() string { + return d.c.CommandID().String() +} + +func (d *CommandData) Name() string { + return d.c.CommandName() +} + +func (d *CommandData) Mention() string { + return fmt.Sprintf("", d.c.CommandName(), d.c.CommandID().String()) +} + +func (d *CommandData) Options() map[string]interface{} { + res := make(map[string]interface{}) + + if slashCMD, ok := d.c.(discord.SlashCommandInteractionData); ok { + for _, opt := range slashCMD.Options { + res[opt.Name] = NewCommandOptionData(d.caches, d.guildID, slashCMD, opt) + } + } + + return res +} + +func (d *CommandData) Args() map[string]interface{} { + return d.Options() +} + +func NewCommandOptionData(caches cache.Caches, guildID common.ID, c discord.SlashCommandInteractionData, o discord.SlashCommandOption) interface{} { + switch o.Type { + case discord.ApplicationCommandOptionTypeString: + return o.String() + case discord.ApplicationCommandOptionTypeInt: + return o.Int() + case discord.ApplicationCommandOptionTypeBool: + return o.Bool() + case discord.ApplicationCommandOptionTypeUser: + userID := o.Snowflake() + resolved, ok := c.Resolved.Users[userID] + if ok { + return UserData{resolved} + } + return UserData{u: discord.User{ID: userID}} + case discord.ApplicationCommandOptionTypeChannel: + channelID := o.Snowflake() + return NewChannelData(caches, channelID, nil) + case discord.ApplicationCommandOptionTypeRole: + roleID := o.Snowflake() + resolved, ok := c.Resolved.Roles[roleID] + if ok { + return NewRoleData(caches, guildID, roleID, &resolved) + } + return NewRoleData(caches, guildID, roleID, nil) + case discord.ApplicationCommandOptionTypeFloat: + return o.Float() + case discord.ApplicationCommandOptionTypeAttachment: + attachmentID := o.Snowflake() + resolved, ok := c.Resolved.Attachments[attachmentID] + if ok { + return NewAttachmentData(resolved) + } + return NewAttachmentData(discord.Attachment{ID: attachmentID}) + } + + return nil +} + +type GuildData struct { + caches cache.Caches + guildID common.ID + guild *discord.Guild +} + +func NewGuildData(caches cache.Caches, guildID common.ID, g *discord.Guild) *GuildData { + return &GuildData{ + caches: caches, + guildID: guildID, + guild: g, + } +} + +func (d *GuildData) ensureGuild() error { + if d.guild != nil { + return nil + } + + guild, ok := d.caches.Guild(d.guildID) + if !ok { + return fmt.Errorf("guild not found in cache") + } + + d.guild = &guild + return nil +} + +func (d *GuildData) String() string { + if err := d.ensureGuild(); err != nil { + return d.guildID.String() + } + return d.guild.Name +} + +func (d *GuildData) ID() string { + return d.guildID.String() +} + +func (d *GuildData) Name() (string, error) { + if err := d.ensureGuild(); err != nil { + return "", err + } + + return d.guild.Name, nil +} + +func (d *GuildData) Description() (string, error) { + if err := d.ensureGuild(); err != nil { + return "", err + } + + if d.guild.Description != nil { + return *d.guild.Description, nil + } + + return "", nil +} + +func (d *GuildData) Icon() (string, error) { + if err := d.ensureGuild(); err != nil { + return "", err + } + + if d.guild.Icon != nil { + return *d.guild.Icon, nil + } + + return "", nil +} + +func (d *GuildData) IconURL() (string, error) { + if err := d.ensureGuild(); err != nil { + return "", err + } + + iconURL := d.guild.IconURL(discord.WithSize(512)) + if iconURL == nil { + return "", nil + } + + return *iconURL, nil +} + +func (d *GuildData) Banner() (string, error) { + if err := d.ensureGuild(); err != nil { + return "", err + } + + if d.guild.Banner != nil { + return *d.guild.Banner, nil + } + + return "", nil +} + +func (d *GuildData) BannerURL() (string, error) { + if err := d.ensureGuild(); err != nil { + return "", err + } + + bannerURL := d.guild.BannerURL(discord.WithSize(1024)) + if bannerURL == nil { + return "", nil + } + + return *bannerURL, nil +} + +func (d *GuildData) MemberCount() (int, error) { + if err := d.ensureGuild(); err != nil { + return 0, err + } + + return d.guild.MemberCount, nil +} + +func (d *GuildData) BoostCount() (int, error) { + if err := d.ensureGuild(); err != nil { + return 0, err + } + + return d.guild.PremiumSubscriptionCount, nil +} + +func (d *GuildData) BoostLevel() (int, error) { + if err := d.ensureGuild(); err != nil { + return 0, err + } + + return int(d.guild.PremiumTier), nil +} + +type ChannelData struct { + caches cache.Caches + channelID common.ID + channel discord.GuildChannel +} + +func NewChannelData(caches cache.Caches, channelID common.ID, c discord.GuildChannel) *ChannelData { + return &ChannelData{ + caches: caches, + channelID: channelID, + channel: c, + } +} + +func (d *ChannelData) ensureChannel() error { + if d.channel != nil { + return nil + } + + channel, ok := d.caches.Channel(d.channelID) + if !ok { + return fmt.Errorf("channel not found in cache") + } + + d.channel = channel + return nil +} + +func (d *ChannelData) String() string { + return d.Mention() +} + +func (d *ChannelData) ID() string { + return d.channelID.String() +} + +func (d *ChannelData) Name() (string, error) { + if err := d.ensureChannel(); err != nil { + return "", err + } + + return d.channel.Name(), nil +} + +func (d *ChannelData) Mention() string { + return fmt.Sprintf("<#%s>", d.channelID) +} + +func (d *ChannelData) Topic() (string, error) { + if err := d.ensureChannel(); err != nil { + return "", err + } + + if text, ok := d.channel.(discord.GuildTextChannel); ok { + topic := text.Topic() + if topic != nil { + return *topic, nil + } + } + + return "", nil +} + +type RoleData struct { + caches cache.Caches + guildID common.ID + roleID common.ID + role *discord.Role +} + +func NewRoleData(caches cache.Caches, guildID common.ID, roleID common.ID, role *discord.Role) *RoleData { + return &RoleData{ + caches: caches, + guildID: guildID, + roleID: roleID, + role: role, + } +} + +func (d *RoleData) ensureRole() error { + if d.role != nil { + return nil + } + + role, ok := d.caches.Role(d.guildID, d.roleID) + if !ok { + return fmt.Errorf("role not found in cache") + } + + d.role = &role + return nil +} + +func (d *RoleData) String() string { + return d.Mention() +} + +func (d *RoleData) ID() string { + return d.roleID.String() +} + +func (d *RoleData) Mention() string { + return fmt.Sprintf("<@&%s>", d.roleID.String()) +} + +func (d *RoleData) Name() (string, error) { + if err := d.ensureRole(); err != nil { + return "", err + } + + return d.role.Name, nil +} + +type AttachmentData struct { + a discord.Attachment +} + +func NewAttachmentData(a discord.Attachment) *AttachmentData { + return &AttachmentData{a: a} +} + +func (d *AttachmentData) String() string { + return d.URL() +} + +func (d *AttachmentData) ID() string { + return d.a.ID.String() +} + +func (d *AttachmentData) URL() string { + return d.a.URL +} diff --git a/embedg-service/actions/template/func.go b/embedg-service/actions/template/func.go new file mode 100644 index 000000000..4de451573 --- /dev/null +++ b/embedg-service/actions/template/func.go @@ -0,0 +1,1202 @@ +package template + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "math/rand" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +var standardFuncMap = map[string]interface{}{ + // conversion functions + "toString": ToString, + "toInt": tmplToInt, + "toInt64": ToInt64, + "toFloat": ToFloat64, + "toRune": ToRune, + "toByte": ToByte, + + // string manipulation + "hasPrefix": strings.HasPrefix, + "hasSuffix": strings.HasSuffix, + "joinStr": joinStrings, + "lower": strings.ToLower, + "slice": slice, + "split": strings.Split, + "title": strings.Title, + "trimSpace": strings.TrimSpace, + "upper": strings.ToUpper, + "urlescape": url.PathEscape, + "urlunescape": url.PathUnescape, + + // regexp + "reQuoteMeta": regexp.QuoteMeta, + + // math + "add": add, + "cbrt": tmplCbrt, + "div": tmplDiv, + "fdiv": tmplFDiv, + "log": tmplLog, + "mathConst": tmplMathConstant, + "max": tmplMax, + "min": tmplMin, + "mod": tmplMod, + "mult": tmplMult, + "pow": tmplPow, + "round": tmplRound, + "roundCeil": tmplRoundCeil, + "roundEven": tmplRoundEven, + "roundFloor": tmplRoundFloor, + "sqrt": tmplSqrt, + "sub": tmplSub, + + // bitwise ops + "bitwiseAnd": tmplBitwiseAnd, + "bitwiseOr": tmplBitwiseOr, + "bitwiseXor": tmplBitwiseXor, + "bitwiseNot": tmplBitwiseNot, + "bitwiseAndNot": tmplBitwiseAndNot, + "bitwiseLeftShift": tmplBitwiseLeftShift, + "bitwiseRightShift": tmplBitwiseRightShift, + + // misc + "humanizeThousands": tmplHumanizeThousands, + "dict": Dictionary, + "sdict": StringKeyDictionary, + "structToSdict": StructToSdict, + "cslice": CreateSlice, + "kindOf": KindOf, + + "in": in, + "inFold": inFold, + "json": tmplJson, + "jsonToSdict": tmplJSONToSDict, + "randInt": randInt, + "seq": sequence, + + "shuffle": shuffle, + + // time functions + "currentTime": tmplCurrentTime, + "parseTime": tmplParseTime, + "formatTime": tmplFormatTime, + "loadLocation": time.LoadLocation, + "newDate": tmplNewDate, + "timestampToTime": tmplTimestampToTime, + "weekNumber": tmplWeekNumber, +} + +// dictionary creates a map[string]interface{} from the given parameters by +// walking the parameters and treating them as key-value pairs. The number +// of parameters must be even. +func Dictionary(values ...interface{}) (Dict, error) { + if len(values) == 1 { + val, isNil := indirect(reflect.ValueOf(values[0])) + if isNil || values[0] == nil { + return nil, errors.New("dict: nil value passed") + } + + if Dict, ok := val.Interface().(Dict); ok { + return Dict, nil + } + + switch val.Kind() { + case reflect.Map: + iter := val.MapRange() + mapCopy := make(map[interface{}]interface{}) + for iter.Next() { + mapCopy[iter.Key().Interface()] = iter.Value().Interface() + } + return Dict(mapCopy), nil + default: + return nil, errors.New("cannot convert data of type: " + reflect.TypeOf(values[0]).String()) + } + + } + + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + + dict := make(map[interface{}]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key := values[i] + dict[key] = values[i+1] + } + + return Dict(dict), nil +} + +func StringKeyDictionary(values ...interface{}) (SDict, error) { + + if len(values) == 1 { + val, isNil := indirect(reflect.ValueOf(values[0])) + if isNil || values[0] == nil { + return nil, errors.New("Sdict: nil value passed") + } + + if sdict, ok := val.Interface().(SDict); ok { + return sdict, nil + } + + switch val.Kind() { + case reflect.Map: + iter := val.MapRange() + mapCopy := make(map[string]interface{}) + for iter.Next() { + + key, isNil := indirect(iter.Key()) + if isNil { + return nil, errors.New("map with nil key encountered") + } + if key.Kind() == reflect.String { + mapCopy[key.String()] = iter.Value().Interface() + } else { + return nil, errors.New("map has non string key of type: " + key.Type().String()) + } + } + return SDict(mapCopy), nil + default: + return nil, errors.New("cannot convert data of type: " + reflect.TypeOf(values[0]).String()) + } + + } + + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key := values[i] + s, ok := key.(string) + if !ok { + return nil, errors.New("Only string keys supported in sdict") + } + + dict[s] = values[i+1] + } + + return SDict(dict), nil +} + +func KindOf(input interface{}, flag ...bool) (string, error) { //flag used only for indirect vs direct for now. + + switch len(flag) { + + case 0: + return reflect.ValueOf(input).Kind().String(), nil + case 1: + if flag[0] { + val, isNil := indirect(reflect.ValueOf(input)) + if isNil || input == nil { + return "invalid", nil + } + return val.Kind().String(), nil + } + return reflect.ValueOf(input).Kind().String(), nil + default: + return "", errors.New("Too many flags") + } +} + +func StructToSdict(value interface{}) (SDict, error) { + val, isNil := indirect(reflect.ValueOf(value)) + typeOfS := val.Type() + if isNil || value == nil { + return nil, errors.New("Expected - struct, got - Nil ") + } + + if val.Kind() != reflect.Struct { + return nil, errors.New(fmt.Sprintf("Expected - struct, got - %s", val.Type().String())) + } + + fields := make(map[string]interface{}) + for i := 0; i < val.NumField(); i++ { + curr := val.Field(i) + if curr.CanInterface() { + fields[typeOfS.Field(i).Name] = curr.Interface() + } + } + return SDict(fields), nil + +} + +func CreateSlice(values ...interface{}) (Slice, error) { + slice := make([]interface{}, len(values)) + copy(slice, values) + return Slice(slice), nil +} + +// indirect is taken from 'text/template/exec.go' +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} + +// in returns whether v is in the set l. l may be an array or slice. +func in(l interface{}, v interface{}) bool { + lv, _ := indirect(reflect.ValueOf(l)) + vv := reflect.ValueOf(v) + + if !reflect.ValueOf(vv).IsZero() { + switch lv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < lv.Len(); i++ { + lvv := lv.Index(i) + lvv, isNil := indirect(lvv) + if isNil { + continue + } + switch lvv.Kind() { + case reflect.String: + if vv.Type() == lvv.Type() && vv.String() == lvv.String() { + return true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if vv.Int() == lvv.Int() { + return true + } + } + case reflect.Float32, reflect.Float64: + switch vv.Kind() { + case reflect.Float32, reflect.Float64: + if vv.Float() == lvv.Float() { + return true + } + } + } + } + case reflect.String: + if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) { + return true + } + } + } + + return false +} + +// in returns whether v is in the set l. l may only be a slice of strings, or a string, v may only be a string +// it differs from "in" because its case insensitive +func inFold(l interface{}, v string) bool { + lv, _ := indirect(reflect.ValueOf(l)) + vv := reflect.ValueOf(v) + + switch lv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < lv.Len(); i++ { + lvv := lv.Index(i) + lvv, isNil := indirect(lvv) + if isNil { + continue + } + switch lvv.Kind() { + case reflect.String: + if vv.Type() == lvv.Type() && strings.EqualFold(vv.String(), lvv.String()) { + return true + } + } + } + case reflect.String: + if vv.Type() == lv.Type() && strings.Contains(strings.ToLower(lv.String()), strings.ToLower(vv.String())) { + return true + } + } + + return false +} + +func add(args ...interface{}) interface{} { + if len(args) < 1 { + return 0 + } + + switch args[0].(type) { + case float32, float64: + sumF := float64(0) + for _, v := range args { + sumF += ToFloat64(v) + } + return sumF + default: + sumI := 0 + for _, v := range args { + sumI += tmplToInt(v) + } + return sumI + } +} + +func tmplSub(args ...interface{}) interface{} { + if len(args) < 1 { + return 0 + } + + switch args[0].(type) { + case float32, float64: + subF := ToFloat64(args[0]) + for i, v := range args { + if i == 0 { + continue + } + subF -= ToFloat64(v) + } + return subF + default: + subI := tmplToInt(args[0]) + for i, v := range args { + if i == 0 { + continue + } + subI -= tmplToInt(v) + } + return subI + } +} + +var mathConstantsMap = map[string]float64{ + //base + "e": math.E, + "pi": math.Pi, + "phi": math.Phi, + + // square roots + "sqrt2": math.Sqrt2, + "sqrte": math.SqrtE, + "sqrtpi": math.SqrtPi, + "sqrtphi": math.SqrtPhi, + + // logarithms + "ln2": math.Ln2, + "log2e": math.Log2E, + "ln10": math.Ln10, + "log10e": math.Log10E, + + // floating-point limit values + "maxfloat32": math.MaxFloat32, + "smallestnonzerofloat32": math.SmallestNonzeroFloat32, + "maxfloat64": math.MaxFloat64, + "smallestnonzerofloat64": math.SmallestNonzeroFloat64, + + // integer limit values + "maxint": math.MaxInt, + "minint": math.MinInt, + "maxint8": math.MaxInt8, + "minint8": math.MinInt8, + "maxint16": math.MaxInt16, + "minint16": math.MinInt16, + "maxint32": math.MaxInt32, + "minint32": math.MinInt32, + "maxint64": math.MaxInt64, + "minint64": math.MinInt64, + "maxuint": math.MaxUint, + "maxuint8": math.MaxUint8, + "maxuint16": math.MaxUint16, + "maxuint32": math.MaxUint32, + "maxuint64": math.MaxUint64, +} + +func tmplMathConstant(arg string) float64 { + constant := mathConstantsMap[strings.ToLower(arg)] + if constant == 0 { + return math.NaN() + } + + return constant +} + +func tmplMult(args ...interface{}) interface{} { + if len(args) < 1 { + return 0 + } + + switch args[0].(type) { + case float32, float64: + sumF := ToFloat64(args[0]) + for i, v := range args { + if i == 0 { + continue + } + + sumF *= ToFloat64(v) + } + return sumF + default: + sumI := tmplToInt(args[0]) + for i, v := range args { + if i == 0 { + continue + } + + sumI *= tmplToInt(v) + } + return sumI + } +} + +func tmplDiv(args ...interface{}) interface{} { + if len(args) < 1 { + return 0 + } + + switch args[0].(type) { + case float32, float64: + sumF := ToFloat64(args[0]) + for i, v := range args { + if i == 0 { + continue + } + + sumF /= ToFloat64(v) + } + return sumF + default: + sumI := tmplToInt(args[0]) + for i, v := range args { + if i == 0 { + continue + } + + sumI /= tmplToInt(v) + } + return sumI + } +} + +func tmplMod(args ...interface{}) float64 { + if len(args) != 2 { + return math.NaN() + } + + return math.Mod(ToFloat64(args[0]), ToFloat64(args[1])) +} + +func tmplFDiv(args ...interface{}) interface{} { + if len(args) < 1 { + return 0 + } + + sumF := ToFloat64(args[0]) + for i, v := range args { + if i == 0 { + continue + } + + sumF /= ToFloat64(v) + } + + return sumF +} + +func tmplSqrt(arg interface{}) float64 { + switch arg.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + return math.Sqrt(ToFloat64(arg)) + default: + return math.Sqrt(-1) + } +} + +func tmplCbrt(arg interface{}) float64 { + switch arg.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + return math.Cbrt(ToFloat64(arg)) + default: + return math.NaN() + } +} + +func tmplPow(argX, argY interface{}) float64 { + var xyValue float64 + var xySlice []float64 + + switchSlice := []interface{}{argX, argY} + + for _, v := range switchSlice { + switch v.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + xyValue = ToFloat64(v) + default: + xyValue = math.NaN() + } + xySlice = append(xySlice, xyValue) + } + return math.Pow(xySlice[0], xySlice[1]) +} + +func tmplMax(argX, argY interface{}) float64 { + var xyValue float64 + var xySlice []float64 + + switchSlice := []interface{}{argX, argY} + + for _, v := range switchSlice { + switch v.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + xyValue = ToFloat64(v) + default: + xyValue = math.NaN() + } + xySlice = append(xySlice, xyValue) + } + return math.Max(xySlice[0], xySlice[1]) +} + +func tmplMin(argX, argY interface{}) float64 { + var xyValue float64 + var xySlice []float64 + + switchSlice := []interface{}{argX, argY} + + for _, v := range switchSlice { + switch v.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + xyValue = ToFloat64(v) + default: + xyValue = math.NaN() + } + xySlice = append(xySlice, xyValue) + } + return math.Min(xySlice[0], xySlice[1]) +} + +/* +tmplLog is a function for templates using (log base of x = logarithm) as return value. +It is using natural logarithm as default to change the base. +*/ +func tmplLog(arguments ...interface{}) (float64, error) { + var x, base, logarithm float64 + + x = ToFloat64(arguments[0]) + + if len(arguments) < 1 || len(arguments) > 2 { + return 0, errors.New("wrong number of arguments") + } else if len(arguments) == 1 { + base = math.E + } else { + base = ToFloat64(arguments[1]) + } + /*In an exponential function, the base is always defined to be positive, + but can't be equal to 1. Because of that also x can't be a negative.*/ + if base == 1 || base <= 0 { + logarithm = math.NaN() + } else if base == math.E { + logarithm = math.Log(x) + } else { + logarithm = math.Log(x) / math.Log(base) + } + + return logarithm, nil +} + +func tmplBitwiseAnd(arg1, arg2 interface{}) int { + return tmplToInt(arg1) & tmplToInt(arg2) +} + +func tmplBitwiseOr(args ...interface{}) (res int) { + for _, arg := range args { + res |= tmplToInt(arg) + } + return +} + +func tmplBitwiseXor(arg1, arg2 interface{}) int { + return tmplToInt(arg1) ^ tmplToInt(arg2) +} + +func tmplBitwiseNot(arg interface{}) int { + return ^tmplToInt(arg) +} + +func tmplBitwiseAndNot(arg1, arg2 interface{}) int { + return tmplToInt(arg1) &^ tmplToInt(arg2) +} + +func tmplBitwiseLeftShift(arg1, arg2 interface{}) int { + return tmplToInt(arg1) << tmplToInt(arg2) +} + +func tmplBitwiseRightShift(arg1, arg2 interface{}) int { + return tmplToInt(arg1) >> tmplToInt(arg2) +} + +// tmplHumanizeThousands comma separates thousands +func tmplHumanizeThousands(input interface{}) string { + var f1, f2 string + + i := tmplToInt(input) + if i < 0 { + i = i * -1 + f2 = "-" + } + str := strconv.Itoa(i) + + idx := 0 + for i = len(str) - 1; i >= 0; i-- { + idx++ + if idx == 4 { + idx = 1 + f1 = f1 + "," + } + f1 = f1 + string(str[i]) + } + + for i = len(f1) - 1; i >= 0; i-- { + f2 = f2 + string(f1[i]) + } + return f2 +} + +func randInt(args ...interface{}) (int, error) { + min := int64(0) + max := int64(10) + if len(args) >= 2 { + min = ToInt64(args[0]) + max = ToInt64(args[1]) + } else if len(args) == 1 { + max = ToInt64(args[0]) + } + + diff := max - min + if diff <= 0 { + return 0, errors.New("start must be strictly less than stop") + } + + r := rand.Int63n(diff) + return int(r + min), nil +} + +func tmplRound(args ...interface{}) float64 { + if len(args) < 1 { + return 0 + } + return math.Round(ToFloat64(args[0])) +} + +func tmplRoundCeil(args ...interface{}) float64 { + if len(args) < 1 { + return 0 + } + return math.Ceil(ToFloat64(args[0])) +} + +func tmplRoundFloor(args ...interface{}) float64 { + if len(args) < 1 { + return 0 + } + return math.Floor(ToFloat64(args[0])) +} + +func tmplRoundEven(args ...interface{}) float64 { + if len(args) < 1 { + return 0 + } + return math.RoundToEven(ToFloat64(args[0])) +} + +var ErrStringTooLong = errors.New("String is too long (max 1MB)") + +const MaxStringLength = 1000000 + +func joinStrings(sep string, args ...interface{}) (string, error) { + + var builder strings.Builder + + for _, v := range args { + if builder.Len() != 0 { + builder.WriteString(sep) + } + + switch t := v.(type) { + + case string: + builder.WriteString(t) + + case int, uint, int32, uint32, int64, uint64: + builder.WriteString(ToString(v)) + + case float64: + builder.WriteString(fmt.Sprintf("%g", v)) + + case fmt.Stringer: + builder.WriteString(t.String()) + + default: + cast, ok := castToStringSlice(reflect.ValueOf(v)) + if !ok { + break + } + + for j, s := range cast { + if j != 0 { + builder.WriteString(sep) + } + + builder.WriteString(s) + if builder.Len() > MaxStringLength { + return "", ErrStringTooLong + } + } + } + + if builder.Len() > MaxStringLength { + return "", ErrStringTooLong + } + + } + + return builder.String(), nil +} + +var stringSliceType = reflect.TypeOf([]string(nil)) + +func castToStringSlice(rv reflect.Value) ([]string, bool) { + rv, _ = indirect(rv) + switch rv.Kind() { + case reflect.Array, reflect.Slice: + // ok + default: + return nil, false + } + + // fast path + if rv.Type() == stringSliceType { + return rv.Interface().([]string), true + } + + ret := make([]string, rv.Len()) + for i := 0; i < rv.Len(); i++ { + irv, _ := indirect(rv.Index(i)) + if irv.Kind() != reflect.String { + return nil, false + } + ret[i] = irv.String() + } + return ret, true +} + +func sequence(start, stop int) ([]int, error) { + + if stop < start { + return nil, errors.New("stop is less than start?") + } + + if stop-start > 10000 { + return nil, errors.New("Sequence max length is 10000") + } + + out := make([]int, stop-start) + + ri := 0 + for i := start; i < stop; i++ { + out[ri] = i + ri++ + } + return out, nil +} + +// shuffle returns the given rangeable list in a randomised order. +func shuffle(seq interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("both count and seq must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + if seqv.Kind() != reflect.Slice { + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + shuffled := reflect.MakeSlice(seqv.Type(), seqv.Len(), seqv.Len()) + + rand.Seed(time.Now().UTC().UnixNano()) + randomIndices := rand.Perm(seqv.Len()) + + for index, value := range randomIndices { + shuffled.Index(value).Set(seqv.Index(index)) + } + + return shuffled.Interface(), nil +} + +func tmplToInt(from interface{}) int { + switch t := from.(type) { + case int: + return t + case int32: + return int(t) + case int64: + return int(t) + case float32: + return int(t) + case float64: + return int(t) + case uint: + return int(t) + case uint8: + return int(t) + case uint32: + return int(t) + case uint64: + return int(t) + case string: + parsed, _ := strconv.ParseInt(t, 10, 64) + return int(parsed) + case time.Duration: + return int(t) + case time.Month: + return int(t) + case time.Weekday: + return int(t) + default: + return 0 + } +} + +func ToInt64(from interface{}) int64 { + switch t := from.(type) { + case int: + return int64(t) + case int32: + return int64(t) + case int64: + return int64(t) + case float32: + return int64(t) + case float64: + return int64(t) + case uint: + return int64(t) + case uint32: + return int64(t) + case uint64: + return int64(t) + case string: + parsed, _ := strconv.ParseInt(t, 10, 64) + return parsed + case time.Duration: + return int64(t) + case time.Month: + return int64(t) + case time.Weekday: + return int64(t) + default: + return 0 + } +} + +func ToString(from interface{}) string { + switch t := from.(type) { + case int: + return strconv.Itoa(t) + case int32: + return strconv.FormatInt(int64(t), 10) + case int64: + return strconv.FormatInt(t, 10) + case float32: + return strconv.FormatFloat(float64(t), 'E', -1, 32) + case float64: + return strconv.FormatFloat(t, 'E', -1, 64) + case uint: + return strconv.FormatUint(uint64(t), 10) + case uint32: + return strconv.FormatUint(uint64(t), 10) + case uint64: + return strconv.FormatUint(uint64(t), 10) + case []rune: + return string(t) + case []byte: + return string(t) + case fmt.Stringer: + return t.String() + case string: + return t + default: + return "" + } +} + +func ToFloat64(from interface{}) float64 { + switch t := from.(type) { + case int: + return float64(t) + case int32: + return float64(t) + case int64: + return float64(t) + case float32: + return float64(t) + case float64: + return float64(t) + case uint: + return float64(t) + case uint32: + return float64(t) + case uint64: + return float64(t) + case string: + parsed, _ := strconv.ParseFloat(t, 64) + return parsed + case time.Duration: + return float64(t) + case time.Month: + return float64(t) + case time.Weekday: + return float64(t) + default: + return 0 + } +} + +func ToRune(from interface{}) []rune { + switch t := from.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + return []rune(ToString(t)) + case string: + return []rune(t) + default: + return nil + } +} + +func ToByte(from interface{}) []byte { + switch t := from.(type) { + case int, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64: + return []byte(ToString(t)) + case string: + return []byte(t) + default: + return nil + } +} + +func tmplJson(v interface{}, flags ...bool) (string, error) { + var b []byte + var err error + + switch len(flags) { + + case 0: + b, err = json.Marshal(v) + if err != nil { + return "", err + } + + case 1: + if flags[0] { + b, err = json.MarshalIndent(v, "", "\t") + if err != nil { + return "", err + } + } else { + b, err = json.Marshal(v) + if err != nil { + return "", err + } + } + + default: + return "", errors.New("Too many flags") + } + + return string(b), nil +} + +func tmplJSONToSDict(v interface{}) (SDict, error) { + var toSDict SDict + err := json.Unmarshal([]byte(ToString(v)), &toSDict) + if err != nil { + return nil, err + } + + return toSDict, nil +} + +func tmplFormatTime(t time.Time, args ...string) string { + layout := time.RFC822 + if len(args) > 0 { + layout = args[0] + } + + return t.Format(layout) +} + +func tmplTimestampToTime(v interface{}) time.Time { + return time.Unix(ToInt64(v), 0).UTC() +} + +type variadicFunc func([]reflect.Value) (reflect.Value, error) + +// callVariadic allows the given function to be called with either a variadic +// sequence of arguments (i.e., fixed in the template definition) or a slice +// (i.e., from a pipeline or context variable). In effect, a limited `flatten` +// operation. +func callVariadic(f variadicFunc, skipNil bool, values ...reflect.Value) (reflect.Value, error) { + var vs []reflect.Value + for _, val := range values { + v, _ := indirect(val) + switch { + case !v.IsValid(): + if !skipNil { + vs = append(vs, v) + } else { + continue + } + case v.Kind() == reflect.Array || v.Kind() == reflect.Slice: + for i := 0; i < v.Len(); i++ { + irv, _ := indirect(v.Index(i)) + vs = append(vs, irv) + } + default: + vs = append(vs, v) + } + } + + return f(vs) +} + +// slice returns the result of creating a new slice with the given arguments. +// "slice x 1 2" is, in Go syntax, x[1:2], and "slice x 1" is equivalent to +// x[1:]. +func slice(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) { + v, _ := indirect(item) + if !v.IsValid() { + return reflect.Value{}, errors.New("index of untyped nil") + } + + var args []int + for _, i := range indices { + index, _ := indirect(i) + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + args = append(args, int(index.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + args = append(args, int(index.Uint())) + case reflect.Invalid: + return reflect.Value{}, errors.New("cannot index slice/array with nil") + default: + return reflect.Value{}, fmt.Errorf("cannot index slice/array with type %s", index.Type()) + } + } + + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + startIndex := 0 + endIndex := 0 + + switch len(args) { + case 0: + // No start or end index provided same as slice[:] + return v, nil + case 1: + // Only start index provided, same as slice[i:] + startIndex = args[0] + endIndex = v.Len() + // args = append(args, v.Len()+1-args[0]) + case 2: + // Both start and end index provided + startIndex = args[0] + endIndex = args[1] + default: + return reflect.Value{}, fmt.Errorf("unexpected slice arguments %d", len(args)) + } + + if startIndex < 0 || startIndex >= v.Len() { + return reflect.Value{}, fmt.Errorf("start index out of range: %d", startIndex) + } else if endIndex <= startIndex || endIndex > v.Len() { + return reflect.Value{}, fmt.Errorf("end index out of range: %d", endIndex) + } + + return v.Slice(startIndex, endIndex), nil + default: + return reflect.Value{}, fmt.Errorf("can't index item of type %s", v.Type()) + } +} + +func tmplCurrentTime() time.Time { + return time.Now().UTC() +} + +func tmplParseTime(input string, layout interface{}, locations ...string) (time.Time, error) { + loc := time.UTC + + var err error + if len(locations) > 0 { + loc, err = time.LoadLocation(locations[0]) + if err != nil { + return time.Time{}, err + } + } + + var parsed time.Time + + rv, _ := indirect(reflect.ValueOf(layout)) + switch rv.Kind() { + case reflect.Slice, reflect.Array: + if rv.Len() > 50 { + return time.Time{}, errors.New("max number of layouts is 50") + } + + for i := 0; i < rv.Len(); i++ { + lv, _ := indirect(rv.Index(i)) + if lv.Kind() != reflect.String { + return time.Time{}, errors.New("layout must be either a slice of strings or a single string") + } + + parsed, err = time.ParseInLocation(lv.String(), input, loc) + if err == nil { + // found a layout that matched + break + } + } + case reflect.String: + parsed, _ = time.ParseInLocation(rv.String(), input, loc) + default: + return time.Time{}, errors.New("layout must be either a slice of strings or a single string") + } + + // if no layout matched, parsed will be the zero Time. + // thus, users can call