diff --git a/internal/infra/persistence/migrator/goose/postgres/202608160003_create_message_gateway.sql b/internal/infra/persistence/migrator/goose/postgres/202608160003_create_message_gateway.sql new file mode 100644 index 00000000..dab497d5 --- /dev/null +++ b/internal/infra/persistence/migrator/goose/postgres/202608160003_create_message_gateway.sql @@ -0,0 +1,40 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS w_message_channels ( + id BIGINT PRIMARY KEY, + name VARCHAR(128) NOT NULL, + type VARCHAR(32) NOT NULL, + owner_scope VARCHAR(16) NOT NULL DEFAULT 'system', + owner_id BIGINT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + credentials TEXT NOT NULL DEFAULT '', + extra TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_w_message_channels_type ON w_message_channels (type); + +CREATE TABLE IF NOT EXISTS w_message_bindings ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + channel_id BIGINT NOT NULL, + platform_user_id VARCHAR(128) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS uniq_w_message_bindings_channel_platform + ON w_message_bindings (channel_id, platform_user_id); +CREATE INDEX IF NOT EXISTS idx_w_message_bindings_user ON w_message_bindings (user_id); + +CREATE TABLE IF NOT EXISTS w_message_pairing_codes ( + code VARCHAR(16) PRIMARY KEY, + channel_id BIGINT NOT NULL, + platform_user_id VARCHAR(128) NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_w_message_pairing_lookup + ON w_message_pairing_codes (channel_id, platform_user_id); + +-- +goose Down +DROP TABLE IF EXISTS w_message_pairing_codes; +DROP TABLE IF EXISTS w_message_bindings; +DROP TABLE IF EXISTS w_message_channels; diff --git a/internal/infra/persistence/migrator/goose/sqlite/202608160003_create_message_gateway.sql b/internal/infra/persistence/migrator/goose/sqlite/202608160003_create_message_gateway.sql new file mode 100644 index 00000000..aae7221e --- /dev/null +++ b/internal/infra/persistence/migrator/goose/sqlite/202608160003_create_message_gateway.sql @@ -0,0 +1,40 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS w_message_channels ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, + owner_scope TEXT NOT NULL DEFAULT 'system', + owner_id INTEGER NULL, + enabled INTEGER NOT NULL DEFAULT 1, + credentials TEXT NOT NULL DEFAULT '', + extra TEXT NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_w_message_channels_type ON w_message_channels (type); + +CREATE TABLE IF NOT EXISTS w_message_bindings ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + channel_id INTEGER NOT NULL, + platform_user_id TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS uniq_w_message_bindings_channel_platform + ON w_message_bindings (channel_id, platform_user_id); +CREATE INDEX IF NOT EXISTS idx_w_message_bindings_user ON w_message_bindings (user_id); + +CREATE TABLE IF NOT EXISTS w_message_pairing_codes ( + code TEXT PRIMARY KEY, + channel_id INTEGER NOT NULL, + platform_user_id TEXT NOT NULL, + expires_at DATETIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_w_message_pairing_lookup + ON w_message_pairing_codes (channel_id, platform_user_id); + +-- +goose Down +DROP TABLE IF EXISTS w_message_pairing_codes; +DROP TABLE IF EXISTS w_message_bindings; +DROP TABLE IF EXISTS w_message_channels; diff --git a/internal/infra/persistence/migrator/migrator_test.go b/internal/infra/persistence/migrator/migrator_test.go index 45214e01..1ea490d2 100644 --- a/internal/infra/persistence/migrator/migrator_test.go +++ b/internal/infra/persistence/migrator/migrator_test.go @@ -18,7 +18,7 @@ import ( "gorm.io/gorm" ) -const expectedMigratedSystemConfigCount = 30 +const expectedMigratedSystemConfigCount = 35 func TestMigrateInitializesSQLiteDatabase(t *testing.T) { sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ diff --git a/internal/model/message_gateway.go b/internal/model/message_gateway.go new file mode 100644 index 00000000..644cec01 --- /dev/null +++ b/internal/model/message_gateway.go @@ -0,0 +1,56 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package model + +import "time" + +const ( + // MessageChannelTypeTelegram is a Telegram bot channel. + MessageChannelTypeTelegram = "telegram" + // MessageChannelTypeQQ is an official QQ bot channel. + MessageChannelTypeQQ = "qq" + // MessageOwnerScopeSystem is an instance-level shared bot. + MessageOwnerScopeSystem = "system" +) + +// MessageChannel is an admin-configured messaging adapter. +type MessageChannel struct { + ID uint64 `json:"id,string" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:128;not null"` + Type string `json:"type" gorm:"size:32;not null;index"` + OwnerScope string `json:"owner_scope" gorm:"size:16;not null;default:system"` + OwnerID *uint64 `json:"owner_id,string"` + Enabled bool `json:"enabled" gorm:"not null;default:true"` + Credentials string `json:"-" gorm:"type:text"` + Extra string `json:"extra" gorm:"type:text"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +// TableName returns w_message_channels. +func (MessageChannel) TableName() string { return "w_message_channels" } + +// MessageBinding maps a platform user to a Wavelet user on one channel. +type MessageBinding struct { + ID uint64 `json:"id,string" gorm:"primaryKey"` + UserID uint64 `json:"user_id,string" gorm:"not null;index"` + ChannelID uint64 `json:"channel_id,string" gorm:"not null;index"` + PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` +} + +// TableName returns w_message_bindings. +func (MessageBinding) TableName() string { return "w_message_bindings" } + +// MessagePairingCode is a one-time bind code. +type MessagePairingCode struct { + Code string `json:"code" gorm:"primaryKey;size:16"` + ChannelID uint64 `json:"channel_id,string" gorm:"not null"` + PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null"` + ExpiresAt time.Time `json:"expires_at" gorm:"not null"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` +} + +// TableName returns w_message_pairing_codes. +func (MessagePairingCode) TableName() string { return "w_message_pairing_codes" } diff --git a/internal/testhelper/test_helper.go b/internal/testhelper/test_helper.go index 77204610..04b964d1 100644 --- a/internal/testhelper/test_helper.go +++ b/internal/testhelper/test_helper.go @@ -53,6 +53,9 @@ func SetupTestEnvironment(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) &model.Template{}, &model.AccessToken{}, &model.Schedule{}, + &model.MessageChannel{}, + &model.MessageBinding{}, + &model.MessagePairingCode{}, ) if err != nil { t.Fatalf("failed to auto migrate tables: %v", err)