mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
refactor(msg_gateway): restructure and rename message_gateway aligned with custom_example
This commit is contained in:
@@ -15,7 +15,7 @@ Wavelet 的消息推送机制采用了**元数据驱动 + 统一触发器 + 异
|
||||
|
||||
| 目录/包名 | 职责定位 | 包含内容与设计细节 |
|
||||
| :--- | :--- | :--- |
|
||||
| **`backend/plugins/domain/message_gateway/push/`** | 推送基础设施层 | 静态定义、不依赖系统数据库和任何框架。定义了统一接口 `Pusher` 和多实现(Lark, Webhook, Email 等),提供配置验证及发送功能。 |
|
||||
| **`backend/plugins/domain/msg_gateway/push/`** | 推送基础设施层 | 静态定义、不依赖系统数据库和任何框架。定义了统一接口 `Pusher` 和多实现(Lark, Webhook, Email 等),提供配置验证及发送功能。 |
|
||||
| **`internal/apps/admin/push/`** | 通知服务与后台任务层 | 包含以下核心文件:<br>1. `events.go`:定义通知事件的结构模型(`NotificationMessage`, `EventMetadata`)、内置事件的动态注册中心(`BuiltInEvents` 及 `RegisterBuiltInEvent` 函数)以及统一触发器类 `EventTrigger`(包括其底层的派发引擎逻辑)。<br>2. `tasks.go`:定义 Asynq 后台异步发送任务、处理器 `PushHandler` 及其校验逻辑,并记录推送历史审计。<br>3. `routers.go`:管理端接口,负责获取事件配置列表和更新配置。 |
|
||||
| **`internal/apps/admin/push/custom_events/`** | 自定义通知事件包 | 事件元数据定义与 push 侧处理逻辑;**一个 Go 文件代表一个事件**。在 `register.go` 统一装配,禁止 `init()` 副作用。 |
|
||||
| **`internal/listener/`** | 域事件分发层 | 核心域发射事件(如 `EmitAdminLoggedIn`),push 在 bootstrap 阶段通过 `OnAdminLoggedIn` 订阅,避免 auth/user 直接依赖 push。 |
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import (
|
||||
"Wavelet/plugins/domain/admin"
|
||||
"Wavelet/plugins/domain/auth"
|
||||
"Wavelet/plugins/domain/cap"
|
||||
"Wavelet/plugins/domain/message_gateway"
|
||||
"Wavelet/plugins/domain/msg_gateway"
|
||||
"Wavelet/plugins/domain/risk_control"
|
||||
"Wavelet/plugins/domain/system"
|
||||
"Wavelet/plugins/domain/upload"
|
||||
@@ -105,7 +105,7 @@ func newWaveletApp(profile core.Profile, opts ...core.AppOption) *core.App {
|
||||
admin.New(),
|
||||
user.New(),
|
||||
auth.New(),
|
||||
message_gateway.New(),
|
||||
msg_gateway.New(),
|
||||
risk_control.New(),
|
||||
upload.New(),
|
||||
cap.New(),
|
||||
|
||||
@@ -82,8 +82,8 @@ func TestNewWaveletAppProfiles(t *testing.T) {
|
||||
_, ok = app.Plugin("user")
|
||||
assert.True(t, ok, "user plugin missing")
|
||||
|
||||
_, ok = app.Plugin("message_gateway")
|
||||
assert.True(t, ok, "message_gateway plugin missing")
|
||||
_, ok = app.Plugin("msg_gateway")
|
||||
assert.True(t, ok, "msg_gateway plugin missing")
|
||||
|
||||
_, ok = app.Plugin("risk_control")
|
||||
assert.True(t, ok, "risk_control plugin missing")
|
||||
|
||||
+418
-414
File diff suppressed because it is too large
Load Diff
+418
-414
File diff suppressed because it is too large
Load Diff
+283
-280
@@ -204,6 +204,265 @@ definitions:
|
||||
ttl_minutes:
|
||||
type: integer
|
||||
type: object
|
||||
do.BindRequest:
|
||||
properties:
|
||||
channel_id:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
type: object
|
||||
do.BindingDTO:
|
||||
properties:
|
||||
channel_id:
|
||||
example: "0"
|
||||
type: string
|
||||
channel_name:
|
||||
type: string
|
||||
channel_type:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
example: "0"
|
||||
type: string
|
||||
platform_user_id:
|
||||
type: string
|
||||
user_id:
|
||||
example: "0"
|
||||
type: string
|
||||
type: object
|
||||
do.ChannelDTO:
|
||||
properties:
|
||||
credentials:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
extra:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
id:
|
||||
example: "0"
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
owner_id:
|
||||
example: "0"
|
||||
type: string
|
||||
owner_scope:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
do.CreateChannelRequest:
|
||||
properties:
|
||||
credentials:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
extra:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
do.CreatePushChannelRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
other:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
type: object
|
||||
do.CreatePushEventRequest:
|
||||
properties:
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enabled:
|
||||
type: boolean
|
||||
event_key:
|
||||
type: string
|
||||
targets:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
task_type:
|
||||
type: string
|
||||
template:
|
||||
type: string
|
||||
type: object
|
||||
do.Definition:
|
||||
properties:
|
||||
fields:
|
||||
items:
|
||||
$ref: '#/definitions/do.Field'
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
do.Field:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
required:
|
||||
type: boolean
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
do.PublicChannelDTO:
|
||||
properties:
|
||||
id:
|
||||
example: "0"
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
do.TestPushChannelRequest:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
other:
|
||||
type: string
|
||||
target:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
do.TestPushRequest:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/push.Config'
|
||||
target:
|
||||
type: string
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
do.UpdateChannelRequest:
|
||||
properties:
|
||||
credentials:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
extra:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
do.UpdatePushChannelRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
other:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
do.UpdatePushEventRequest:
|
||||
properties:
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enabled:
|
||||
type: boolean
|
||||
targets:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
template:
|
||||
type: string
|
||||
required:
|
||||
- template
|
||||
type: object
|
||||
entity.PushChannel:
|
||||
properties:
|
||||
created_at:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
other:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
entity.PushEvent:
|
||||
properties:
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
created_at:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
event_key:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
targets:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
task_type:
|
||||
type: string
|
||||
template:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
handler.batchDownloadRequest:
|
||||
properties:
|
||||
ids:
|
||||
@@ -337,33 +596,6 @@ definitions:
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
model.BindRequest:
|
||||
properties:
|
||||
channel_id:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
type: object
|
||||
model.BindingDTO:
|
||||
properties:
|
||||
channel_id:
|
||||
example: "0"
|
||||
type: string
|
||||
channel_name:
|
||||
type: string
|
||||
channel_type:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
example: "0"
|
||||
type: string
|
||||
platform_user_id:
|
||||
type: string
|
||||
user_id:
|
||||
example: "0"
|
||||
type: string
|
||||
type: object
|
||||
model.BrowserItem:
|
||||
properties:
|
||||
browser:
|
||||
@@ -371,87 +603,6 @@ definitions:
|
||||
count:
|
||||
type: integer
|
||||
type: object
|
||||
model.ChannelDTO:
|
||||
properties:
|
||||
credentials:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
extra:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
id:
|
||||
example: "0"
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
owner_id:
|
||||
example: "0"
|
||||
type: string
|
||||
owner_scope:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
model.CreateChannelRequest:
|
||||
properties:
|
||||
credentials:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
extra:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
model.CreatePushChannelRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
other:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
type: object
|
||||
model.CreatePushEventRequest:
|
||||
properties:
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enabled:
|
||||
type: boolean
|
||||
event_key:
|
||||
type: string
|
||||
targets:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
task_type:
|
||||
type: string
|
||||
template:
|
||||
type: string
|
||||
type: object
|
||||
model.CreateScheduleRequest:
|
||||
properties:
|
||||
cron:
|
||||
@@ -569,15 +720,6 @@ definitions:
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
model.Definition:
|
||||
properties:
|
||||
fields:
|
||||
items:
|
||||
$ref: '#/definitions/model.Field'
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
model.DispatchTaskRequest:
|
||||
properties:
|
||||
end_time:
|
||||
@@ -619,15 +761,6 @@ definitions:
|
||||
description: '"select" 或 "exec"'
|
||||
type: string
|
||||
type: object
|
||||
model.Field:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
required:
|
||||
type: boolean
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
model.ListUsersResponse:
|
||||
properties:
|
||||
total:
|
||||
@@ -679,66 +812,6 @@ definitions:
|
||||
description: 用于加载更早日志的 cursor
|
||||
type: integer
|
||||
type: object
|
||||
model.PublicChannelDTO:
|
||||
properties:
|
||||
id:
|
||||
example: "0"
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
model.PushChannel:
|
||||
properties:
|
||||
created_at:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
other:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
model.PushEvent:
|
||||
properties:
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
created_at:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
event_key:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
targets:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
task_type:
|
||||
type: string
|
||||
template:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
model.Schedule:
|
||||
properties:
|
||||
created_at:
|
||||
@@ -910,30 +983,6 @@ definitions:
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
model.TestPushChannelRequest:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
other:
|
||||
type: string
|
||||
target:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
model.TestPushRequest:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/push.Config'
|
||||
target:
|
||||
type: string
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
model.TestSMTPRequest:
|
||||
properties:
|
||||
smtp_host:
|
||||
@@ -998,55 +1047,6 @@ definitions:
|
||||
- max_size_mb
|
||||
- ttl_minutes
|
||||
type: object
|
||||
model.UpdateChannelRequest:
|
||||
properties:
|
||||
credentials:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
extra:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
model.UpdatePushChannelRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
other:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
model.UpdatePushEventRequest:
|
||||
properties:
|
||||
channels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enabled:
|
||||
type: boolean
|
||||
targets:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
template:
|
||||
type: string
|
||||
required:
|
||||
- template
|
||||
type: object
|
||||
model.UpdateScheduleRequest:
|
||||
properties:
|
||||
cron:
|
||||
@@ -1269,6 +1269,9 @@ definitions:
|
||||
key:
|
||||
description: AppID 或 SMTP 用户名
|
||||
type: string
|
||||
other:
|
||||
description: 附加配置 (如 ChatID / UserKey / 扩展 JSON)
|
||||
type: string
|
||||
secret:
|
||||
description: 签名密钥或 SMTP 密码/Token
|
||||
type: string
|
||||
@@ -2006,7 +2009,7 @@ paths:
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/model.ChannelDTO'
|
||||
$ref: '#/definitions/do.ChannelDTO'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
@@ -2024,7 +2027,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.CreateChannelRequest'
|
||||
$ref: '#/definitions/do.CreateChannelRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2035,7 +2038,7 @@ paths:
|
||||
- $ref: '#/definitions/response.Any'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/model.ChannelDTO'
|
||||
$ref: '#/definitions/do.ChannelDTO'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
@@ -2086,7 +2089,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.UpdateChannelRequest'
|
||||
$ref: '#/definitions/do.UpdateChannelRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2097,7 +2100,7 @@ paths:
|
||||
- $ref: '#/definitions/response.Any'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/model.ChannelDTO'
|
||||
$ref: '#/definitions/do.ChannelDTO'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
@@ -2155,7 +2158,7 @@ paths:
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/model.Definition'
|
||||
$ref: '#/definitions/do.Definition'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
@@ -2177,7 +2180,7 @@ paths:
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/model.PushChannel'
|
||||
$ref: '#/definitions/entity.PushChannel'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
@@ -2195,7 +2198,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.CreatePushChannelRequest'
|
||||
$ref: '#/definitions/do.CreatePushChannelRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2206,7 +2209,7 @@ paths:
|
||||
- $ref: '#/definitions/response.Any'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/model.PushChannel'
|
||||
$ref: '#/definitions/entity.PushChannel'
|
||||
type: object
|
||||
security:
|
||||
- SessionCookie: []
|
||||
@@ -2251,7 +2254,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.UpdatePushChannelRequest'
|
||||
$ref: '#/definitions/do.UpdatePushChannelRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2262,7 +2265,7 @@ paths:
|
||||
- $ref: '#/definitions/response.Any'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/model.PushChannel'
|
||||
$ref: '#/definitions/entity.PushChannel'
|
||||
type: object
|
||||
security:
|
||||
- SessionCookie: []
|
||||
@@ -2295,7 +2298,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.TestPushChannelRequest'
|
||||
$ref: '#/definitions/do.TestPushChannelRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2322,7 +2325,7 @@ paths:
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/model.PushEvent'
|
||||
$ref: '#/definitions/entity.PushEvent'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
@@ -2340,7 +2343,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.CreatePushEventRequest'
|
||||
$ref: '#/definitions/do.CreatePushEventRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2351,7 +2354,7 @@ paths:
|
||||
- $ref: '#/definitions/response.Any'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/model.PushEvent'
|
||||
$ref: '#/definitions/entity.PushEvent'
|
||||
type: object
|
||||
security:
|
||||
- SessionCookie: []
|
||||
@@ -2399,7 +2402,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.UpdatePushEventRequest'
|
||||
$ref: '#/definitions/do.UpdatePushEventRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2501,7 +2504,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.TestPushRequest'
|
||||
$ref: '#/definitions/do.TestPushRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -4141,7 +4144,7 @@ paths:
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/model.BindingDTO'
|
||||
$ref: '#/definitions/do.BindingDTO'
|
||||
type: array
|
||||
type: object
|
||||
"401":
|
||||
@@ -4164,7 +4167,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BindRequest'
|
||||
$ref: '#/definitions/do.BindRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -4175,7 +4178,7 @@ paths:
|
||||
- $ref: '#/definitions/response.Any'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/model.BindingDTO'
|
||||
$ref: '#/definitions/do.BindingDTO'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
@@ -4233,7 +4236,7 @@ paths:
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/model.PublicChannelDTO'
|
||||
$ref: '#/definitions/do.PublicChannelDTO'
|
||||
type: array
|
||||
type: object
|
||||
"401":
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/core/extpoints"
|
||||
"Wavelet/plugins/domain/admin"
|
||||
"Wavelet/plugins/domain/message_gateway"
|
||||
"Wavelet/plugins/domain/msg_gateway"
|
||||
"Wavelet/plugins/domain/user"
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestRoutesMountedBeforeAuthServiceAreGuarded(t *testing.T) {
|
||||
core.WithPlugins(
|
||||
dbProvider(),
|
||||
user.New(),
|
||||
message_gateway.New(),
|
||||
msg_gateway.New(),
|
||||
authProvider(),
|
||||
),
|
||||
)
|
||||
@@ -147,7 +147,7 @@ func TestAuthConsumersDeclareAuthDependency(t *testing.T) {
|
||||
deps []reflect.Type
|
||||
}{
|
||||
{"user", user.New().Inject()},
|
||||
{"message_gateway", message_gateway.New().Inject()},
|
||||
{"msg_gateway", msg_gateway.New().Inject()},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Contains(t, tc.deps, want,
|
||||
@@ -194,7 +194,7 @@ func TestAuthGuardFailsClosed(t *testing.T) {
|
||||
prefix string
|
||||
}{
|
||||
{"user", user.New().Apply, http.MethodPost, "/api/v1/user/change-password"},
|
||||
{"message_gateway", message_gateway.New().Apply, http.MethodGet, "/api/v1/message-gateway"},
|
||||
{"msg_gateway", msg_gateway.New().Apply, http.MethodGet, "/api/v1/message-gateway"},
|
||||
{"admin", admin.New().Apply, http.MethodGet, "/api/v1/admin"},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"Wavelet/pkg/idgen"
|
||||
"Wavelet/plugins/domain/admin"
|
||||
"Wavelet/plugins/domain/auth"
|
||||
"Wavelet/plugins/domain/message_gateway"
|
||||
"Wavelet/plugins/domain/msg_gateway"
|
||||
"Wavelet/plugins/domain/risk_control"
|
||||
"Wavelet/plugins/domain/system"
|
||||
"Wavelet/plugins/domain/user"
|
||||
@@ -47,13 +47,13 @@ func setupTestDB(t *testing.T) *gorm.DB {
|
||||
&user.AccessToken{},
|
||||
&auth.AuthSource{},
|
||||
&auth.ExternalAccount{},
|
||||
&message_gateway.MessageChannel{},
|
||||
&message_gateway.MessageBinding{},
|
||||
&message_gateway.MessagePairingCode{},
|
||||
&msg_gateway.MessageChannel{},
|
||||
&msg_gateway.MessageBinding{},
|
||||
&msg_gateway.MessagePairingCode{},
|
||||
&admin.SystemConfig{},
|
||||
&message_gateway.PushChannel{},
|
||||
&message_gateway.PushEvent{},
|
||||
&message_gateway.PushHistory{},
|
||||
&msg_gateway.PushChannel{},
|
||||
&msg_gateway.PushEvent{},
|
||||
&msg_gateway.PushHistory{},
|
||||
))
|
||||
|
||||
db.SetDB(testDB)
|
||||
@@ -262,15 +262,15 @@ func TestMessageGatewayPlugin(t *testing.T) {
|
||||
require.NoError(t, cache.New().Apply(ctx))
|
||||
require.NoError(t, logger.New().Apply(ctx))
|
||||
|
||||
p := message_gateway.New()
|
||||
assert.Equal(t, "message_gateway", p.Name())
|
||||
assert.Equal(t, "message_gateway", p.Manifest().Name)
|
||||
p := msg_gateway.New()
|
||||
assert.Equal(t, "msg_gateway", p.Name())
|
||||
assert.Equal(t, "msg_gateway", p.Manifest().Name)
|
||||
require.NoError(t, p.Apply(ctx))
|
||||
|
||||
// 1. Migrations
|
||||
entry, ok := ctx.Migrations().Get("message_gateway")
|
||||
entry, ok := ctx.Migrations().Get("msg_gateway")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "message_gateway", entry.PluginID)
|
||||
assert.Equal(t, "msg_gateway", entry.PluginID)
|
||||
|
||||
// 2. Routes
|
||||
routes := ctx.Router().Routes()
|
||||
@@ -287,24 +287,24 @@ func TestMessageGatewayPlugin(t *testing.T) {
|
||||
assert.True(t, hasBindings)
|
||||
|
||||
// 3. Tasks & Schedules
|
||||
taskDef, ok := ctx.Tasks().Get("message_gateway:push_notification")
|
||||
taskDef, ok := ctx.Tasks().Get("msg_gateway:push_notification")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 3, taskDef.Retry)
|
||||
|
||||
schedDef, ok := ctx.Schedules().Get("message_gateway:cleanup_pairing_codes")
|
||||
schedDef, ok := ctx.Schedules().Get("msg_gateway:cleanup_pairing_codes")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "*/10 * * * *", schedDef.Spec)
|
||||
|
||||
// 4. EventBus Trigger
|
||||
var receivedEvent message_gateway.PushNotificationEvent
|
||||
var receivedEvent msg_gateway.PushNotificationEvent
|
||||
var eventFired bool
|
||||
ctx.Events().On("notification:push", func(c context.Context, e message_gateway.PushNotificationEvent) error {
|
||||
ctx.Events().On("notification:push", func(c context.Context, e msg_gateway.PushNotificationEvent) error {
|
||||
eventFired = true
|
||||
receivedEvent = e
|
||||
return nil
|
||||
})
|
||||
|
||||
err := ctx.Events().Emit(context.Background(), "notification:push", message_gateway.PushNotificationEvent{
|
||||
err := ctx.Events().Emit(context.Background(), "notification:push", msg_gateway.PushNotificationEvent{
|
||||
UserID: 99,
|
||||
Channel: "telegram",
|
||||
Title: "System Alert",
|
||||
@@ -317,7 +317,7 @@ func TestMessageGatewayPlugin(t *testing.T) {
|
||||
assert.Equal(t, "System Alert", receivedEvent.Title)
|
||||
|
||||
// 5. Settings
|
||||
schema, ok := ctx.Settings().Get("message_gateway.pairing_code_expiry_minutes")
|
||||
schema, ok := ctx.Settings().Get("msg_gateway.pairing_code_expiry_minutes")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 15, schema.Default)
|
||||
}
|
||||
@@ -480,7 +480,7 @@ func TestAllDomainPluginsCombined(t *testing.T) {
|
||||
// Apply Domain plugins
|
||||
require.NoError(t, auth.New().Apply(ctx))
|
||||
require.NoError(t, user.New().Apply(ctx))
|
||||
require.NoError(t, message_gateway.New().Apply(ctx))
|
||||
require.NoError(t, msg_gateway.New().Apply(ctx))
|
||||
require.NoError(t, risk_control.New().Apply(ctx))
|
||||
require.NoError(t, admin.New().Apply(ctx))
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
// Field is one admin form field.
|
||||
type Field struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// Definition describes a channel type form.
|
||||
type Definition struct {
|
||||
Type string `json:"type"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
|
||||
// ChannelDTO represents a channel for admin consumption.
|
||||
type ChannelDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
OwnerScope string `json:"owner_scope"`
|
||||
OwnerID *uint64 `json:"owner_id,string,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
|
||||
// CreateChannelRequest is admin create payload.
|
||||
type CreateChannelRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
|
||||
// UpdateChannelRequest is admin update payload.
|
||||
type UpdateChannelRequest struct {
|
||||
Name string `json:"name"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package model defines the domain entities, DTOs, and schemas for message_gateway.
|
||||
package model
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Channel type and scope constants.
|
||||
const (
|
||||
ChannelTypeTelegram = "telegram"
|
||||
ChannelTypeQQ = "qq"
|
||||
MessageChannelTypeTelegram = "telegram"
|
||||
MessageChannelTypeQQ = "qq"
|
||||
MessageOwnerScopeSystem = "system"
|
||||
|
||||
TypeCustom = "custom"
|
||||
TypeEmail = "email"
|
||||
TypeTelegram = "telegram"
|
||||
)
|
||||
|
||||
// Capability describes what an adapter can send and receive.
|
||||
type Capability struct {
|
||||
Text bool
|
||||
Image bool
|
||||
File bool
|
||||
Reply bool
|
||||
Group bool
|
||||
}
|
||||
|
||||
// ChannelConfig is the decrypted runtime config passed to a factory.
|
||||
type ChannelConfig struct {
|
||||
ID uint64
|
||||
Type string
|
||||
Name string
|
||||
Credentials map[string]string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// Recipient is the outbound destination on a platform.
|
||||
type Recipient struct {
|
||||
ChatID string
|
||||
PlatformUserID string
|
||||
}
|
||||
|
||||
// Attachment is a downloaded inbound file sitting on local disk.
|
||||
type Attachment struct {
|
||||
Path string
|
||||
FileName string
|
||||
MIME string
|
||||
Error string
|
||||
}
|
||||
|
||||
// InboundMessage is a normalized private-chat message.
|
||||
type InboundMessage struct {
|
||||
ChannelID uint64
|
||||
ChannelType string
|
||||
PlatformUserID string
|
||||
ChatID string
|
||||
MessageID string
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
BindingUserID *uint64
|
||||
}
|
||||
|
||||
// OutboundMessage is a reply or probe send.
|
||||
type OutboundMessage struct {
|
||||
Text string
|
||||
ReplyToID string
|
||||
Attachments []Attachment
|
||||
}
|
||||
|
||||
// MessageChannel is an admin-configured messaging adapter.
|
||||
type MessageChannel struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Type string `json:"type" gorm:"size:32;not null"`
|
||||
Name string `json:"name" gorm:"size:64;not null"`
|
||||
OwnerScope string `json:"owner_scope" gorm:"size:32;not null;default:'system'"`
|
||||
OwnerID *uint64 `json:"owner_id,omitempty"`
|
||||
Credentials string `json:"credentials" gorm:"type:text;not null"`
|
||||
Extra string `json:"extra" gorm:"type:text"`
|
||||
Enabled bool `json:"enabled" gorm:"default:false;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (MessageChannel) TableName() string {
|
||||
return "w_message_channels"
|
||||
}
|
||||
|
||||
// MessageBinding maps a platform user to a Wavelet user on one channel.
|
||||
type MessageBinding struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ChannelID uint64 `json:"channel_id" gorm:"not null;index"`
|
||||
PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"`
|
||||
UserID uint64 `json:"user_id" gorm:"not null;index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (MessageBinding) TableName() string {
|
||||
return "w_message_bindings"
|
||||
}
|
||||
|
||||
// MessagePairingCode is a one-time bind code.
|
||||
type MessagePairingCode struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Code string `json:"code" gorm:"size:32;uniqueIndex;not null"`
|
||||
ChannelID uint64 `json:"channel_id" gorm:"not null;index"`
|
||||
PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"`
|
||||
UserID uint64 `json:"user_id" gorm:"not null;index"`
|
||||
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (MessagePairingCode) TableName() string {
|
||||
return "w_message_pairing_codes"
|
||||
}
|
||||
|
||||
// PushChannel 消息通道模型
|
||||
type PushChannel struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Description string `json:"description" gorm:"size:255"`
|
||||
Type string `json:"type" gorm:"size:50;not null;index"`
|
||||
URL string `json:"url" gorm:"type:text"`
|
||||
Token string `json:"token" gorm:"type:text"`
|
||||
Other string `json:"other" gorm:"type:text"`
|
||||
Enabled bool `json:"enabled" gorm:"index;not null;default:true"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
// TableName 指定 GORM 表名
|
||||
func (PushChannel) TableName() string {
|
||||
return "w_push_channels"
|
||||
}
|
||||
|
||||
// Validate 验证与标准化字段
|
||||
func (c *PushChannel) Validate() error {
|
||||
c.Name = strings.TrimSpace(c.Name)
|
||||
if c.Name == "" {
|
||||
return errors.New(errs.ErrChannelNameRequired)
|
||||
}
|
||||
c.Type = strings.TrimSpace(c.Type)
|
||||
if c.Type == "" {
|
||||
return errors.New(errs.ErrChannelTypeRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushEvent 系统通知事件模型
|
||||
type PushEvent struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
EventKey string `json:"event_key" gorm:"uniqueIndex;size:80;not null"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
TaskType string `json:"task_type" gorm:"size:100;index;not null;default:''"`
|
||||
Channels []string `json:"channels" gorm:"type:text;serializer:json;not null"`
|
||||
Targets []string `json:"targets" gorm:"type:text;serializer:json;not null"`
|
||||
Template string `json:"template" gorm:"type:text;not null"`
|
||||
Enabled bool `json:"enabled" gorm:"index;not null;default:false"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
// TableName 指定 GORM 表名
|
||||
func (PushEvent) TableName() string {
|
||||
return "w_push_events"
|
||||
}
|
||||
|
||||
// Validate 验证 PushEvent 实体字段
|
||||
func (e *PushEvent) Validate() error {
|
||||
e.EventKey = strings.TrimSpace(e.EventKey)
|
||||
if e.EventKey == "" {
|
||||
return errors.New(errs.ErrEventKeyRequired)
|
||||
}
|
||||
e.Name = strings.TrimSpace(e.Name)
|
||||
if e.Name == "" {
|
||||
return errors.New(errs.ErrNameRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushHistory 推送日志/历史实体
|
||||
type PushHistory struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
EventKey string `json:"event_key" gorm:"size:80;not null;index"`
|
||||
Channel string `json:"channel" gorm:"size:50;not null;index"`
|
||||
Target string `json:"target" gorm:"size:255;not null"`
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
Level string `json:"level" gorm:"size:20;not null;default:'INFO'"`
|
||||
Status string `json:"status" gorm:"size:20;not null;index"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"type:text"`
|
||||
Payload string `json:"payload" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
}
|
||||
|
||||
// TableName 指定 GORM 表名
|
||||
func (PushHistory) TableName() string {
|
||||
return "w_push_histories"
|
||||
}
|
||||
|
||||
// BindRequest is the user bind body.
|
||||
type BindRequest struct {
|
||||
ChannelID string `json:"channel_id"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// BindingDTO is a user-facing binding row.
|
||||
type BindingDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
UserID uint64 `json:"user_id,string"`
|
||||
ChannelID uint64 `json:"channel_id,string"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelType string `json:"channel_type"`
|
||||
PlatformUserID string `json:"platform_user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// PublicChannelDTO is an enabled channel a user can bind to.
|
||||
type PublicChannelDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// PushNotificationEvent defines the payload for eventbus notification trigger.
|
||||
type PushNotificationEvent struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
Channel string `json:"channel"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
+11
-10
@@ -7,8 +7,9 @@ package qq
|
||||
import (
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/util"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -33,7 +34,7 @@ type qqEvent struct {
|
||||
|
||||
// Adapter is an official QQ Bot C2C channel.
|
||||
type Adapter struct {
|
||||
cfg model.ChannelConfig
|
||||
cfg do.ChannelConfig
|
||||
onInbound service.Handler
|
||||
api openapi.OpenAPI
|
||||
tokenSrc oauth2.TokenSource
|
||||
@@ -43,7 +44,7 @@ type Adapter struct {
|
||||
}
|
||||
|
||||
// New constructs a QQ adapter.
|
||||
func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, error) {
|
||||
func New(cfg do.ChannelConfig, onInbound service.Handler) (service.Channel, error) {
|
||||
if strings.TrimSpace(cfg.Credentials["app_id"]) == "" || strings.TrimSpace(cfg.Credentials["app_secret"]) == "" {
|
||||
return nil, fmt.Errorf("qq: app_id and app_secret are required")
|
||||
}
|
||||
@@ -51,11 +52,11 @@ func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, e
|
||||
}
|
||||
|
||||
// Type returns qq.
|
||||
func (a *Adapter) Type() string { return model.ChannelTypeQQ }
|
||||
func (a *Adapter) Type() string { return consts.ChannelTypeQQ }
|
||||
|
||||
// Capabilities reports C2C text/media support.
|
||||
func (a *Adapter) Capabilities() model.Capability {
|
||||
return model.Capability{Text: true, Image: true, File: true, Reply: true}
|
||||
func (a *Adapter) Capabilities() do.Capability {
|
||||
return do.Capability{Text: true, Image: true, File: true, Reply: true}
|
||||
}
|
||||
|
||||
// Connect starts the official WebSocket session (C2C intent).
|
||||
@@ -128,7 +129,7 @@ func (a *Adapter) Disconnect(_ context.Context) error {
|
||||
}
|
||||
|
||||
// Send posts a C2C text reply.
|
||||
func (a *Adapter) Send(ctx context.Context, to model.Recipient, msg model.OutboundMessage) error {
|
||||
func (a *Adapter) Send(ctx context.Context, to do.Recipient, msg do.OutboundMessage) error {
|
||||
a.mu.Lock()
|
||||
api := a.api
|
||||
a.mu.Unlock()
|
||||
@@ -152,9 +153,9 @@ func (a *Adapter) handleEvent(ctx context.Context, ev qqEvent) {
|
||||
if disconnected || a.onInbound == nil {
|
||||
return
|
||||
}
|
||||
_ = a.onInbound(ctx, model.InboundMessage{
|
||||
_ = a.onInbound(ctx, do.InboundMessage{
|
||||
ChannelID: a.cfg.ID,
|
||||
ChannelType: model.ChannelTypeQQ,
|
||||
ChannelType: consts.ChannelTypeQQ,
|
||||
PlatformUserID: ev.UserID,
|
||||
ChatID: ev.UserID,
|
||||
MessageID: ev.MessageID,
|
||||
+5
-5
@@ -4,14 +4,14 @@
|
||||
package qq
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleEvent_DropsNonC2C(t *testing.T) {
|
||||
var got int
|
||||
a := &Adapter{onInbound: func(ctx context.Context, msg model.InboundMessage) error {
|
||||
a := &Adapter{onInbound: func(_ context.Context, _ do.InboundMessage) error {
|
||||
got++
|
||||
return nil
|
||||
}}
|
||||
@@ -22,8 +22,8 @@ func TestHandleEvent_DropsNonC2C(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleEvent_C2CText(t *testing.T) {
|
||||
var got model.InboundMessage
|
||||
a := &Adapter{cfg: model.ChannelConfig{ID: 3}, onInbound: func(ctx context.Context, msg model.InboundMessage) error {
|
||||
var got do.InboundMessage
|
||||
a := &Adapter{cfg: do.ChannelConfig{ID: 3}, onInbound: func(_ context.Context, msg do.InboundMessage) error {
|
||||
got = msg
|
||||
return nil
|
||||
}}
|
||||
@@ -34,7 +34,7 @@ func TestHandleEvent_C2CText(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNew_RequiresCreds(t *testing.T) {
|
||||
_, err := New(model.ChannelConfig{}, nil)
|
||||
_, err := New(do.ChannelConfig{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
+17
-16
@@ -7,8 +7,9 @@ package telegram
|
||||
import (
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/util"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -22,13 +23,13 @@ import (
|
||||
|
||||
// Adapter is a Telegram private-chat channel.
|
||||
type Adapter struct {
|
||||
cfg model.ChannelConfig
|
||||
cfg do.ChannelConfig
|
||||
onInbound service.Handler
|
||||
bot *tele.Bot
|
||||
}
|
||||
|
||||
// New constructs a Telegram adapter. Call service.Register from the runner.
|
||||
func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, error) {
|
||||
func New(cfg do.ChannelConfig, onInbound service.Handler) (service.Channel, error) {
|
||||
if strings.TrimSpace(cfg.Credentials["bot_token"]) == "" {
|
||||
return nil, fmt.Errorf("telegram: bot_token is required")
|
||||
}
|
||||
@@ -36,11 +37,11 @@ func New(cfg model.ChannelConfig, onInbound service.Handler) (service.Channel, e
|
||||
}
|
||||
|
||||
// Type returns telegram.
|
||||
func (a *Adapter) Type() string { return model.ChannelTypeTelegram }
|
||||
func (a *Adapter) Type() string { return consts.ChannelTypeTelegram }
|
||||
|
||||
// Capabilities reports private-chat media support.
|
||||
func (a *Adapter) Capabilities() model.Capability {
|
||||
return model.Capability{Text: true, Image: true, File: true, Reply: true}
|
||||
func (a *Adapter) Capabilities() do.Capability {
|
||||
return do.Capability{Text: true, Image: true, File: true, Reply: true}
|
||||
}
|
||||
|
||||
// longPollWindow is how long Telegram may hold a getUpdates call open before
|
||||
@@ -50,7 +51,7 @@ func (a *Adapter) Capabilities() model.Capability {
|
||||
const longPollWindow = 10 * time.Second
|
||||
|
||||
// buildTeleSettings assembles the telebot settings.
|
||||
func buildTeleSettings(cfg model.ChannelConfig) tele.Settings {
|
||||
func buildTeleSettings(cfg do.ChannelConfig) tele.Settings {
|
||||
pref := tele.Settings{
|
||||
Token: cfg.Credentials["bot_token"],
|
||||
Poller: &tele.LongPoller{Timeout: longPollWindow},
|
||||
@@ -99,7 +100,7 @@ func (a *Adapter) Disconnect(_ context.Context) error {
|
||||
}
|
||||
|
||||
// Send replies to a private chat.
|
||||
func (a *Adapter) Send(_ context.Context, to model.Recipient, msg model.OutboundMessage) error {
|
||||
func (a *Adapter) Send(_ context.Context, to do.Recipient, msg do.OutboundMessage) error {
|
||||
if a.bot == nil {
|
||||
return fmt.Errorf("telegram: not connected")
|
||||
}
|
||||
@@ -118,9 +119,9 @@ func (a *Adapter) handleTeleMessage(ctx context.Context, m *tele.Message) {
|
||||
if a.onInbound == nil {
|
||||
return
|
||||
}
|
||||
msg := model.InboundMessage{
|
||||
msg := do.InboundMessage{
|
||||
ChannelID: a.cfg.ID,
|
||||
ChannelType: model.ChannelTypeTelegram,
|
||||
ChannelType: consts.ChannelTypeTelegram,
|
||||
PlatformUserID: strconv.FormatInt(m.Sender.ID, 10),
|
||||
ChatID: strconv.FormatInt(m.Chat.ID, 10),
|
||||
MessageID: strconv.Itoa(m.ID),
|
||||
@@ -146,7 +147,7 @@ func (a *Adapter) handleTeleMessage(ctx context.Context, m *tele.Message) {
|
||||
// downloadMedia fetches message media into a scratch directory, returned so the
|
||||
// caller can remove it once the inbound handler no longer needs the paths.
|
||||
// An empty dir means nothing was downloaded.
|
||||
func (a *Adapter) downloadMedia(m *tele.Message) (string, []model.Attachment) {
|
||||
func (a *Adapter) downloadMedia(m *tele.Message) (string, []do.Attachment) {
|
||||
var files []*tele.File
|
||||
var names []string
|
||||
if m.Photo != nil {
|
||||
@@ -166,16 +167,16 @@ func (a *Adapter) downloadMedia(m *tele.Message) (string, []model.Attachment) {
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "wg-tg-*")
|
||||
if err != nil {
|
||||
return "", []model.Attachment{{Error: err.Error()}}
|
||||
return "", []do.Attachment{{Error: err.Error()}}
|
||||
}
|
||||
out := make([]model.Attachment, 0, len(files))
|
||||
out := make([]do.Attachment, 0, len(files))
|
||||
for i, f := range files {
|
||||
path := filepath.Join(dir, names[i])
|
||||
if err := a.bot.Download(f, path); err != nil {
|
||||
out = append(out, model.Attachment{FileName: names[i], Error: err.Error()})
|
||||
out = append(out, do.Attachment{FileName: names[i], Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
out = append(out, model.Attachment{Path: path, FileName: names[i]})
|
||||
out = append(out, do.Attachment{Path: path, FileName: names[i]})
|
||||
}
|
||||
return dir, out
|
||||
}
|
||||
+7
-7
@@ -4,7 +4,7 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// telebot 以 int(timeout/time.Second) 下发给 getUpdates。写成裸整数会被解释为
|
||||
// 纳秒,令 timeout=0,长轮询退化为对 Bot API 的空转轮询。
|
||||
func TestBuildTeleSettingsLongPollWindow(t *testing.T) {
|
||||
pref := buildTeleSettings(model.ChannelConfig{
|
||||
pref := buildTeleSettings(do.ChannelConfig{
|
||||
Credentials: map[string]string{"bot_token": "token"},
|
||||
Extra: map[string]string{"base_url": "https://tg.example.com/api/"},
|
||||
})
|
||||
@@ -35,7 +35,7 @@ func TestBuildTeleSettingsLongPollWindow(t *testing.T) {
|
||||
|
||||
func TestHandleUpdate_DropsGroups(t *testing.T) {
|
||||
var got int
|
||||
a := &Adapter{onInbound: func(ctx context.Context, msg model.InboundMessage) error {
|
||||
a := &Adapter{onInbound: func(_ context.Context, _ do.InboundMessage) error {
|
||||
got++
|
||||
return nil
|
||||
}}
|
||||
@@ -51,10 +51,10 @@ func TestHandleUpdate_DropsGroups(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleUpdate_PrivateText(t *testing.T) {
|
||||
var got model.InboundMessage
|
||||
var got do.InboundMessage
|
||||
a := &Adapter{
|
||||
cfg: model.ChannelConfig{ID: 7, Type: "telegram"},
|
||||
onInbound: func(ctx context.Context, msg model.InboundMessage) error {
|
||||
cfg: do.ChannelConfig{ID: 7, Type: "telegram"},
|
||||
onInbound: func(_ context.Context, msg do.InboundMessage) error {
|
||||
got = msg
|
||||
return nil
|
||||
},
|
||||
@@ -71,7 +71,7 @@ func TestHandleUpdate_PrivateText(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNew_RequiresToken(t *testing.T) {
|
||||
_, err := New(model.ChannelConfig{}, nil)
|
||||
_, err := New(do.ChannelConfig{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
+62
-5
@@ -1,12 +1,69 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package errs defines error sentinels and user-facing error message constants
|
||||
// for the message_gateway plugin.
|
||||
package errs
|
||||
// Package consts defines constants, sentinel errors, and user-facing error messages
|
||||
// for the msg_gateway plugin.
|
||||
package consts
|
||||
|
||||
import "errors"
|
||||
|
||||
// Channel type and scope constants.
|
||||
const (
|
||||
ChannelTypeTelegram = "telegram"
|
||||
ChannelTypeQQ = "qq"
|
||||
MessageChannelTypeTelegram = "telegram"
|
||||
MessageChannelTypeQQ = "qq"
|
||||
MessageOwnerScopeSystem = "system"
|
||||
|
||||
TypeCustom = "custom"
|
||||
TypeEmail = "email"
|
||||
TypeTelegram = "telegram"
|
||||
ChannelCustom = "custom"
|
||||
ChannelEmail = "email"
|
||||
ChannelLark = "lark"
|
||||
ChannelDingTalk = "dingtalk"
|
||||
ChannelTelegram = "telegram"
|
||||
ChannelBark = "bark"
|
||||
ChannelDiscord = "discord"
|
||||
ChannelSlack = "slack"
|
||||
ChannelPushover = "pushover"
|
||||
|
||||
DefaultLevelInfo = "INFO"
|
||||
KeyTitle = "title"
|
||||
KeyContent = "content"
|
||||
KeyLevel = "level"
|
||||
|
||||
// KeyURL represents the URL field key.
|
||||
KeyURL = "url"
|
||||
// KeyToken represents the Token field key.
|
||||
KeyToken = "token"
|
||||
// KeyOther represents the Other field key.
|
||||
KeyOther = "other"
|
||||
|
||||
// TypeText represents standard text input type.
|
||||
TypeText = "text"
|
||||
// TypePassword represents password input type.
|
||||
TypePassword = "password"
|
||||
// TypeTextarea represents textarea input type.
|
||||
TypeTextarea = "textarea"
|
||||
)
|
||||
|
||||
// Task and Schedule identifier constants.
|
||||
const (
|
||||
TaskPushNotification = "msg_gateway:push_notification"
|
||||
TaskCleanupPairingCodes = "msg_gateway:cleanup_pairing_codes"
|
||||
TaskDispatchBotMsg = "msg_gateway:dispatch_bot_msg"
|
||||
TaskTypeDispatchBotMsg = "dispatch_bot_msg"
|
||||
SendNotificationTask = "push:send"
|
||||
TaskTypeSendNotification = "send_notification"
|
||||
)
|
||||
|
||||
// Pairing code constants.
|
||||
const (
|
||||
CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
CodeLength = 8
|
||||
)
|
||||
|
||||
// Sentinel errors.
|
||||
var (
|
||||
ErrCodeInvalid = errors.New("invalid or expired pairing code")
|
||||
@@ -17,11 +74,11 @@ var (
|
||||
ErrChannelIDRequired = errors.New("channel_id is required")
|
||||
ErrChannelDisabled = errors.New("channel is not enabled")
|
||||
|
||||
// ErrRecordNotFound maps GORM's missing-row sentinel at the repository boundary so
|
||||
// ErrRecordNotFound maps GORM's missing-row sentinel at the DAO boundary so
|
||||
// upper layers never import gorm. Its text matches gorm.ErrRecordNotFound verbatim.
|
||||
ErrRecordNotFound = errors.New("record not found")
|
||||
|
||||
// ErrUnsupportedUserLookupField rejects a column name that the repository is not
|
||||
// ErrUnsupportedUserLookupField rejects a column name that the DAO is not
|
||||
// allowed to interpolate into a WHERE clause.
|
||||
ErrUnsupportedUserLookupField = errors.New("unsupported user lookup field")
|
||||
)
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handler
|
||||
package controller
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/response"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// @Tags admin-message-gateway
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.Definition}
|
||||
// @Success 200 {object} response.Any{data=[]do.Definition}
|
||||
// @Router /api/v1/admin/message-gateway/channels/definitions [get]
|
||||
func ListAdminChannelDefinitions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(service.ListDefinitions()))
|
||||
@@ -31,7 +31,7 @@ func ListAdminChannelDefinitions(c *gin.Context) {
|
||||
// @Tags admin-message-gateway
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.ChannelDTO}
|
||||
// @Success 200 {object} response.Any{data=[]do.ChannelDTO}
|
||||
// @Router /api/v1/admin/message-gateway/channels [get]
|
||||
func ListAdminChannels(c *gin.Context) {
|
||||
rows, err := service.ListChannels(c.Request.Context())
|
||||
@@ -45,14 +45,14 @@ func ListAdminChannels(c *gin.Context) {
|
||||
func parseAdminChannelID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, errs.ErrInvalidChannelID)
|
||||
response.AbortBadRequest(c, consts.ErrInvalidChannelID)
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func handleAdminChannelError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) {
|
||||
if err.Error() == errs.ErrChannelNotFound {
|
||||
if err.Error() == consts.ErrChannelNotFound {
|
||||
response.AbortNotFound(c, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -66,8 +66,8 @@ func handleAdminChannelError(c *gin.Context, err error, fallback func(c *gin.Con
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body model.CreateChannelRequest true "create body"
|
||||
// @Success 200 {object} response.Any{data=model.ChannelDTO}
|
||||
// @Param request body do.CreateChannelRequest true "create body"
|
||||
// @Success 200 {object} response.Any{data=do.ChannelDTO}
|
||||
// @Failure 400 {object} response.Any
|
||||
// @Router /api/v1/admin/message-gateway/channels [post]
|
||||
func CreateAdminChannel(c *gin.Context) {
|
||||
@@ -82,8 +82,8 @@ func CreateAdminChannel(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "channel id"
|
||||
// @Param request body model.UpdateChannelRequest true "update body"
|
||||
// @Success 200 {object} response.Any{data=model.ChannelDTO}
|
||||
// @Param request body do.UpdateChannelRequest true "update body"
|
||||
// @Success 200 {object} response.Any{data=do.ChannelDTO}
|
||||
// @Failure 400 {object} response.Any
|
||||
// @Failure 404 {object} response.Any
|
||||
// @Router /api/v1/admin/message-gateway/channels/{id} [patch]
|
||||
+15
-15
@@ -1,13 +1,13 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handler
|
||||
package controller
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/response"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// @Success 200 {object} response.Any "通道配置定义列表"
|
||||
// @Router /api/v1/admin/push/channels/definitions [get]
|
||||
func ListPushChannelDefinitions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(model.ListPushDefinitions()))
|
||||
c.JSON(http.StatusOK, response.OK(do.ListPushDefinitions()))
|
||||
}
|
||||
|
||||
// ListPushChannels 获取消息通道列表
|
||||
@@ -33,7 +33,7 @@ func ListPushChannelDefinitions(c *gin.Context) {
|
||||
// @Tags admin-push
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.PushChannel} "消息通道列表"
|
||||
// @Success 200 {object} response.Any{data=[]entity.PushChannel} "消息通道列表"
|
||||
// @Router /api/v1/admin/push/channels [get]
|
||||
func ListPushChannels(c *gin.Context) {
|
||||
channels, err := service.ListPushChannels(c.Request.Context())
|
||||
@@ -48,7 +48,7 @@ func ListPushChannels(c *gin.Context) {
|
||||
func parsePushChannelID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, errs.ErrInvalidChannelID)
|
||||
response.AbortBadRequest(c, consts.ErrInvalidChannelID)
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
@@ -56,8 +56,8 @@ func parsePushChannelID(c *gin.Context) (uint64, bool) {
|
||||
|
||||
// handlePushChannelNotFoundError maps a missing channel row to 404, others to fallback.
|
||||
func handlePushChannelNotFoundError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, errs.ErrChannelNotFound)
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, consts.ErrChannelNotFound)
|
||||
return
|
||||
}
|
||||
fallback(c, err.Error())
|
||||
@@ -70,8 +70,8 @@ func handlePushChannelNotFoundError(c *gin.Context, err error, fallback func(c *
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body model.CreatePushChannelRequest true "创建参数"
|
||||
// @Success 200 {object} response.Any{data=model.PushChannel} "创建成功"
|
||||
// @Param request body do.CreatePushChannelRequest true "创建参数"
|
||||
// @Success 200 {object} response.Any{data=entity.PushChannel} "创建成功"
|
||||
// @Router /api/v1/admin/push/channels [post]
|
||||
func CreatePushChannel(c *gin.Context) {
|
||||
handleJSONRequest(c, service.CreatePushChannel)
|
||||
@@ -85,8 +85,8 @@ func CreatePushChannel(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "通道ID"
|
||||
// @Param request body model.UpdatePushChannelRequest true "更新参数"
|
||||
// @Success 200 {object} response.Any{data=model.PushChannel} "更新成功"
|
||||
// @Param request body do.UpdatePushChannelRequest true "更新参数"
|
||||
// @Success 200 {object} response.Any{data=entity.PushChannel} "更新成功"
|
||||
// @Router /api/v1/admin/push/channels/{id} [put]
|
||||
func UpdatePushChannel(c *gin.Context) {
|
||||
handleEntityUpdate(c, parsePushChannelID, service.UpdatePushChannel, func(c *gin.Context, err error) {
|
||||
@@ -123,11 +123,11 @@ func DeletePushChannel(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body model.TestPushChannelRequest true "测试参数"
|
||||
// @Param request body do.TestPushChannelRequest true "测试参数"
|
||||
// @Success 200 {object} response.Any "测试触发成功"
|
||||
// @Router /api/v1/admin/push/channels/test [post]
|
||||
func TestPushChannel(c *gin.Context) {
|
||||
var req model.TestPushChannelRequest
|
||||
var req do.TestPushChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
+16
-15
@@ -1,13 +1,13 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handler
|
||||
package controller
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/response"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// @Tags admin-push
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.PushEvent} "通知事件列表"
|
||||
// @Success 200 {object} response.Any{data=[]entity.PushEvent} "通知事件列表"
|
||||
// @Router /api/v1/admin/push/events [get]
|
||||
func ListPushEvents(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
@@ -49,7 +49,7 @@ func ListBuiltInPushEvents(c *gin.Context) {
|
||||
func parsePushEventID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, errs.ErrInvalidEventID)
|
||||
response.AbortBadRequest(c, consts.ErrInvalidEventID)
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
@@ -57,8 +57,8 @@ func parsePushEventID(c *gin.Context) (uint64, bool) {
|
||||
|
||||
// handlePushEventNotFoundError maps a missing event row to 404, others to fallback.
|
||||
func handlePushEventNotFoundError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, errs.ErrEventNotFound)
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
response.AbortNotFound(c, consts.ErrEventNotFound)
|
||||
return
|
||||
}
|
||||
fallback(c, err.Error())
|
||||
@@ -71,8 +71,8 @@ func handlePushEventNotFoundError(c *gin.Context, err error, fallback func(c *gi
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body model.CreatePushEventRequest true "创建参数"
|
||||
// @Success 200 {object} response.Any{data=model.PushEvent} "创建成功"
|
||||
// @Param request body do.CreatePushEventRequest true "创建参数"
|
||||
// @Success 200 {object} response.Any{data=entity.PushEvent} "创建成功"
|
||||
// @Router /api/v1/admin/push/events [post]
|
||||
func CreatePushEvent(c *gin.Context) {
|
||||
handleJSONRequest(c, service.CreatePushEvent)
|
||||
@@ -108,7 +108,7 @@ func DeletePushEvent(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "事件 ID"
|
||||
// @Param request body model.UpdatePushEventRequest true "更新参数"
|
||||
// @Param request body do.UpdatePushEventRequest true "更新参数"
|
||||
// @Success 200 {object} response.Any{data=string} "修改成功"
|
||||
// @Router /api/v1/admin/push/events/{id} [put]
|
||||
func UpdatePushEvent(c *gin.Context) {
|
||||
@@ -117,7 +117,7 @@ func UpdatePushEvent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req model.UpdatePushEventRequest
|
||||
var req do.UpdatePushEventRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
@@ -175,8 +175,9 @@ func ListPushHistories(c *gin.Context) {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
total, results, err := service.ListPushHistories(c.Request.Context(), model.PushHistoryListFilter{
|
||||
total, results, err := service.ListPushHistories(c.Request.Context(), do.PushHistoryListFilter{
|
||||
EventKey: c.Query("event_key"),
|
||||
Channel: c.Query("channel"),
|
||||
Status: c.Query("status"),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
@@ -199,11 +200,11 @@ func ListPushHistories(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body model.TestPushRequest true "测试请求体"
|
||||
// @Param request body do.TestPushRequest true "测试请求体"
|
||||
// @Success 200 {object} response.Any{data=string} "测试成功"
|
||||
// @Router /api/v1/admin/push/test [post]
|
||||
func TestPush(c *gin.Context) {
|
||||
var req model.TestPushRequest
|
||||
var req do.TestPushRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handler
|
||||
// Package controller provides HTTP endpoints for msg_gateway.
|
||||
package controller
|
||||
|
||||
import (
|
||||
"Wavelet/core/extpoints"
|
||||
+17
-18
@@ -1,16 +1,15 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package handler provides HTTP endpoints for message_gateway.
|
||||
package handler
|
||||
package controller
|
||||
|
||||
import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/ginutil"
|
||||
"Wavelet/pkg/response"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
@@ -70,12 +69,12 @@ func handleEntityUpdate[Req any, Res any](
|
||||
// @Tags message-gateway
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.PublicChannelDTO}
|
||||
// @Success 200 {object} response.Any{data=[]do.PublicChannelDTO}
|
||||
// @Failure 401 {object} response.Any
|
||||
// @Router /api/v1/message-gateway/channels [get]
|
||||
func ListChannels(c *gin.Context) {
|
||||
if user, ok := currentUser(c); !ok || user == nil {
|
||||
response.AbortUnauthorized(c, errs.ErrLoginRequired)
|
||||
response.AbortUnauthorized(c, consts.ErrLoginRequired)
|
||||
return
|
||||
}
|
||||
rows, err := service.ListEnabledPublicChannels(c.Request.Context())
|
||||
@@ -92,13 +91,13 @@ func ListChannels(c *gin.Context) {
|
||||
// @Tags message-gateway
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} response.Any{data=[]model.BindingDTO}
|
||||
// @Success 200 {object} response.Any{data=[]do.BindingDTO}
|
||||
// @Failure 401 {object} response.Any
|
||||
// @Router /api/v1/message-gateway/bindings [get]
|
||||
func ListBindings(c *gin.Context) {
|
||||
user, ok := currentUser(c)
|
||||
if !ok || user == nil {
|
||||
response.AbortUnauthorized(c, errs.ErrLoginRequired)
|
||||
response.AbortUnauthorized(c, consts.ErrLoginRequired)
|
||||
return
|
||||
}
|
||||
rows, err := service.ListUserBindings(c.Request.Context(), user.ID)
|
||||
@@ -116,25 +115,25 @@ func ListBindings(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body model.BindRequest true "bind body"
|
||||
// @Success 200 {object} response.Any{data=model.BindingDTO}
|
||||
// @Param request body do.BindRequest true "bind body"
|
||||
// @Success 200 {object} response.Any{data=do.BindingDTO}
|
||||
// @Failure 400 {object} response.Any
|
||||
// @Failure 409 {object} response.Any
|
||||
// @Router /api/v1/message-gateway/bindings [post]
|
||||
func BindBinding(c *gin.Context) {
|
||||
user, ok := currentUser(c)
|
||||
if !ok || user == nil {
|
||||
response.AbortUnauthorized(c, errs.ErrLoginRequired)
|
||||
response.AbortUnauthorized(c, consts.ErrLoginRequired)
|
||||
return
|
||||
}
|
||||
var req model.BindRequest
|
||||
var req do.BindRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
dto, err := service.BindChannel(c.Request.Context(), user.ID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrPlatformAlreadyBound) {
|
||||
if errors.Is(err, consts.ErrPlatformAlreadyBound) {
|
||||
response.AbortConflict(c, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -158,20 +157,20 @@ func BindBinding(c *gin.Context) {
|
||||
func UnbindBinding(c *gin.Context) {
|
||||
user, ok := currentUser(c)
|
||||
if !ok || user == nil {
|
||||
response.AbortUnauthorized(c, errs.ErrLoginRequired)
|
||||
response.AbortUnauthorized(c, consts.ErrLoginRequired)
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, errs.ErrInvalidBindingID)
|
||||
response.AbortBadRequest(c, consts.ErrInvalidBindingID)
|
||||
return
|
||||
}
|
||||
if err := service.UnbindChannel(c.Request.Context(), user.ID, id); err != nil {
|
||||
if errors.Is(err, errs.ErrBindingNotFound) {
|
||||
if errors.Is(err, consts.ErrBindingNotFound) {
|
||||
response.AbortNotFound(c, err.Error())
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errs.ErrBindingForbidden) {
|
||||
if errors.Is(err, consts.ErrBindingForbidden) {
|
||||
response.AbortForbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
+34
-34
@@ -1,15 +1,15 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package repository provides data persistence for the message_gateway plugin.
|
||||
package repository
|
||||
// Package dao provides database persistence and caching for the msg_gateway plugin.
|
||||
package dao
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/idgen"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
@@ -53,16 +53,16 @@ func GetDB(ctx context.Context) *gorm.DB {
|
||||
}
|
||||
|
||||
// mapNotFound translates GORM's missing-row sentinel into the plugin-level
|
||||
// errs.ErrRecordNotFound so the service and handler layers stay free of gorm imports.
|
||||
// consts.ErrRecordNotFound so the service and controller layers stay free of gorm imports.
|
||||
func mapNotFound(err error) error {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errs.ErrRecordNotFound
|
||||
return consts.ErrRecordNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateMessageChannel inserts a channel row.
|
||||
func CreateMessageChannel(ctx context.Context, ch *model.MessageChannel) error {
|
||||
func CreateMessageChannel(ctx context.Context, ch *entity.MessageChannel) error {
|
||||
if ch.ID == 0 {
|
||||
ch.ID = idgen.NextUint64ID()
|
||||
}
|
||||
@@ -70,13 +70,13 @@ func CreateMessageChannel(ctx context.Context, ch *model.MessageChannel) error {
|
||||
}
|
||||
|
||||
// UpdateMessageChannel saves a channel row.
|
||||
func UpdateMessageChannel(ctx context.Context, ch *model.MessageChannel) error {
|
||||
func UpdateMessageChannel(ctx context.Context, ch *entity.MessageChannel) error {
|
||||
return GetDB(ctx).Save(ch).Error
|
||||
}
|
||||
|
||||
// GetMessageChannel loads a channel by id.
|
||||
func GetMessageChannel(ctx context.Context, id uint64) (*model.MessageChannel, error) {
|
||||
var ch model.MessageChannel
|
||||
func GetMessageChannel(ctx context.Context, id uint64) (*entity.MessageChannel, error) {
|
||||
var ch entity.MessageChannel
|
||||
if err := GetDB(ctx).Where("id = ?", id).First(&ch).Error; err != nil {
|
||||
return nil, mapNotFound(err)
|
||||
}
|
||||
@@ -84,8 +84,8 @@ func GetMessageChannel(ctx context.Context, id uint64) (*model.MessageChannel, e
|
||||
}
|
||||
|
||||
// ListMessageChannels returns all channels newest first.
|
||||
func ListMessageChannels(ctx context.Context) ([]model.MessageChannel, error) {
|
||||
var rows []model.MessageChannel
|
||||
func ListMessageChannels(ctx context.Context) ([]entity.MessageChannel, error) {
|
||||
var rows []entity.MessageChannel
|
||||
if err := GetDB(ctx).Order("id DESC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,18 +95,18 @@ func ListMessageChannels(ctx context.Context) ([]model.MessageChannel, error) {
|
||||
// DeleteMessageChannel removes pairings, bindings, then the channel.
|
||||
func DeleteMessageChannel(ctx context.Context, id uint64) error {
|
||||
return GetDB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("channel_id = ?", id).Delete(&model.MessagePairingCode{}).Error; err != nil {
|
||||
if err := tx.Where("channel_id = ?", id).Delete(&entity.MessagePairingCode{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("channel_id = ?", id).Delete(&model.MessageBinding{}).Error; err != nil {
|
||||
if err := tx.Where("channel_id = ?", id).Delete(&entity.MessageBinding{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.MessageChannel{}, id).Error
|
||||
return tx.Delete(&entity.MessageChannel{}, id).Error
|
||||
})
|
||||
}
|
||||
|
||||
// CreateMessageBinding inserts a binding.
|
||||
func CreateMessageBinding(ctx context.Context, b *model.MessageBinding) error {
|
||||
func CreateMessageBinding(ctx context.Context, b *entity.MessageBinding) error {
|
||||
if b.ID == 0 {
|
||||
b.ID = idgen.NextUint64ID()
|
||||
}
|
||||
@@ -114,8 +114,8 @@ func CreateMessageBinding(ctx context.Context, b *model.MessageBinding) error {
|
||||
}
|
||||
|
||||
// GetBindingByChannelPlatform finds a binding for a platform user on a channel.
|
||||
func GetBindingByChannelPlatform(ctx context.Context, channelID uint64, platformUserID string) (*model.MessageBinding, error) {
|
||||
var b model.MessageBinding
|
||||
func GetBindingByChannelPlatform(ctx context.Context, channelID uint64, platformUserID string) (*entity.MessageBinding, error) {
|
||||
var b entity.MessageBinding
|
||||
err := GetDB(ctx).Where("channel_id = ? AND platform_user_id = ?", channelID, platformUserID).First(&b).Error
|
||||
if err != nil {
|
||||
return nil, mapNotFound(err)
|
||||
@@ -124,8 +124,8 @@ func GetBindingByChannelPlatform(ctx context.Context, channelID uint64, platform
|
||||
}
|
||||
|
||||
// ListBindingsByUser lists bindings for a Wavelet user.
|
||||
func ListBindingsByUser(ctx context.Context, userID uint64) ([]model.MessageBinding, error) {
|
||||
var rows []model.MessageBinding
|
||||
func ListBindingsByUser(ctx context.Context, userID uint64) ([]entity.MessageBinding, error) {
|
||||
var rows []entity.MessageBinding
|
||||
if err := GetDB(ctx).Where("user_id = ?", userID).Order("id DESC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,8 +133,8 @@ func ListBindingsByUser(ctx context.Context, userID uint64) ([]model.MessageBind
|
||||
}
|
||||
|
||||
// ListBindingsByChannel lists bindings on one messaging channel.
|
||||
func ListBindingsByChannel(ctx context.Context, channelID uint64) ([]model.MessageBinding, error) {
|
||||
var rows []model.MessageBinding
|
||||
func ListBindingsByChannel(ctx context.Context, channelID uint64) ([]entity.MessageBinding, error) {
|
||||
var rows []entity.MessageBinding
|
||||
if err := GetDB(ctx).Where("channel_id = ?", channelID).Order("id DESC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -142,8 +142,8 @@ func ListBindingsByChannel(ctx context.Context, channelID uint64) ([]model.Messa
|
||||
}
|
||||
|
||||
// GetMessageBinding loads a binding by id.
|
||||
func GetMessageBinding(ctx context.Context, id uint64) (*model.MessageBinding, error) {
|
||||
var b model.MessageBinding
|
||||
func GetMessageBinding(ctx context.Context, id uint64) (*entity.MessageBinding, error) {
|
||||
var b entity.MessageBinding
|
||||
if err := GetDB(ctx).Where("id = ?", id).First(&b).Error; err != nil {
|
||||
return nil, mapNotFound(err)
|
||||
}
|
||||
@@ -152,12 +152,12 @@ func GetMessageBinding(ctx context.Context, id uint64) (*model.MessageBinding, e
|
||||
|
||||
// DeleteMessageBinding deletes a binding by id.
|
||||
func DeleteMessageBinding(ctx context.Context, id uint64) error {
|
||||
return GetDB(ctx).Delete(&model.MessageBinding{}, id).Error
|
||||
return GetDB(ctx).Delete(&entity.MessageBinding{}, id).Error
|
||||
}
|
||||
|
||||
// UpsertPairingCode reuses an unexpired code for the same channel+platform user.
|
||||
func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, code string, expiresAt time.Time) (*model.MessagePairingCode, error) {
|
||||
var existing model.MessagePairingCode
|
||||
func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, code string, expiresAt time.Time) (*entity.MessagePairingCode, error) {
|
||||
var existing entity.MessagePairingCode
|
||||
err := GetDB(ctx).
|
||||
Where("channel_id = ? AND platform_user_id = ? AND expires_at > ?", channelID, platformUserID, time.Now()).
|
||||
First(&existing).Error
|
||||
@@ -167,7 +167,7 @@ func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, co
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
row := &model.MessagePairingCode{
|
||||
row := &entity.MessagePairingCode{
|
||||
Code: code,
|
||||
ChannelID: channelID,
|
||||
PlatformUserID: platformUserID,
|
||||
@@ -180,8 +180,8 @@ func UpsertPairingCode(ctx context.Context, channelID uint64, platformUserID, co
|
||||
}
|
||||
|
||||
// GetPairingCode loads a pairing code by normalized code string.
|
||||
func GetPairingCode(ctx context.Context, code string) (*model.MessagePairingCode, error) {
|
||||
var row model.MessagePairingCode
|
||||
func GetPairingCode(ctx context.Context, code string) (*entity.MessagePairingCode, error) {
|
||||
var row entity.MessagePairingCode
|
||||
if err := GetDB(ctx).Where("code = ?", code).First(&row).Error; err != nil {
|
||||
return nil, mapNotFound(err)
|
||||
}
|
||||
@@ -190,17 +190,17 @@ func GetPairingCode(ctx context.Context, code string) (*model.MessagePairingCode
|
||||
|
||||
// DeletePairingCode removes a pairing code.
|
||||
func DeletePairingCode(ctx context.Context, code string) error {
|
||||
return GetDB(ctx).Where("code = ?", code).Delete(&model.MessagePairingCode{}).Error
|
||||
return GetDB(ctx).Where("code = ?", code).Delete(&entity.MessagePairingCode{}).Error
|
||||
}
|
||||
|
||||
// DeleteExpiredPairingCodes removes expired pairing rows.
|
||||
func DeleteExpiredPairingCodes(ctx context.Context) error {
|
||||
return GetDB(ctx).Where("expires_at <= ?", time.Now()).Delete(&model.MessagePairingCode{}).Error
|
||||
return GetDB(ctx).Where("expires_at <= ?", time.Now()).Delete(&entity.MessagePairingCode{}).Error
|
||||
}
|
||||
|
||||
// ListEnabledMessageChannels returns enabled channels.
|
||||
func ListEnabledMessageChannels(ctx context.Context) ([]model.MessageChannel, error) {
|
||||
var rows []model.MessageChannel
|
||||
func ListEnabledMessageChannels(ctx context.Context) ([]entity.MessageChannel, error) {
|
||||
var rows []entity.MessageChannel
|
||||
if err := GetDB(ctx).Where("enabled = ?", true).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
+46
-52
@@ -1,13 +1,14 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
package dao
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -48,8 +49,8 @@ func GetCache(ctx context.Context) contracts.CacheService {
|
||||
}
|
||||
|
||||
// ListPushChannelsRecord returns all push channels ordered by creation time descending.
|
||||
func ListPushChannelsRecord(ctx context.Context) ([]model.PushChannel, error) {
|
||||
var channels []model.PushChannel
|
||||
func ListPushChannelsRecord(ctx context.Context) ([]entity.PushChannel, error) {
|
||||
var channels []entity.PushChannel
|
||||
if err := GetDB(ctx).Order("created_at DESC").Find(&channels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -57,17 +58,17 @@ func ListPushChannelsRecord(ctx context.Context) ([]model.PushChannel, error) {
|
||||
}
|
||||
|
||||
// GetPushChannelByIDRecord loads a push channel by primary key.
|
||||
func GetPushChannelByIDRecord(ctx context.Context, id uint64) (model.PushChannel, error) {
|
||||
var channel model.PushChannel
|
||||
func GetPushChannelByIDRecord(ctx context.Context, id uint64) (entity.PushChannel, error) {
|
||||
var channel entity.PushChannel
|
||||
if err := GetDB(ctx).Where("id = ?", id).First(&channel).Error; err != nil {
|
||||
return model.PushChannel{}, mapNotFound(err)
|
||||
return entity.PushChannel{}, mapNotFound(err)
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// GetPushChannelByNameRecord loads a push channel by its unique name.
|
||||
func GetPushChannelByNameRecord(ctx context.Context, name string) (*model.PushChannel, error) {
|
||||
var channel model.PushChannel
|
||||
func GetPushChannelByNameRecord(ctx context.Context, name string) (*entity.PushChannel, error) {
|
||||
var channel entity.PushChannel
|
||||
if err := GetDB(ctx).Where("name = ?", name).First(&channel).Error; err != nil {
|
||||
return nil, mapNotFound(err)
|
||||
}
|
||||
@@ -77,14 +78,14 @@ func GetPushChannelByNameRecord(ctx context.Context, name string) (*model.PushCh
|
||||
// CountPushChannelsByNameRecord returns how many channels share the given name.
|
||||
func CountPushChannelsByNameRecord(ctx context.Context, name string) (int64, error) {
|
||||
var count int64
|
||||
if err := GetDB(ctx).Model(&model.PushChannel{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
||||
if err := GetDB(ctx).Model(&entity.PushChannel{}).Where("name = ?", name).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CreatePushChannelRecord persists a new channel and invalidates cache.
|
||||
func CreatePushChannelRecord(ctx context.Context, channel *model.PushChannel) error {
|
||||
func CreatePushChannelRecord(ctx context.Context, channel *entity.PushChannel) error {
|
||||
if err := GetDB(ctx).Create(channel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,7 +94,7 @@ func CreatePushChannelRecord(ctx context.Context, channel *model.PushChannel) er
|
||||
}
|
||||
|
||||
// SavePushChannelRecord updates a channel and invalidates cache.
|
||||
func SavePushChannelRecord(ctx context.Context, channel *model.PushChannel) error {
|
||||
func SavePushChannelRecord(ctx context.Context, channel *entity.PushChannel) error {
|
||||
if err := GetDB(ctx).Save(channel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,7 +103,7 @@ func SavePushChannelRecord(ctx context.Context, channel *model.PushChannel) erro
|
||||
}
|
||||
|
||||
// DeletePushChannelRecord removes a channel and invalidates cache.
|
||||
func DeletePushChannelRecord(ctx context.Context, channel *model.PushChannel) error {
|
||||
func DeletePushChannelRecord(ctx context.Context, channel *entity.PushChannel) error {
|
||||
if err := GetDB(ctx).Delete(channel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,8 +132,8 @@ func getCachedOrQuery[T any](ctx context.Context, cacheKey string, ttl time.Dura
|
||||
}
|
||||
|
||||
// GetActivePushChannelByName loads an enabled push channel, preferring the cache layer.
|
||||
func GetActivePushChannelByName(ctx context.Context, name string) (*model.PushChannel, error) {
|
||||
channel, err := getCachedOrQuery(ctx, "push:channel:active:"+name, activePushChannelCacheTTL, func(db *gorm.DB, dest *model.PushChannel) error {
|
||||
func GetActivePushChannelByName(ctx context.Context, name string) (*entity.PushChannel, error) {
|
||||
channel, err := getCachedOrQuery(ctx, "push:channel:active:"+name, activePushChannelCacheTTL, func(db *gorm.DB, dest *entity.PushChannel) error {
|
||||
return db.Where("name = ? AND enabled = ?", name, true).First(dest).Error
|
||||
})
|
||||
if err != nil {
|
||||
@@ -149,8 +150,8 @@ func DeleteActivePushChannelCache(ctx context.Context, name string) {
|
||||
}
|
||||
|
||||
// ListPushEventsRecord returns all push events ordered by creation time descending.
|
||||
func ListPushEventsRecord(ctx context.Context) ([]model.PushEvent, error) {
|
||||
var events []model.PushEvent
|
||||
func ListPushEventsRecord(ctx context.Context) ([]entity.PushEvent, error) {
|
||||
var events []entity.PushEvent
|
||||
if err := GetDB(ctx).Order("created_at DESC").Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -158,19 +159,19 @@ func ListPushEventsRecord(ctx context.Context) ([]model.PushEvent, error) {
|
||||
}
|
||||
|
||||
// GetPushEventByIDRecord loads a push event by primary key.
|
||||
func GetPushEventByIDRecord(ctx context.Context, id uint64) (model.PushEvent, error) {
|
||||
var event model.PushEvent
|
||||
func GetPushEventByIDRecord(ctx context.Context, id uint64) (entity.PushEvent, error) {
|
||||
var event entity.PushEvent
|
||||
if err := GetDB(ctx).First(&event, id).Error; err != nil {
|
||||
return model.PushEvent{}, mapNotFound(err)
|
||||
return entity.PushEvent{}, mapNotFound(err)
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// GetPushEventByKeyRecord loads a push event by event key.
|
||||
func GetPushEventByKeyRecord(ctx context.Context, key string) (model.PushEvent, error) {
|
||||
var event model.PushEvent
|
||||
func GetPushEventByKeyRecord(ctx context.Context, key string) (entity.PushEvent, error) {
|
||||
var event entity.PushEvent
|
||||
if err := GetDB(ctx).Where("event_key = ?", key).First(&event).Error; err != nil {
|
||||
return model.PushEvent{}, mapNotFound(err)
|
||||
return entity.PushEvent{}, mapNotFound(err)
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
@@ -178,14 +179,14 @@ func GetPushEventByKeyRecord(ctx context.Context, key string) (model.PushEvent,
|
||||
// CountPushEventsByKeyRecord returns how many events use the given event key.
|
||||
func CountPushEventsByKeyRecord(ctx context.Context, key string) (int64, error) {
|
||||
var count int64
|
||||
if err := GetDB(ctx).Model(&model.PushEvent{}).Where("event_key = ?", key).Count(&count).Error; err != nil {
|
||||
if err := GetDB(ctx).Model(&entity.PushEvent{}).Where("event_key = ?", key).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CreatePushEventRecord persists a new push event and invalidates cache.
|
||||
func CreatePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
||||
func CreatePushEventRecord(ctx context.Context, event *entity.PushEvent) error {
|
||||
if err := GetDB(ctx).Create(event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,7 +195,7 @@ func CreatePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
||||
}
|
||||
|
||||
// SavePushEventRecord updates a push event and invalidates cache.
|
||||
func SavePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
||||
func SavePushEventRecord(ctx context.Context, event *entity.PushEvent) error {
|
||||
if err := GetDB(ctx).Save(event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,7 +204,7 @@ func SavePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
||||
}
|
||||
|
||||
// UpdatePushEventEnabledRecord toggles the enabled flag for a push event.
|
||||
func UpdatePushEventEnabledRecord(ctx context.Context, event *model.PushEvent, enabled bool) error {
|
||||
func UpdatePushEventEnabledRecord(ctx context.Context, event *entity.PushEvent, enabled bool) error {
|
||||
event.Enabled = enabled
|
||||
if err := GetDB(ctx).Model(event).Update("enabled", enabled).Error; err != nil {
|
||||
return err
|
||||
@@ -213,7 +214,7 @@ func UpdatePushEventEnabledRecord(ctx context.Context, event *model.PushEvent, e
|
||||
}
|
||||
|
||||
// DeletePushEventRecord removes a push event and invalidates cache.
|
||||
func DeletePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
||||
func DeletePushEventRecord(ctx context.Context, event *entity.PushEvent) error {
|
||||
if err := GetDB(ctx).Delete(event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -222,8 +223,8 @@ func DeletePushEventRecord(ctx context.Context, event *model.PushEvent) error {
|
||||
}
|
||||
|
||||
// ListActivePushEventsByTaskTypeRecord returns enabled events bound to a task type.
|
||||
func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string) ([]model.PushEvent, error) {
|
||||
var events []model.PushEvent
|
||||
func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string) ([]entity.PushEvent, error) {
|
||||
var events []entity.PushEvent
|
||||
if err := GetDB(ctx).Where("task_type = ? AND enabled = ?", taskType, true).Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -231,8 +232,8 @@ func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string)
|
||||
}
|
||||
|
||||
// GetActivePushEventByKey loads an enabled push event, preferring the cache layer.
|
||||
func GetActivePushEventByKey(ctx context.Context, key string) (*model.PushEvent, error) {
|
||||
event, err := getCachedOrQuery(ctx, "push:event:active:"+key, activePushEventCacheTTL, func(db *gorm.DB, dest *model.PushEvent) error {
|
||||
func GetActivePushEventByKey(ctx context.Context, key string) (*entity.PushEvent, error) {
|
||||
event, err := getCachedOrQuery(ctx, "push:event:active:"+key, activePushEventCacheTTL, func(db *gorm.DB, dest *entity.PushEvent) error {
|
||||
return db.Where("event_key = ? AND enabled = ?", key, true).First(dest).Error
|
||||
})
|
||||
if err != nil {
|
||||
@@ -249,8 +250,8 @@ func DeleteActivePushEventCache(ctx context.Context, key string) {
|
||||
}
|
||||
|
||||
// ListPushHistoriesRecord returns paginated push history records.
|
||||
func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFilter) (int64, []model.PushHistory, error) {
|
||||
query := GetDB(ctx).Model(&model.PushHistory{}).Order("created_at DESC")
|
||||
func ListPushHistoriesRecord(ctx context.Context, filter do.PushHistoryListFilter) (int64, []entity.PushHistory, error) {
|
||||
query := GetDB(ctx).Model(&entity.PushHistory{}).Order("created_at DESC")
|
||||
if filter.EventKey != "" {
|
||||
query = query.Where("event_key = ?", filter.EventKey)
|
||||
}
|
||||
@@ -263,7 +264,7 @@ func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFi
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var results []model.PushHistory
|
||||
var results []entity.PushHistory
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
if err := query.Offset(offset).Limit(filter.PageSize).Find(&results).Error; err != nil {
|
||||
return 0, nil, err
|
||||
@@ -273,28 +274,23 @@ func ListPushHistoriesRecord(ctx context.Context, filter model.PushHistoryListFi
|
||||
}
|
||||
|
||||
// CreatePushHistoryRecord persists a push history audit record.
|
||||
func CreatePushHistoryRecord(ctx context.Context, history *model.PushHistory) error {
|
||||
func CreatePushHistoryRecord(ctx context.Context, history *entity.PushHistory) error {
|
||||
return GetDB(ctx).Create(history).Error
|
||||
}
|
||||
|
||||
// PushHistoryQuery returns a scoped query builder for push histories.
|
||||
func PushHistoryQuery(ctx context.Context) *gorm.DB {
|
||||
return GetDB(ctx).Model(&model.PushHistory{})
|
||||
return GetDB(ctx).Model(&entity.PushHistory{})
|
||||
}
|
||||
|
||||
// smtpConfigKeys are the system-config rows backing the built-in email channel.
|
||||
var smtpConfigKeys = []string{"smtp_host", "smtp_port", "smtp_username", "smtp_password"}
|
||||
|
||||
// LoadSMTPConfigRecord reads the SMTP settings in one query.
|
||||
//
|
||||
// A key that is simply absent leaves its field empty, which is how an unconfigured
|
||||
// mailer is represented. A read that fails is returned as an error, so callers
|
||||
// cannot mistake an unhealthy database for "no SMTP configured" and silently drop
|
||||
// the notification.
|
||||
func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) {
|
||||
func LoadSMTPConfigRecord(ctx context.Context) (do.SMTPConfig, error) {
|
||||
db := GetDB(ctx)
|
||||
if db == nil {
|
||||
return model.SMTPConfig{}, errors.New("database not available")
|
||||
return do.SMTPConfig{}, errors.New("database not available")
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
@@ -305,10 +301,10 @@ func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) {
|
||||
Select("key", "value").
|
||||
Where("key IN ?", smtpConfigKeys).
|
||||
Find(&rows).Error; err != nil {
|
||||
return model.SMTPConfig{}, fmt.Errorf("read smtp system configs: %w", err)
|
||||
return do.SMTPConfig{}, fmt.Errorf("read smtp system configs: %w", err)
|
||||
}
|
||||
|
||||
var cfg model.SMTPConfig
|
||||
var cfg do.SMTPConfig
|
||||
for _, row := range rows {
|
||||
switch row.Key {
|
||||
case "smtp_host":
|
||||
@@ -325,8 +321,6 @@ func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) {
|
||||
}
|
||||
|
||||
// userLookupColumns allow-lists the columns FindUserByFieldRecord may filter on.
|
||||
// The column name is concatenated into the WHERE clause, so anything not listed
|
||||
// here must never reach the database.
|
||||
var userLookupColumns = map[string]struct{}{
|
||||
"id": {},
|
||||
"username": {},
|
||||
@@ -336,11 +330,11 @@ var userLookupColumns = map[string]struct{}{
|
||||
// contract is not wired yet. field must be one of userLookupColumns.
|
||||
func FindUserByFieldRecord(ctx context.Context, field string, value any) (*contracts.UserDTO, error) {
|
||||
if _, ok := userLookupColumns[field]; !ok {
|
||||
return nil, errs.ErrUnsupportedUserLookupField
|
||||
return nil, consts.ErrUnsupportedUserLookupField
|
||||
}
|
||||
db := GetDB(ctx)
|
||||
if db == nil {
|
||||
return nil, errs.ErrRecordNotFound
|
||||
return nil, consts.ErrRecordNotFound
|
||||
}
|
||||
var user contracts.UserDTO
|
||||
if err := db.Table("w_users").Where(field+" = ?", value).First(&user).Error; err != nil {
|
||||
@@ -354,7 +348,7 @@ func FindUserByFieldRecord(ctx context.Context, field string, value any) (*contr
|
||||
func FindFirstAdminUserRecord(ctx context.Context) (*contracts.UserDTO, error) {
|
||||
db := GetDB(ctx)
|
||||
if db == nil {
|
||||
return nil, errs.ErrRecordNotFound
|
||||
return nil, consts.ErrRecordNotFound
|
||||
}
|
||||
var adminUser contracts.UserDTO
|
||||
if err := db.Table("w_users").Where("is_admin = ?", true).Order("id ASC").First(&adminUser).Error; err != nil {
|
||||
+14
-14
@@ -1,12 +1,12 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository_test
|
||||
package dao_test
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/testhelper"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
@@ -35,19 +35,19 @@ func TestFindUserByFieldRecordRejectsUnlistedColumns(t *testing.T) {
|
||||
t.Fatalf("seed user failed: %v", err)
|
||||
}
|
||||
|
||||
repository.SetDBServiceForTest(stubDBService{db: db})
|
||||
t.Cleanup(func() { repository.SetDBServiceForTest(nil) })
|
||||
dao.SetDBServiceForTest(stubDBService{db: db})
|
||||
t.Cleanup(func() { dao.SetDBServiceForTest(nil) })
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := repository.FindUserByFieldRecord(ctx, "username", "seeded")
|
||||
user, err := dao.FindUserByFieldRecord(ctx, "username", "seeded")
|
||||
if err != nil {
|
||||
t.Fatalf("allowlisted lookup by username failed: %v", err)
|
||||
}
|
||||
if user.ID != 77 {
|
||||
t.Errorf("allowlisted lookup returned ID %d, want 77", user.ID)
|
||||
}
|
||||
if _, err := repository.FindUserByFieldRecord(ctx, "id", uint64(77)); err != nil {
|
||||
if _, err := dao.FindUserByFieldRecord(ctx, "id", uint64(77)); err != nil {
|
||||
t.Errorf("allowlisted lookup by id failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestFindUserByFieldRecordRejectsUnlistedColumns(t *testing.T) {
|
||||
{"empty field", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if _, err := repository.FindUserByFieldRecord(ctx, tc.field, "seeded"); !errors.Is(err, errs.ErrUnsupportedUserLookupField) {
|
||||
if _, err := dao.FindUserByFieldRecord(ctx, tc.field, "seeded"); !errors.Is(err, consts.ErrUnsupportedUserLookupField) {
|
||||
t.Errorf("%s: got err %v, want ErrUnsupportedUserLookupField", tc.name, err)
|
||||
}
|
||||
}
|
||||
@@ -100,10 +100,10 @@ func TestLoadSMTPConfigRecordMapsEveryKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
repository.SetDBServiceForTest(stubDBService{db: db})
|
||||
t.Cleanup(func() { repository.SetDBServiceForTest(nil) })
|
||||
dao.SetDBServiceForTest(stubDBService{db: db})
|
||||
t.Cleanup(func() { dao.SetDBServiceForTest(nil) })
|
||||
|
||||
cfg, err := repository.LoadSMTPConfigRecord(context.Background())
|
||||
cfg, err := dao.LoadSMTPConfigRecord(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSMTPConfigRecord: %v", err)
|
||||
}
|
||||
@@ -122,10 +122,10 @@ func TestLoadSMTPConfigRecordSurfacesReadFailure(t *testing.T) {
|
||||
t.Fatalf("open bare sqlite: %v", err)
|
||||
}
|
||||
|
||||
repository.SetDBServiceForTest(stubDBService{db: bare})
|
||||
t.Cleanup(func() { repository.SetDBServiceForTest(nil) })
|
||||
dao.SetDBServiceForTest(stubDBService{db: bare})
|
||||
t.Cleanup(func() { dao.SetDBServiceForTest(nil) })
|
||||
|
||||
if _, err := repository.LoadSMTPConfigRecord(context.Background()); err == nil {
|
||||
if _, err := dao.LoadSMTPConfigRecord(context.Background()); err == nil {
|
||||
t.Fatal("LoadSMTPConfigRecord returned nil error although the config table cannot be read")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package do defines domain objects, DTOs, and request/response payloads for msg_gateway.
|
||||
package do
|
||||
|
||||
import "time"
|
||||
|
||||
// Capability describes what an adapter can send and receive.
|
||||
type Capability struct {
|
||||
Text bool
|
||||
Image bool
|
||||
File bool
|
||||
Reply bool
|
||||
Group bool
|
||||
}
|
||||
|
||||
// ChannelConfig is the decrypted runtime config passed to a factory.
|
||||
type ChannelConfig struct {
|
||||
ID uint64
|
||||
Type string
|
||||
Name string
|
||||
Credentials map[string]string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// Recipient is the outbound destination on a platform.
|
||||
type Recipient struct {
|
||||
ChatID string
|
||||
PlatformUserID string
|
||||
}
|
||||
|
||||
// Attachment is a downloaded inbound file sitting on local disk.
|
||||
type Attachment struct {
|
||||
Path string
|
||||
FileName string
|
||||
MIME string
|
||||
Error string
|
||||
}
|
||||
|
||||
// InboundMessage is a normalized private-chat message.
|
||||
type InboundMessage struct {
|
||||
ChannelID uint64
|
||||
ChannelType string
|
||||
PlatformUserID string
|
||||
ChatID string
|
||||
MessageID string
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
BindingUserID *uint64
|
||||
}
|
||||
|
||||
// OutboundMessage is a reply or probe send.
|
||||
type OutboundMessage struct {
|
||||
Text string
|
||||
ReplyToID string
|
||||
Attachments []Attachment
|
||||
}
|
||||
|
||||
// BindRequest is the user bind body.
|
||||
type BindRequest struct {
|
||||
ChannelID string `json:"channel_id"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// BindingDTO is a user-facing binding row.
|
||||
type BindingDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
UserID uint64 `json:"user_id,string"`
|
||||
ChannelID uint64 `json:"channel_id,string"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelType string `json:"channel_type"`
|
||||
PlatformUserID string `json:"platform_user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// PublicChannelDTO is an enabled channel a user can bind to.
|
||||
type PublicChannelDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Field is one admin form field.
|
||||
type Field struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// Definition describes a channel type form.
|
||||
type Definition struct {
|
||||
Type string `json:"type"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
|
||||
// ChannelDTO represents a channel for admin consumption.
|
||||
type ChannelDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
OwnerScope string `json:"owner_scope"`
|
||||
OwnerID *uint64 `json:"owner_id,string,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
|
||||
// CreateChannelRequest is admin create payload.
|
||||
type CreateChannelRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
|
||||
// UpdateChannelRequest is admin update payload.
|
||||
type UpdateChannelRequest struct {
|
||||
Name string `json:"name"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Credentials map[string]string `json:"credentials"`
|
||||
Extra map[string]string `json:"extra"`
|
||||
}
|
||||
+70
-92
@@ -1,44 +1,13 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
package do
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
pkgpush "Wavelet/plugins/domain/msg_gateway/push"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
pkgpush "Wavelet/plugins/domain/message_gateway/push"
|
||||
)
|
||||
|
||||
// Push channel and payload constants.
|
||||
const (
|
||||
ChannelCustom = "custom"
|
||||
ChannelEmail = "email"
|
||||
ChannelLark = "lark"
|
||||
ChannelDingTalk = "dingtalk"
|
||||
ChannelTelegram = "telegram"
|
||||
ChannelBark = "bark"
|
||||
ChannelDiscord = "discord"
|
||||
ChannelSlack = "slack"
|
||||
ChannelPushover = "pushover"
|
||||
DefaultLevelInfo = "INFO"
|
||||
KeyTitle = "title"
|
||||
KeyContent = "content"
|
||||
KeyLevel = "level"
|
||||
|
||||
// KeyURL represents the URL field key.
|
||||
KeyURL = "url"
|
||||
// KeyToken represents the Token field key.
|
||||
KeyToken = "token"
|
||||
// KeyOther represents the Other field key.
|
||||
KeyOther = "other"
|
||||
|
||||
// TypeText represents standard text input type.
|
||||
TypeText = "text"
|
||||
// TypePassword represents password input type.
|
||||
TypePassword = "password"
|
||||
// TypeTextarea represents textarea input type.
|
||||
TypeTextarea = "textarea"
|
||||
)
|
||||
|
||||
// SMTPConfig mirrors the system SMTP settings consumed by the push service.
|
||||
@@ -140,12 +109,12 @@ type NotificationMessage struct {
|
||||
Ext map[string]any `json:"ext,omitempty"`
|
||||
}
|
||||
|
||||
// Flatten converts the structured NotificationMessage back to a flat map (original json structure).
|
||||
// Flatten converts the structured NotificationMessage back to a flat map.
|
||||
func (m NotificationMessage) Flatten() map[string]any {
|
||||
res := map[string]any{
|
||||
KeyTitle: m.Title,
|
||||
KeyContent: m.Content,
|
||||
KeyLevel: m.Level,
|
||||
consts.KeyTitle: m.Title,
|
||||
consts.KeyContent: m.Content,
|
||||
consts.KeyLevel: m.Level,
|
||||
}
|
||||
for k, v := range m.Ext {
|
||||
res[k] = v
|
||||
@@ -161,7 +130,7 @@ type EventMetadata struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// SendPayload is the async push dispatch载荷 consumed by the notification worker.
|
||||
// SendPayload is the async push dispatch payload consumed by the notification worker.
|
||||
type SendPayload struct {
|
||||
EventKey string `json:"event_key"`
|
||||
Config pkgpush.Config `json:"config"`
|
||||
@@ -181,6 +150,15 @@ type PushHistoryListFilter struct {
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// PushNotificationEvent defines the payload for eventbus notification trigger.
|
||||
type PushNotificationEvent struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
Channel string `json:"channel"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
pushDefMu sync.RWMutex
|
||||
pushDefinitions = make(map[string]PushDefinition)
|
||||
@@ -199,15 +177,15 @@ func ListPushDefinitions() []PushDefinition {
|
||||
defer pushDefMu.RUnlock()
|
||||
|
||||
order := []string{
|
||||
ChannelCustom,
|
||||
ChannelLark,
|
||||
ChannelDingTalk,
|
||||
ChannelTelegram,
|
||||
ChannelBark,
|
||||
ChannelDiscord,
|
||||
ChannelSlack,
|
||||
ChannelPushover,
|
||||
ChannelEmail,
|
||||
consts.ChannelCustom,
|
||||
consts.ChannelLark,
|
||||
consts.ChannelDingTalk,
|
||||
consts.ChannelTelegram,
|
||||
consts.ChannelBark,
|
||||
consts.ChannelDiscord,
|
||||
consts.ChannelSlack,
|
||||
consts.ChannelPushover,
|
||||
consts.ChannelEmail,
|
||||
}
|
||||
res := make([]PushDefinition, 0, len(pushDefinitions))
|
||||
for _, t := range order {
|
||||
@@ -230,25 +208,25 @@ func ListPushDefinitions() []PushDefinition {
|
||||
return res
|
||||
}
|
||||
|
||||
//nolint:funlen,goconst // Channel definitions registration table
|
||||
//nolint:funlen,goconst,dupl // Channel definitions registration table
|
||||
func init() {
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelCustom,
|
||||
Type: consts.ChannelCustom,
|
||||
Name: "自定义消息通道",
|
||||
Description: "使用自定义 HTTP POST 请求向外部 Webhook 发送数据。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "请求地址",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "在此填写完整的请求地址,必须使用 HTTPS 协议",
|
||||
Description: "接口请求的完整 HTTPS URL,例如 https://api.example.com/webhook",
|
||||
},
|
||||
{
|
||||
Key: KeyOther,
|
||||
Key: consts.KeyOther,
|
||||
Label: "请求体 (JSON)",
|
||||
Type: TypeTextarea,
|
||||
Type: consts.TypeTextarea,
|
||||
Required: true,
|
||||
Placeholder: "在此输入请求体,支持模板变量,必须为合法的 JSON 格式",
|
||||
Description: "可使用的变量:$title, $description, $content, $url, $to。例如 {\"text\": \"$content\"}",
|
||||
@@ -257,30 +235,30 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelLark,
|
||||
Type: consts.ChannelLark,
|
||||
Name: "飞书群机器人",
|
||||
Description: "配置飞书群自定义机器人的 Webhook 接口投递。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "Webhook 地址",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN",
|
||||
Description: "从飞书群机器人设置中复制的 Webhook URL",
|
||||
},
|
||||
{
|
||||
Key: KeyToken,
|
||||
Key: consts.KeyToken,
|
||||
Label: "签名校验密钥 (Secret) (可选)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: false,
|
||||
Placeholder: "可选,若机器人启用了安全设置中的签名校验,请在此输入",
|
||||
Description: "飞书群机器人安全设置中的签名校验 Key",
|
||||
},
|
||||
{
|
||||
Key: KeyOther,
|
||||
Key: consts.KeyOther,
|
||||
Label: "自定义卡片 JSON 模版 (可选)",
|
||||
Type: TypeTextarea,
|
||||
Type: consts.TypeTextarea,
|
||||
Required: false,
|
||||
Placeholder: "可选,留空则默认使用系统内置的精美互动卡片",
|
||||
Description: "若填写,必须是合法的飞书卡片 JSON 格式",
|
||||
@@ -289,22 +267,22 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelDingTalk,
|
||||
Type: consts.ChannelDingTalk,
|
||||
Name: "钉钉群机器人",
|
||||
Description: "配置钉钉群自定义机器人的 Webhook 接口投递。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "Webhook 地址",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN",
|
||||
Description: "从钉钉群机器人设置中获取的完整 Webhook URL",
|
||||
},
|
||||
{
|
||||
Key: KeyToken,
|
||||
Key: consts.KeyToken,
|
||||
Label: "加签密钥 (Secret) (可选)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: false,
|
||||
Placeholder: "可选,若机器人启用了安全设置中的加签校验,请在此输入 SEC 开头的密钥",
|
||||
Description: "钉钉群机器人安全设置中的加签 Secret",
|
||||
@@ -313,30 +291,30 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelTelegram,
|
||||
Type: consts.ChannelTelegram,
|
||||
Name: "Telegram 机器人",
|
||||
Description: "配置 Telegram 机器人推送消息。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "API 基础地址 (可选)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: false,
|
||||
Placeholder: "https://api.telegram.org",
|
||||
Description: "接口请求的 HTTPS 基础地址,留空默认为 https://api.telegram.org",
|
||||
},
|
||||
{
|
||||
Key: KeyToken,
|
||||
Key: consts.KeyToken,
|
||||
Label: "机器人 Token (Bot Token)",
|
||||
Type: TypePassword,
|
||||
Type: consts.TypePassword,
|
||||
Required: true,
|
||||
Placeholder: "在此输入 Telegram 机器人的 Bot Token",
|
||||
Description: "通过 BotFather 申请到的机器人 Access Token",
|
||||
},
|
||||
{
|
||||
Key: KeyOther,
|
||||
Key: consts.KeyOther,
|
||||
Label: "默认会话 ID (Chat ID) (可选)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: false,
|
||||
Placeholder: "例如 -100123456789 或 @channel_name",
|
||||
Description: "默认的消息接收 Chat ID。如果通知事件中未配置 targets,将推送到此 ID",
|
||||
@@ -345,30 +323,30 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelBark,
|
||||
Type: consts.ChannelBark,
|
||||
Name: "Bark (iOS 推送)",
|
||||
Description: "配置 Bark 推送通知至 iPhone / iPad 客户端。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyToken,
|
||||
Key: consts.KeyToken,
|
||||
Label: "设备 Key (Device Key)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "Bark App 首页显示的 Device Key",
|
||||
Description: "从 Bark App 复制的设备专属 Key",
|
||||
},
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "Bark 服务器地址 (可选)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: false,
|
||||
Placeholder: "https://api.day.app",
|
||||
Description: "Bark 服务器地址,留空默认使用官方公共服务器 https://api.day.app",
|
||||
},
|
||||
{
|
||||
Key: KeyOther,
|
||||
Key: consts.KeyOther,
|
||||
Label: "额外配置 JSON (可选)",
|
||||
Type: TypeTextarea,
|
||||
Type: consts.TypeTextarea,
|
||||
Required: false,
|
||||
Placeholder: "{\"group\": \"Wavelet\", \"sound\": \"minuet\", \"icon\": \"https://...\"}",
|
||||
Description: "可选的 JSON 配置,支持 group (分组)、sound (铃声)、icon (自定义图标)",
|
||||
@@ -377,14 +355,14 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelDiscord,
|
||||
Type: consts.ChannelDiscord,
|
||||
Name: "Discord 频道",
|
||||
Description: "配置 Discord 频道的 Incoming Webhook 消息推送。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "Webhook 地址",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "https://discord.com/api/webhooks/...",
|
||||
Description: "从 Discord 频道集成设置中复制的 Webhook URL",
|
||||
@@ -393,14 +371,14 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelSlack,
|
||||
Type: consts.ChannelSlack,
|
||||
Name: "Slack 频道",
|
||||
Description: "配置 Slack 频道的 Incoming Webhook 消息推送。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "Webhook 地址",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "https://hooks.slack.com/services/...",
|
||||
Description: "从 Slack 应用配置中复制的 Incoming Webhook URL",
|
||||
@@ -409,22 +387,22 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelPushover,
|
||||
Type: consts.ChannelPushover,
|
||||
Name: "Pushover 推送",
|
||||
Description: "配置 Pushover 即时推送到手机/桌面客户端。",
|
||||
Fields: []PushField{
|
||||
{
|
||||
Key: KeyToken,
|
||||
Key: consts.KeyToken,
|
||||
Label: "应用 Token (App Token)",
|
||||
Type: TypePassword,
|
||||
Type: consts.TypePassword,
|
||||
Required: true,
|
||||
Placeholder: "Pushover 创建应用生成的 API Token / Key",
|
||||
Description: "从 Pushover 控制台创建的 Application API Token",
|
||||
},
|
||||
{
|
||||
Key: KeyURL,
|
||||
Key: consts.KeyURL,
|
||||
Label: "用户 Key (User Key)",
|
||||
Type: TypeText,
|
||||
Type: consts.TypeText,
|
||||
Required: true,
|
||||
Placeholder: "Pushover 账号主页的 User Key",
|
||||
Description: "Pushover 个人账号的 User Key",
|
||||
@@ -433,7 +411,7 @@ func init() {
|
||||
})
|
||||
|
||||
RegisterPushChannelDefinition(PushDefinition{
|
||||
Type: ChannelEmail,
|
||||
Type: consts.ChannelEmail,
|
||||
Name: "邮件推送通道",
|
||||
Description: "邮件推送通道直接使用系统全局 SMTP 设置进行发送,无需在此填写服务器配置。",
|
||||
Fields: []PushField{},
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package entity defines GORM table mapping entities for msg_gateway.
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// MessageChannel is an admin-configured messaging adapter entity.
|
||||
type MessageChannel struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Type string `json:"type" gorm:"size:32;not null"`
|
||||
Name string `json:"name" gorm:"size:64;not null"`
|
||||
OwnerScope string `json:"owner_scope" gorm:"size:32;not null;default:'system'"`
|
||||
OwnerID *uint64 `json:"owner_id,omitempty"`
|
||||
Credentials string `json:"credentials" gorm:"type:text;not null"`
|
||||
Extra string `json:"extra" gorm:"type:text"`
|
||||
Enabled bool `json:"enabled" gorm:"default:false;not null"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (MessageChannel) TableName() string {
|
||||
return "w_message_channels"
|
||||
}
|
||||
|
||||
// MessageBinding maps a platform user to a Wavelet user on one channel.
|
||||
type MessageBinding struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ChannelID uint64 `json:"channel_id" gorm:"not null;index"`
|
||||
PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"`
|
||||
UserID uint64 `json:"user_id" gorm:"not null;index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (MessageBinding) TableName() string {
|
||||
return "w_message_bindings"
|
||||
}
|
||||
|
||||
// MessagePairingCode is a one-time bind code.
|
||||
type MessagePairingCode struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Code string `json:"code" gorm:"size:32;uniqueIndex;not null"`
|
||||
ChannelID uint64 `json:"channel_id" gorm:"not null;index"`
|
||||
PlatformUserID string `json:"platform_user_id" gorm:"size:128;not null;index"`
|
||||
UserID uint64 `json:"user_id" gorm:"not null;index"`
|
||||
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (MessagePairingCode) TableName() string {
|
||||
return "w_message_pairing_codes"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PushChannel 消息通道实体
|
||||
type PushChannel struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Description string `json:"description" gorm:"size:255"`
|
||||
Type string `json:"type" gorm:"size:50;not null;index"`
|
||||
URL string `json:"url" gorm:"type:text"`
|
||||
Token string `json:"token" gorm:"type:text"`
|
||||
Other string `json:"other" gorm:"type:text"`
|
||||
Enabled bool `json:"enabled" gorm:"index;not null;default:true"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
// TableName 指定 GORM 表名
|
||||
func (PushChannel) TableName() string {
|
||||
return "w_push_channels"
|
||||
}
|
||||
|
||||
// Validate 验证与标准化字段
|
||||
func (c *PushChannel) Validate() error {
|
||||
c.Name = strings.TrimSpace(c.Name)
|
||||
if c.Name == "" {
|
||||
return errors.New(consts.ErrChannelNameRequired)
|
||||
}
|
||||
c.Type = strings.TrimSpace(c.Type)
|
||||
if c.Type == "" {
|
||||
return errors.New(consts.ErrChannelTypeRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushEvent 系统通知事件实体
|
||||
type PushEvent struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
EventKey string `json:"event_key" gorm:"uniqueIndex;size:80;not null"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
TaskType string `json:"task_type" gorm:"size:100;index;not null;default:''"`
|
||||
Channels []string `json:"channels" gorm:"type:text;serializer:json;not null"`
|
||||
Targets []string `json:"targets" gorm:"type:text;serializer:json;not null"`
|
||||
Template string `json:"template" gorm:"type:text;not null"`
|
||||
Enabled bool `json:"enabled" gorm:"index;not null;default:false"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
// TableName 指定 GORM 表名
|
||||
func (PushEvent) TableName() string {
|
||||
return "w_push_events"
|
||||
}
|
||||
|
||||
// Validate 验证 PushEvent 实体字段
|
||||
func (e *PushEvent) Validate() error {
|
||||
e.EventKey = strings.TrimSpace(e.EventKey)
|
||||
if e.EventKey == "" {
|
||||
return errors.New(consts.ErrEventKeyRequired)
|
||||
}
|
||||
e.Name = strings.TrimSpace(e.Name)
|
||||
if e.Name == "" {
|
||||
return errors.New(consts.ErrNameRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushHistory 推送日志/历史实体
|
||||
type PushHistory struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
EventKey string `json:"event_key" gorm:"size:80;not null;index"`
|
||||
Channel string `json:"channel" gorm:"size:50;not null;index"`
|
||||
Target string `json:"target" gorm:"size:255;not null"`
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
Level string `json:"level" gorm:"size:20;not null;default:'INFO'"`
|
||||
Status string `json:"status" gorm:"size:20;not null;index"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"type:text"`
|
||||
Payload string `json:"payload" gorm:"type:text"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
}
|
||||
|
||||
// TableName 指定 GORM 表名
|
||||
func (PushHistory) TableName() string {
|
||||
return "w_push_histories"
|
||||
}
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package message_gateway_test
|
||||
package msg_gateway_test
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/testhelper"
|
||||
"Wavelet/plugins/domain/message_gateway"
|
||||
"Wavelet/plugins/domain/msg_gateway"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -31,17 +31,17 @@ func (m *mockDBService) Named(_ string) *gorm.DB {
|
||||
|
||||
func TestUpsertPairingCode_ReusesUnexpired(t *testing.T) {
|
||||
testDB, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
message_gateway.SetDBServiceForTest(&mockDBService{db: testDB})
|
||||
msg_gateway.SetDBServiceForTest(&mockDBService{db: testDB})
|
||||
defer func() {
|
||||
message_gateway.SetDBServiceForTest(nil)
|
||||
msg_gateway.SetDBServiceForTest(nil)
|
||||
cleanup()
|
||||
}()
|
||||
ctx := context.Background()
|
||||
first, err := message_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ABCD1234", time.Now().Add(15*time.Minute))
|
||||
first, err := msg_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ABCD1234", time.Now().Add(15*time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := message_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ZZZZ9999", time.Now().Add(15*time.Minute))
|
||||
second, err := msg_gateway.UpsertPairingCode(ctx, 1, "tg-1", "ZZZZ9999", time.Now().Add(15*time.Minute))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package message_gateway
|
||||
package msg_gateway
|
||||
|
||||
import (
|
||||
"strings"
|
||||
+88
-86
@@ -1,8 +1,8 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package message_gateway provides the Bot gateway, multi-channel notification dispatching, and asynchronous push worker domain plugin for Cordis.
|
||||
package message_gateway
|
||||
// Package msg_gateway provides the Bot gateway, multi-channel notification dispatching, and asynchronous push worker domain plugin for Cordis.
|
||||
package msg_gateway
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
"Wavelet/core/extpoints"
|
||||
"Wavelet/pkg/ginutil"
|
||||
"Wavelet/pkg/util"
|
||||
"Wavelet/plugins/domain/message_gateway/channels/qq"
|
||||
"Wavelet/plugins/domain/message_gateway/channels/telegram"
|
||||
"Wavelet/plugins/domain/message_gateway/handler"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/channels/qq"
|
||||
"Wavelet/plugins/domain/msg_gateway/channels/telegram"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/controller"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"context"
|
||||
"embed"
|
||||
"reflect"
|
||||
@@ -26,7 +28,7 @@ import (
|
||||
//go:embed migrations/*/*.sql
|
||||
var mgMigrations embed.FS
|
||||
|
||||
// Option configures the message_gateway plugin.
|
||||
// Option configures the msg_gateway plugin.
|
||||
type Option func(*Plugin)
|
||||
|
||||
// WithAutoStartRunner enables automatic bot runner startup in the background.
|
||||
@@ -42,7 +44,7 @@ type Plugin struct {
|
||||
cancelRunner context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a new message_gateway domain plugin.
|
||||
// New creates a new msg_gateway domain plugin.
|
||||
func New(opts ...Option) *Plugin {
|
||||
p := &Plugin{}
|
||||
for _, opt := range opts {
|
||||
@@ -53,12 +55,12 @@ func New(opts ...Option) *Plugin {
|
||||
return p
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for the message_gateway domain plugin.
|
||||
// Name returns the unique identifier for the msg_gateway domain plugin.
|
||||
func (p *Plugin) Name() string {
|
||||
return "message_gateway"
|
||||
return "msg_gateway"
|
||||
}
|
||||
|
||||
// Inject declares required dependencies for the message_gateway domain plugin.
|
||||
// Inject declares required dependencies for the msg_gateway domain plugin.
|
||||
func (p *Plugin) Inject() []reflect.Type {
|
||||
return []reflect.Type{
|
||||
reflect.TypeFor[contracts.DBService](),
|
||||
@@ -72,7 +74,7 @@ func (p *Plugin) Inject() []reflect.Type {
|
||||
// Manifest returns the plugin metadata.
|
||||
func (p *Plugin) Manifest() core.Manifest {
|
||||
return core.Manifest{
|
||||
Name: "message_gateway",
|
||||
Name: "msg_gateway",
|
||||
Version: "1.0.0",
|
||||
Description: "Bot gateway, multi-channel notification push, and async worker dispatch plugin",
|
||||
Author: "Wavelet Team",
|
||||
@@ -83,29 +85,29 @@ type mgAppConfig struct {
|
||||
SessionSecret string `config:"session_secret" env:"APP_SESSION_SECRET" secret:"true"`
|
||||
}
|
||||
|
||||
// DeclareConfig declares configuration bindings for the message_gateway plugin.
|
||||
// DeclareConfig declares configuration bindings for the msg_gateway plugin.
|
||||
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
|
||||
return []core.ConfigBinding{
|
||||
{Prefix: "app", Target: &mgAppConfig{}},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply registers message_gateway migrations, routes, tasks, schedules, events, and settings into the Context.
|
||||
// Apply registers msg_gateway migrations, routes, tasks, schedules, events, and settings into the Context.
|
||||
func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
var cfg mgAppConfig
|
||||
if err := ctx.Config().Bind("app", &cfg); err == nil && cfg.SessionSecret != "" {
|
||||
service.SetCredentialSecret(cfg.SessionSecret)
|
||||
}
|
||||
core.Bind[contracts.DBService](ctx, repository.SetDBService)
|
||||
core.Bind[contracts.DBService](ctx, dao.SetDBService)
|
||||
core.Bind[contracts.CacheService](ctx, func(cache contracts.CacheService) {
|
||||
repository.SetCacheService(cache)
|
||||
dao.SetCacheService(cache)
|
||||
service.SetCacheService(cache)
|
||||
})
|
||||
core.Bind[contracts.TaskService](ctx, service.SetTaskService)
|
||||
core.Bind[contracts.UserService](ctx, service.SetUserService)
|
||||
ctx.OnDispose(func() error {
|
||||
repository.SetDBService(nil)
|
||||
repository.SetCacheService(nil)
|
||||
dao.SetDBService(nil)
|
||||
dao.SetCacheService(nil)
|
||||
service.SetCacheService(nil)
|
||||
service.SetTaskService(nil)
|
||||
service.SetUserService(nil)
|
||||
@@ -126,25 +128,25 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
}
|
||||
|
||||
// 1. Register migrations
|
||||
ctx.Migrations().Register("message_gateway", mgMigrations)
|
||||
ctx.Migrations().Register("msg_gateway", mgMigrations)
|
||||
|
||||
// 2. Register User HTTP Routes
|
||||
handler.RegisterUserRoutes(ctx.Router().Group("/api/v1"), loginMW)
|
||||
controller.RegisterUserRoutes(ctx.Router().Group("/api/v1"), loginMW)
|
||||
|
||||
// 3. Register Admin Message Gateway HTTP Routes
|
||||
handler.RegisterAdminRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW)
|
||||
controller.RegisterAdminRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW)
|
||||
|
||||
// 4. Register Admin Push HTTP Routes
|
||||
handler.RegisterAdminPushRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW)
|
||||
controller.RegisterAdminPushRoutes(ctx.Router().Group("/api/v1/admin"), loginMW, adminMW)
|
||||
|
||||
service.Register(model.MessageChannelTypeTelegram, telegram.New)
|
||||
service.Register(model.MessageChannelTypeQQ, qq.New)
|
||||
service.Register(consts.MessageChannelTypeTelegram, telegram.New)
|
||||
service.Register(consts.MessageChannelTypeQQ, qq.New)
|
||||
|
||||
const defaultTaskRetry = 3
|
||||
pushHandler := &service.PushHandler{}
|
||||
|
||||
// 5. Register background tasks
|
||||
ctx.Task().Register("message_gateway:push_notification", func(c context.Context, payload []byte) error {
|
||||
ctx.Task().Register(consts.TaskPushNotification, func(c context.Context, payload []byte) error {
|
||||
return pushHandler.Execute(c, payload)
|
||||
},
|
||||
extpoints.WithTaskType("push_notification"),
|
||||
@@ -163,8 +165,8 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
ctx.Task().Register(service.TaskDispatchBotMsg, &service.BotDispatchHandler{},
|
||||
extpoints.WithTaskMeta(service.BotDispatchMeta))
|
||||
|
||||
ctx.Task().Register("message_gateway:cleanup_pairing_codes", func(c context.Context, _ []byte) error {
|
||||
return repository.DeleteExpiredPairingCodes(c)
|
||||
ctx.Task().Register(consts.TaskCleanupPairingCodes, func(c context.Context, _ []byte) error {
|
||||
return dao.DeleteExpiredPairingCodes(c)
|
||||
},
|
||||
extpoints.WithTaskType("cleanup_pairing_codes"),
|
||||
extpoints.WithTaskName("清理过期配对码"),
|
||||
@@ -176,17 +178,17 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
)
|
||||
|
||||
// 6. Register Cron Schedules
|
||||
ctx.Schedule().RegisterCron("*/10 * * * *", "message_gateway:cleanup_pairing_codes", map[string]any{"action": "cleanup"})
|
||||
ctx.Schedule().RegisterCron("*/10 * * * *", consts.TaskCleanupPairingCodes, map[string]any{"action": "cleanup"})
|
||||
|
||||
// 7. Register EventBus listeners for decoupled push triggers
|
||||
ctx.Events().On("notification:push", func(c context.Context, e model.PushNotificationEvent) error {
|
||||
meta := model.EventMetadata{
|
||||
ctx.Events().On("notification:push", func(c context.Context, e do.PushNotificationEvent) error {
|
||||
meta := do.EventMetadata{
|
||||
Key: "eventbus:" + e.Channel,
|
||||
Name: e.Title,
|
||||
DefaultTemplate: model.NotificationMessage{
|
||||
DefaultTemplate: do.NotificationMessage{
|
||||
Title: e.Title,
|
||||
Content: e.Content,
|
||||
Level: model.DefaultLevelInfo,
|
||||
Level: consts.DefaultLevelInfo,
|
||||
Ext: e.Metadata,
|
||||
},
|
||||
Description: "EventBus triggered notification",
|
||||
@@ -211,14 +213,14 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
|
||||
// 10. Register Settings Schemas
|
||||
ctx.Settings().Register(extpoints.SettingSchema{
|
||||
Key: "message_gateway.pairing_code_expiry_minutes",
|
||||
Key: "msg_gateway.pairing_code_expiry_minutes",
|
||||
Default: 15,
|
||||
Description: "Expiry duration for bot pairing codes in minutes",
|
||||
Type: "integer",
|
||||
Category: "messaging",
|
||||
})
|
||||
ctx.Settings().Register(extpoints.SettingSchema{
|
||||
Key: "message_gateway.max_bindings_per_user",
|
||||
Key: "msg_gateway.max_bindings_per_user",
|
||||
Default: 5,
|
||||
Description: "Maximum platform bot bindings per user",
|
||||
Type: "integer",
|
||||
@@ -250,77 +252,77 @@ const (
|
||||
CodeLength = service.CodeLength
|
||||
)
|
||||
|
||||
// MessageChannel is an alias for model.MessageChannel.
|
||||
type MessageChannel = model.MessageChannel
|
||||
// MessageChannel is an alias for entity.MessageChannel.
|
||||
type MessageChannel = entity.MessageChannel
|
||||
|
||||
// MessageBinding is an alias for model.MessageBinding.
|
||||
type MessageBinding = model.MessageBinding
|
||||
// MessageBinding is an alias for entity.MessageBinding.
|
||||
type MessageBinding = entity.MessageBinding
|
||||
|
||||
// MessagePairingCode is an alias for model.MessagePairingCode.
|
||||
type MessagePairingCode = model.MessagePairingCode
|
||||
// MessagePairingCode is an alias for entity.MessagePairingCode.
|
||||
type MessagePairingCode = entity.MessagePairingCode
|
||||
|
||||
// PushChannel is an alias for model.PushChannel.
|
||||
type PushChannel = model.PushChannel
|
||||
// PushChannel is an alias for entity.PushChannel.
|
||||
type PushChannel = entity.PushChannel
|
||||
|
||||
// PushEvent is an alias for model.PushEvent.
|
||||
type PushEvent = model.PushEvent
|
||||
// PushEvent is an alias for entity.PushEvent.
|
||||
type PushEvent = entity.PushEvent
|
||||
|
||||
// PushHistory is an alias for model.PushHistory.
|
||||
type PushHistory = model.PushHistory
|
||||
// PushHistory is an alias for entity.PushHistory.
|
||||
type PushHistory = entity.PushHistory
|
||||
|
||||
// PushNotificationEvent is an alias for model.PushNotificationEvent.
|
||||
type PushNotificationEvent = model.PushNotificationEvent
|
||||
// PushNotificationEvent is an alias for do.PushNotificationEvent.
|
||||
type PushNotificationEvent = do.PushNotificationEvent
|
||||
|
||||
// ChannelConfig is an alias for model.ChannelConfig.
|
||||
type ChannelConfig = model.ChannelConfig
|
||||
// ChannelConfig is an alias for do.ChannelConfig.
|
||||
type ChannelConfig = do.ChannelConfig
|
||||
|
||||
// Capability is an alias for model.Capability.
|
||||
type Capability = model.Capability
|
||||
// Capability is an alias for do.Capability.
|
||||
type Capability = do.Capability
|
||||
|
||||
// Recipient is an alias for model.Recipient.
|
||||
type Recipient = model.Recipient
|
||||
// Recipient is an alias for do.Recipient.
|
||||
type Recipient = do.Recipient
|
||||
|
||||
// Attachment is an alias for model.Attachment.
|
||||
type Attachment = model.Attachment
|
||||
// Attachment is an alias for do.Attachment.
|
||||
type Attachment = do.Attachment
|
||||
|
||||
// InboundMessage is an alias for model.InboundMessage.
|
||||
type InboundMessage = model.InboundMessage
|
||||
// InboundMessage is an alias for do.InboundMessage.
|
||||
type InboundMessage = do.InboundMessage
|
||||
|
||||
// OutboundMessage is an alias for model.OutboundMessage.
|
||||
type OutboundMessage = model.OutboundMessage
|
||||
// OutboundMessage is an alias for do.OutboundMessage.
|
||||
type OutboundMessage = do.OutboundMessage
|
||||
|
||||
// BindingDTO is an alias for model.BindingDTO.
|
||||
type BindingDTO = model.BindingDTO
|
||||
// BindingDTO is an alias for do.BindingDTO.
|
||||
type BindingDTO = do.BindingDTO
|
||||
|
||||
// PublicChannelDTO is an alias for model.PublicChannelDTO.
|
||||
type PublicChannelDTO = model.PublicChannelDTO
|
||||
// PublicChannelDTO is an alias for do.PublicChannelDTO.
|
||||
type PublicChannelDTO = do.PublicChannelDTO
|
||||
|
||||
// Definition is an alias for model.Definition.
|
||||
type Definition = model.Definition
|
||||
// Definition is an alias for do.Definition.
|
||||
type Definition = do.Definition
|
||||
|
||||
// ChannelDTO is an alias for model.ChannelDTO.
|
||||
type ChannelDTO = model.ChannelDTO
|
||||
// ChannelDTO is an alias for do.ChannelDTO.
|
||||
type ChannelDTO = do.ChannelDTO
|
||||
|
||||
// CreateChannelRequest is an alias for model.CreateChannelRequest.
|
||||
type CreateChannelRequest = model.CreateChannelRequest
|
||||
// CreateChannelRequest is an alias for do.CreateChannelRequest.
|
||||
type CreateChannelRequest = do.CreateChannelRequest
|
||||
|
||||
// UpdateChannelRequest is an alias for model.UpdateChannelRequest.
|
||||
type UpdateChannelRequest = model.UpdateChannelRequest
|
||||
// UpdateChannelRequest is an alias for do.UpdateChannelRequest.
|
||||
type UpdateChannelRequest = do.UpdateChannelRequest
|
||||
|
||||
// PushDefinition is an alias for model.PushDefinition.
|
||||
type PushDefinition = model.PushDefinition
|
||||
// PushDefinition is an alias for do.PushDefinition.
|
||||
type PushDefinition = do.PushDefinition
|
||||
|
||||
// PushField is an alias for model.PushField.
|
||||
type PushField = model.PushField
|
||||
// PushField is an alias for do.PushField.
|
||||
type PushField = do.PushField
|
||||
|
||||
// NotificationMessage is an alias for model.NotificationMessage.
|
||||
type NotificationMessage = model.NotificationMessage
|
||||
// NotificationMessage is an alias for do.NotificationMessage.
|
||||
type NotificationMessage = do.NotificationMessage
|
||||
|
||||
// EventMetadata is an alias for model.EventMetadata.
|
||||
type EventMetadata = model.EventMetadata
|
||||
// EventMetadata is an alias for do.EventMetadata.
|
||||
type EventMetadata = do.EventMetadata
|
||||
|
||||
// SendPayload is an alias for model.SendPayload.
|
||||
type SendPayload = model.SendPayload
|
||||
// SendPayload is an alias for do.SendPayload.
|
||||
type SendPayload = do.SendPayload
|
||||
|
||||
// Handler is an alias for service.Handler.
|
||||
type Handler = service.Handler
|
||||
@@ -342,8 +344,8 @@ type PushHandler = service.PushHandler
|
||||
|
||||
// Re-exported variables and functions.
|
||||
var (
|
||||
SetDBServiceForTest = repository.SetDBServiceForTest
|
||||
UpsertPairingCode = repository.UpsertPairingCode
|
||||
SetDBServiceForTest = dao.SetDBServiceForTest
|
||||
UpsertPairingCode = dao.UpsertPairingCode
|
||||
Register = service.Register
|
||||
Lookup = service.Lookup
|
||||
GenerateCode = service.GenerateCode
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package message_gateway_test
|
||||
package msg_gateway_test
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/plugins/domain/message_gateway"
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
func TestPushRegistry(t *testing.T) {
|
||||
ctx := core.NewContext(context.Background())
|
||||
require.NoError(t, message_gateway.New().Apply(ctx))
|
||||
require.NoError(t, msg_gateway.New().Apply(ctx))
|
||||
|
||||
registry, err := core.Inject[contracts.PushRegistry](ctx)
|
||||
require.NoError(t, err)
|
||||
+10
-10
@@ -1,11 +1,11 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package message_gateway_test
|
||||
package msg_gateway_test
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/plugins/domain/message_gateway"
|
||||
"Wavelet/plugins/domain/msg_gateway"
|
||||
"context"
|
||||
"io/fs"
|
||||
"testing"
|
||||
@@ -14,32 +14,32 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMessageGatewayPluginUnit(t *testing.T) {
|
||||
func TestMsgGatewayPluginUnit(t *testing.T) {
|
||||
ctx := core.NewContext(context.Background())
|
||||
p := message_gateway.New()
|
||||
assert.Equal(t, "message_gateway", p.Name())
|
||||
p := msg_gateway.New()
|
||||
assert.Equal(t, "msg_gateway", p.Name())
|
||||
assert.Equal(t, "1.0.0", p.Manifest().Version)
|
||||
require.NoError(t, p.Apply(ctx))
|
||||
|
||||
// Verify migrations
|
||||
entry, ok := ctx.Migrations().Get("message_gateway")
|
||||
entry, ok := ctx.Migrations().Get("msg_gateway")
|
||||
require.True(t, ok)
|
||||
entries, err := fs.ReadDir(entry.FS, entry.Dir)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, entries)
|
||||
|
||||
// Verify tasks
|
||||
task, ok := ctx.Tasks().Get("message_gateway:push_notification")
|
||||
task, ok := ctx.Tasks().Get("msg_gateway:push_notification")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 3, task.Retry)
|
||||
|
||||
// Verify schedules
|
||||
sched, ok := ctx.Schedules().Get("message_gateway:cleanup_pairing_codes")
|
||||
sched, ok := ctx.Schedules().Get("msg_gateway:cleanup_pairing_codes")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "*/10 * * * *", sched.Spec)
|
||||
|
||||
// Verify settings
|
||||
setting, ok := ctx.Settings().Get("message_gateway.max_bindings_per_user")
|
||||
setting, ok := ctx.Settings().Get("msg_gateway.max_bindings_per_user")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 5, setting.Default)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestMessageGatewayPluginUnit(t *testing.T) {
|
||||
// Register,则每次触发都投递到无人处理的任务类型,清理逻辑静默失效。
|
||||
func TestEveryScheduleHasTaskHandler(t *testing.T) {
|
||||
ctx := core.NewContext(context.Background())
|
||||
require.NoError(t, message_gateway.New().Apply(ctx))
|
||||
require.NoError(t, msg_gateway.New().Apply(ctx))
|
||||
|
||||
schedules := ctx.Schedules().Schedules()
|
||||
require.NotEmpty(t, schedules)
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package message_gateway
|
||||
package msg_gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
+60
-59
@@ -4,9 +4,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -22,19 +23,19 @@ import (
|
||||
const defaultTelegramAPI = "https://api.telegram.org"
|
||||
|
||||
// ListDefinitions returns the admin form schema of every supported channel type.
|
||||
func ListDefinitions() []model.Definition {
|
||||
return []model.Definition{
|
||||
func ListDefinitions() []do.Definition {
|
||||
return []do.Definition{
|
||||
{
|
||||
Type: model.MessageChannelTypeTelegram,
|
||||
Fields: []model.Field{
|
||||
{Key: "token", Type: model.TypePassword, Required: true},
|
||||
{Key: "api_base", Type: model.TypeText, Required: false},
|
||||
Type: consts.MessageChannelTypeTelegram,
|
||||
Fields: []do.Field{
|
||||
{Key: "token", Type: consts.TypePassword, Required: true},
|
||||
{Key: "api_base", Type: consts.TypeText, Required: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: model.MessageChannelTypeQQ,
|
||||
Fields: []model.Field{
|
||||
{Key: "app_id", Type: model.TypeText, Required: true},
|
||||
Type: consts.MessageChannelTypeQQ,
|
||||
Fields: []do.Field{
|
||||
{Key: "app_id", Type: consts.TypeText, Required: true},
|
||||
{Key: "client_secret", Type: "password", Required: true},
|
||||
},
|
||||
},
|
||||
@@ -42,25 +43,25 @@ func ListDefinitions() []model.Definition {
|
||||
}
|
||||
|
||||
// CreateChannel validates the admin payload and persists an encrypted channel.
|
||||
func CreateChannel(ctx context.Context, req model.CreateChannelRequest) (model.ChannelDTO, error) {
|
||||
func CreateChannel(ctx context.Context, req do.CreateChannelRequest) (do.ChannelDTO, error) {
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
return model.ChannelDTO{}, errors.New(errs.ErrNameRequired)
|
||||
return do.ChannelDTO{}, errors.New(consts.ErrNameRequired)
|
||||
}
|
||||
channelType := strings.TrimSpace(req.Type)
|
||||
if channelType != model.MessageChannelTypeTelegram && channelType != model.MessageChannelTypeQQ {
|
||||
return model.ChannelDTO{}, errors.New(errs.ErrTypeInvalid)
|
||||
if channelType != consts.MessageChannelTypeTelegram && channelType != consts.MessageChannelTypeQQ {
|
||||
return do.ChannelDTO{}, errors.New(consts.ErrTypeInvalid)
|
||||
}
|
||||
creds := req.Credentials
|
||||
if creds == nil {
|
||||
creds = map[string]string{}
|
||||
}
|
||||
if err := ValidateCredentials(channelType, creds, false); err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
cipher, err := EncryptCredentials(creds)
|
||||
if err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
extra := req.Extra
|
||||
if extra == nil {
|
||||
@@ -70,32 +71,32 @@ func CreateChannel(ctx context.Context, req model.CreateChannelRequest) (model.C
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
row := &model.MessageChannel{
|
||||
row := &entity.MessageChannel{
|
||||
Name: name,
|
||||
Type: channelType,
|
||||
OwnerScope: model.MessageOwnerScopeSystem,
|
||||
OwnerScope: consts.MessageOwnerScopeSystem,
|
||||
Enabled: enabled,
|
||||
Credentials: cipher,
|
||||
Extra: EncodeExtra(extra),
|
||||
}
|
||||
if err := repository.CreateMessageChannel(ctx, row); err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
if err := dao.CreateMessageChannel(ctx, row); err != nil {
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
return ToDTO(row, creds, extra), nil
|
||||
}
|
||||
|
||||
// UpdateChannel patches a channel; empty secrets keep the stored ciphertext.
|
||||
func UpdateChannel(ctx context.Context, id uint64, req model.UpdateChannelRequest) (model.ChannelDTO, error) {
|
||||
row, err := repository.GetMessageChannel(ctx, id)
|
||||
func UpdateChannel(ctx context.Context, id uint64, req do.UpdateChannelRequest) (do.ChannelDTO, error) {
|
||||
row, err := dao.GetMessageChannel(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return model.ChannelDTO{}, errors.New(errs.ErrChannelNotFound)
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return do.ChannelDTO{}, errors.New(consts.ErrChannelNotFound)
|
||||
}
|
||||
return model.ChannelDTO{}, err
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
creds, err := DecryptCredentials(row.Credentials)
|
||||
if err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
extra := ParseExtra(row.Extra)
|
||||
|
||||
@@ -120,30 +121,30 @@ func UpdateChannel(ctx context.Context, id uint64, req model.UpdateChannelReques
|
||||
merged[k] = v
|
||||
}
|
||||
if err := ValidateCredentials(row.Type, merged, true); err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
creds = merged
|
||||
}
|
||||
|
||||
cipher, err := EncryptCredentials(creds)
|
||||
if err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
row.Credentials = cipher
|
||||
row.Extra = EncodeExtra(extra)
|
||||
if err := repository.UpdateMessageChannel(ctx, row); err != nil {
|
||||
return model.ChannelDTO{}, err
|
||||
if err := dao.UpdateMessageChannel(ctx, row); err != nil {
|
||||
return do.ChannelDTO{}, err
|
||||
}
|
||||
return ToDTO(row, creds, extra), nil
|
||||
}
|
||||
|
||||
// ListChannels returns every channel with secrets masked.
|
||||
func ListChannels(ctx context.Context) ([]model.ChannelDTO, error) {
|
||||
rows, err := repository.ListMessageChannels(ctx)
|
||||
func ListChannels(ctx context.Context) ([]do.ChannelDTO, error) {
|
||||
rows, err := dao.ListMessageChannels(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]model.ChannelDTO, 0, len(rows))
|
||||
out := make([]do.ChannelDTO, 0, len(rows))
|
||||
for i := range rows {
|
||||
creds, _ := DecryptCredentials(rows[i].Credentials)
|
||||
extra := ParseExtra(rows[i].Extra)
|
||||
@@ -154,21 +155,21 @@ func ListChannels(ctx context.Context) ([]model.ChannelDTO, error) {
|
||||
|
||||
// DeleteChannel removes a channel together with its bindings and pairing codes.
|
||||
func DeleteChannel(ctx context.Context, id uint64) error {
|
||||
if _, err := repository.GetMessageChannel(ctx, id); err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return errors.New(errs.ErrChannelNotFound)
|
||||
if _, err := dao.GetMessageChannel(ctx, id); err != nil {
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return errors.New(consts.ErrChannelNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return repository.DeleteMessageChannel(ctx, id)
|
||||
return dao.DeleteMessageChannel(ctx, id)
|
||||
}
|
||||
|
||||
// ProbeChannel verifies the stored credentials against the upstream platform.
|
||||
func ProbeChannel(ctx context.Context, id uint64) error {
|
||||
row, err := repository.GetMessageChannel(ctx, id)
|
||||
row, err := dao.GetMessageChannel(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return errors.New(errs.ErrChannelNotFound)
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return errors.New(consts.ErrChannelNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -177,12 +178,12 @@ func ProbeChannel(ctx context.Context, id uint64) error {
|
||||
return err
|
||||
}
|
||||
switch row.Type {
|
||||
case model.MessageChannelTypeTelegram:
|
||||
case consts.MessageChannelTypeTelegram:
|
||||
return ProbeTelegram(ctx, creds)
|
||||
case model.MessageChannelTypeQQ:
|
||||
case consts.MessageChannelTypeQQ:
|
||||
return ProbeQQ(ctx, creds)
|
||||
default:
|
||||
return errors.New(errs.ErrTypeInvalid)
|
||||
return errors.New(consts.ErrTypeInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ func ProbeChannel(ctx context.Context, id uint64) error {
|
||||
func ProbeTelegram(ctx context.Context, creds map[string]string) error {
|
||||
tok := creds["token"]
|
||||
if strings.TrimSpace(tok) == "" {
|
||||
return errors.New(errs.ErrMissingTelegramToken)
|
||||
return errors.New(consts.ErrMissingTelegramToken)
|
||||
}
|
||||
base := creds["api_base"]
|
||||
base = strings.TrimRight(strings.TrimSpace(base), "/")
|
||||
@@ -210,7 +211,7 @@ func ProbeTelegram(ctx context.Context, creds map[string]string) error {
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%s (%d): %s", errs.ErrTelegramGetMeFailed, resp.StatusCode, string(body))
|
||||
return fmt.Errorf("%s (%d): %s", consts.ErrTelegramGetMeFailed, resp.StatusCode, string(body))
|
||||
}
|
||||
var res struct {
|
||||
OK bool `json:"ok"`
|
||||
@@ -219,7 +220,7 @@ func ProbeTelegram(ctx context.Context, creds map[string]string) error {
|
||||
return err
|
||||
}
|
||||
if !res.OK {
|
||||
return fmt.Errorf("%s: %s", errs.ErrTelegramNotOK, string(body))
|
||||
return fmt.Errorf("%s: %s", consts.ErrTelegramNotOK, string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -229,7 +230,7 @@ func ProbeQQ(_ context.Context, creds map[string]string) error {
|
||||
appID := strings.TrimSpace(creds["app_id"])
|
||||
secret := strings.TrimSpace(creds["client_secret"])
|
||||
if appID == "" || secret == "" {
|
||||
return errors.New(errs.ErrMissingQQCredentials)
|
||||
return errors.New(consts.ErrMissingQQCredentials)
|
||||
}
|
||||
credentials := &token.QQBotCredentials{
|
||||
AppID: appID,
|
||||
@@ -238,10 +239,10 @@ func ProbeQQ(_ context.Context, creds map[string]string) error {
|
||||
tokSrc := token.NewQQBotTokenSource(credentials)
|
||||
tok, err := tokSrc.Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", errs.ErrQQTokenFetchFailed, err)
|
||||
return fmt.Errorf("%s: %w", consts.ErrQQTokenFetchFailed, err)
|
||||
}
|
||||
if tok == nil || tok.AccessToken == "" {
|
||||
return errors.New(errs.ErrQQEmptyToken)
|
||||
return errors.New(consts.ErrQQEmptyToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -249,31 +250,31 @@ func ProbeQQ(_ context.Context, creds map[string]string) error {
|
||||
// ValidateCredentials checks the admin submitted credentials for a channel type.
|
||||
func ValidateCredentials(t string, creds map[string]string, isUpdate bool) error {
|
||||
switch t {
|
||||
case model.MessageChannelTypeTelegram:
|
||||
case consts.MessageChannelTypeTelegram:
|
||||
tok := creds["token"]
|
||||
if strings.TrimSpace(tok) == "" && !isUpdate {
|
||||
return errors.New(errs.ErrTelegramTokenRequired)
|
||||
return errors.New(consts.ErrTelegramTokenRequired)
|
||||
}
|
||||
if base, ok := creds["api_base"]; ok && strings.TrimSpace(base) != "" {
|
||||
if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
|
||||
return errors.New(errs.ErrAPIBaseInvalid)
|
||||
return errors.New(consts.ErrAPIBaseInvalid)
|
||||
}
|
||||
}
|
||||
case model.MessageChannelTypeQQ:
|
||||
case consts.MessageChannelTypeQQ:
|
||||
appID := creds["app_id"]
|
||||
secret := creds["client_secret"]
|
||||
if (strings.TrimSpace(appID) == "" || strings.TrimSpace(secret) == "") && !isUpdate {
|
||||
return errors.New(errs.ErrQQCredentialsRequired)
|
||||
return errors.New(consts.ErrQQCredentialsRequired)
|
||||
}
|
||||
default:
|
||||
return errors.New(errs.ErrTypeInvalid)
|
||||
return errors.New(consts.ErrTypeInvalid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDTO projects a channel row onto the admin DTO with credentials masked.
|
||||
func ToDTO(row *model.MessageChannel, creds, extra map[string]string) model.ChannelDTO {
|
||||
return model.ChannelDTO{
|
||||
func ToDTO(row *entity.MessageChannel, creds, extra map[string]string) do.ChannelDTO {
|
||||
return do.ChannelDTO{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
Type: row.Type,
|
||||
+19
-18
@@ -6,9 +6,10 @@ package service
|
||||
import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -18,9 +19,9 @@ import (
|
||||
|
||||
const (
|
||||
// TaskDispatchBotMsg is the queue pattern for bot downlink dispatch.
|
||||
TaskDispatchBotMsg = "message_gateway:dispatch_bot_msg"
|
||||
TaskDispatchBotMsg = consts.TaskDispatchBotMsg
|
||||
// TaskTypeDispatchBotMsg is the admin type identifier for bot downlink dispatch.
|
||||
TaskTypeDispatchBotMsg = "dispatch_bot_msg"
|
||||
TaskTypeDispatchBotMsg = consts.TaskTypeDispatchBotMsg
|
||||
|
||||
taskQueueDefault = "default"
|
||||
taskParamTypeString = "string"
|
||||
@@ -38,7 +39,7 @@ var BotDispatchMeta = contracts.TaskMetaDTO{
|
||||
Queue: taskQueueDefault,
|
||||
Retryable: true,
|
||||
Params: []contracts.TaskParamDTO{
|
||||
{Name: paramNameText, Label: "消息内容", Type: model.TypeText, Required: true, Placeholder: "要发送的文本", Description: "下发给绑定用户的文本"},
|
||||
{Name: paramNameText, Label: "消息内容", Type: consts.TypeText, Required: true, Placeholder: "要发送的文本", Description: "下发给绑定用户的文本"},
|
||||
{Name: "channel_id", Label: "频道 ID", Type: taskParamTypeString, Required: false, Placeholder: "留空表示全部启用频道", Description: "仅向指定频道的绑定发送"},
|
||||
{Name: "user_id", Label: "用户 ID", Type: taskParamTypeString, Required: false, Placeholder: "留空表示频道下全部绑定", Description: "仅向指定 Wavelet 用户的绑定发送"},
|
||||
},
|
||||
@@ -69,7 +70,7 @@ func (h *BotDispatchHandler) Execute(ctx context.Context, payload []byte) (*cont
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels, err := repository.ListEnabledMessageChannels(ctx)
|
||||
channels, err := dao.ListEnabledMessageChannels(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,7 +83,7 @@ func (h *BotDispatchHandler) Execute(ctx context.Context, payload []byte) (*cont
|
||||
}
|
||||
channels = filtered
|
||||
if len(channels) == 0 {
|
||||
return nil, errors.New(errs.ErrChannelNotFound)
|
||||
return nil, errors.New(consts.ErrChannelNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,20 +108,20 @@ func parseBotDispatchPayload(payload []byte) (botDispatchPayload, error) {
|
||||
var p botDispatchPayload
|
||||
if len(payload) > 0 {
|
||||
if err := json.Unmarshal(payload, &p); err != nil {
|
||||
return p, fmt.Errorf("%s: %w", errs.ErrInvalidJSONFormat, err)
|
||||
return p, fmt.Errorf("%s: %w", consts.ErrInvalidJSONFormat, err)
|
||||
}
|
||||
}
|
||||
p.Text = strings.TrimSpace(p.Text)
|
||||
if p.Text == "" {
|
||||
return p, errors.New(errs.ErrBotDispatchTextRequired)
|
||||
return p, errors.New(consts.ErrBotDispatchTextRequired)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID uint64, text string) (sent, failed int) {
|
||||
func dispatchOnChannel(ctx context.Context, row *entity.MessageChannel, userID uint64, text string) (sent, failed int) {
|
||||
factory, ok := Lookup(row.Type)
|
||||
if !ok {
|
||||
logger.ErrorF(ctx, "bot dispatch: %s type=%s", errs.ErrBotChannelNotRegistered, row.Type)
|
||||
logger.ErrorF(ctx, "bot dispatch: %s type=%s", consts.ErrBotChannelNotRegistered, row.Type)
|
||||
return 0, 1
|
||||
}
|
||||
cfg, err := channelConfigFromRow(row)
|
||||
@@ -139,7 +140,7 @@ func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID ui
|
||||
}
|
||||
defer func() { _ = ch.Disconnect(ctx) }()
|
||||
|
||||
bindings, err := repository.ListBindingsByChannel(ctx, row.ID)
|
||||
bindings, err := dao.ListBindingsByChannel(ctx, row.ID)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "bot dispatch: list bindings %d: %v", row.ID, err)
|
||||
return 0, 1
|
||||
@@ -148,11 +149,11 @@ func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID ui
|
||||
if userID != 0 && bindings[i].UserID != userID {
|
||||
continue
|
||||
}
|
||||
to := model.Recipient{
|
||||
to := do.Recipient{
|
||||
ChatID: bindings[i].PlatformUserID,
|
||||
PlatformUserID: bindings[i].PlatformUserID,
|
||||
}
|
||||
if err := ch.Send(ctx, to, model.OutboundMessage{Text: text}); err != nil {
|
||||
if err := ch.Send(ctx, to, do.OutboundMessage{Text: text}); err != nil {
|
||||
logger.ErrorF(ctx, "bot dispatch: send channel=%d user=%d: %v", row.ID, bindings[i].UserID, err)
|
||||
failed++
|
||||
continue
|
||||
@@ -162,10 +163,10 @@ func dispatchOnChannel(ctx context.Context, row *model.MessageChannel, userID ui
|
||||
return sent, failed
|
||||
}
|
||||
|
||||
func channelConfigFromRow(row *model.MessageChannel) (model.ChannelConfig, error) {
|
||||
func channelConfigFromRow(row *entity.MessageChannel) (do.ChannelConfig, error) {
|
||||
creds, err := DecryptCredentials(row.Credentials)
|
||||
if err != nil {
|
||||
return model.ChannelConfig{}, err
|
||||
return do.ChannelConfig{}, err
|
||||
}
|
||||
if creds == nil {
|
||||
creds = map[string]string{}
|
||||
@@ -180,7 +181,7 @@ func channelConfigFromRow(row *model.MessageChannel) (model.ChannelConfig, error
|
||||
if extra["base_url"] == "" && creds["api_base"] != "" {
|
||||
extra["base_url"] = creds["api_base"]
|
||||
}
|
||||
return model.ChannelConfig{
|
||||
return do.ChannelConfig{
|
||||
ID: row.ID,
|
||||
Type: row.Type,
|
||||
Name: row.Name,
|
||||
+6
-7
@@ -4,7 +4,9 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/message_gateway/service"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"Wavelet/plugins/domain/msg_gateway/service"
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -13,9 +15,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
)
|
||||
|
||||
type dispatchTestDB struct{ db *gorm.DB }
|
||||
@@ -35,9 +34,9 @@ func TestBotDispatchValidatePayload(t *testing.T) {
|
||||
func TestBotDispatchNoChannels(t *testing.T) {
|
||||
testDB, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "dispatch.db")), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testDB.AutoMigrate(&model.MessageChannel{}, &model.MessageBinding{}))
|
||||
repository.SetDBServiceForTest(&dispatchTestDB{db: testDB})
|
||||
t.Cleanup(func() { repository.SetDBServiceForTest(nil) })
|
||||
require.NoError(t, testDB.AutoMigrate(&entity.MessageChannel{}, &entity.MessageBinding{}))
|
||||
dao.SetDBServiceForTest(&dispatchTestDB{db: testDB})
|
||||
t.Cleanup(func() { dao.SetDBServiceForTest(nil) })
|
||||
|
||||
h := &service.BotDispatchHandler{}
|
||||
res, err := h.Execute(context.Background(), []byte(`{"text":"hello"}`))
|
||||
+153
-152
@@ -7,10 +7,11 @@ import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/util"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
pkgpush "Wavelet/plugins/domain/message_gateway/push"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
pkgpush "Wavelet/plugins/domain/msg_gateway/push"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -24,11 +25,11 @@ import (
|
||||
var (
|
||||
builtInEventsMu sync.RWMutex
|
||||
// BuiltInEvents lists all built-in events defined in custom_events.
|
||||
BuiltInEvents []model.EventMetadata
|
||||
BuiltInEvents []do.EventMetadata
|
||||
)
|
||||
|
||||
// RegisterBuiltInEvent registers a built-in event definition.
|
||||
func RegisterBuiltInEvent(meta model.EventMetadata) {
|
||||
func RegisterBuiltInEvent(meta do.EventMetadata) {
|
||||
builtInEventsMu.Lock()
|
||||
defer builtInEventsMu.Unlock()
|
||||
for i, e := range BuiltInEvents {
|
||||
@@ -41,10 +42,10 @@ func RegisterBuiltInEvent(meta model.EventMetadata) {
|
||||
}
|
||||
|
||||
// GetBuiltInEvents returns a copy of registered built-in events.
|
||||
func GetBuiltInEvents() []model.EventMetadata {
|
||||
func GetBuiltInEvents() []do.EventMetadata {
|
||||
builtInEventsMu.RLock()
|
||||
defer builtInEventsMu.RUnlock()
|
||||
out := make([]model.EventMetadata, len(BuiltInEvents))
|
||||
out := make([]do.EventMetadata, len(BuiltInEvents))
|
||||
copy(out, BuiltInEvents)
|
||||
return out
|
||||
}
|
||||
@@ -62,12 +63,12 @@ func (PushRegistryAdapter) SyncEvents(ctx context.Context) error {
|
||||
return SyncEvents(ctx)
|
||||
}
|
||||
|
||||
func eventMetadataFromContract(meta contracts.PushEventMeta) model.EventMetadata {
|
||||
return model.EventMetadata{
|
||||
func eventMetadataFromContract(meta contracts.PushEventMeta) do.EventMetadata {
|
||||
return do.EventMetadata{
|
||||
Key: meta.Key,
|
||||
Name: meta.Name,
|
||||
Description: meta.Description,
|
||||
DefaultTemplate: model.NotificationMessage{
|
||||
DefaultTemplate: do.NotificationMessage{
|
||||
Title: meta.DefaultTemplate.Title,
|
||||
Content: meta.DefaultTemplate.Content,
|
||||
Level: meta.DefaultTemplate.Level,
|
||||
@@ -79,13 +80,13 @@ func eventMetadataFromContract(meta contracts.PushEventMeta) model.EventMetadata
|
||||
// SyncBuiltInEvents seeds a database row for every registered built-in event.
|
||||
func SyncBuiltInEvents(ctx context.Context) error {
|
||||
for _, meta := range GetBuiltInEvents() {
|
||||
_, err := repository.GetPushEventByKeyRecord(ctx, meta.Key)
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
_, err := dao.GetPushEventByKeyRecord(ctx, meta.Key)
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
var defaultTemplateStr string
|
||||
if defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate); err == nil {
|
||||
defaultTemplateStr = string(defaultTemplateBytes)
|
||||
}
|
||||
event := model.PushEvent{
|
||||
event := entity.PushEvent{
|
||||
EventKey: meta.Key,
|
||||
Name: meta.Name,
|
||||
Channels: []string{},
|
||||
@@ -93,7 +94,7 @@ func SyncBuiltInEvents(ctx context.Context) error {
|
||||
Template: defaultTemplateStr,
|
||||
Enabled: false,
|
||||
}
|
||||
if err := repository.CreatePushEventRecord(ctx, &event); err != nil {
|
||||
if err := dao.CreatePushEventRecord(ctx, &event); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
@@ -104,23 +105,23 @@ func SyncBuiltInEvents(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// ListPushEvents lists all configured push events.
|
||||
func ListPushEvents(ctx context.Context) ([]model.PushEvent, error) {
|
||||
return repository.ListPushEventsRecord(ctx)
|
||||
func ListPushEvents(ctx context.Context) ([]entity.PushEvent, error) {
|
||||
return dao.ListPushEventsRecord(ctx)
|
||||
}
|
||||
|
||||
// CreatePushEvent stores a push event configuration for a built-in event or task type.
|
||||
func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (model.PushEvent, error) {
|
||||
func CreatePushEvent(ctx context.Context, req do.CreatePushEventRequest) (entity.PushEvent, error) {
|
||||
eventKey, eventName, defaultTemplateBytes, err := GetEventInfo(ctx, req)
|
||||
if err != nil {
|
||||
return model.PushEvent{}, err
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
|
||||
count, err := repository.CountPushEventsByKeyRecord(ctx, eventKey)
|
||||
count, err := dao.CountPushEventsByKeyRecord(ctx, eventKey)
|
||||
if err != nil {
|
||||
return model.PushEvent{}, err
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
if count > 0 {
|
||||
return model.PushEvent{}, errors.New(errs.ErrEventAlreadyConfigured)
|
||||
return entity.PushEvent{}, errors.New(consts.ErrEventAlreadyConfigured)
|
||||
}
|
||||
|
||||
templateStr := strings.TrimSpace(req.Template)
|
||||
@@ -129,7 +130,7 @@ func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (mod
|
||||
} else {
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal([]byte(templateStr), &tempMap); err != nil {
|
||||
return model.PushEvent{}, errors.New(errs.ErrTemplateInvalidJSON)
|
||||
return entity.PushEvent{}, errors.New(consts.ErrTemplateInvalidJSON)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (mod
|
||||
targets = []string{}
|
||||
}
|
||||
|
||||
event := model.PushEvent{
|
||||
event := entity.PushEvent{
|
||||
EventKey: eventKey,
|
||||
Name: eventName,
|
||||
TaskType: req.TaskType,
|
||||
@@ -152,26 +153,26 @@ func CreatePushEvent(ctx context.Context, req model.CreatePushEventRequest) (mod
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
if err := event.Validate(); err != nil {
|
||||
return model.PushEvent{}, err
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
if err := repository.CreatePushEventRecord(ctx, &event); err != nil {
|
||||
return model.PushEvent{}, err
|
||||
if err := dao.CreatePushEventRecord(ctx, &event); err != nil {
|
||||
return entity.PushEvent{}, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// DeletePushEvent deletes a push event configuration by id.
|
||||
func DeletePushEvent(ctx context.Context, id uint64) error {
|
||||
event, err := repository.GetPushEventByIDRecord(ctx, id)
|
||||
event, err := dao.GetPushEventByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repository.DeletePushEventRecord(ctx, &event)
|
||||
return dao.DeletePushEventRecord(ctx, &event)
|
||||
}
|
||||
|
||||
// UpdatePushEvent replaces mutable push event fields.
|
||||
func UpdatePushEvent(ctx context.Context, id uint64, req model.UpdatePushEventRequest) error {
|
||||
event, err := repository.GetPushEventByIDRecord(ctx, id)
|
||||
func UpdatePushEvent(ctx context.Context, id uint64, req do.UpdatePushEventRequest) error {
|
||||
event, err := dao.GetPushEventByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -183,37 +184,37 @@ func UpdatePushEvent(ctx context.Context, id uint64, req model.UpdatePushEventRe
|
||||
if err := event.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return repository.SavePushEventRecord(ctx, &event)
|
||||
return dao.SavePushEventRecord(ctx, &event)
|
||||
}
|
||||
|
||||
// TogglePushEvent flips the enabled flag of a push event.
|
||||
func TogglePushEvent(ctx context.Context, id uint64) (bool, error) {
|
||||
event, err := repository.GetPushEventByIDRecord(ctx, id)
|
||||
event, err := dao.GetPushEventByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
enabled := !event.Enabled
|
||||
if enabled && len(event.Channels) == 0 {
|
||||
return false, errors.New(errs.ErrEnableWithoutChannels)
|
||||
return false, errors.New(consts.ErrEnableWithoutChannels)
|
||||
}
|
||||
if err := repository.UpdatePushEventEnabledRecord(ctx, &event, enabled); err != nil {
|
||||
if err := dao.UpdatePushEventEnabledRecord(ctx, &event, enabled); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
|
||||
// ListPushHistories returns a paginated push delivery audit page.
|
||||
func ListPushHistories(ctx context.Context, filter model.PushHistoryListFilter) (int64, []model.PushHistory, error) {
|
||||
return repository.ListPushHistoriesRecord(ctx, filter)
|
||||
func ListPushHistories(ctx context.Context, filter do.PushHistoryListFilter) (int64, []entity.PushHistory, error) {
|
||||
return dao.ListPushHistoriesRecord(ctx, filter)
|
||||
}
|
||||
|
||||
// ApplySMTPFallbackToPushConfig fills an email config from the system SMTP settings.
|
||||
func ApplySMTPFallbackToPushConfig(ctx context.Context, cfg *pkgpush.Config) {
|
||||
if cfg.Channel != model.ChannelEmail || (cfg.URL != "" && cfg.Key != "") {
|
||||
if cfg.Channel != consts.ChannelEmail || (cfg.URL != "" && cfg.Key != "") {
|
||||
return
|
||||
}
|
||||
smtp, err := repository.LoadSMTPConfigRecord(ctx)
|
||||
smtp, err := dao.LoadSMTPConfigRecord(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "[Push] 读取 SMTP 系统配置失败: %v", err)
|
||||
return
|
||||
@@ -237,15 +238,15 @@ func RunPushTest(ctx context.Context, cfg pkgpush.Config, target string) error {
|
||||
return err
|
||||
}
|
||||
if err := pusher.ValidateConfig(cfg); err != nil {
|
||||
return fmt.Errorf("%s: %w", errs.ErrValidationFailed, err)
|
||||
return fmt.Errorf("%s: %w", consts.ErrValidationFailed, err)
|
||||
}
|
||||
|
||||
ApplySMTPFallbackToPushConfig(ctx, &cfg)
|
||||
|
||||
testBody := map[string]any{
|
||||
model.KeyTitle: "测试通道推送",
|
||||
model.KeyContent: "当您收到这条消息,说明当前渠道连通性测试通过。",
|
||||
model.KeyLevel: model.DefaultLevelInfo,
|
||||
consts.KeyTitle: "测试通道推送",
|
||||
consts.KeyContent: "当您收到这条消息,说明当前渠道连通性测试通过。",
|
||||
consts.KeyLevel: consts.DefaultLevelInfo,
|
||||
}
|
||||
if _, err := pusher.Send(ctx, cfg, target, testBody, "", nil); err != nil {
|
||||
return err
|
||||
@@ -254,21 +255,21 @@ func RunPushTest(ctx context.Context, cfg pkgpush.Config, target string) error {
|
||||
}
|
||||
|
||||
// ListPushChannels returns every configured push channel.
|
||||
func ListPushChannels(ctx context.Context) ([]model.PushChannel, error) {
|
||||
return repository.ListPushChannelsRecord(ctx)
|
||||
func ListPushChannels(ctx context.Context) ([]entity.PushChannel, error) {
|
||||
return dao.ListPushChannelsRecord(ctx)
|
||||
}
|
||||
|
||||
// CreatePushChannel validates uniqueness and persists a new push channel.
|
||||
func CreatePushChannel(ctx context.Context, req model.CreatePushChannelRequest) (model.PushChannel, error) {
|
||||
count, err := repository.CountPushChannelsByNameRecord(ctx, req.Name)
|
||||
func CreatePushChannel(ctx context.Context, req do.CreatePushChannelRequest) (entity.PushChannel, error) {
|
||||
count, err := dao.CountPushChannelsByNameRecord(ctx, req.Name)
|
||||
if err != nil {
|
||||
return model.PushChannel{}, err
|
||||
return entity.PushChannel{}, err
|
||||
}
|
||||
if count > 0 {
|
||||
return model.PushChannel{}, errors.New(errs.ErrChannelNameExists)
|
||||
return entity.PushChannel{}, errors.New(consts.ErrChannelNameExists)
|
||||
}
|
||||
|
||||
channel := model.PushChannel{
|
||||
channel := entity.PushChannel{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Type: req.Type,
|
||||
@@ -278,19 +279,19 @@ func CreatePushChannel(ctx context.Context, req model.CreatePushChannelRequest)
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
if err := channel.Validate(); err != nil {
|
||||
return model.PushChannel{}, err
|
||||
return entity.PushChannel{}, err
|
||||
}
|
||||
if err := repository.CreatePushChannelRecord(ctx, &channel); err != nil {
|
||||
return model.PushChannel{}, err
|
||||
if err := dao.CreatePushChannelRecord(ctx, &channel); err != nil {
|
||||
return entity.PushChannel{}, err
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// UpdatePushChannel replaces the mutable fields of an existing push channel.
|
||||
func UpdatePushChannel(ctx context.Context, id uint64, req model.UpdatePushChannelRequest) (model.PushChannel, error) {
|
||||
channel, err := repository.GetPushChannelByIDRecord(ctx, id)
|
||||
func UpdatePushChannel(ctx context.Context, id uint64, req do.UpdatePushChannelRequest) (entity.PushChannel, error) {
|
||||
channel, err := dao.GetPushChannelByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return model.PushChannel{}, err
|
||||
return entity.PushChannel{}, err
|
||||
}
|
||||
|
||||
channel.Description = req.Description
|
||||
@@ -300,30 +301,30 @@ func UpdatePushChannel(ctx context.Context, id uint64, req model.UpdatePushChann
|
||||
channel.Other = req.Other
|
||||
channel.Enabled = req.Enabled
|
||||
if err := channel.Validate(); err != nil {
|
||||
return model.PushChannel{}, err
|
||||
return entity.PushChannel{}, err
|
||||
}
|
||||
if err := repository.SavePushChannelRecord(ctx, &channel); err != nil {
|
||||
return model.PushChannel{}, err
|
||||
if err := dao.SavePushChannelRecord(ctx, &channel); err != nil {
|
||||
return entity.PushChannel{}, err
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// DeletePushChannel removes a push channel by id.
|
||||
func DeletePushChannel(ctx context.Context, id uint64) error {
|
||||
channel, err := repository.GetPushChannelByIDRecord(ctx, id)
|
||||
channel, err := dao.GetPushChannelByIDRecord(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repository.DeletePushChannelRecord(ctx, &channel)
|
||||
return dao.DeletePushChannelRecord(ctx, &channel)
|
||||
}
|
||||
|
||||
// LoadChannelForTest resolves the credentials under test, either from a stored
|
||||
// channel name or from the ad-hoc values sent by the caller.
|
||||
func LoadChannelForTest(ctx context.Context, req model.TestPushChannelRequest) (string, string, string, string, error) {
|
||||
func LoadChannelForTest(ctx context.Context, req do.TestPushChannelRequest) (string, string, string, string, error) {
|
||||
if req.Name != "" {
|
||||
channel, err := repository.GetPushChannelByNameRecord(ctx, req.Name)
|
||||
channel, err := dao.GetPushChannelByNameRecord(ctx, req.Name)
|
||||
if err != nil {
|
||||
return "", "", "", "", errors.New(errs.ErrChannelNotFound)
|
||||
return "", "", "", "", errors.New(consts.ErrChannelNotFound)
|
||||
}
|
||||
return channel.URL, channel.Token, channel.Other, channel.Type, nil
|
||||
}
|
||||
@@ -331,17 +332,17 @@ func LoadChannelForTest(ctx context.Context, req model.TestPushChannelRequest) (
|
||||
}
|
||||
|
||||
// PreparePushChannelTest builds the connectivity probe payload for a channel.
|
||||
func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelRequest) (model.SendPayload, error) {
|
||||
func PreparePushChannelTest(ctx context.Context, req do.TestPushChannelRequest) (do.SendPayload, error) {
|
||||
url, token, other, channelType, err := LoadChannelForTest(ctx, req)
|
||||
if err != nil {
|
||||
return model.SendPayload{}, err
|
||||
return do.SendPayload{}, err
|
||||
}
|
||||
|
||||
if channelType == model.ChannelEmail {
|
||||
if channelType == consts.ChannelEmail {
|
||||
url, token, other = ResolveSMTPConfig(ctx, url, token, other)
|
||||
}
|
||||
|
||||
tempChannel := model.PushChannel{
|
||||
tempChannel := entity.PushChannel{
|
||||
Name: "test_temp",
|
||||
URL: url,
|
||||
Token: token,
|
||||
@@ -350,23 +351,23 @@ func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelReques
|
||||
Enabled: true,
|
||||
}
|
||||
if err := tempChannel.Validate(); err != nil {
|
||||
return model.SendPayload{}, err
|
||||
return do.SendPayload{}, err
|
||||
}
|
||||
url = tempChannel.URL
|
||||
|
||||
var config pkgpush.Config
|
||||
var renderedJSON string
|
||||
switch channelType {
|
||||
case model.ChannelLark:
|
||||
config = pkgpush.Config{Channel: model.ChannelLark, URL: url, Secret: token}
|
||||
case consts.ChannelLark:
|
||||
config = pkgpush.Config{Channel: consts.ChannelLark, URL: url, Secret: token}
|
||||
renderedJSON = other
|
||||
case model.ChannelEmail:
|
||||
config = pkgpush.Config{Channel: model.ChannelEmail, URL: url, Key: token, Secret: other}
|
||||
case model.ChannelTelegram:
|
||||
config = pkgpush.Config{Channel: model.ChannelTelegram, URL: url, Secret: token, Key: other}
|
||||
case consts.ChannelEmail:
|
||||
config = pkgpush.Config{Channel: consts.ChannelEmail, URL: url, Key: token, Secret: other}
|
||||
case consts.ChannelTelegram:
|
||||
config = pkgpush.Config{Channel: consts.ChannelTelegram, URL: url, Secret: token, Key: other}
|
||||
default:
|
||||
config = pkgpush.Config{Channel: model.ChannelCustom, URL: url}
|
||||
customPushReq := model.CustomPushRequest{
|
||||
config = pkgpush.Config{Channel: consts.ChannelCustom, URL: url}
|
||||
customPushReq := do.CustomPushRequest{
|
||||
Title: "通道测试通知",
|
||||
Content: "这是一条来自系统的消息通道连通性测试消息。",
|
||||
Description: "系统通道测试",
|
||||
@@ -376,14 +377,14 @@ func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelReques
|
||||
renderedJSON = RenderCustomPayload(other, customPushReq)
|
||||
}
|
||||
|
||||
return model.SendPayload{
|
||||
return do.SendPayload{
|
||||
EventKey: "test_channel",
|
||||
Config: config,
|
||||
Target: req.Target,
|
||||
Body: model.NotificationMessage{
|
||||
Body: do.NotificationMessage{
|
||||
Title: "通道测试通知",
|
||||
Content: "这是一条来自系统的消息通道连通性测试消息。",
|
||||
Level: model.DefaultLevelInfo,
|
||||
Level: consts.DefaultLevelInfo,
|
||||
},
|
||||
Template: renderedJSON,
|
||||
}, nil
|
||||
@@ -391,7 +392,7 @@ func PreparePushChannelTest(ctx context.Context, req model.TestPushChannelReques
|
||||
|
||||
// RenderCustomPayload substitutes the supported template variables of a custom
|
||||
// webhook body, JSON-escaping every injected value.
|
||||
func RenderCustomPayload(template string, req model.CustomPushRequest) string {
|
||||
func RenderCustomPayload(template string, req do.CustomPushRequest) string {
|
||||
result := template
|
||||
result = strings.ReplaceAll(result, "$title", EscapeJSONString(req.Title))
|
||||
result = strings.ReplaceAll(result, "$description", EscapeJSONString(req.Description))
|
||||
@@ -412,20 +413,20 @@ func EscapeJSONString(s string) string {
|
||||
}
|
||||
|
||||
// ListActivePushEventsByTaskType returns enabled push events for a given task type.
|
||||
func ListActivePushEventsByTaskType(ctx context.Context, taskType string) ([]model.PushEvent, error) {
|
||||
return repository.ListActivePushEventsByTaskTypeRecord(ctx, taskType)
|
||||
func ListActivePushEventsByTaskType(ctx context.Context, taskType string) ([]entity.PushEvent, error) {
|
||||
return dao.ListActivePushEventsByTaskTypeRecord(ctx, taskType)
|
||||
}
|
||||
|
||||
// QueryUser resolves a user through the UserService contract, falling back to the
|
||||
// repository read path while the contract is not wired yet.
|
||||
// DAO read path while the contract is not wired yet.
|
||||
func QueryUser(ctx context.Context, fromService func(contracts.UserService) (*contracts.UserDTO, error), dbField string, dbVal any) (*contracts.UserDTO, error) {
|
||||
if userSvc := GetUserService(ctx); userSvc != nil {
|
||||
return fromService(userSvc)
|
||||
}
|
||||
if user, err := repository.FindUserByFieldRecord(ctx, dbField, dbVal); err == nil && user != nil {
|
||||
if user, err := dao.FindUserByFieldRecord(ctx, dbField, dbVal); err == nil && user != nil {
|
||||
return user, nil
|
||||
}
|
||||
return nil, errors.New(errs.ErrUserNotFound)
|
||||
return nil, errors.New(consts.ErrUserNotFound)
|
||||
}
|
||||
|
||||
// FindUserByID resolves a user by primary key.
|
||||
@@ -463,7 +464,7 @@ func LoadUserFromPayload(ctx context.Context, data map[string]any) any {
|
||||
}
|
||||
|
||||
// RecordPushHistory creates a push history audit record.
|
||||
func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMsg string) error {
|
||||
func RecordPushHistory(ctx context.Context, req do.SendPayload, status, errMsg string) error {
|
||||
title := req.Body.Title
|
||||
content := req.Body.Content
|
||||
level := req.Body.Level
|
||||
@@ -471,7 +472,7 @@ func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMs
|
||||
title = "系统通知"
|
||||
}
|
||||
if level == "" {
|
||||
level = model.DefaultLevelInfo
|
||||
level = consts.DefaultLevelInfo
|
||||
}
|
||||
|
||||
target := req.Target
|
||||
@@ -488,7 +489,7 @@ func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMs
|
||||
}
|
||||
}
|
||||
|
||||
history := model.PushHistory{
|
||||
history := entity.PushHistory{
|
||||
EventKey: req.EventKey,
|
||||
Channel: req.Config.Channel,
|
||||
Target: target,
|
||||
@@ -498,7 +499,7 @@ func RecordPushHistory(ctx context.Context, req model.SendPayload, status, errMs
|
||||
Status: status,
|
||||
ErrorMsg: errMsg,
|
||||
}
|
||||
return repository.CreatePushHistoryRecord(ctx, &history)
|
||||
return dao.CreatePushHistoryRecord(ctx, &history)
|
||||
}
|
||||
|
||||
// ResolveTarget parses dynamic placeholders into concrete receiver targets.
|
||||
@@ -520,10 +521,10 @@ func ResolveTarget(ctx context.Context, target string, flatBody map[string]any,
|
||||
if !found {
|
||||
return resolved
|
||||
}
|
||||
if channel == model.ChannelEmail && user.Email != "" {
|
||||
if channel == consts.ChannelEmail && user.Email != "" {
|
||||
return user.Email
|
||||
}
|
||||
if channel != model.ChannelEmail && user.Username != "" {
|
||||
if channel != consts.ChannelEmail && user.Username != "" {
|
||||
return user.Username
|
||||
}
|
||||
return resolved
|
||||
@@ -546,7 +547,7 @@ func ResolveDynamicKeyword(target string, flatBody map[string]any) string {
|
||||
if val, ok := flatBody["username"]; ok {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
case "user.email", model.ChannelEmail:
|
||||
case "user.email", consts.ChannelEmail:
|
||||
if val, ok := flatBody["user.email"]; ok {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
@@ -571,15 +572,15 @@ func ResolveTargetUser(ctx context.Context, resolved, _ string) (contracts.UserD
|
||||
}
|
||||
|
||||
// GetFirstAdminUser resolves the first administrator through the UserService
|
||||
// contract, falling back to the repository read path when it is unavailable.
|
||||
// contract, falling back to the DAO read path when it is unavailable.
|
||||
func GetFirstAdminUser(ctx context.Context) (*contracts.UserDTO, error) {
|
||||
if userSvc := GetUserService(ctx); userSvc != nil {
|
||||
return userSvc.GetFirstAdminUser(ctx)
|
||||
}
|
||||
if adminUser, err := repository.FindFirstAdminUserRecord(ctx); err == nil && adminUser != nil {
|
||||
if adminUser, err := dao.FindFirstAdminUserRecord(ctx); err == nil && adminUser != nil {
|
||||
return adminUser, nil
|
||||
}
|
||||
return nil, errors.New(errs.ErrNoAdminUser)
|
||||
return nil, errors.New(consts.ErrNoAdminUser)
|
||||
}
|
||||
|
||||
// ResolveSystemTarget maps system receiver aliases to administrator contact info.
|
||||
@@ -591,10 +592,10 @@ func ResolveSystemTarget(ctx context.Context, resolved, channel string) (string,
|
||||
if err != nil || adminUser == nil {
|
||||
return resolved, true
|
||||
}
|
||||
if channel == model.ChannelEmail && adminUser.Email != "" {
|
||||
if channel == consts.ChannelEmail && adminUser.Email != "" {
|
||||
return adminUser.Email, true
|
||||
}
|
||||
if channel != model.ChannelEmail && adminUser.Username != "" {
|
||||
if channel != consts.ChannelEmail && adminUser.Username != "" {
|
||||
return adminUser.Username, true
|
||||
}
|
||||
return resolved, true
|
||||
@@ -605,7 +606,7 @@ func ResolveSMTPConfig(ctx context.Context, url, token, other string) (string, s
|
||||
if url != "" && token != "" {
|
||||
return url, token, other
|
||||
}
|
||||
smtp, err := repository.LoadSMTPConfigRecord(ctx)
|
||||
smtp, err := dao.LoadSMTPConfigRecord(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "[Push] 读取 SMTP 系统配置失败: %v", err)
|
||||
return url, token, other
|
||||
@@ -641,18 +642,18 @@ func GetSystemUser(ctx context.Context) *contracts.UserDTO {
|
||||
}
|
||||
|
||||
// FindBuiltInEvent finds a registered built-in event by key.
|
||||
func FindBuiltInEvent(key string) (model.EventMetadata, bool) {
|
||||
func FindBuiltInEvent(key string) (do.EventMetadata, bool) {
|
||||
for _, meta := range GetBuiltInEvents() {
|
||||
if meta.Key == key {
|
||||
return meta, true
|
||||
}
|
||||
}
|
||||
return model.EventMetadata{}, false
|
||||
return do.EventMetadata{}, false
|
||||
}
|
||||
|
||||
// GetEventInfo derives the event key, display name and default template for a
|
||||
// task-completion based event or a registered built-in event key.
|
||||
func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string, string, []byte, error) {
|
||||
func GetEventInfo(ctx context.Context, req do.CreatePushEventRequest) (string, string, []byte, error) {
|
||||
if req.TaskType != "" {
|
||||
taskName := req.TaskType
|
||||
if taskSvc := GetTaskService(ctx); taskSvc != nil {
|
||||
@@ -662,10 +663,10 @@ func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string
|
||||
}
|
||||
eventKey := "task_completed:" + req.TaskType
|
||||
eventName := "任务完成: " + taskName
|
||||
defaultTemplate := model.NotificationMessage{
|
||||
defaultTemplate := do.NotificationMessage{
|
||||
Title: "任务完成: " + taskName,
|
||||
Content: "异步任务 {{task_name}} (ID: {{task_id}}) 已完成。状态: {{task_status}},耗时: {{task_duration}} ms。",
|
||||
Level: model.DefaultLevelInfo,
|
||||
Level: consts.DefaultLevelInfo,
|
||||
}
|
||||
defaultTemplateBytes, err := json.Marshal(defaultTemplate)
|
||||
if err != nil {
|
||||
@@ -675,12 +676,12 @@ func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string
|
||||
}
|
||||
|
||||
if req.EventKey == "" {
|
||||
return "", "", nil, errors.New(errs.ErrEventKeyOrTaskType)
|
||||
return "", "", nil, errors.New(consts.ErrEventKeyOrTaskType)
|
||||
}
|
||||
|
||||
meta, found := FindBuiltInEvent(req.EventKey)
|
||||
if !found {
|
||||
return "", "", nil, errors.New(errs.ErrUnsupportedEventKey)
|
||||
return "", "", nil, errors.New(consts.ErrUnsupportedEventKey)
|
||||
}
|
||||
|
||||
defaultTemplateBytes, err := json.Marshal(meta.DefaultTemplate)
|
||||
@@ -691,7 +692,7 @@ func GetEventInfo(ctx context.Context, req model.CreatePushEventRequest) (string
|
||||
}
|
||||
|
||||
// EnqueuePushTask dispatches a notification payload to the async push worker.
|
||||
func EnqueuePushTask(ctx context.Context, payload model.SendPayload) error {
|
||||
func EnqueuePushTask(ctx context.Context, payload do.SendPayload) error {
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -700,7 +701,7 @@ func EnqueuePushTask(ctx context.Context, payload model.SendPayload) error {
|
||||
_, err = taskSvc.Dispatch(ctx, "send_notification", payloadBytes, contracts.TaskTriggerSystem)
|
||||
return err
|
||||
}
|
||||
return errors.New(errs.ErrTaskServiceUnavailable)
|
||||
return errors.New(consts.ErrTaskServiceUnavailable)
|
||||
}
|
||||
|
||||
// GetFlatBody flattens nested body map.
|
||||
@@ -736,9 +737,9 @@ func FlattenMap(prefix string, m, result map[string]any) {
|
||||
|
||||
const (
|
||||
// SendNotificationTask is the asynq task name for push notification.
|
||||
SendNotificationTask = "push:send"
|
||||
SendNotificationTask = consts.SendNotificationTask
|
||||
// TaskTypeSendNotification is the admin task manager type identifier.
|
||||
TaskTypeSendNotification = "send_notification"
|
||||
TaskTypeSendNotification = consts.TaskTypeSendNotification
|
||||
)
|
||||
|
||||
// SendNotificationMeta represents the task metadata.
|
||||
@@ -778,16 +779,16 @@ type PushHandler struct{}
|
||||
// ValidatePayload validates and normalizes push parameters.
|
||||
func (h *PushHandler) ValidatePayload(payload []byte) ([]byte, error) {
|
||||
if len(payload) == 0 {
|
||||
return nil, errors.New(errs.ErrPayloadRequired)
|
||||
return nil, errors.New(consts.ErrPayloadRequired)
|
||||
}
|
||||
|
||||
var req model.SendPayload
|
||||
var req do.SendPayload
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", errs.ErrInvalidJSONFormat, err)
|
||||
return nil, fmt.Errorf("%s: %w", consts.ErrInvalidJSONFormat, err)
|
||||
}
|
||||
|
||||
if req.Config.Channel == "" {
|
||||
return nil, errors.New(errs.ErrChannelTypeRequired)
|
||||
return nil, errors.New(consts.ErrChannelTypeRequired)
|
||||
}
|
||||
|
||||
return json.Marshal(req)
|
||||
@@ -795,17 +796,17 @@ func (h *PushHandler) ValidatePayload(payload []byte) ([]byte, error) {
|
||||
|
||||
// Execute performs the push send and logs delivery history audit.
|
||||
func (h *PushHandler) Execute(ctx context.Context, payload []byte) error {
|
||||
var req model.SendPayload
|
||||
var req do.SendPayload
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
logger.ErrorF(ctx, "[Push] 解析推送参数失败: %v", err)
|
||||
return fmt.Errorf("%s: %w", errs.ErrParsePayloadFailed, err)
|
||||
return fmt.Errorf("%s: %w", consts.ErrParsePayloadFailed, err)
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "[Push] 开始推送通知: 事件 = %s, 渠道 = %s, 接收目标 = %s", req.EventKey, req.Config.Channel, req.Target)
|
||||
|
||||
pusher, err := pkgpush.GetPusher(req.Config.Channel)
|
||||
if err != nil {
|
||||
errWrap := fmt.Errorf("%s: %w", errs.ErrGetPusherFailed, err)
|
||||
errWrap := fmt.Errorf("%s: %w", consts.ErrGetPusherFailed, err)
|
||||
logger.ErrorF(ctx, "[Push] 推送失败: %v", errWrap)
|
||||
h.recordHistory(ctx, req, "failed", errWrap.Error())
|
||||
return errWrap
|
||||
@@ -829,7 +830,7 @@ func (h *PushHandler) Execute(ctx context.Context, payload []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PushHandler) recordHistory(ctx context.Context, req model.SendPayload, status, errMsg string) {
|
||||
func (h *PushHandler) recordHistory(ctx context.Context, req do.SendPayload, status, errMsg string) {
|
||||
if dbErr := RecordPushHistory(ctx, req, status, errMsg); dbErr != nil {
|
||||
logger.ErrorF(ctx, "[Push] 写入推送历史审计记录失败: %v", dbErr)
|
||||
}
|
||||
@@ -873,7 +874,7 @@ func HandleTaskCompleted(ctx context.Context, e contracts.TaskCompletedEvent) {
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
meta := model.EventMetadata{
|
||||
meta := do.EventMetadata{
|
||||
Key: event.EventKey,
|
||||
Name: event.Name,
|
||||
Description: "异步任务执行完毕触发的自动通知",
|
||||
@@ -942,7 +943,7 @@ type EventTrigger struct{}
|
||||
var DefaultTrigger = &EventTrigger{}
|
||||
|
||||
// Trigger receives event metadata and processes the event notification dispatch asynchronously.
|
||||
func (t *EventTrigger) Trigger(ctx context.Context, meta model.EventMetadata, body map[string]any) {
|
||||
func (t *EventTrigger) Trigger(ctx context.Context, meta do.EventMetadata, body map[string]any) {
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
util.Go(func() {
|
||||
if body == nil {
|
||||
@@ -952,9 +953,9 @@ func (t *EventTrigger) Trigger(ctx context.Context, meta model.EventMetadata, bo
|
||||
body["user"] = GetSystemUser(asyncCtx)
|
||||
}
|
||||
|
||||
eventPtr, err := repository.GetActivePushEventByKey(asyncCtx, meta.Key)
|
||||
eventPtr, err := dao.GetActivePushEventByKey(asyncCtx, meta.Key)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return
|
||||
}
|
||||
logger.ErrorF(asyncCtx, "push_event_trigger: failed to get active event %s: %v", meta.Key, err)
|
||||
@@ -971,8 +972,8 @@ func (t *EventTrigger) Trigger(ctx context.Context, meta model.EventMetadata, bo
|
||||
})
|
||||
}
|
||||
|
||||
func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMetadata, flatBody, body map[string]any) (model.NotificationMessage, string) {
|
||||
var msg model.NotificationMessage
|
||||
func (t *EventTrigger) buildMessage(event *entity.PushEvent, meta do.EventMetadata, flatBody, body map[string]any) (do.NotificationMessage, string) {
|
||||
var msg do.NotificationMessage
|
||||
renderedTemplate := ""
|
||||
|
||||
templateSource := event.Template
|
||||
@@ -982,7 +983,7 @@ func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMeta
|
||||
if err != nil {
|
||||
msg.Title = event.Name
|
||||
msg.Content = renderedTemplate
|
||||
msg.Level = model.DefaultLevelInfo
|
||||
msg.Level = consts.DefaultLevelInfo
|
||||
}
|
||||
} else {
|
||||
msg = t.parseDefaultTemplate(meta, flatBody)
|
||||
@@ -992,7 +993,7 @@ func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMeta
|
||||
msg.Ext = make(map[string]any)
|
||||
}
|
||||
for k, v := range body {
|
||||
if k == model.KeyTitle || k == model.KeyContent || k == model.KeyLevel {
|
||||
if k == consts.KeyTitle || k == consts.KeyContent || k == consts.KeyLevel {
|
||||
continue
|
||||
}
|
||||
if _, exists := msg.Ext[k]; !exists {
|
||||
@@ -1003,8 +1004,8 @@ func (t *EventTrigger) buildMessage(event *model.PushEvent, meta model.EventMeta
|
||||
return msg, renderedTemplate
|
||||
}
|
||||
|
||||
func (t *EventTrigger) parseCustomTemplate(event *model.PushEvent, templateSource string, flatBody map[string]any) (model.NotificationMessage, string, error) {
|
||||
var msg model.NotificationMessage
|
||||
func (t *EventTrigger) parseCustomTemplate(event *entity.PushEvent, templateSource string, flatBody map[string]any) (do.NotificationMessage, string, error) {
|
||||
var msg do.NotificationMessage
|
||||
renderedTemplate := pkgpush.ParseTemplate(templateSource, flatBody)
|
||||
|
||||
var tMap map[string]any
|
||||
@@ -1012,33 +1013,33 @@ func (t *EventTrigger) parseCustomTemplate(event *model.PushEvent, templateSourc
|
||||
return msg, renderedTemplate, err
|
||||
}
|
||||
|
||||
if title, ok := tMap[model.KeyTitle].(string); ok && title != "" {
|
||||
if title, ok := tMap[consts.KeyTitle].(string); ok && title != "" {
|
||||
msg.Title = title
|
||||
} else {
|
||||
msg.Title = event.Name
|
||||
}
|
||||
delete(tMap, model.KeyTitle)
|
||||
delete(tMap, consts.KeyTitle)
|
||||
|
||||
if content, ok := tMap[model.KeyContent].(string); ok && content != "" {
|
||||
if content, ok := tMap[consts.KeyContent].(string); ok && content != "" {
|
||||
msg.Content = content
|
||||
} else {
|
||||
msg.Content = renderedTemplate
|
||||
}
|
||||
delete(tMap, model.KeyContent)
|
||||
delete(tMap, consts.KeyContent)
|
||||
|
||||
if level, ok := tMap[model.KeyLevel].(string); ok && level != "" {
|
||||
if level, ok := tMap[consts.KeyLevel].(string); ok && level != "" {
|
||||
msg.Level = level
|
||||
} else {
|
||||
msg.Level = model.DefaultLevelInfo
|
||||
msg.Level = consts.DefaultLevelInfo
|
||||
}
|
||||
delete(tMap, model.KeyLevel)
|
||||
delete(tMap, consts.KeyLevel)
|
||||
|
||||
msg.Ext = tMap
|
||||
return msg, renderedTemplate, nil
|
||||
}
|
||||
|
||||
func (t *EventTrigger) parseDefaultTemplate(meta model.EventMetadata, flatBody map[string]any) model.NotificationMessage {
|
||||
var msg model.NotificationMessage
|
||||
func (t *EventTrigger) parseDefaultTemplate(meta do.EventMetadata, flatBody map[string]any) do.NotificationMessage {
|
||||
var msg do.NotificationMessage
|
||||
msg.Title = pkgpush.ParseTemplate(meta.DefaultTemplate.Title, flatBody)
|
||||
msg.Content = pkgpush.ParseTemplate(meta.DefaultTemplate.Content, flatBody)
|
||||
msg.Level = pkgpush.ParseTemplate(meta.DefaultTemplate.Level, flatBody)
|
||||
@@ -1056,9 +1057,9 @@ func (t *EventTrigger) parseDefaultTemplate(meta model.EventMetadata, flatBody m
|
||||
return msg
|
||||
}
|
||||
|
||||
func (t *EventTrigger) enqueuePushTasks(ctx context.Context, meta model.EventMetadata, event *model.PushEvent, msg model.NotificationMessage, flatBody map[string]any) {
|
||||
func (t *EventTrigger) enqueuePushTasks(ctx context.Context, meta do.EventMetadata, event *entity.PushEvent, msg do.NotificationMessage, flatBody map[string]any) {
|
||||
for _, channelName := range event.Channels {
|
||||
customChannel, err := repository.GetActivePushChannelByName(ctx, channelName)
|
||||
customChannel, err := dao.GetActivePushChannelByName(ctx, channelName)
|
||||
if err == nil {
|
||||
t.enqueueCustomPushChannelTasks(ctx, meta, event, customChannel, msg, flatBody)
|
||||
continue
|
||||
@@ -1067,7 +1068,7 @@ func (t *EventTrigger) enqueuePushTasks(ctx context.Context, meta model.EventMet
|
||||
}
|
||||
}
|
||||
|
||||
func (t *EventTrigger) enqueueCustomPushChannelTasks(ctx context.Context, meta model.EventMetadata, event *model.PushEvent, channel *model.PushChannel, msg model.NotificationMessage, flatBody map[string]any) {
|
||||
func (t *EventTrigger) enqueueCustomPushChannelTasks(ctx context.Context, meta do.EventMetadata, event *entity.PushEvent, channel *entity.PushChannel, msg do.NotificationMessage, flatBody map[string]any) {
|
||||
if len(event.Targets) == 0 {
|
||||
t.enqueueSingleCustomPushChannelTask(ctx, meta, channel, "", msg)
|
||||
return
|
||||
@@ -1079,22 +1080,22 @@ func (t *EventTrigger) enqueueCustomPushChannelTasks(ctx context.Context, meta m
|
||||
}
|
||||
}
|
||||
|
||||
func (t *EventTrigger) enqueueSingleCustomPushChannelTask(ctx context.Context, meta model.EventMetadata, channel *model.PushChannel, target string, msg model.NotificationMessage) {
|
||||
func (t *EventTrigger) enqueueSingleCustomPushChannelTask(ctx context.Context, meta do.EventMetadata, channel *entity.PushChannel, target string, msg do.NotificationMessage) {
|
||||
var config pkgpush.Config
|
||||
var renderedTemplate string
|
||||
|
||||
switch channel.Type {
|
||||
case model.ChannelLark:
|
||||
config = pkgpush.Config{Channel: model.ChannelLark, URL: channel.URL, Secret: channel.Token}
|
||||
case consts.ChannelLark:
|
||||
config = pkgpush.Config{Channel: consts.ChannelLark, URL: channel.URL, Secret: channel.Token}
|
||||
renderedTemplate = channel.Other
|
||||
case model.ChannelEmail:
|
||||
case consts.ChannelEmail:
|
||||
url, token, other := ResolveSMTPConfig(ctx, channel.URL, channel.Token, channel.Other)
|
||||
config = pkgpush.Config{Channel: model.ChannelEmail, URL: url, Key: token, Secret: other}
|
||||
case model.ChannelTelegram:
|
||||
config = pkgpush.Config{Channel: model.ChannelTelegram, URL: channel.URL, Secret: channel.Token, Key: channel.Other}
|
||||
config = pkgpush.Config{Channel: consts.ChannelEmail, URL: url, Key: token, Secret: other}
|
||||
case consts.ChannelTelegram:
|
||||
config = pkgpush.Config{Channel: consts.ChannelTelegram, URL: channel.URL, Secret: channel.Token, Key: channel.Other}
|
||||
default:
|
||||
config = pkgpush.Config{Channel: model.ChannelCustom, URL: channel.URL}
|
||||
customPushReq := model.CustomPushRequest{
|
||||
config = pkgpush.Config{Channel: consts.ChannelCustom, URL: channel.URL}
|
||||
customPushReq := do.CustomPushRequest{
|
||||
Title: msg.Title,
|
||||
Content: msg.Content,
|
||||
Description: meta.Description,
|
||||
@@ -1106,7 +1107,7 @@ func (t *EventTrigger) enqueueSingleCustomPushChannelTask(ctx context.Context, m
|
||||
renderedTemplate = RenderCustomPayload(channel.Other, customPushReq)
|
||||
}
|
||||
|
||||
payload := model.SendPayload{
|
||||
payload := do.SendPayload{
|
||||
EventKey: meta.Key,
|
||||
Config: config,
|
||||
Target: target,
|
||||
@@ -1124,10 +1125,10 @@ func SyncEvents(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// AdminLogin is the metadata definition for the admin login event.
|
||||
var AdminLogin = model.EventMetadata{
|
||||
var AdminLogin = do.EventMetadata{
|
||||
Key: "admin_login",
|
||||
Name: "管理员登录",
|
||||
DefaultTemplate: model.NotificationMessage{
|
||||
DefaultTemplate: do.NotificationMessage{
|
||||
Title: "管理员登录提醒",
|
||||
Content: "管理员 {{user.username}} 于 {{time}} 从 IP {{ip}} 登录系统。",
|
||||
Level: "INFO",
|
||||
+53
-52
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package service implements domain business logic and channel runners for message_gateway.
|
||||
// Package service implements domain business logic and channel runners for msg_gateway.
|
||||
package service
|
||||
|
||||
import (
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/util"
|
||||
"Wavelet/plugins/domain/message_gateway/errs"
|
||||
"Wavelet/plugins/domain/message_gateway/model"
|
||||
"Wavelet/plugins/domain/message_gateway/repository"
|
||||
"Wavelet/plugins/domain/msg_gateway/consts"
|
||||
"Wavelet/plugins/domain/msg_gateway/dao"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/do"
|
||||
"Wavelet/plugins/domain/msg_gateway/model/entity"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -26,18 +27,18 @@ import (
|
||||
)
|
||||
|
||||
// Handler processes one inbound message.
|
||||
type Handler func(ctx context.Context, msg model.InboundMessage) error
|
||||
type Handler func(ctx context.Context, msg do.InboundMessage) error
|
||||
|
||||
// Factory constructs a Channel from decrypted config.
|
||||
type Factory func(cfg model.ChannelConfig, onInbound Handler) (Channel, error)
|
||||
type Factory func(cfg do.ChannelConfig, onInbound Handler) (Channel, error)
|
||||
|
||||
// Channel is one connected messaging adapter.
|
||||
type Channel interface {
|
||||
Type() string
|
||||
Connect(ctx context.Context) error
|
||||
Disconnect(ctx context.Context) error
|
||||
Send(ctx context.Context, to model.Recipient, msg model.OutboundMessage) error
|
||||
Capabilities() model.Capability
|
||||
Send(ctx context.Context, to do.Recipient, msg do.OutboundMessage) error
|
||||
Capabilities() do.Capability
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -60,11 +61,11 @@ func Lookup(typ string) (Factory, bool) {
|
||||
return fn, ok
|
||||
}
|
||||
|
||||
// CodeAlphabet excludes easily confused runes 0/O/1/I.
|
||||
const CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
|
||||
// CodeLength is the raw pairing code size.
|
||||
const CodeLength = 8
|
||||
// Re-exported constants.
|
||||
const (
|
||||
CodeAlphabet = consts.CodeAlphabet
|
||||
CodeLength = consts.CodeLength
|
||||
)
|
||||
|
||||
// GenerateCode returns an 8-character pairing code.
|
||||
func GenerateCode() (string, error) {
|
||||
@@ -282,87 +283,87 @@ func GetUserService(ctx context.Context) contracts.UserService {
|
||||
}
|
||||
|
||||
// BindChannel consumes a pairing code and binds the platform identity to the user.
|
||||
func BindChannel(ctx context.Context, userID uint64, req model.BindRequest) (model.BindingDTO, error) {
|
||||
func BindChannel(ctx context.Context, userID uint64, req do.BindRequest) (do.BindingDTO, error) {
|
||||
channelID, err := strconv.ParseUint(strings.TrimSpace(req.ChannelID), 10, 64)
|
||||
if err != nil || channelID == 0 {
|
||||
return model.BindingDTO{}, errs.ErrChannelIDRequired
|
||||
return do.BindingDTO{}, consts.ErrChannelIDRequired
|
||||
}
|
||||
code := NormalizeCode(req.Code)
|
||||
if code == "" {
|
||||
return model.BindingDTO{}, errs.ErrCodeInvalid
|
||||
return do.BindingDTO{}, consts.ErrCodeInvalid
|
||||
}
|
||||
pairing, err := repository.GetPairingCode(ctx, code)
|
||||
pairing, err := dao.GetPairingCode(ctx, code)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return model.BindingDTO{}, errs.ErrCodeInvalid
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return do.BindingDTO{}, consts.ErrCodeInvalid
|
||||
}
|
||||
return model.BindingDTO{}, err
|
||||
return do.BindingDTO{}, err
|
||||
}
|
||||
if !pairing.ExpiresAt.After(time.Now()) {
|
||||
return model.BindingDTO{}, errs.ErrCodeInvalid
|
||||
return do.BindingDTO{}, consts.ErrCodeInvalid
|
||||
}
|
||||
if pairing.ChannelID != channelID {
|
||||
return model.BindingDTO{}, errs.ErrChannelMismatch
|
||||
return do.BindingDTO{}, consts.ErrChannelMismatch
|
||||
}
|
||||
ch, err := repository.GetMessageChannel(ctx, channelID)
|
||||
ch, err := dao.GetMessageChannel(ctx, channelID)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return model.BindingDTO{}, errs.ErrCodeInvalid
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return do.BindingDTO{}, consts.ErrCodeInvalid
|
||||
}
|
||||
return model.BindingDTO{}, err
|
||||
return do.BindingDTO{}, err
|
||||
}
|
||||
if !ch.Enabled {
|
||||
return model.BindingDTO{}, errs.ErrChannelDisabled
|
||||
return do.BindingDTO{}, consts.ErrChannelDisabled
|
||||
}
|
||||
|
||||
existing, err := repository.GetBindingByChannelPlatform(ctx, channelID, pairing.PlatformUserID)
|
||||
if err != nil && !errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return model.BindingDTO{}, err
|
||||
existing, err := dao.GetBindingByChannelPlatform(ctx, channelID, pairing.PlatformUserID)
|
||||
if err != nil && !errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return do.BindingDTO{}, err
|
||||
}
|
||||
if err == nil && existing != nil {
|
||||
if existing.UserID != userID {
|
||||
return model.BindingDTO{}, errs.ErrPlatformAlreadyBound
|
||||
return do.BindingDTO{}, consts.ErrPlatformAlreadyBound
|
||||
}
|
||||
_ = repository.DeletePairingCode(ctx, pairing.Code)
|
||||
_ = dao.DeletePairingCode(ctx, pairing.Code)
|
||||
return ToBindingDTO(existing, ch), nil
|
||||
}
|
||||
|
||||
row := &model.MessageBinding{
|
||||
row := &entity.MessageBinding{
|
||||
UserID: userID,
|
||||
ChannelID: channelID,
|
||||
PlatformUserID: pairing.PlatformUserID,
|
||||
}
|
||||
if err := repository.CreateMessageBinding(ctx, row); err != nil {
|
||||
return model.BindingDTO{}, err
|
||||
if err := dao.CreateMessageBinding(ctx, row); err != nil {
|
||||
return do.BindingDTO{}, err
|
||||
}
|
||||
if err := repository.DeletePairingCode(ctx, pairing.Code); err != nil {
|
||||
return model.BindingDTO{}, err
|
||||
if err := dao.DeletePairingCode(ctx, pairing.Code); err != nil {
|
||||
return do.BindingDTO{}, err
|
||||
}
|
||||
return ToBindingDTO(row, ch), nil
|
||||
}
|
||||
|
||||
// ListEnabledPublicChannels returns the channels a user may bind to.
|
||||
func ListEnabledPublicChannels(ctx context.Context) ([]model.PublicChannelDTO, error) {
|
||||
rows, err := repository.ListEnabledMessageChannels(ctx)
|
||||
func ListEnabledPublicChannels(ctx context.Context) ([]do.PublicChannelDTO, error) {
|
||||
rows, err := dao.ListEnabledMessageChannels(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]model.PublicChannelDTO, 0, len(rows))
|
||||
out := make([]do.PublicChannelDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, model.PublicChannelDTO{ID: row.ID, Name: row.Name, Type: row.Type})
|
||||
out = append(out, do.PublicChannelDTO{ID: row.ID, Name: row.Name, Type: row.Type})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListUserBindings returns the binding rows of one user enriched with channel info.
|
||||
func ListUserBindings(ctx context.Context, userID uint64) ([]model.BindingDTO, error) {
|
||||
rows, err := repository.ListBindingsByUser(ctx, userID)
|
||||
func ListUserBindings(ctx context.Context, userID uint64) ([]do.BindingDTO, error) {
|
||||
rows, err := dao.ListBindingsByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]model.BindingDTO, 0, len(rows))
|
||||
out := make([]do.BindingDTO, 0, len(rows))
|
||||
for i := range rows {
|
||||
ch, err := repository.GetMessageChannel(ctx, rows[i].ChannelID)
|
||||
ch, err := dao.GetMessageChannel(ctx, rows[i].ChannelID)
|
||||
if err != nil {
|
||||
out = append(out, ToBindingDTO(&rows[i], nil))
|
||||
continue
|
||||
@@ -374,22 +375,22 @@ func ListUserBindings(ctx context.Context, userID uint64) ([]model.BindingDTO, e
|
||||
|
||||
// UnbindChannel removes a binding owned by the given user.
|
||||
func UnbindChannel(ctx context.Context, userID, bindingID uint64) error {
|
||||
row, err := repository.GetMessageBinding(ctx, bindingID)
|
||||
row, err := dao.GetMessageBinding(ctx, bindingID)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrRecordNotFound) {
|
||||
return errs.ErrBindingNotFound
|
||||
if errors.Is(err, consts.ErrRecordNotFound) {
|
||||
return consts.ErrBindingNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if row.UserID != userID {
|
||||
return errs.ErrBindingForbidden
|
||||
return consts.ErrBindingForbidden
|
||||
}
|
||||
return repository.DeleteMessageBinding(ctx, bindingID)
|
||||
return dao.DeleteMessageBinding(ctx, bindingID)
|
||||
}
|
||||
|
||||
// ToBindingDTO projects a binding row and its optional channel onto the user DTO.
|
||||
func ToBindingDTO(row *model.MessageBinding, ch *model.MessageChannel) model.BindingDTO {
|
||||
dto := model.BindingDTO{
|
||||
func ToBindingDTO(row *entity.MessageBinding, ch *entity.MessageChannel) do.BindingDTO {
|
||||
dto := do.BindingDTO{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
ChannelID: row.ChannelID,
|
||||
@@ -66,11 +66,6 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
})
|
||||
ctx.Router().RegisterWhitelist("/api/v1/config/public")
|
||||
|
||||
// 3. Custom injection
|
||||
ctx.Router().GET("/custom", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(gin.H{"custom": true}))
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user