From aa9f562e1e2c79d7e2a6f52cc3b3e122c77fab2e Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Wed, 23 Sep 2026 17:04:01 +0800 Subject: [PATCH] refactor: retire legacy tinode database routing --- INSTALL.md | 4 +- docker/README.md | 4 +- docker/tinode/Dockerfile | 2 +- docker/tinode/Dockerfile.build | 2 +- docker/tinode/config.template | 4 +- docker/tinode/entrypoint.sh | 4 +- docs/sql/IM-共库运行态缺口修复SQL.sql | 61 ++ scripts/IM-租户业务库初始化.sql | 72 +- scripts/apply_hotgo_tenant_db.go | 706 +----------------- server/db/mongodb/adapter.go | 2 +- server/db/mysql/adapter.go | 3 + server/db/mysql/adapter_dsn_test.go | 18 +- server/db/rethinkdb/adapter.go | 2 +- server/im_http.go | 39 +- server/im_http_test.go | 95 +++ server/meetingim.conf | 2 +- .../local_message_crypto_integration_test.go | 4 +- server/tinode.conf | 2 +- tinode-db/tinode.conf | 12 +- 19 files changed, 294 insertions(+), 744 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index e70f04d..efa7fc9 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -80,8 +80,8 @@ See [instructions](./docker/README.md) 5. Open `tinode.conf` (located at `$GOPATH/pkg/mod/github.com/tinode/chat@vX.XX.X/server/`). Check that the database connection parameters are correct for your database. If you are using MySQL make sure [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name) in `"mysql"` section is appropriate for your MySQL installation. Option `parseTime=true` is required. ```js "mysql": { - "dsn": "root@tcp(localhost)/tinode?parseTime=true", - "database": "tinode" + "dsn": "root@tcp(localhost)/meeting_im?parseTime=true", + "database": "meeting_im" }, ``` diff --git a/docker/README.md b/docker/README.md index 4c6aeef..162ece8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -203,9 +203,9 @@ You can specify the following environment variables when issuing `docker run` co | `FS_CORS_ORIGINS`[^2] | string | `["*"]` | Cors origins when media is served from the file system. See `AWS_CORS_ORIGINS` for details. | | `ICE_SERVERS_FILE`[^2] | string | | Path to JSON file with configuration of ICE servers to be used for video calls. | | `MEDIA_HANDLER`[^2] | string | `sys_config` | Legacy override for the large file handler. The generated runtime config uses `sys_config` so production can resolve `oss` from backend configuration. | -| `MYSQL_DSN`[^2] | string | 'root@tcp(mysql)/tinode?​parseTime=true​&collation=utf8mb4_0900_ai_ci' | MySQL [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name). | +| `MYSQL_DSN`[^2] | string | 'root@tcp(mysql)/meeting_im?​parseTime=true​&collation=utf8mb4_0900_ai_ci' | MySQL [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name). | | `PLUGIN_PYTHON_CHAT_BOT_ENABLED`[^2] | bool | `false` | Enable calling into the plugin provided by Python chatbot. | -| `POSTGRES_DSN`[^2] | string | 'postgresql://postgres:postgres@​localhost:5432/tinode?​sslmode=disable​&connect_timeout=10' | PostgreSQL [DSN](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). | +| `POSTGRES_DSN`[^2] | string | 'postgresql://postgres:postgres@​localhost:5432/meeting_im?​sslmode=disable​&connect_timeout=10' | PostgreSQL [DSN](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). | | `RESET_DB` | bool | `false` | Drop and recreate the database. | | `SAMPLE_DATA` | string | _see comment →_ | File with sample data to load. Default `data.json` when resetting or generating new DB, none when upgrading. Use `` (empty string) to disable. | | `SMTP_AUTH_MECHANISM`[^2] | string | `"plain"` | SMTP authentication mechanism to use; one of "login", "cram-md5", "plain". | diff --git a/docker/tinode/Dockerfile b/docker/tinode/Dockerfile index 301df6f..78e8dd6 100644 --- a/docker/tinode/Dockerfile +++ b/docker/tinode/Dockerfile @@ -112,7 +112,7 @@ ENV ADMIN_INTERNAL_API_SECRET= ENV ADMIN_INTERNAL_API_TIMEOUT_MS=3000 # The PostgreSQL DSN connection. -ENV POSTGRES_DSN='postgresql://postgres:postgres@localhost:5432/tinode?sslmode=disable&connect_timeout=10' +ENV POSTGRES_DSN='postgresql://postgres:postgres@localhost:5432/meeting_im?sslmode=disable&connect_timeout=10' # Disable chatbot plugin by default. ENV PLUGIN_PYTHON_CHAT_BOT_ENABLED=false diff --git a/docker/tinode/Dockerfile.build b/docker/tinode/Dockerfile.build index d0f6db6..0ac9849 100644 --- a/docker/tinode/Dockerfile.build +++ b/docker/tinode/Dockerfile.build @@ -14,7 +14,7 @@ COPY . . RUN CGO_ENABLED=0 go build -tags mysql -o /out/tinode ./server RUN CGO_ENABLED=0 go build -tags mysql -o /out/init-db ./tinode-db # scripts/apply_hotgo_tenant_db.go is `//go:build ignore`-tagged (a standalone -# migration script, not part of any package) - build it by exact file path. +# startup database validator, not part of any package) - build it by exact path. RUN CGO_ENABLED=0 go build -o /out/apply-hotgo-tenant-db ./scripts/apply_hotgo_tenant_db.go # ---- Stage 2: runtime image ---- diff --git a/docker/tinode/config.template b/docker/tinode/config.template index 0c0b2c8..49cb2f0 100644 --- a/docker/tinode/config.template +++ b/docker/tinode/config.template @@ -91,11 +91,11 @@ "dsn": "$POSTGRES_DSN" }, "rethinkdb": { - "database": "tinode", + "database": "meeting_im", "addresses": "rethinkdb" }, "mongodb": { - "database": "tinode", + "database": "meeting_im", "addresses": "mongodb", "replica_set": "rs0" } diff --git a/docker/tinode/entrypoint.sh b/docker/tinode/entrypoint.sh index 5f59388..d8dca8f 100644 --- a/docker/tinode/entrypoint.sh +++ b/docker/tinode/entrypoint.sh @@ -164,8 +164,8 @@ else exit 1 fi - # Create/upgrade the hotgo tenant registry and the workbench/org/meetings/ - # reports/mailbox/etc. tables that init-db above does not manage. + # Validate that startup is pointed at the shared meeting_im database. Schema + # changes are applied separately and are never performed during pod startup. run_with_retries "apply-hotgo-tenant-db" apply_hotgo_tenant_db_once if [ $? -ne 0 ]; then echo "./apply-hotgo-tenant-db failed. Quitting." diff --git a/docs/sql/IM-共库运行态缺口修复SQL.sql b/docs/sql/IM-共库运行态缺口修复SQL.sql index b554070..8e09783 100644 --- a/docs/sql/IM-共库运行态缺口修复SQL.sql +++ b/docs/sql/IM-共库运行态缺口修复SQL.sql @@ -56,6 +56,67 @@ ALTER TABLE im_auth DROP INDEX auth_userid, ADD UNIQUE INDEX auth_tenant_userid (tenant_id, userid); +-- Topic/subscription runtime indexes for shared-database deployments. +-- These tables are on hot paths for topic lookup, message seq updates, +-- unread calculation and subscription permission checks. Keep tenant_id as +-- the leading column so one tenant does not scan another tenant's rows. +SET @schema_name := DATABASE(); + +SET @sql := IF(( + SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema = @schema_name + AND table_name = 'im_topics' + AND index_name = 'uk_topics_tenant_name' +) = 0, + 'ALTER TABLE im_topics ADD UNIQUE INDEX uk_topics_tenant_name(tenant_id, name)', + 'SELECT ''skip uk_topics_tenant_name'' AS status' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql := IF(( + SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema = @schema_name + AND table_name = 'im_topics' + AND index_name = 'idx_topics_tenant_name_state_seqid' +) = 0, + 'ALTER TABLE im_topics ADD INDEX idx_topics_tenant_name_state_seqid(tenant_id, name, state, seqid)', + 'SELECT ''skip idx_topics_tenant_name_state_seqid'' AS status' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql := IF(( + SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema = @schema_name + AND table_name = 'im_topics' + AND index_name = 'idx_topics_tenant_owner' +) = 0, + 'ALTER TABLE im_topics ADD INDEX idx_topics_tenant_owner(tenant_id, owner)', + 'SELECT ''skip idx_topics_tenant_owner'' AS status' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql := IF(( + SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema = @schema_name + AND table_name = 'im_subscriptions' + AND index_name = 'idx_subscriptions_tenant_topic_deletedat' +) = 0, + 'ALTER TABLE im_subscriptions ADD INDEX idx_subscriptions_tenant_topic_deletedat(tenant_id, topic, deletedat)', + 'SELECT ''skip idx_subscriptions_tenant_topic_deletedat'' AS status' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql := IF(( + SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema = @schema_name + AND table_name = 'im_subscriptions' + AND index_name = 'idx_subscriptions_tenant_user_topic_deletedat' +) = 0, + 'ALTER TABLE im_subscriptions ADD INDEX idx_subscriptions_tenant_user_topic_deletedat(tenant_id, userid, topic, deletedat)', + 'SELECT ''skip idx_subscriptions_tenant_user_topic_deletedat'' AS status' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + -- Default app release rows used by /v1/im/app/releases/latest. INSERT INTO hotgo.im_app_releases ( platform, channel, latest_version, min_supported_version, update_type, diff --git a/scripts/IM-租户业务库初始化.sql b/scripts/IM-租户业务库初始化.sql index d75e9cc..0ffb286 100644 --- a/scripts/IM-租户业务库初始化.sql +++ b/scripts/IM-租户业务库初始化.sql @@ -1,15 +1,15 @@ -- IM tenant business database initialization script. --- Source baseline: local MySQL database `tinode` on 2026-09-13. +-- Source baseline: shared MySQL database `meeting_im` on 2026-09-13. -- Purpose: create a clean business database for a new IM tenant. -- -- Usage: -- 1. Replace placeholders before execution: --- __TENANT_DB__ -> new tenant business database, for example `tinode_acme`. +-- __TENANT_DB__ -> target tenant business database, normally `meeting_im`. -- __TENANT_ID__ -> hotgo.im_tenant.id assigned to this tenant. -- __TENANT_CODE__ -> public enterprise code, for example `acme`. -- __TENANT_NAME__ -> tenant display name. -- 2. Execute this SQL with a MySQL account that can CREATE DATABASE and write hotgo.im_tenant. --- 3. Keep business tables empty; do not copy users, topics, messages, files, Kafka logs, or audit data from `tinode`. +-- 3. Keep business tables empty; do not copy users, topics, messages, files, Kafka logs, or audit data from another database. -- -- Notes: -- - This script intentionally seeds only stable metadata/default configuration. @@ -168,9 +168,10 @@ CREATE TABLE IF NOT EXISTS `im_attendance_records` ( CREATE TABLE IF NOT EXISTS `im_attendance_rules` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT '2' COMMENT '商户ID', `rule_id` varchar(64) NOT NULL, `name` varchar(128) NOT NULL, - `workday` tinyint NOT NULL DEFAULT '1', + `workday` varchar(32) NOT NULL DEFAULT '1,2,3,4,5', `location_required` tinyint NOT NULL DEFAULT '0', `wifi_required` tinyint NOT NULL DEFAULT '0', `allow_outside_checkin` tinyint NOT NULL DEFAULT '1', @@ -181,12 +182,13 @@ CREATE TABLE IF NOT EXISTS `im_attendance_rules` ( `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), - UNIQUE KEY `uk_im_attendance_rules_rule` (`rule_id`), - KEY `idx_im_attendance_rules_state` (`state`,`effective_from`,`effective_to`) + UNIQUE KEY `uk_im_attendance_rules_tenant_rule` (`tenant_id`,`rule_id`), + KEY `idx_im_attendance_rules_state` (`tenant_id`,`state`,`effective_from`,`effective_to`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE IF NOT EXISTS `im_attendance_shifts` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT '2' COMMENT '商户ID', `shift_id` varchar(64) NOT NULL, `rule_id` varchar(64) NOT NULL, `name` varchar(128) NOT NULL, @@ -199,8 +201,8 @@ CREATE TABLE IF NOT EXISTS `im_attendance_shifts` ( `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), - UNIQUE KEY `uk_im_attendance_shifts_shift` (`shift_id`), - KEY `idx_im_attendance_shifts_rule` (`rule_id`,`state`,`sort_order`) + UNIQUE KEY `uk_im_attendance_shifts_tenant_shift` (`tenant_id`,`shift_id`), + KEY `idx_im_attendance_shifts_rule` (`tenant_id`,`rule_id`,`state`,`sort_order`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE IF NOT EXISTS `im_audit_logs` ( @@ -397,6 +399,7 @@ CREATE TABLE IF NOT EXISTS `im_favorites` ( CREATE TABLE IF NOT EXISTS `im_feedback_config` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT '2' COMMENT '商户ID', `categories` json DEFAULT NULL, `max_length` int NOT NULL DEFAULT '1000', `attachment_policy` json DEFAULT NULL, @@ -404,7 +407,8 @@ CREATE TABLE IF NOT EXISTS `im_feedback_config` ( `diagnostics_notice` text NOT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_feedback_config_tenant` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE IF NOT EXISTS `im_feedback_submissions` ( @@ -951,6 +955,7 @@ CREATE TABLE IF NOT EXISTS `im_login_failure_logs` ( CREATE TABLE IF NOT EXISTS `im_mailbox_config` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT '2' COMMENT '商户ID', `enabled` tinyint NOT NULL DEFAULT '1', `allow_anonymous` tinyint NOT NULL DEFAULT '1', `required_fields` json DEFAULT NULL, @@ -961,7 +966,8 @@ CREATE TABLE IF NOT EXISTS `im_mailbox_config` ( `notice_updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + UNIQUE KEY `uk_im_mailbox_config_tenant` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE IF NOT EXISTS `im_mailbox_submissions` ( @@ -1491,6 +1497,7 @@ CREATE TABLE IF NOT EXISTS `im_verification_codes` ( CREATE TABLE IF NOT EXISTS `im_workbench_apps` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint NOT NULL DEFAULT '2' COMMENT '商户ID', `code` varchar(64) NOT NULL, `name` varchar(128) NOT NULL, `icon_file_id` varchar(64) NOT NULL DEFAULT '', @@ -1502,8 +1509,8 @@ CREATE TABLE IF NOT EXISTS `im_workbench_apps` ( `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`), - UNIQUE KEY `uk_im_workbench_apps_code` (`code`), - KEY `idx_im_workbench_apps_enabled_order` (`enabled`,`sort_order`) + UNIQUE KEY `uk_im_workbench_apps_tenant_code` (`tenant_id`,`code`), + KEY `idx_im_workbench_apps_enabled_order` (`tenant_id`,`enabled`,`sort_order`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE IF NOT EXISTS `im_youth_mode_settings` ( @@ -1533,15 +1540,15 @@ VALUES (1, 'version', CURRENT_TIMESTAMP(3), 123) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); -- Workbench default apps. These are product navigation entries, not tenant business data. -INSERT INTO `im_workbench_apps` (`code`, `name`, `route`, `sort_order`, `unread_key`, `enabled`, `permission`) +INSERT INTO `im_workbench_apps` (`tenant_id`, `code`, `name`, `route`, `sort_order`, `unread_key`, `enabled`, `permission`) VALUES - ('attendance', '打卡', '/attendance', 10, 'attendance', 1, 'enabled'), - ('monthly_reports', '月报', '/reports/monthly', 20, 'monthly_reports', 1, 'enabled'), - ('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'), - ('organization', '部门框架', '/organization', 40, 'organization', 1, 'enabled'), - ('executive_mailbox', '举报信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled'), - ('meetings', '会议通知', '/meetings', 60, 'meetings', 1, 'enabled'), - ('meeting_notes', '会议笔记', '/meeting-notes', 70, '', 1, 'enabled') + (2, 'attendance', '打卡', '/attendance', 10, 'attendance', 1, 'enabled'), + (2, 'monthly_reports', '月报', '/reports/monthly', 20, 'monthly_reports', 1, 'enabled'), + (2, 'daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'), + (2, 'organization', '部门框架', '/organization', 40, 'organization', 1, 'enabled'), + (2, 'executive_mailbox', '举报信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled'), + (2, 'meetings', '会议通知', '/meetings', 60, 'meetings', 1, 'enabled'), + (2, 'meeting_notes', '会议笔记', '/meeting-notes', 70, '', 1, 'enabled') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `route` = VALUES(`route`), @@ -1552,17 +1559,17 @@ VALUES `updated_at` = CURRENT_TIMESTAMP(3); -- Default attendance settings. Tenants can edit or replace them later. -INSERT INTO `im_attendance_rules` (`rule_id`, `name`, `workday`, `location_required`, `wifi_required`, `allow_outside_checkin`, `appeal_allowed`) -VALUES ('att_rule_default', '默认考勤规则', 1, 0, 0, 1, 1) +INSERT INTO `im_attendance_rules` (`tenant_id`, `rule_id`, `name`, `workday`, `location_required`, `wifi_required`, `allow_outside_checkin`, `appeal_allowed`) +VALUES (2, 'att_rule_default', '默认考勤规则', '1,2,3,4,5', 0, 0, 1, 1) ON DUPLICATE KEY UPDATE `updated_at` = CURRENT_TIMESTAMP(3); -INSERT INTO `im_attendance_shifts` (`shift_id`, `rule_id`, `name`, `work_start`, `work_end`, `checkin_start`, `checkout_end`, `sort_order`) -VALUES ('shift_default', 'att_rule_default', '默认班次', '09:00:00', '18:00:00', '08:30:00', '20:00:00', 10) +INSERT INTO `im_attendance_shifts` (`tenant_id`, `shift_id`, `rule_id`, `name`, `work_start`, `work_end`, `checkin_start`, `checkout_end`, `sort_order`) +VALUES (2, 'shift_default', 'att_rule_default', '默认班次', '09:00:00', '18:00:00', '08:00:00', '19:00:00', 10) ON DUPLICATE KEY UPDATE `updated_at` = CURRENT_TIMESTAMP(3); -- Default mailbox configuration. -INSERT INTO `im_mailbox_config` (`id`, `enabled`, `allow_anonymous`, `required_fields`, `max_length`, `attachment_policy`, `notice_title`, `notice_content`) -VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。') +INSERT INTO `im_mailbox_config` (`tenant_id`, `enabled`, `allow_anonymous`, `required_fields`, `max_length`, `attachment_policy`, `notice_title`, `notice_content`) +VALUES (2, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求。') ON DUPLICATE KEY UPDATE `enabled` = VALUES(`enabled`), `allow_anonymous` = VALUES(`allow_anonymous`), @@ -1594,8 +1601,8 @@ VALUES `updated_at` = CURRENT_TIMESTAMP(3); -- Default feedback configuration. -INSERT INTO `im_feedback_config` (`id`, `categories`, `max_length`, `attachment_policy`, `allow_contact`, `diagnostics_notice`) -VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。') +INSERT INTO `im_feedback_config` (`tenant_id`, `categories`, `max_length`, `attachment_policy`, `allow_contact`, `diagnostics_notice`) +VALUES (2, '["bug","suggestion","complaint","other"]', 2000, '{"max_files":5,"purpose":"feedback"}', 1, '') ON DUPLICATE KEY UPDATE `categories` = VALUES(`categories`), `max_length` = VALUES(`max_length`), @@ -1612,15 +1619,10 @@ SET FOREIGN_KEY_CHECKS = 1; -- Keep this section in the same change set as tenant DB initialization. -- If the tenant id is generated by Admin Server, replace __TENANT_ID__ with the generated id. -INSERT INTO `hotgo`.`im_tenant` (`id`, `code`, `name`, `tenant_desc`, `db_name`, `db_dsn`, `state`, `created_by`, `updated_by`) -VALUES (2,'system', 'system merchant', 'system default merchant', 'meeting_im', '', 1, 0, 0) +INSERT INTO `hotgo`.`im_tenant` (`id`, `code`, `name`, `tenant_desc`, `state`, `created_by`, `updated_by`) +VALUES (2,'system', 'system merchant', 'system default merchant', 1, 0, 0) ON DUPLICATE KEY UPDATE `code` = VALUES(`code`), `name` = VALUES(`name`), - `db_name` = VALUES(`db_name`), `state` = VALUES(`state`), `updated_at` = CURRENT_TIMESTAMP(3); - -INSERT INTO `hotgo`.`im_tenant_config` (`tenant_id`, `max_users`, `max_groups`, `max_group_members`, `created_by`, `updated_by`) -VALUES (2, 0, 0, 0, 0, 0) - ON DUPLICATE KEY UPDATE `updated_at` = CURRENT_TIMESTAMP(3); diff --git a/scripts/apply_hotgo_tenant_db.go b/scripts/apply_hotgo_tenant_db.go index 0375bfe..9c47cf2 100644 --- a/scripts/apply_hotgo_tenant_db.go +++ b/scripts/apply_hotgo_tenant_db.go @@ -8,15 +8,30 @@ import ( "os" "strings" - _ "github.com/go-sql-driver/mysql" + mysqlDriver "github.com/go-sql-driver/mysql" ) +const sharedBusinessDatabase = "meeting_im" + +// This startup helper intentionally performs validation only. Database schema +// changes are managed by the checked-in SQL migrations and must not be applied +// implicitly while an IM Server pod is starting. func main() { - dsn := strings.TrimPrefix(os.Getenv("IM_MYSQL_DSN"), "mysql://") + rawDSN := strings.TrimSpace(os.Getenv("IM_MYSQL_DSN")) + dsn := strings.TrimPrefix(rawDSN, "mysql://") if dsn == "" { fatal("IM_MYSQL_DSN is required") } - db, err := sql.Open("mysql", dsn) + + config, err := mysqlDriver.ParseDSN(dsn) + if err != nil { + fatal(fmt.Errorf("parse IM_MYSQL_DSN: %w", err)) + } + if !strings.EqualFold(strings.TrimSpace(config.DBName), sharedBusinessDatabase) { + fatal(fmt.Errorf("IM_MYSQL_DSN must select %s; got %q", sharedBusinessDatabase, config.DBName)) + } + + db, err := sql.Open("mysql", config.FormatDSN()) if err != nil { fatal(err) } @@ -25,693 +40,18 @@ func main() { fatal(err) } - statements := []string{ - "CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci", - `CREATE TABLE IF NOT EXISTS hotgo.im_tenant( - id BIGINT NOT NULL AUTO_INCREMENT, - code VARCHAR(64) NOT NULL, - name VARCHAR(128) NOT NULL, - tenant_desc VARCHAR(256), - db_name VARCHAR(128) NOT NULL DEFAULT '', - db_dsn VARCHAR(1024) NOT NULL DEFAULT '', - state SMALLINT NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - created_by BIGINT NOT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - updated_by BIGINT NOT NULL, - PRIMARY KEY(id), - UNIQUE INDEX uk_im_tenant_code(code) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config( - id BIGINT NOT NULL AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, - max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', - max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', - max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - created_by BIGINT NOT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - updated_by BIGINT NOT NULL, - PRIMARY KEY(id), - UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id), - CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - "CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci", - // createIMSecurityTables, createIMNotificationTables and createIMRelationTables - // (server/db/mysql/adapter.go) are only reached from CreateDb's fresh-install - // path, which our pre-existing tinode DB never runs. Recreate their tables here. - `CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications( - id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - submission_id VARCHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL, - name_cipher TEXT NOT NULL, - id_type VARCHAR(32) NOT NULL, - id_number_cipher TEXT NOT NULL, - id_number_hash CHAR(64) NOT NULL, - masked_name VARCHAR(64) NOT NULL, - masked_id_number VARCHAR(64) NOT NULL, - review_reason VARCHAR(256), - verified_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_realname_submission(submission_id), - INDEX idx_im_realname_user_latest(user_id, created_at), - INDEX idx_im_realname_id_hash(id_number_hash) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_action_tokens( - id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - token_id VARCHAR(64) NOT NULL, - action VARCHAR(64) NOT NULL, - target VARCHAR(128) NOT NULL DEFAULT '', - token_hash CHAR(64) NOT NULL, - state SMALLINT NOT NULL, - expires_at DATETIME(3) NOT NULL, - consumed_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_action_tokens_token(token_id), - INDEX idx_im_action_tokens_user_action(user_id, action, target, state, expires_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_system_notifications( - id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - event_id VARCHAR(64) NOT NULL, - event_type VARCHAR(64) NOT NULL, - title VARCHAR(128) NOT NULL, - summary VARCHAR(512) NOT NULL, - action JSON, - read_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_system_notifications_event(user_id, event_id), - INDEX idx_im_system_notifications_inbox(user_id, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_friend_requests( - id BIGINT NOT NULL AUTO_INCREMENT, - request_id VARCHAR(64) NOT NULL, - applicant_id BIGINT NOT NULL, - target_id BIGINT NOT NULL, - message VARCHAR(200) NOT NULL DEFAULT '', - source VARCHAR(32) NOT NULL DEFAULT '', - source_topic VARCHAR(64) NOT NULL DEFAULT '', - status VARCHAR(16) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL DEFAULT '', - expires_at DATETIME(3) NOT NULL, - handled_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_friend_requests_request(request_id), - UNIQUE INDEX uk_im_friend_requests_idempotency(applicant_id, idempotency_key), - INDEX idx_im_friend_requests_pair(applicant_id, target_id, status, created_at), - INDEX idx_im_friend_requests_inbox(target_id, status, created_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_friendships( - id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - peer_id BIGINT NOT NULL, - status VARCHAR(16) NOT NULL, - remark VARCHAR(64) NOT NULL DEFAULT '', - topic CHAR(25) NOT NULL DEFAULT '', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_friendships_pair(user_id, peer_id), - INDEX idx_im_friendships_peer(peer_id, status) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_user_blocks( - id BIGINT NOT NULL, - blocker_id BIGINT NOT NULL, - blocked_id BIGINT NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_user_blocks_pair(blocker_id, blocked_id), - INDEX idx_im_user_blocks_blocked(blocked_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_favorites( - id BIGINT NOT NULL AUTO_INCREMENT, - favorite_id VARCHAR(64) NOT NULL, - user_id BIGINT NOT NULL, - item_type VARCHAR(16) NOT NULL, - source_topic CHAR(25) NOT NULL, - source_seq INT NOT NULL, - snapshot JSON, - attachment_refs JSON, - availability VARCHAR(16) NOT NULL DEFAULT 'available', - idempotency_key VARCHAR(128) NOT NULL DEFAULT '', - collected_at DATETIME(3) NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_favorites_favorite(favorite_id), - UNIQUE INDEX uk_im_favorites_source(user_id, source_topic, source_seq), - UNIQUE INDEX uk_im_favorites_idempotency(user_id, ((NULLIF(idempotency_key, '')))), - INDEX idx_im_favorites_inbox(user_id, id DESC), - INDEX idx_im_favorites_type(user_id, item_type, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_call_records( - id BIGINT NOT NULL AUTO_INCREMENT, - call_id VARCHAR(64) NOT NULL, - topic CHAR(25) NOT NULL, - seq_id INT NOT NULL, - caller_id BIGINT NOT NULL, - callee_id BIGINT NOT NULL, - call_type VARCHAR(16) NOT NULL, - status VARCHAR(32) NOT NULL, - end_reason VARCHAR(64) NOT NULL DEFAULT '', - duration_ms BIGINT NOT NULL DEFAULT 0, - started_at DATETIME(3) NOT NULL, - accepted_at DATETIME(3), - finished_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_call_records_call(call_id), - UNIQUE INDEX uk_im_call_records_topic_seq(topic, seq_id), - INDEX idx_im_call_records_caller(caller_id, id DESC), - INDEX idx_im_call_records_callee(callee_id, id DESC), - INDEX idx_im_call_records_status(status, updated_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps( - id BIGINT NOT NULL AUTO_INCREMENT, - code VARCHAR(64) NOT NULL, - name VARCHAR(128) NOT NULL, - icon_file_id VARCHAR(64) NOT NULL DEFAULT '', - route VARCHAR(128) NOT NULL, - sort_order INT NOT NULL DEFAULT 0, - unread_key VARCHAR(64) NOT NULL DEFAULT '', - enabled TINYINT NOT NULL DEFAULT 1, - permission VARCHAR(64) NOT NULL DEFAULT 'enabled', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_workbench_apps_code(code), - INDEX idx_im_workbench_apps_enabled_order(enabled, sort_order) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_org_departments( - id BIGINT NOT NULL AUTO_INCREMENT, - department_id VARCHAR(64) NOT NULL, - name VARCHAR(128) NOT NULL, - parent_id VARCHAR(64) NOT NULL DEFAULT 'root', - sort_order INT NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 1, - state VARCHAR(16) NOT NULL DEFAULT 'active', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_org_departments_department(department_id), - INDEX idx_im_org_departments_parent(parent_id, state, sort_order, id), - INDEX idx_im_org_departments_name(name) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_org_members( - id BIGINT NOT NULL AUTO_INCREMENT, - department_id VARCHAR(64) NOT NULL, - user_id BIGINT NOT NULL, - display_name VARCHAR(128) NOT NULL DEFAULT '', - position VARCHAR(128) NOT NULL DEFAULT '', - avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', - mobile VARCHAR(32) NOT NULL DEFAULT '', - email VARCHAR(128) NOT NULL DEFAULT '', - employment_state VARCHAR(16) NOT NULL DEFAULT 'active', - contactable TINYINT NOT NULL DEFAULT 1, - sort_order INT NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 1, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_org_members_department_user(department_id, user_id), - INDEX idx_im_org_members_department(department_id, employment_state, sort_order, id), - INDEX idx_im_org_members_user(user_id), - INDEX idx_im_org_members_display_name(display_name), - CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_meetings( - id BIGINT NOT NULL AUTO_INCREMENT, - meeting_id VARCHAR(64) NOT NULL, - organizer_id BIGINT NOT NULL, - title VARCHAR(200) NOT NULL, - start_at DATETIME(3) NOT NULL, - end_at DATETIME(3) NOT NULL, - location VARCHAR(256) NOT NULL DEFAULT '', - state VARCHAR(16) NOT NULL DEFAULT 'scheduled', - reminder_minutes JSON, - idempotency_key VARCHAR(128), - version INT NOT NULL DEFAULT 1, - canceled_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_meetings_meeting(meeting_id), - UNIQUE INDEX uk_im_meetings_idempotency(organizer_id, idempotency_key), - INDEX idx_im_meetings_organizer(organizer_id, start_at), - INDEX idx_im_meetings_state_time(state, start_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants( - id BIGINT NOT NULL AUTO_INCREMENT, - meeting_id VARCHAR(64) NOT NULL, - user_id BIGINT NOT NULL, - response VARCHAR(16) NOT NULL DEFAULT 'pending', - response_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_meeting_participants_user(meeting_id, user_id), - INDEX idx_im_meeting_participants_user_time(user_id, meeting_id), - INDEX idx_im_meeting_participants_response(meeting_id, response), - CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY(meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_daily_reports( - id BIGINT NOT NULL AUTO_INCREMENT, - report_id VARCHAR(64) NOT NULL, - author_id BIGINT NOT NULL, - report_date DATE NOT NULL, - content TEXT NOT NULL, - attachments JSON, - status VARCHAR(16) NOT NULL DEFAULT 'draft', - read_state VARCHAR(16) NOT NULL DEFAULT 'unread', - version INT NOT NULL DEFAULT 1, - submitted_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_daily_reports_report(report_id), - UNIQUE INDEX uk_im_daily_reports_author_date(author_id, report_date), - INDEX idx_im_daily_reports_author_id(author_id, id DESC), - INDEX idx_im_daily_reports_date_status(report_date, status, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports( - id BIGINT NOT NULL AUTO_INCREMENT, - report_id VARCHAR(64) NOT NULL, - author_id BIGINT NOT NULL, - report_month CHAR(7) NOT NULL, - work_summary TEXT NOT NULL, - next_plan TEXT NOT NULL, - attachments JSON, - status VARCHAR(16) NOT NULL DEFAULT 'draft', - read_state VARCHAR(16) NOT NULL DEFAULT 'unread', - version INT NOT NULL DEFAULT 1, - submitted_at DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_monthly_reports_report(report_id), - UNIQUE INDEX uk_im_monthly_reports_author_month(author_id, report_month), - INDEX idx_im_monthly_reports_author_id(author_id, id DESC), - INDEX idx_im_monthly_reports_month_status(report_month, status, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config( - id BIGINT NOT NULL AUTO_INCREMENT, - enabled TINYINT NOT NULL DEFAULT 1, - allow_anonymous TINYINT NOT NULL DEFAULT 1, - required_fields JSON, - max_length INT NOT NULL DEFAULT 5000, - attachment_policy JSON, - notice_title VARCHAR(128) NOT NULL DEFAULT '', - notice_content TEXT NOT NULL, - notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions( - id BIGINT NOT NULL AUTO_INCREMENT, - submission_id VARCHAR(64) NOT NULL, - sender_id BIGINT NOT NULL, - subject VARCHAR(200) NOT NULL, - content TEXT NOT NULL, - anonymous TINYINT NOT NULL DEFAULT 0, - attachment_file_ids JSON, - notice_version VARCHAR(64) NOT NULL DEFAULT '', - idempotency_key VARCHAR(128) NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'received', - submitted_at DATETIME(3) NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_mailbox_submissions_submission(submission_id), - UNIQUE INDEX uk_im_mailbox_submissions_idempotency(sender_id, idempotency_key), - INDEX idx_im_mailbox_submissions_sender(sender_id, id DESC), - INDEX idx_im_mailbox_submissions_status(status, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_user_profiles( - id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', - nickname VARCHAR(64) NOT NULL DEFAULT '', - birth_date DATE, - signature VARCHAR(256) NOT NULL DEFAULT '', - gender VARCHAR(16) NOT NULL DEFAULT '', - region VARCHAR(128) NOT NULL DEFAULT '', - email VARCHAR(128) NOT NULL DEFAULT '', - public_account VARCHAR(64) NOT NULL DEFAULT '', - version INT NOT NULL DEFAULT 1, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_user_profiles_user(user_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings( - id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - add_me_methods JSON, - profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends', - last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends', - read_receipts TINYINT NOT NULL DEFAULT 1, - friend_request_verification TINYINT NOT NULL DEFAULT 1, - group_invite_verification TINYINT NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 1, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_user_privacy_settings_user(user_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_app_releases( - id BIGINT NOT NULL AUTO_INCREMENT, - platform VARCHAR(32) NOT NULL, - channel VARCHAR(32) NOT NULL DEFAULT 'stable', - latest_version VARCHAR(64) NOT NULL, - min_supported_version VARCHAR(64) NOT NULL, - update_type VARCHAR(16) NOT NULL DEFAULT 'none', - release_notes TEXT NOT NULL, - store_url VARCHAR(512) NOT NULL DEFAULT '', - package_url VARCHAR(512) NOT NULL DEFAULT '', - checksum VARCHAR(128) NOT NULL DEFAULT '', - remind_after_seconds INT NOT NULL DEFAULT 0, - published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens( - id BIGINT NOT NULL, - device_id VARCHAR(128) NOT NULL, - user_id BIGINT NOT NULL, - provider VARCHAR(32) NOT NULL, - token VARCHAR(512) NOT NULL, - app_version VARCHAR(64) NOT NULL DEFAULT '', - platform VARCHAR(32) NOT NULL DEFAULT '', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_device_push_tokens_device(device_id), - UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token), - INDEX idx_im_device_push_tokens_user(user_id, updated_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings( - id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - purpose VARCHAR(64) NOT NULL, - password_hash VARBINARY(255) NOT NULL, - enabled TINYINT NOT NULL DEFAULT 0, - policy_version INT NOT NULL DEFAULT 1, - restrictions JSON, - enabled_at DATETIME(3), - failed_attempts INT NOT NULL DEFAULT 0, - locked_until DATETIME(3), - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_youth_mode_settings_user_purpose(user_id, purpose) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges( - id BIGINT NOT NULL AUTO_INCREMENT, - challenge_id VARCHAR(64) NOT NULL, - user_id BIGINT NOT NULL, - action VARCHAR(64) NOT NULL, - level VARCHAR(16) NOT NULL, - notice_code VARCHAR(64) NOT NULL, - notice_params JSON, - state VARCHAR(16) NOT NULL DEFAULT 'pending', - expires_at DATETIME(3) NOT NULL, - decision VARCHAR(16) NOT NULL DEFAULT '', - action_token_id VARCHAR(64) NOT NULL DEFAULT '', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_risk_challenges_challenge(challenge_id), - INDEX idx_im_risk_challenges_user_state(user_id, state, expires_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons( - id BIGINT NOT NULL AUTO_INCREMENT, - target_type VARCHAR(16) NOT NULL, - reason_code VARCHAR(64) NOT NULL, - label VARCHAR(128) NOT NULL, - requires_description TINYINT NOT NULL DEFAULT 0, - allows_evidence TINYINT NOT NULL DEFAULT 1, - sort_order INT NOT NULL DEFAULT 0, - enabled TINYINT NOT NULL DEFAULT 1, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_abuse_reasons_type_code(target_type, reason_code), - INDEX idx_im_abuse_reasons_type_order(target_type, enabled, sort_order) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports( - id BIGINT NOT NULL AUTO_INCREMENT, - report_id VARCHAR(64) NOT NULL, - reporter_id BIGINT NOT NULL, - target_type VARCHAR(16) NOT NULL, - target_user_id BIGINT NOT NULL DEFAULT 0, - target_topic VARCHAR(32) NOT NULL DEFAULT '', - target_seq INT NOT NULL DEFAULT 0, - reason_code VARCHAR(64) NOT NULL, - description TEXT NOT NULL, - evidence_file_ids JSON, - snapshot JSON, - idempotency_key VARCHAR(128) NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'received', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_abuse_reports_report(report_id), - UNIQUE INDEX uk_im_abuse_reports_idempotency(reporter_id, idempotency_key), - INDEX idx_im_abuse_reports_target(target_type, target_topic, target_seq, target_user_id), - INDEX idx_im_abuse_reports_reporter(reporter_id, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_feedback_config( - id BIGINT NOT NULL AUTO_INCREMENT, - categories JSON, - max_length INT NOT NULL DEFAULT 1000, - attachment_policy JSON, - allow_contact TINYINT NOT NULL DEFAULT 1, - diagnostics_notice TEXT NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions( - id BIGINT NOT NULL AUTO_INCREMENT, - feedback_id VARCHAR(64) NOT NULL, - user_id BIGINT NOT NULL, - category VARCHAR(64) NOT NULL, - content TEXT NOT NULL, - contact VARCHAR(128) NOT NULL DEFAULT '', - attachment_file_ids JSON, - client_context JSON, - include_diagnostics TINYINT NOT NULL DEFAULT 0, - idempotency_key VARCHAR(128) NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'received', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - PRIMARY KEY(id), - UNIQUE INDEX uk_im_feedback_submissions_feedback(feedback_id), - UNIQUE INDEX uk_im_feedback_submissions_idempotency(user_id, idempotency_key), - INDEX idx_im_feedback_submissions_user(user_id, id DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - } - for _, stmt := range statements { - if _, err = db.Exec(stmt); err != nil { - fatal(fmt.Errorf("exec failed: %s: %w", stmt, err)) - } - } - for _, stmt := range []string{ - "ALTER TABLE hotgo.im_tenant ADD COLUMN tenant_desc VARCHAR(256) AFTER name", - "ALTER TABLE hotgo.im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc", - "ALTER TABLE hotgo.im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name", - "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled TINYINT NOT NULL DEFAULT 0 AFTER password_hash", - "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN policy_version INT NOT NULL DEFAULT 1 AFTER enabled", - "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN restrictions JSON AFTER policy_version", - "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions", - "ALTER TABLE tinode.im_users ADD COLUMN phone VARCHAR(32) NULL AFTER useragent", - "ALTER TABLE tinode.im_user_profiles ADD COLUMN email VARCHAR(128) NOT NULL DEFAULT '' AFTER region", - "ALTER TABLE tinode.im_user_privacy_settings ADD COLUMN friend_request_verification TINYINT NOT NULL DEFAULT 1 AFTER read_receipts", - "ALTER TABLE tinode.im_user_privacy_settings ADD COLUMN group_invite_verification TINYINT NOT NULL DEFAULT 0 AFTER read_receipts", - } { - if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") { - fatal(fmt.Errorf("exec failed: %s: %w", stmt, err)) - } - } - if _, err = db.Exec(`INSERT INTO hotgo.im_tenant(code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by) - VALUES('default','Default Tenant',NULL,'tinode','',1,0,0) - ON DUPLICATE KEY UPDATE name=VALUES(name), db_name=VALUES(db_name), updated_at=CURRENT_TIMESTAMP(3)`); err != nil { + var database string + if err = db.QueryRow("SELECT DATABASE()").Scan(&database); err != nil { fatal(err) } - if _, err = db.Exec(`INSERT INTO hotgo.im_tenant_config(tenant_id,max_users,max_groups,max_group_members,created_by,updated_by) - SELECT id,0,0,0,0,0 FROM hotgo.im_tenant WHERE code='default' - ON DUPLICATE KEY UPDATE updated_at=CURRENT_TIMESTAMP(3)`); err != nil { - fatal(err) - } - if _, err = db.Exec(`INSERT INTO tinode.im_workbench_apps(code,name,route,sort_order,unread_key,enabled,permission) - VALUES - ('attendance','打卡','/attendance',10,'attendance',1,'enabled'), - ('monthly_reports','月报','/reports/monthly',20,'monthly_reports',1,'enabled'), - ('daily_reports','日报','/reports/daily',30,'daily_reports',1,'enabled'), - ('organization','部门框架','/organization',40,'organization',1,'enabled'), - ('executive_mailbox','举报信箱','/executive-mailbox',50,'executive_mailbox',1,'enabled'), - ('meetings','会议通知','/meetings',60,'meetings',1,'enabled'), - ('meeting_notes','会议笔记','/meeting-notes',70,'',1,'enabled') - ON DUPLICATE KEY UPDATE name=VALUES(name),route=VALUES(route),sort_order=VALUES(sort_order),unread_key=VALUES(unread_key),updated_at=CURRENT_TIMESTAMP(3)`); err != nil { - fatal(err) - } - if _, err = db.Exec(`INSERT INTO tinode.im_mailbox_config(id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content) - VALUES(1,1,1,'["subject","content"]',5000,'{"max_files":5,"purpose":"executive_mailbox"}','总经理信箱','请如实填写诉求,匿名提交将按企业政策保护展示身份。') - ON DUPLICATE KEY UPDATE updated_at=updated_at`); err != nil { - fatal(err) - } - if _, err = db.Exec(`INSERT INTO tinode.im_app_releases(platform,channel,latest_version,min_supported_version,update_type,release_notes,remind_after_seconds) - VALUES - ('ios','stable','1.0.0','1.0.0','none','当前已是最新版本。',0), - ('android','stable','1.0.0','1.0.0','none','当前已是最新版本。',0) - ON DUPLICATE KEY UPDATE updated_at=CURRENT_TIMESTAMP(3)`); err != nil { - fatal(err) - } - if _, err = db.Exec(`INSERT INTO tinode.im_abuse_reasons(target_type,reason_code,label,requires_description,allows_evidence,sort_order) - VALUES - ('user','fraud','诈骗欺诈',0,1,10), - ('user','harassment','骚扰辱骂',0,1,20), - ('user','illegal','违法违规',1,1,30), - ('group','fraud','诈骗欺诈',0,1,10), - ('group','spam','垃圾广告',0,1,20), - ('group','illegal','违法违规',1,1,30), - ('message','fraud','诈骗欺诈',0,1,10), - ('message','spam','垃圾广告',0,1,20), - ('message','harassment','骚扰辱骂',0,1,30), - ('message','illegal','违法违规',1,1,40) - ON DUPLICATE KEY UPDATE label=VALUES(label),requires_description=VALUES(requires_description),allows_evidence=VALUES(allows_evidence),sort_order=VALUES(sort_order),updated_at=CURRENT_TIMESTAMP(3)`); err != nil { - fatal(err) - } - if _, err = db.Exec(`INSERT INTO tinode.im_feedback_config(id,categories,max_length,attachment_policy,allow_contact,diagnostics_notice) - VALUES(1,'["bug","suggestion","complaint","other"]',1000,'{"max_files":5,"purpose":"feedback"}',1,'仅在你同意时上传脱敏诊断信息。') - ON DUPLICATE KEY UPDATE updated_at=updated_at`); err != nil { - fatal(err) - } - if err = dropTenantIDColumns(db, "tinode"); err != nil { - fatal(err) + if !strings.EqualFold(database, sharedBusinessDatabase) { + fatal(fmt.Errorf("connected database is %q, want %q", database, sharedBusinessDatabase)) } - var code, dbName string - var state int - if err = db.QueryRow("SELECT code, db_name, state FROM hotgo.im_tenant WHERE code='default'").Scan(&code, &dbName, &state); err != nil { - fatal(err) - } - fmt.Printf("hotgo.im_tenant default: code=%s db_name=%s state=%d\n", code, dbName, state) - rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA='tinode' AND COLUMN_NAME='tenant_id' - ORDER BY table_name`) - if err != nil { - fatal(err) - } - defer rows.Close() - var tables []string - for rows.Next() { - var table string - if err = rows.Scan(&table); err != nil { - fatal(err) - } - tables = append(tables, table) - } - if err = rows.Err(); err != nil { - fatal(err) - } - if len(tables) == 0 { - fmt.Println("tinode tenant_id columns: none") - } else { - fmt.Printf("tinode tenant_id columns: %s\n", strings.Join(tables, ",")) - } + fmt.Printf("IM shared database validated: %s\n", database) } func fatal(v any) { fmt.Fprintln(os.Stderr, v) os.Exit(1) } - -func dropTenantIDColumns(db *sql.DB, schema string) error { - fks, err := queryPairs(db, `SELECT DISTINCT table_name, constraint_name - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND REFERENCED_TABLE_NAME IS NOT NULL`, - schema) - if err != nil { - return err - } - for _, fk := range fks { - if _, err = db.Exec("ALTER TABLE " + quoteIdent(schema) + "." + quoteIdent(fk[0]) + " DROP FOREIGN KEY " + quoteIdent(fk[1])); err != nil { - return fmt.Errorf("drop foreign key %s.%s: %w", fk[0], fk[1], err) - } - } - - indexes, err := queryPairs(db, `SELECT DISTINCT table_name, index_name - FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND INDEX_NAME<>'PRIMARY'`, - schema) - if err != nil { - return err - } - for _, idx := range indexes { - if _, err = db.Exec("ALTER TABLE " + quoteIdent(schema) + "." + quoteIdent(idx[0]) + " DROP INDEX " + quoteIdent(idx[1])); err != nil { - return fmt.Errorf("drop index %s.%s: %w", idx[0], idx[1], err) - } - } - - rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' - ORDER BY table_name`, schema) - if err != nil { - return err - } - defer rows.Close() - var tables []string - for rows.Next() { - var table string - if err = rows.Scan(&table); err != nil { - return err - } - tables = append(tables, table) - } - if err = rows.Err(); err != nil { - return err - } - for _, table := range tables { - if _, err = db.Exec("ALTER TABLE " + quoteIdent(schema) + "." + quoteIdent(table) + " DROP COLUMN tenant_id"); err != nil { - return fmt.Errorf("drop column %s.tenant_id: %w", table, err) - } - } - return nil -} - -func queryPairs(db *sql.DB, query string, args ...any) ([][2]string, error) { - rows, err := db.Query(query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - var pairs [][2]string - for rows.Next() { - var first, second string - if err = rows.Scan(&first, &second); err != nil { - return nil, err - } - pairs = append(pairs, [2]string{first, second}) - } - return pairs, rows.Err() -} - -func quoteIdent(name string) string { - return "`" + strings.ReplaceAll(name, "`", "``") + "`" -} diff --git a/server/db/mongodb/adapter.go b/server/db/mongodb/adapter.go index cc959fe..659f135 100644 --- a/server/db/mongodb/adapter.go +++ b/server/db/mongodb/adapter.go @@ -43,7 +43,7 @@ const ( adapterName = "mongodb" defaultHost = "localhost:27017" - defaultDatabase = "tinode" + defaultDatabase = "meeting_im" defaultMaxResults = 1024 // This is capped by the Session's send queue limit (128). diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index 58666e4..697f9ce 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -248,6 +248,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error { if a.dbName == "" { a.dbName = defaultDatabase } + if strings.EqualFold(strings.TrimSpace(a.dbName), "tinode") { + return errors.New("mysql database tinode is deprecated; use meeting_im") + } config.Config.DBName = a.dbName a.baseConfig = &config a.tenantDBRouting = config.TenantDBRouting diff --git a/server/db/mysql/adapter_dsn_test.go b/server/db/mysql/adapter_dsn_test.go index 820b23f..b737720 100644 --- a/server/db/mysql/adapter_dsn_test.go +++ b/server/db/mysql/adapter_dsn_test.go @@ -4,6 +4,8 @@ package mysql import ( + "encoding/json" + "strings" "testing" ms "github.com/go-sql-driver/mysql" @@ -17,17 +19,17 @@ func TestNormalizeMySQLDSN(t *testing.T) { }{ { name: "go driver dsn", - dsn: "root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true", + dsn: "root:secret@tcp(127.0.0.1:3306)/meeting_im?parseTime=true", user: "root", }, { name: "url scheme dsn", - dsn: "mysql://root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true", + dsn: "mysql://root:secret@tcp(127.0.0.1:3306)/meeting_im?parseTime=true", user: "root", }, { name: "legacy scheme dsn", - dsn: "mysql:root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true", + dsn: "mysql:root:secret@tcp(127.0.0.1:3306)/meeting_im?parseTime=true", user: "root", }, } @@ -43,3 +45,13 @@ func TestNormalizeMySQLDSN(t *testing.T) { }) } } + +func TestOpenRejectsDeprecatedTinodeDatabase(t *testing.T) { + config := json.RawMessage(`{"dsn":"mysql://root:secret@tcp(127.0.0.1:3306)/tinode?parseTime=true"}`) + adp := &adapter{} + + err := adp.Open(config) + if err == nil || !strings.Contains(err.Error(), "tinode is deprecated") { + t.Fatalf("Open() error = %v, want deprecated database error", err) + } +} diff --git a/server/db/rethinkdb/adapter.go b/server/db/rethinkdb/adapter.go index dc60878..dfb219a 100644 --- a/server/db/rethinkdb/adapter.go +++ b/server/db/rethinkdb/adapter.go @@ -36,7 +36,7 @@ const ( adapterName = "rethinkdb" defaultHost = "localhost:28015" - defaultDatabase = "tinode" + defaultDatabase = "meeting_im" defaultMaxResults = 1024 // This is capped by the Session's send queue limit (128). diff --git a/server/im_http.go b/server/im_http.go index 8868813..e90d52b 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -3816,7 +3816,7 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) { source := strings.TrimSpace(body.Source) sourceTopic := strings.TrimSpace(body.SourceTopic) switch source { - case "search", "qr": + case "search", "qr", "profile_card": if sourceTopic != "" { writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "source_topic is only valid for group source", nil) return @@ -6037,6 +6037,10 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, identifier s writeIMSuccess(wrt, req, blockedUserProfilePayload(user, ctx)) return } + if ctx.Relationship == types.RelationshipNone { + writeIMSuccess(wrt, req, limitedUserProfilePayload(user, ctx, privacy)) + return + } writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "profile is private", nil) return } @@ -9030,6 +9034,39 @@ func blockedUserProfilePayload(user *types.User, ctx *types.RelationshipContext) return data } +func limitedUserProfilePayload(user *types.User, ctx *types.RelationshipContext, privacy *types.UserPrivacySettings) map[string]any { + data := userSummaryPayload(user, "") + data["username"] = user.Username + if original := userOriginalDisplayName(user); original != "" { + data["profile_display_name"] = original + data["original_display_name"] = original + } + data["relationship"] = ctx.Relationship + data["topic"] = ctx.Topic + data["capabilities"] = map[string]any{ + "can_send": false, + "can_call": false, + "can_add_friend": ctx.Relationship == types.RelationshipNone && imPrivacyAllowsAdd(privacy, "profile_card"), + } + if ctx.RequestID != "" { + data["request_id"] = ctx.RequestID + } + if public, ok := user.Public.(map[string]any); ok { + copyLimitedProfileFields(data, public) + } else if public, ok := user.Public.(types.KVMap); ok { + copyLimitedProfileFields(data, map[string]any(public)) + } + return data +} + +func copyLimitedProfileFields(data map[string]any, public map[string]any) { + for _, key := range []string{"nickname", "public_account"} { + if value, ok := public[key]; ok { + data[key] = value + } + } +} + func copyOptionalProfileFields(data map[string]any, public map[string]any) { for _, key := range []string{"nickname", "gender", "region", "public_account", "signature"} { if value, ok := public[key]; ok { diff --git a/server/im_http_test.go b/server/im_http_test.go index ee9afd9..9b3a902 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -7549,6 +7549,37 @@ func TestServeIMFriendRequestCreate(t *testing.T) { } } +func TestServeIMFriendRequestCreateAllowsProfileCardSource(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockStore := expectIMBearer(t, ctrl, "token-1", 7, 1) + mockStore.EXPECT().GetUidString().Return("card-1") + oldRelations := store.Relations + oldProfiles := store.Profiles + relations := &testRelationsStore{context: &types.RelationshipContext{Relationship: types.RelationshipNone}} + store.Relations = relations + store.Profiles = &testProfilesStore{} + defer func() { + store.Relations = oldRelations + store.Profiles = oldProfiles + }() + + req := httptest.NewRequest(http.MethodPost, "/v1/im/friend-requests", + bytes.NewBufferString(`{"target_user_id":"`+types.Uid(2).UserId()+`","message":"你好","source":"profile_card"}`)) + req.Header.Set("Authorization", "Bearer token-1") + req.Header.Set("Idempotency-Key", "fr-card-1") + rec := httptest.NewRecorder() + + serveIMFriendRequests(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + if relations.created == nil || relations.created.Source != "profile_card" || relations.created.SourceTopic != "" { + t.Fatalf("unexpected friend request created from profile card: %#v", relations.created) + } +} + func TestServeIMFriendRequestCreateReturnsNotFoundForMissingTarget(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -8105,6 +8136,70 @@ func TestServeIMUserProfile(t *testing.T) { } } +func TestServeIMUserProfilePrivateNonFriendReturnsLimitedProfile(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + expectIMBearer(t, ctrl, "token-1", 7, 1) + oldUsers := store.Users + oldRelations := store.Relations + oldProfiles := store.Profiles + users := mock_store.NewMockUsersPersistenceInterface(ctrl) + peer := types.Uid(2) + users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{ + ObjHeader: types.ObjHeader{Id: peer.String()}, + TenantID: 7, + Username: "alice7", + Public: map[string]any{ + "display_name": "Alice", + "nickname": "Alice", + "avatar_file_id": "file-1", + "signature": "hello", + "gender": "female", + "region": "Shanghai", + "public_account": "alice001", + }, + }, nil) + store.Users = users + store.Relations = &testRelationsStore{context: &types.RelationshipContext{ + TenantID: 7, + UserID: 1, + PeerID: peer, + Topic: types.Uid(1).P2PName(peer), + Relationship: types.RelationshipNone, + CanSend: false, + }} + store.Profiles = &testProfilesStore{} + defer func() { + store.Users = oldUsers + store.Relations = oldRelations + store.Profiles = oldProfiles + }() + + req := httptest.NewRequest(http.MethodGet, "/v1/im/users/"+peer.UserId()+"/profile", nil) + req.Header.Set("Authorization", "Bearer token-1") + rec := httptest.NewRecorder() + + serveIMUserResource(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) + if data["relationship"] != types.RelationshipNone || data["display_name"] != "Alice" || + data["avatar_file_id"] != "file-1" || data["public_account"] != "alice001" || data["username"] != "alice7" { + t.Fatalf("unexpected limited profile response: %#v", data) + } + capabilities := data["capabilities"].(map[string]any) + if capabilities["can_send"] != false || capabilities["can_call"] != false || capabilities["can_add_friend"] != true { + t.Fatalf("unexpected limited profile capabilities: %#v", data) + } + for _, field := range []string{"signature", "gender", "region"} { + if _, ok := data[field]; ok { + t.Fatalf("limited profile should omit private field %q: %#v", field, data) + } + } +} + func TestServeIMUserProfileBlockedByMeReturnsMinimalProfile(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/server/meetingim.conf b/server/meetingim.conf index 3d6c6a1..e51c15a 100644 --- a/server/meetingim.conf +++ b/server/meetingim.conf @@ -81,7 +81,7 @@ "use_adapter": "mysql", "adapters": { "mysql": { - "dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci", + "dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci", "tenant_registry_db": "hotgo", "tenant_db_routing": true, "max_open_conns": 64, diff --git a/server/store/local_message_crypto_integration_test.go b/server/store/local_message_crypto_integration_test.go index 62b3e7f..51a0e73 100644 --- a/server/store/local_message_crypto_integration_test.go +++ b/server/store/local_message_crypto_integration_test.go @@ -72,7 +72,7 @@ func TestLocalMySQLMessageCryptoFlow(t *testing.T) { "max_results":1024, "use_adapter":"mysql", "adapters":{"mysql":{ - "dsn":"mysql://root:root@tcp(127.0.0.1:3306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci", + "dsn":"mysql://root:root@tcp(127.0.0.1:3306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci", "tenant_registry_db":"hotgo", "tenant_db_routing":true, "sql_timeout":10 @@ -86,7 +86,7 @@ func TestLocalMySQLMessageCryptoFlow(t *testing.T) { store.SetAdminInternalConfigProvider(localCryptoProvider{key: base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef"))}) store.InvalidateMessageEncryptionKeys(1) - db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci") + db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci") if err != nil { t.Fatal(err) } diff --git a/server/tinode.conf b/server/tinode.conf index 3d6c6a1..e51c15a 100644 --- a/server/tinode.conf +++ b/server/tinode.conf @@ -81,7 +81,7 @@ "use_adapter": "mysql", "adapters": { "mysql": { - "dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci", + "dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci", "tenant_registry_db": "hotgo", "tenant_db_routing": true, "max_open_conns": 64, diff --git a/tinode-db/tinode.conf b/tinode-db/tinode.conf index b16ee08..32128e3 100644 --- a/tinode-db/tinode.conf +++ b/tinode-db/tinode.conf @@ -5,21 +5,21 @@ "use_adapter": "mysql", "adapters": { "postgres": { - "database": "tinode", - "dsn": "postgresql://postgres:postgres@localhost:5432/tinode?sslmode=disable" + "database": "meeting_im", + "dsn": "postgresql://postgres:postgres@localhost:5432/meeting_im?sslmode=disable" }, "mysql": { - "database": "tinode", - "dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci", + "database": "meeting_im", + "dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci", "tenant_registry_db": "hotgo", "tenant_db_routing": true }, "rethinkdb": { - "database": "tinode", + "database": "meeting_im", "addresses": "localhost:28015" }, "mongodb": { - "database": "tinode", + "database": "meeting_im", "addresses": "localhost:27017", //"replica_set": "rs0", //"auth_source": "admin",