feat: add file safety config integration

This commit is contained in:
richjohnrambo
2026-08-23 23:50:21 +08:00
parent 7b942da975
commit ed9413d923
20 changed files with 1407 additions and 2 deletions
@@ -0,0 +1,161 @@
# IM 文件安全第三方接入记录
更新时间:2026-08-23
## 1. 接入范围
本次记录三条文件与安全链路:
1. 阿里云 OSS:用于文件对象存储和签名下载,media handler 标识为 `oss`。
2. 阿里云内容安全增强版:用于文件内容审核,当前 provider 标识为 `aliyun_green`,任务类型为 `content`。
3. 阿里云云安全中心恶意文件检测:用于病毒、木马、WebShell、勒索等恶意文件检测,当前 provider 标识为 `aliyun_sas`,任务类型为 `malware`。
密钥、账号、Bucket 等敏感信息不写入本文档,也不写入源码。运行环境应通过配置中心或部署环境注入。
## 2. 阿里云 OSS 接入
新增媒体 handler:`server/media/oss`。
当前支持:
- `Upload`:上传文件到 OSS Bucket,并把 OSS object key 写入 `FileDef.Location`。
- `Headers`:对 GET/HEAD 生成 OSS 签名 URL,并返回 `308 Permanent Redirect`。
- `Delete`:按 object key 批量删除 OSS 对象。
- `GetIdFromUrl`:从 Tinode 文件 URL 中解析文件 ID。
对象路径规则:
```text
tenants/{tenantID}/{yyyy}/{mm}/{fileID}
```
配置字段:
| 字段 | 说明 |
| --- | --- |
| `access_key_id` | 阿里云 AccessKey ID,部署环境注入 |
| `access_key_secret` | 阿里云 AccessKey Secret,部署环境注入 |
| `endpoint` | OSS 普通 Bucket endpoint |
| `bucket` | OSS Bucket 名称 |
| `region_id` | OSS Bucket 所属地域 |
| `serve_url` | Tinode 对外文件 URL 前缀,默认 `/v0/file/s/` |
| `presign_ttl` | 签名 URL 有效秒数,默认 `120` |
| `cache_control` | 下载缓存头 |
| `cors_origins` | CORS 允许来源 |
| `verify_bucket` | 启动时是否联网校验 Bucket 存在 |
| `use_sys_config` | 为 `true` 时从 `hotgo.hg_sys_config` 读取 OSS 配置 |
当前 `server/tinode.conf` 仍默认使用 `fs`。切换 OSS 时,将 `media.use_handler` 改为 `oss`,并在 `media.handlers.oss` 中设置 `use_sys_config=true` 后,运行时会从后台库 `hotgo.hg_sys_config` 的 `im_third_party` 分组读取 OSS 配置。
代码入口:
- Handler 实现:`server/media/oss/oss.go`
- 后台配置读取:`store.ThirdPartyConfig.Get("im_third_party", ...)`
- Handler 注册:`server/main.go`
- 单元测试:`server/media/oss/oss_tenant_test.go`
注意事项:
- 不使用 AccessPoint endpoint,当前按普通 Bucket endpoint 接入。
- 不在源码或文档中保存真实 AK/SK。
- 下载链路采用签名 URL 重定向,不由 Tinode 代理大文件内容。
## 3. 数据库状态机
新增租户业务库表:`im_file_review_tasks`。
独立租户库方案下,该表不包含 `tenant_id`。每个租户库保存本租户自己的文件审核任务。
核心字段:
| 字段 | 说明 |
| --- | --- |
| `task_id` | 服务端生成的审核任务 ID |
| `file_id` | 对应 `im_fileuploads.id` |
| `user_id` | 上传用户 |
| `task_type` | `content` 或 `malware` |
| `provider` | `aliyun_green` 或 `aliyun_sas` |
| `object_name` | OSS 对象名快照 |
| `mime_type` | 文件 MIME 快照 |
| `file_size` | 文件大小快照 |
| `provider_task_id` | 第三方返回的任务 ID |
| `status` | `pending`、`running`、`clean`、`risky`、`failed` |
| `risk_level` | 第三方风险等级 |
| `risk_labels` | 命中的风险标签 JSON |
| `result_json` | 第三方原始结果 JSON |
| `attempts` / `next_retry_at` | 重试控制 |
| `completed_at` | 审核完成时间 |
唯一约束:
- `task_id` 全局唯一。
- `file_id + task_type + provider` 唯一,避免同一文件重复创建同类任务。
## 4. 当前代码入口
文件上传完成后,在数据库事务内自动创建审核任务:
- 普通/独立库路径:`server/db/mysql/adapter.go` 的 `FileFinishUpload`
- 共享库兼容路径:`server/db/mysql/tenant_adapter.go` 的 `TenantFileFinishUpload`
- 建表逻辑:`server/db/mysql/file_review_schema.go`
- 任务插入:`server/db/mysql/file_review_tasks.go`
- 状态常量和结构:`server/store/types/safety.go`
当前策略是异步审核:上传成功后先生成 `pending` 任务,不阻塞上传响应。
## 5. 阿里云内容安全接入
新增封装包:`server/filesafety/aliyun`。
当前支持:
- `SubmitOSSFile`:提交同账号 OSS 文件审核任务。
- `PollOSSFile`:轮询文件审核结果。
- 默认服务:`document_detection`。
- 支持参数:`ossBucketName`、`ossObjectName`、`ossRegionId`、`dataId`。
文件安全配置通过 `server/filesafety.LoadConfig` 从后台库 `hotgo.hg_sys_config` 的 `im_third_party` 分组读取。内容安全启用后,`server/filesafety.NewGreenClientFromConfig` 可直接构建阿里云内容安全客户端;恶意文件检测当前先落配置和任务状态,具体云安全中心 API worker 后续补充。
结果映射:
| 阿里云风险等级 | 内部状态 |
| --- | --- |
| 空 | `running` |
| `none` / `low` | `clean` |
| 其他风险等级 | `risky` |
后续 worker 需要读取 `provider=aliyun_green,status=pending/running` 的任务,调用提交和轮询接口,并回写 `provider_task_id/status/risk_level/risk_labels/result_json`。
## 6. 阿里云恶意文件检测接入
当前已完成状态机和任务落库,上传成功会创建:
- `task_type=malware`
- `provider=aliyun_sas`
- `status=pending`
后续还需要补云安全中心恶意文件检测 SDK/HTTP client,并实现 worker 提交、轮询和结果映射。
建议映射:
| 三方结果 | 内部状态 |
| --- | --- |
| 未完成 / 排队中 | `running` |
| 无风险 | `clean` |
| 命中恶意类型 | `risky` |
| 调用失败且可重试 | `pending`,更新 `attempts/next_retry_at` |
| 调用失败且不可重试 | `failed` |
## 7. 本地库变更
已在本地 `tinode` 业务库创建并更新 `im_file_review_tasks` 表。
后续新库或已有库启动时,会通过 MySQL adapter 的 ensure 流程自动补表。
## 8. 后续待办
1. 实现文件安全 worker:批量领取 `pending` 任务、提交第三方、轮询结果、回写状态。
2. 接入阿里云云安全中心恶意文件检测 client。
3. 增加审核策略:是否阻断下载、是否只告警、是否对高风险文件自动下架。
4. 在文件下载授权处读取审核结果,按策略限制 `risky/failed/running` 文件访问。
5. 增加后台查询与人工处置接口,用于查看任务、重试、放行、下架。
@@ -0,0 +1,211 @@
-- IM 第三方配置字典与文件安全表
-- 更新时间:2026-08-23
--
-- 执行范围:
-- 1. hotgo 控制库:后台字典项与第三方配置项。
-- 2. tinode 租户业务库:文件安全审核任务表。
--
-- 注意:
-- - 本脚本不写入真实 AccessKey、Secret、密码等敏感值。
-- - 独立租户库方案下,业务表不包含 tenant_id。
-- - 多租户时,im_file_review_tasks 需要在每个租户业务库执行。
USE hotgo;
-- 1. 后台字典类型
INSERT INTO hg_sys_dict_type
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 0, 'IM 存储服务商', 'im_storage_provider', 210, 'IM 文件存储 provider 字典', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_type WHERE `type` = 'im_storage_provider');
INSERT INTO hg_sys_dict_type
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 0, 'IM 文件审核服务商', 'im_file_review_provider', 220, 'IM 文件审核 provider 字典', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_type WHERE `type` = 'im_file_review_provider');
INSERT INTO hg_sys_dict_type
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 0, 'IM 文件审核任务类型', 'im_file_review_task_type', 230, 'IM 文件审核任务类型字典', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_type WHERE `type` = 'im_file_review_task_type');
INSERT INTO hg_sys_dict_type
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT 0, 'IM 文件审核状态', 'im_file_review_status', 240, 'IM 文件审核任务状态字典', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_type WHERE `type` = 'im_file_review_status');
-- 2. 后台字典数据
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '阿里云 OSS', 'oss', 'string', 'im_storage_provider', 'success', 1, 10, '阿里云对象存储 OSS', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_storage_provider' AND `value` = 'oss');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '阿里云内容安全增强版', 'aliyun_green', 'string', 'im_file_review_provider', 'info', 1, 10, 'FileModeration + document_detection', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_provider' AND `value` = 'aliyun_green');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '阿里云云安全中心', 'aliyun_sas', 'string', 'im_file_review_provider', 'warning', 1, 20, '恶意文件检测', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_provider' AND `value` = 'aliyun_sas');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '内容审核', 'content', 'string', 'im_file_review_task_type', 'info', 1, 10, '文件内容审核任务', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_task_type' AND `value` = 'content');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '恶意文件检测', 'malware', 'string', 'im_file_review_task_type', 'warning', 1, 20, '病毒、木马、WebShell 等恶意文件检测任务', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_task_type' AND `value` = 'malware');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '待处理', 'pending', 'string', 'im_file_review_status', 'default', 1, 10, '等待 worker 提交或重试', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_status' AND `value` = 'pending');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '处理中', 'running', 'string', 'im_file_review_status', 'info', 1, 20, '第三方审核任务处理中', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_status' AND `value` = 'running');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '无风险', 'clean', 'string', 'im_file_review_status', 'success', 1, 30, '审核通过或无风险', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_status' AND `value` = 'clean');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '有风险', 'risky', 'string', 'im_file_review_status', 'error', 1, 40, '命中风险标签或恶意文件', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_status' AND `value` = 'risky');
INSERT INTO hg_sys_dict_data
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT '失败', 'failed', 'string', 'im_file_review_status', 'warning', 1, 50, '不可重试失败或超过重试次数', 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_dict_data WHERE `type` = 'im_file_review_status' AND `value` = 'failed');
-- 3. 第三方配置项。配置真值存 hg_sys_config;字典只提供 UI 选项。
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS 启用状态', 'bool', 'oss_enabled', '0', '0', 10, '是否启用阿里云 OSS 作为 IM 文件存储', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_enabled');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS 服务商', 'string', 'oss_provider', 'oss', 'oss', 11, '取值来自字典 im_storage_provider', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_provider');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS Endpoint', 'string', 'oss_endpoint', '', '', 12, '例如 oss-cn-shenzhen.aliyuncs.com,禁止写 AccessPoint endpoint', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_endpoint');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS Bucket', 'string', 'oss_bucket', '', '', 13, 'IM 文件对象存储 Bucket', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_bucket');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS Region', 'string', 'oss_region_id', 'cn-shenzhen', 'cn-shenzhen', 14, 'OSS Bucket 所属地域', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_region_id');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS AccessKey ID', 'string', 'oss_access_key_id', '', '', 15, '敏感配置,建议由部署环境或密钥系统注入', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_access_key_id');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS AccessKey Secret', 'string', 'oss_access_key_secret', '', '', 16, '敏感配置,后台和日志必须脱敏展示', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_access_key_secret');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', 'OSS 签名 URL TTL', 'int', 'oss_presign_ttl', '120', '120', 17, '签名下载 URL 有效秒数', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'oss_presign_ttl');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '文件内容安全启用状态', 'bool', 'file_review_content_enabled', '0', '0', 30, '是否启用阿里云内容安全增强版文件审核', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'file_review_content_enabled');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '内容安全 Provider', 'string', 'file_review_content_provider', 'aliyun_green', 'aliyun_green', 31, '取值来自字典 im_file_review_provider', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'file_review_content_provider');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '内容安全 Region', 'string', 'aliyun_green_region_id', 'cn-shanghai', 'cn-shanghai', 32, '阿里云内容安全增强版地域', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'aliyun_green_region_id');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '内容安全 Endpoint', 'string', 'aliyun_green_endpoint', 'green-cip.cn-shanghai.aliyuncs.com', 'green-cip.cn-shanghai.aliyuncs.com', 33, '阿里云内容安全增强版 endpoint', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'aliyun_green_endpoint');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '内容安全服务', 'string', 'aliyun_green_document_service', 'document_detection', 'document_detection', 34, 'FileModeration 服务名', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'aliyun_green_document_service');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '恶意文件检测启用状态', 'bool', 'file_review_malware_enabled', '0', '0', 40, '是否启用阿里云云安全中心恶意文件检测', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'file_review_malware_enabled');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '恶意文件检测 Provider', 'string', 'file_review_malware_provider', 'aliyun_sas', 'aliyun_sas', 41, '取值来自字典 im_file_review_provider', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'file_review_malware_provider');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '恶意文件检测 Region', 'string', 'aliyun_sas_region_id', 'cn-shanghai', 'cn-shanghai', 42, '阿里云云安全中心恶意文件检测地域', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'aliyun_sas_region_id');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '阿里云 AccessKey ID', 'string', 'aliyun_access_key_id', '', '', 90, '内容安全和恶意文件检测共用的敏感配置,建议由部署环境或密钥系统注入', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'aliyun_access_key_id');
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT 'im_third_party', '阿里云 AccessKey Secret', 'string', 'aliyun_access_key_secret', '', '', 91, '内容安全和恶意文件检测共用的敏感配置,后台和日志必须脱敏展示', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_sys_config WHERE `group` = 'im_third_party' AND `key` = 'aliyun_access_key_secret');
USE tinode;
-- 4. 文件安全审核任务表
CREATE TABLE IF NOT EXISTS im_file_review_tasks (
id BIGINT NOT NULL,
task_id VARCHAR(64) NOT NULL,
file_id BIGINT NOT NULL,
user_id BIGINT NOT NULL DEFAULT 0,
task_type VARCHAR(32) NOT NULL,
provider VARCHAR(64) NOT NULL,
storage_provider VARCHAR(32) NOT NULL DEFAULT '',
object_name VARCHAR(2048) NOT NULL DEFAULT '',
mime_type VARCHAR(255) NOT NULL DEFAULT '',
file_size BIGINT NOT NULL DEFAULT 0,
provider_task_id VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'pending',
risk_level VARCHAR(32) NOT NULL DEFAULT '',
risk_labels JSON,
result_json JSON,
error_code VARCHAR(64) NOT NULL DEFAULT '',
error_message VARCHAR(512) NOT NULL DEFAULT '',
attempts INT NOT NULL DEFAULT 0,
next_retry_at DATETIME(3),
completed_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_file_review_tasks_task(task_id),
UNIQUE INDEX uk_im_file_review_tasks_file_provider(file_id, task_type, provider),
INDEX idx_im_file_review_tasks_pending(status, next_retry_at, id),
INDEX idx_im_file_review_tasks_file(file_id),
INDEX idx_im_file_review_tasks_object(storage_provider, object_name(191)),
INDEX idx_im_file_review_tasks_provider_task(provider, provider_task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+13
View File
@@ -4,6 +4,9 @@ go 1.26.0
require (
firebase.google.com/go v3.13.0+incompatible
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4
github.com/alibabacloud-go/green-20220302/v3 v3.5.4
github.com/alibabacloud-go/tea v1.5.2
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aws/aws-sdk-go-v2 v1.42.1
github.com/aws/aws-sdk-go-v2/config v1.32.27
@@ -43,6 +46,10 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
github.com/aliyun/credentials-go v1.4.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
@@ -56,6 +63,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.31.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.43.5 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
@@ -64,11 +72,15 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
@@ -79,6 +91,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/net v0.56.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
require (
+189
View File
@@ -1,5 +1,6 @@
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
@@ -27,6 +28,7 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
@@ -35,8 +37,46 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4 h1:o6veen0IZ/Fe1JawwhwQMZcbw67CVDY1pQwXcNWPyQo=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4/go.mod h1:eHjVxrT9g8uVYN/nyAwOFQEfkVA154ChiqRc2XnNKYU=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/green-20220302/v3 v3.5.4 h1:ZnlHri7tTLl28JWB5S2J6MYvD0ewJFI7hedK4E7+gSk=
github.com/alibabacloud-go/green-20220302/v3 v3.5.4/go.mod h1:PQTtoEuWOsuq7H7sgTRcXDw4Ut/Hekgy5z2mWGtO9OQ=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.5.2 h1:DUzV/cRTbJC++rmADfqVu//GuWbaD64ZJ3yW3v2ukNU=
github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
@@ -81,8 +121,13 @@ github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2io
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -90,12 +135,15 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
@@ -113,19 +161,35 @@ github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxo
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
@@ -136,6 +200,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.17 h1:73NfMHdiqo9JFU9+7a
github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -153,6 +219,10 @@ 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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -170,6 +240,13 @@ github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZyc
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -193,6 +270,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk=
@@ -206,6 +284,9 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E=
github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -225,6 +306,9 @@ github.com/tinode/jsonco v1.0.0 h1:zVcpjzDvjuA1G+HLrckI5EiiRyq9jgV3x37OQl6e5FE=
github.com/tinode/jsonco v1.0.0/go.mod h1:Bnavu3302Qfn2pILMNwASkelodgeew3IvDrbdzU84u8=
github.com/tinode/snowflake v1.0.0 h1:YciQ9ZKn1TrnvpS8yZErt044XJaxWVtR9aMO9rOZVOE=
github.com/tinode/snowflake v1.0.0/go.mod h1:5JiaCe3o7QdDeyRcAeZBGVghwRS+ygt2CF/hxmAoptQ=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twilio/twilio-go v1.30.9 h1:4W4GEV2q0sLQ9xsr1N/97JQlt0c82hZ0ij4qTErstv8=
github.com/twilio/twilio-go v1.30.9/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
@@ -235,6 +319,8 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
@@ -267,54 +353,138 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -323,16 +493,30 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.287.0 h1:CQDMqUiqZZ0U/Yge3zyjAhNQ0OSYEH0PaA7l4xtEen4=
google.golang.org/api v0.287.0/go.mod h1:pPW85yt3Iuc3unkpaMhFtMmOqnTdCwCqEOaUlnuxRlQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7 h1:lQG76ePMKmtujel4VIVMiFoHVWVNtJdawbCZJtWlVXU=
google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7/go.mod h1:LwlOWYBU335L+sR55UuR5fbbU8KmEX+3tUHf3SwMmhM=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -346,6 +530,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2 h1:tczPZjdz6soV2thcuq1IFOuNLrBUGonFyUXBbIWXWis=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2/go.mod h1:c7Wo0IjB7JL9B9Avv0UZKorYJCUhiergpj3u1WtGT1E=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@@ -355,3 +542,5 @@ gopkg.in/yaml.v2 v2.2.8/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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+6
View File
@@ -221,6 +221,12 @@ type RegistrationConfigAdapter interface {
RegistrationSMSEnabled() (bool, error)
}
// ThirdPartyConfigAdapter reads global third-party integration settings from
// the control database.
type ThirdPartyConfigAdapter interface {
ThirdPartyConfig(group string, keys ...string) (map[string]string, error)
}
// TenantRoutedAdapter can return a tenant-specific adapter, e.g. for the
// "one tenant, one database" deployment model.
type TenantRoutedAdapter interface {
+15 -1
View File
@@ -255,6 +255,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
if err = ensureIMHTTPInfrastructureTables(a.db); err != nil {
return errors.New("mysql adapter failed to ensure HTTP infrastructure schema: " + err.Error())
}
if err = ensureIMFileReviewTables(a.db); err != nil {
return errors.New("mysql adapter failed to ensure file review schema: " + err.Error())
}
if err = ensureSnowflakePrimaryKeys(a.db); err != nil {
return errors.New("mysql adapter failed to ensure snowflake primary keys: " + err.Error())
}
@@ -458,6 +461,10 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
db.Close()
return nil, nil, err
}
if err = ensureIMFileReviewTables(db); err != nil {
db.Close()
return nil, nil, err
}
if err = ensureSnowflakePrimaryKeys(db); err != nil {
db.Close()
return nil, nil, err
@@ -877,6 +884,9 @@ func (a *adapter) CreateDb(reset bool) error {
if err = ensureIMHTTPInfrastructureTables(tx); err != nil {
return err
}
if err = ensureIMFileReviewTables(tx); err != nil {
return err
}
// Deletion log
if _, err = tx.Exec(
@@ -4465,9 +4475,13 @@ func (a *adapter) FileFinishUpload(fd *t.FileDef, success bool, size int64) (*t.
if err != nil {
return nil, err
}
fd.UpdatedAt = now
fd.Size = size
if err = insertFileReviewTasks(ctx, tx, fd); err != nil {
return nil, err
}
fd.Status = t.UploadCompleted
fd.Size = size
} else {
// Deleting the record: there is no value in keeping it in the DB.
_, err = tx.ExecContext(ctx, "DELETE FROM im_fileuploads WHERE id=?", store.DecodeUid(fd.Uid()))
+37
View File
@@ -0,0 +1,37 @@
//go:build mysql
// +build mysql
package mysql
// ensureIMFileReviewTables installs async file safety review state in the
// currently selected tenant database. Tenant databases intentionally do not
// carry tenant_id columns.
func ensureIMFileReviewTables(exec sqlExecutor) error {
statements := []string{
`CREATE TABLE IF NOT EXISTS im_file_review_tasks (
id BIGINT NOT NULL, task_id VARCHAR(64) NOT NULL, file_id BIGINT NOT NULL,
user_id BIGINT NOT NULL DEFAULT 0, task_type VARCHAR(32) NOT NULL,
provider VARCHAR(64) NOT NULL, storage_provider VARCHAR(32) NOT NULL DEFAULT '',
object_name VARCHAR(2048) NOT NULL DEFAULT '', mime_type VARCHAR(255) NOT NULL DEFAULT '',
file_size BIGINT NOT NULL DEFAULT 0, provider_task_id VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'pending', risk_level VARCHAR(32) NOT NULL DEFAULT '',
risk_labels JSON, result_json JSON, error_code VARCHAR(64) NOT NULL DEFAULT '',
error_message VARCHAR(512) NOT NULL DEFAULT '', attempts INT NOT NULL DEFAULT 0,
next_retry_at DATETIME(3), completed_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_file_review_tasks_task(task_id),
UNIQUE INDEX uk_im_file_review_tasks_file_provider(file_id, task_type, provider),
INDEX idx_im_file_review_tasks_pending(status, next_retry_at, id),
INDEX idx_im_file_review_tasks_file(file_id),
INDEX idx_im_file_review_tasks_object(storage_provider, object_name(191)),
INDEX idx_im_file_review_tasks_provider_task(provider, provider_task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
}
for _, statement := range statements {
if _, err := exec.Exec(statement); err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,32 @@
//go:build mysql
package mysql
import (
"strings"
"testing"
)
func TestEnsureIMFileReviewTablesUsesSelectedTenantDatabase(t *testing.T) {
exec := &recordingGroupSchemaExecutor{}
if err := ensureIMFileReviewTables(exec); err != nil {
t.Fatal(err)
}
if len(exec.statements) != 1 {
t.Fatalf("created %d file review tables, want 1", len(exec.statements))
}
joined := strings.ToLower(strings.Join(exec.statements, "\n"))
for _, expected := range []string{
"im_file_review_tasks",
"unique index uk_im_file_review_tasks_file_provider(file_id, task_type, provider)",
"index idx_im_file_review_tasks_pending(status, next_retry_at, id)",
"index idx_im_file_review_tasks_object(storage_provider, object_name(191))",
} {
if !strings.Contains(joined, expected) {
t.Fatalf("file review schema is missing %q", expected)
}
}
if strings.Contains(joined, "tinode.") || strings.Contains(joined, "create database") || strings.Contains(joined, "tenant_id") {
t.Fatalf("file review schema must stay inside the selected tenant database: %s", joined)
}
}
+47
View File
@@ -0,0 +1,47 @@
//go:build mysql
// +build mysql
package mysql
import (
"context"
"database/sql"
"github.com/tinode/chat/server/store"
t "github.com/tinode/chat/server/store/types"
)
type fileReviewExecContext interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
}
func insertFileReviewTasks(ctx context.Context, exec fileReviewExecContext, fd *t.FileDef) error {
fileID := decodeUID(fd.Uid())
userID := int64(0)
if fd.User != "" {
userID = decodeUID(t.ParseUid(fd.User))
}
tasks := []struct {
taskType string
provider string
}{
{taskType: t.FileReviewTaskTypeContent, provider: t.FileReviewProviderAliyunGreen},
{taskType: t.FileReviewTaskTypeMalware, provider: t.FileReviewProviderAliyunSAS},
}
for _, task := range tasks {
id, err := nextSnowflakeID()
if err != nil {
return err
}
taskID := store.EncodeUid(id).String()
_, err = exec.ExecContext(ctx, `INSERT IGNORE INTO im_file_review_tasks
(id,task_id,file_id,user_id,task_type,provider,storage_provider,object_name,mime_type,file_size,status,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
id, taskID, fileID, userID, task.taskType, task.provider, "", fd.Location, fd.MimeType, fd.Size,
t.FileReviewStatusPending, fd.UpdatedAt, fd.UpdatedAt)
if err != nil {
return err
}
}
return nil
}
+1
View File
@@ -12,6 +12,7 @@
# - Default tenant business database: tinode
# - im_users, im_topics, im_subscriptions, im_messages and extension tables
# - im_http_idempotency and im_business_events_outbox for reliable HTTP writes
# - im_file_review_tasks for asynchronous content safety and malware checks
# - business tables do not store tenant_id in independent-database mode
#
# The previous shared-table tenant_id draft has been removed from this file to
+5 -1
View File
@@ -5439,8 +5439,12 @@ func (a *adapter) TenantFileFinishUpload(id t.TenantID, fd *t.FileDef, success b
if success {
_, err = tx.Exec("UPDATE im_fileuploads SET updatedat=?,status=?,size=?,etag=?,location=? WHERE tenant_id=? AND id=?",
now, t.UploadCompleted, size, fd.ETag, fd.Location, id, decodeUID(fd.Uid()))
if err == nil {
fd.UpdatedAt = now
fd.Size = size
err = insertFileReviewTasks(ctx, tx, fd)
}
fd.Status = t.UploadCompleted
fd.Size = size
} else {
_, err = tx.Exec("DELETE FROM im_fileuploads WHERE tenant_id=? AND id=?", id, decodeUID(fd.Uid()))
fd.Status = t.UploadFailed
+70
View File
@@ -0,0 +1,70 @@
//go:build mysql
// +build mysql
package mysql
import (
"database/sql"
"strings"
"github.com/jmoiron/sqlx"
t "github.com/tinode/chat/server/store/types"
)
func (a *adapter) ThirdPartyConfig(group string, keys ...string) (map[string]string, error) {
db := a.registryDB
if db == nil {
db = a.db
}
if db == nil {
return nil, t.ErrInternal
}
group = strings.TrimSpace(group)
if group == "" {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
rows := []struct {
Key string `db:"key"`
Value string `db:"value"`
}{}
var err error
if len(keys) == 0 {
err = db.SelectContext(ctx, &rows, `SELECT `+"`key`"+`, value FROM hg_sys_config
WHERE `+"`group`"+`=? AND status=1`, group)
} else {
normalized := make([]string, 0, len(keys))
for _, key := range keys {
if key = strings.TrimSpace(key); key != "" {
normalized = append(normalized, key)
}
}
if len(normalized) == 0 {
return map[string]string{}, nil
}
query, args, inErr := sqlx.In(`SELECT `+"`key`"+`, value FROM hg_sys_config
WHERE `+"`group`"+`=? AND status=1 AND `+"`key`"+` IN (?)`, group, normalized)
if inErr != nil {
return nil, inErr
}
query = db.Rebind(query)
err = db.SelectContext(ctx, &rows, query, args...)
}
if err != nil {
if err == sql.ErrNoRows {
return map[string]string{}, nil
}
return nil, err
}
result := make(map[string]string, len(rows))
for _, row := range rows {
result[row.Key] = row.Value
}
return result, nil
}
+243
View File
@@ -0,0 +1,243 @@
// Package aliyun wraps Alibaba Cloud Green file moderation APIs.
package aliyun
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
green "github.com/alibabacloud-go/green-20220302/v3/client"
"github.com/alibabacloud-go/tea/dara"
"github.com/tinode/chat/server/store/types"
)
const defaultDocumentService = "document_detection"
type GreenConfig struct {
AccessKeyID string
AccessKeySecret string
RegionID string
Endpoint string
DocumentService string
}
type OSSObject struct {
BucketName string
ObjectName string
RegionID string
DataID string
}
type SubmitResult struct {
ProviderTaskID string
RequestID string
}
type PollResult struct {
Status string
RiskLevel string
RiskLabels []string
Raw map[string]any
RequestID string
}
type GreenClient struct {
service string
client *green.Client
}
func NewGreenClient(conf GreenConfig) (*GreenClient, error) {
if strings.TrimSpace(conf.AccessKeyID) == "" {
return nil, errors.New("missing Access Key ID")
}
if strings.TrimSpace(conf.AccessKeySecret) == "" {
return nil, errors.New("missing Access Key Secret")
}
if strings.TrimSpace(conf.RegionID) == "" {
return nil, errors.New("missing Region ID")
}
service := strings.TrimSpace(conf.DocumentService)
if service == "" {
service = defaultDocumentService
}
clientConf := &openapi.Config{
AccessKeyId: dara.String(conf.AccessKeyID),
AccessKeySecret: dara.String(conf.AccessKeySecret),
RegionId: dara.String(conf.RegionID),
Protocol: dara.String("https"),
}
if endpoint := strings.TrimSpace(conf.Endpoint); endpoint != "" {
clientConf.Endpoint = dara.String(endpoint)
}
client, err := green.NewClient(clientConf)
if err != nil {
return nil, err
}
return &GreenClient{service: service, client: client}, nil
}
func (c *GreenClient) SubmitOSSFile(ctx context.Context, object OSSObject) (*SubmitResult, error) {
params, err := buildOSSServiceParameters(object)
if err != nil {
return nil, err
}
resp, err := c.client.FileModerationWithContext(ctx, &green.FileModerationRequest{
Service: dara.String(c.service),
ServiceParameters: dara.String(params),
}, &dara.RuntimeOptions{})
if err != nil {
return nil, err
}
if resp == nil || resp.Body == nil || resp.Body.Code == nil || *resp.Body.Code != 200 || resp.Body.Data == nil || resp.Body.Data.TaskId == nil {
return nil, responseError("file moderation submit failed", responseCode(resp), responseMessage(resp))
}
return &SubmitResult{ProviderTaskID: *resp.Body.Data.TaskId, RequestID: responseRequestID(resp)}, nil
}
func (c *GreenClient) PollOSSFile(ctx context.Context, providerTaskID string) (*PollResult, error) {
params, err := json.Marshal(map[string]string{"taskId": strings.TrimSpace(providerTaskID)})
if err != nil {
return nil, err
}
resp, err := c.client.DescribeFileModerationResultWithContext(ctx, &green.DescribeFileModerationResultRequest{
Service: dara.String(c.service),
ServiceParameters: dara.String(string(params)),
}, &dara.RuntimeOptions{})
if err != nil {
return nil, err
}
if resp == nil || resp.Body == nil || resp.Body.Code == nil || *resp.Body.Code != 200 {
return nil, responseError("file moderation poll failed", describeResponseCode(resp), describeResponseMessage(resp))
}
raw := map[string]any{}
if data := resp.Body.Data; data != nil {
encoded, _ := json.Marshal(data)
json.Unmarshal(encoded, &raw)
riskLevel := strings.TrimSpace(dara.StringValue(data.RiskLevel))
return &PollResult{
Status: statusFromRiskLevel(riskLevel),
RiskLevel: riskLevel,
RiskLabels: labelsFromFileResult(data),
Raw: raw,
RequestID: describeResponseRequestID(resp),
}, nil
}
return &PollResult{Status: types.FileReviewStatusRunning, Raw: raw, RequestID: describeResponseRequestID(resp)}, nil
}
func buildOSSServiceParameters(object OSSObject) (string, error) {
if strings.TrimSpace(object.BucketName) == "" || strings.TrimSpace(object.ObjectName) == "" || strings.TrimSpace(object.RegionID) == "" {
return "", types.ErrMalformed
}
params := map[string]string{
"ossBucketName": strings.TrimSpace(object.BucketName),
"ossObjectName": strings.TrimSpace(object.ObjectName),
"ossRegionId": strings.TrimSpace(object.RegionID),
}
if dataID := strings.TrimSpace(object.DataID); dataID != "" {
params["dataId"] = dataID
}
encoded, err := json.Marshal(params)
return string(encoded), err
}
func statusFromRiskLevel(riskLevel string) string {
switch strings.ToLower(strings.TrimSpace(riskLevel)) {
case "":
return types.FileReviewStatusRunning
case "none", "low":
return types.FileReviewStatusClean
default:
return types.FileReviewStatusRisky
}
}
func labelsFromFileResult(data *green.DescribeFileModerationResultResponseBodyData) []string {
seen := map[string]bool{}
var labels []string
for _, page := range data.PageResult {
if page == nil {
continue
}
for _, image := range page.ImageResult {
if image == nil {
continue
}
for _, result := range image.LabelResult {
if result == nil {
continue
}
if label := strings.TrimSpace(dara.StringValue(result.Label)); label != "" && !seen[label] {
seen[label] = true
labels = append(labels, label)
}
}
}
for _, text := range page.TextResult {
if text == nil {
continue
}
for _, label := range strings.Split(dara.StringValue(text.Labels), ",") {
label = strings.TrimSpace(label)
if label != "" && !seen[label] {
seen[label] = true
labels = append(labels, label)
}
}
}
}
return labels
}
func responseError(prefix string, code int32, msg string) error {
if msg == "" {
return errors.New(prefix)
}
return errors.New(prefix + ": " + msg + " (" + strconv.FormatInt(int64(code), 10) + ")")
}
func responseCode(resp *green.FileModerationResponse) int32 {
if resp != nil && resp.Body != nil && resp.Body.Code != nil {
return *resp.Body.Code
}
return 0
}
func responseMessage(resp *green.FileModerationResponse) string {
if resp != nil && resp.Body != nil && resp.Body.Message != nil {
return *resp.Body.Message
}
return ""
}
func responseRequestID(resp *green.FileModerationResponse) string {
if resp != nil && resp.Body != nil && resp.Body.RequestId != nil {
return *resp.Body.RequestId
}
return ""
}
func describeResponseCode(resp *green.DescribeFileModerationResultResponse) int32 {
if resp != nil && resp.Body != nil && resp.Body.Code != nil {
return *resp.Body.Code
}
return 0
}
func describeResponseMessage(resp *green.DescribeFileModerationResultResponse) string {
if resp != nil && resp.Body != nil && resp.Body.Message != nil {
return *resp.Body.Message
}
return ""
}
func describeResponseRequestID(resp *green.DescribeFileModerationResultResponse) string {
if resp != nil && resp.Body != nil && resp.Body.RequestId != nil {
return *resp.Body.RequestId
}
return ""
}
+70
View File
@@ -0,0 +1,70 @@
package aliyun
import (
"encoding/json"
"testing"
green "github.com/alibabacloud-go/green-20220302/v3/client"
"github.com/alibabacloud-go/tea/dara"
"github.com/tinode/chat/server/store/types"
)
func TestBuildOSSServiceParameters(t *testing.T) {
got, err := buildOSSServiceParameters(OSSObject{
BucketName: "im-oss-t1",
ObjectName: "tenants/1/2026/08/file",
RegionID: "cn-shenzhen",
DataID: "task-1",
})
if err != nil {
t.Fatal(err)
}
var params map[string]string
if err := json.Unmarshal([]byte(got), &params); err != nil {
t.Fatal(err)
}
for key, want := range map[string]string{
"ossBucketName": "im-oss-t1",
"ossObjectName": "tenants/1/2026/08/file",
"ossRegionId": "cn-shenzhen",
"dataId": "task-1",
} {
if params[key] != want {
t.Fatalf("params[%s] = %q, want %q", key, params[key], want)
}
}
}
func TestStatusFromRiskLevel(t *testing.T) {
cases := map[string]string{
"": types.FileReviewStatusRunning,
"none": types.FileReviewStatusClean,
"low": types.FileReviewStatusClean,
"medium": types.FileReviewStatusRisky,
"high": types.FileReviewStatusRisky,
}
for input, want := range cases {
if got := statusFromRiskLevel(input); got != want {
t.Fatalf("statusFromRiskLevel(%q) = %q, want %q", input, got, want)
}
}
}
func TestLabelsFromFileResult(t *testing.T) {
got := labelsFromFileResult(&green.DescribeFileModerationResultResponseBodyData{
PageResult: []*green.DescribeFileModerationResultResponseBodyDataPageResult{
{ImageResult: []*green.DescribeFileModerationResultResponseBodyDataPageResultImageResult{
{LabelResult: []*green.DescribeFileModerationResultResponseBodyDataPageResultImageResultLabelResult{
{Label: dara.String("porn")},
{Label: dara.String("porn")},
}},
}},
{TextResult: []*green.DescribeFileModerationResultResponseBodyDataPageResultTextResult{
{Labels: dara.String("violence,ad")},
}},
},
})
if len(got) != 3 || got[0] != "porn" || got[1] != "violence" || got[2] != "ad" {
t.Fatalf("labelsFromFileResult = %#v", got)
}
}
+104
View File
@@ -0,0 +1,104 @@
package filesafety
import (
"errors"
"strconv"
"strings"
"github.com/tinode/chat/server/filesafety/aliyun"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
const thirdPartyConfigGroup = "im_third_party"
type ContentReviewConfig struct {
Enabled bool
Provider string
Green aliyun.GreenConfig
}
type MalwareReviewConfig struct {
Enabled bool
Provider string
RegionID string
}
type Config struct {
Content ContentReviewConfig
Malware MalwareReviewConfig
}
func LoadConfig() (*Config, error) {
values, err := store.ThirdPartyConfig.Get(thirdPartyConfigGroup,
"file_review_content_enabled",
"file_review_content_provider",
"aliyun_green_region_id",
"aliyun_green_endpoint",
"aliyun_green_document_service",
"file_review_malware_enabled",
"file_review_malware_provider",
"aliyun_sas_region_id",
"aliyun_access_key_id",
"aliyun_access_key_secret",
)
if err != nil {
return nil, err
}
config := &Config{
Content: ContentReviewConfig{
Enabled: boolValue(values["file_review_content_enabled"]),
Provider: stringDefault(values["file_review_content_provider"], types.FileReviewProviderAliyunGreen),
Green: aliyun.GreenConfig{
AccessKeyID: strings.TrimSpace(values["aliyun_access_key_id"]),
AccessKeySecret: strings.TrimSpace(values["aliyun_access_key_secret"]),
RegionID: strings.TrimSpace(values["aliyun_green_region_id"]),
Endpoint: strings.TrimSpace(values["aliyun_green_endpoint"]),
DocumentService: strings.TrimSpace(values["aliyun_green_document_service"]),
},
},
Malware: MalwareReviewConfig{
Enabled: boolValue(values["file_review_malware_enabled"]),
Provider: stringDefault(values["file_review_malware_provider"], types.FileReviewProviderAliyunSAS),
RegionID: strings.TrimSpace(values["aliyun_sas_region_id"]),
},
}
if config.Content.Enabled && config.Content.Provider != types.FileReviewProviderAliyunGreen {
return nil, errors.New("unsupported file content review provider: " + config.Content.Provider)
}
if config.Malware.Enabled && config.Malware.Provider != types.FileReviewProviderAliyunSAS {
return nil, errors.New("unsupported malware review provider: " + config.Malware.Provider)
}
return config, nil
}
func NewGreenClientFromConfig() (*aliyun.GreenClient, error) {
config, err := LoadConfig()
if err != nil {
return nil, err
}
if !config.Content.Enabled {
return nil, types.ErrUnsupported
}
return aliyun.NewGreenClient(config.Content.Green)
}
func boolValue(value string) bool {
if parsed, err := strconv.ParseBool(strings.TrimSpace(value)); err == nil {
return parsed
}
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "yes", "on":
return true
default:
return false
}
}
func stringDefault(value, fallback string) string {
if value = strings.TrimSpace(value); value != "" {
return value
}
return fallback
}
+51
View File
@@ -0,0 +1,51 @@
package filesafety
import (
"testing"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
type fakeThirdPartyConfig map[string]string
func (f fakeThirdPartyConfig) Get(group string, keys ...string) (map[string]string, error) {
result := map[string]string{}
for _, key := range keys {
if value, ok := f[key]; ok {
result[key] = value
}
}
return result, nil
}
func TestLoadConfig(t *testing.T) {
original := store.ThirdPartyConfig
store.ThirdPartyConfig = fakeThirdPartyConfig{
"file_review_content_enabled": "1",
"file_review_content_provider": types.FileReviewProviderAliyunGreen,
"aliyun_green_region_id": "cn-shanghai",
"aliyun_green_endpoint": "green-cip.cn-shanghai.aliyuncs.com",
"aliyun_green_document_service": "document_detection",
"file_review_malware_enabled": "true",
"file_review_malware_provider": types.FileReviewProviderAliyunSAS,
"aliyun_sas_region_id": "cn-shanghai",
"aliyun_access_key_id": "ak",
"aliyun_access_key_secret": "sk",
}
defer func() { store.ThirdPartyConfig = original }()
config, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig failed: %v", err)
}
if !config.Content.Enabled || config.Content.Provider != types.FileReviewProviderAliyunGreen {
t.Fatalf("unexpected content config: %+v", config.Content)
}
if !config.Malware.Enabled || config.Malware.Provider != types.FileReviewProviderAliyunSAS {
t.Fatalf("unexpected malware config: %+v", config.Malware)
}
if config.Content.Green.AccessKeyID != "ak" || config.Content.Green.RegionID != "cn-shanghai" {
t.Fatalf("unexpected green config: %+v", config.Content.Green)
}
}
+58
View File
@@ -38,11 +38,13 @@ type ossConfig struct {
AccessKeySecret string `json:"access_key_secret"`
Endpoint string `json:"endpoint"`
BucketName string `json:"bucket"`
RegionID string `json:"region_id"`
CorsOrigins []string `json:"cors_origins"`
ServeURL string `json:"serve_url"`
PresignTTL int `json:"presign_ttl"`
CacheControl string `json:"cache_control"`
VerifyBucket bool `json:"verify_bucket"`
UseSysConfig bool `json:"use_sys_config"`
}
type ossHandler struct {
@@ -69,6 +71,9 @@ func (oh *ossHandler) Init(jsconf string) error {
if err = json.Unmarshal([]byte(jsconf), &oh.conf); err != nil {
return errors.New("failed to parse config: " + err.Error())
}
if err = oh.loadSysConfig(); err != nil {
return err
}
if oh.conf.AccessKeyID == "" {
return errors.New("missing Access Key ID")
@@ -114,6 +119,59 @@ func (oh *ossHandler) Init(jsconf string) error {
return err
}
func (oh *ossHandler) loadSysConfig() error {
if !oh.conf.UseSysConfig {
return nil
}
values, err := store.ThirdPartyConfig.Get("im_third_party",
"oss_enabled",
"oss_provider",
"oss_endpoint",
"oss_bucket",
"oss_region_id",
"oss_access_key_id",
"oss_access_key_secret",
"oss_presign_ttl",
)
if err != nil {
return errors.New("failed to load OSS config from sys config: " + err.Error())
}
if !truthy(values["oss_enabled"]) {
return errors.New("OSS is disabled in sys config")
}
if provider := strings.TrimSpace(values["oss_provider"]); provider != "" && provider != handlerName {
return errors.New("unsupported storage provider in sys config: " + provider)
}
applyString(&oh.conf.Endpoint, values["oss_endpoint"])
applyString(&oh.conf.BucketName, values["oss_bucket"])
applyString(&oh.conf.RegionID, values["oss_region_id"])
applyString(&oh.conf.AccessKeyID, values["oss_access_key_id"])
applyString(&oh.conf.AccessKeySecret, values["oss_access_key_secret"])
if ttl := strings.TrimSpace(values["oss_presign_ttl"]); ttl != "" {
parsed, err := strconv.Atoi(ttl)
if err != nil {
return errors.New("invalid OSS presign TTL in sys config: " + err.Error())
}
oh.conf.PresignTTL = parsed
}
return nil
}
func applyString(target *string, value string) {
if value = strings.TrimSpace(value); value != "" {
*target = value
}
}
func truthy(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
// Headers adds CORS headers and redirects GET and HEAD requests to OSS.
func (oh *ossHandler) Headers(tenantID types.TenantID, method string, url *url.URL, headers http.Header, serve bool) (http.Header, int, error) {
headers, status := media.CORSHandler(method, headers, oh.corsOrigins, serve)
+39
View File
@@ -4,9 +4,22 @@ import (
"testing"
"time"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
type fakeThirdPartyConfig map[string]string
func (f fakeThirdPartyConfig) Get(group string, keys ...string) (map[string]string, error) {
result := map[string]string{}
for _, key := range keys {
if value, ok := f[key]; ok {
result[key] = value
}
}
return result, nil
}
func TestTenantObjectKey(t *testing.T) {
got, err := tenantObjectKey(types.TenantID(42), "abc123", time.Date(2026, 8, 11, 9, 0, 0, 0, time.UTC))
if err != nil {
@@ -32,3 +45,29 @@ func TestGetIdFromUrl(t *testing.T) {
t.Fatalf("GetIdFromUrl = %v, want %v", got, fid)
}
}
func TestLoadSysConfig(t *testing.T) {
original := store.ThirdPartyConfig
store.ThirdPartyConfig = fakeThirdPartyConfig{
"oss_enabled": "1",
"oss_provider": "oss",
"oss_endpoint": "oss-cn-shenzhen.aliyuncs.com",
"oss_bucket": "im-files",
"oss_region_id": "cn-shenzhen",
"oss_access_key_id": "ak",
"oss_access_key_secret": "sk",
"oss_presign_ttl": "300",
}
defer func() { store.ThirdPartyConfig = original }()
handler := ossHandler{conf: ossConfig{UseSysConfig: true}}
if err := handler.loadSysConfig(); err != nil {
t.Fatalf("loadSysConfig failed: %v", err)
}
if handler.conf.Endpoint != "oss-cn-shenzhen.aliyuncs.com" || handler.conf.BucketName != "im-files" {
t.Fatalf("unexpected OSS config: %+v", handler.conf)
}
if handler.conf.RegionID != "cn-shenzhen" || handler.conf.PresignTTL != 300 {
t.Fatalf("unexpected OSS region or ttl: %+v", handler.conf)
}
}
+18
View File
@@ -155,6 +155,24 @@ func (registrationConfigMapper) SMSEnabled() (bool, error) {
return configAdp.RegistrationSMSEnabled()
}
// ThirdPartyConfigPersistenceInterface exposes global third-party integration settings.
type ThirdPartyConfigPersistenceInterface interface {
Get(group string, keys ...string) (map[string]string, error)
}
type thirdPartyConfigMapper struct{}
// ThirdPartyConfig reads third-party settings from the control database.
var ThirdPartyConfig ThirdPartyConfigPersistenceInterface = thirdPartyConfigMapper{}
func (thirdPartyConfigMapper) Get(group string, keys ...string) (map[string]string, error) {
configAdp, ok := adp.(adapter.ThirdPartyConfigAdapter)
if !ok {
return nil, types.ErrUnsupported
}
return configAdp.ThirdPartyConfig(group, keys...)
}
// GetByCode resolves a tenant by its public enterprise code.
func (tenantsMapper) GetByCode(code string) (*types.Tenant, error) {
tenantAdp, ok := adp.(adapter.TenantAdapter)
+37
View File
@@ -10,6 +10,18 @@ const (
AbuseReportStatusReceived = "received"
FeedbackStatusReceived = "received"
FileReviewTaskTypeContent = "content"
FileReviewTaskTypeMalware = "malware"
FileReviewProviderAliyunGreen = "aliyun_green"
FileReviewProviderAliyunSAS = "aliyun_sas"
FileReviewStatusPending = "pending"
FileReviewStatusRunning = "running"
FileReviewStatusClean = "clean"
FileReviewStatusRisky = "risky"
FileReviewStatusFailed = "failed"
)
type RiskChallenge struct {
@@ -84,3 +96,28 @@ type FeedbackSubmission struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type FileReviewTask struct {
ID int64
TaskID string
FileID Uid
UserID Uid
TaskType string
Provider string
StorageProvider string
ObjectName string
MimeType string
FileSize int64
ProviderTaskID string
Status string
RiskLevel string
RiskLabels []string
ResultJSON map[string]any
ErrorCode string
ErrorMessage string
Attempts int
NextRetryAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
CompletedAt *time.Time
}