diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b01277c..71713a0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -834,6 +834,7 @@ importers: ts-jest: 27.1.4 ts-node: ^10.0.0 typescript: ^4.3.3 + url-regex: ^5.0.0 vinyl-fs: ^3.0.3 dependencies: '@socket.io/admin-ui': 0.5.1_socket.io@4.5.1 @@ -874,6 +875,7 @@ importers: tailchat-server-sdk: link:packages/sdk ts-node: 10.9.1_t4lrjbt3sxauai4t5o275zsepa typescript: 4.7.4 + url-regex: 5.0.0 devDependencies: '@babel/helper-compilation-targets': 7.18.9 '@types/accepts': 1.3.5 @@ -24433,7 +24435,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_k2dsl7zculo2nmh5s33pladmoa + ts-node: 10.9.1_t4lrjbt3sxauai4t5o275zsepa transitivePeerDependencies: - bufferutil - canvas @@ -37236,7 +37238,6 @@ packages: typescript: 4.7.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - dev: false /ts-node/10.9.1_typescript@4.7.4: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} diff --git a/server/lib/utils.ts b/server/lib/utils.ts index cd334fea..0dc0a6a8 100644 --- a/server/lib/utils.ts +++ b/server/lib/utils.ts @@ -1,5 +1,6 @@ import randomString from 'crypto-random-string'; import _ from 'lodash'; +import urlRegex from 'url-regex'; /** * 返回电子邮箱的地址 @@ -33,6 +34,13 @@ export function isValidStr(str: unknown): str is string { return typeof str == 'string' && str !== ''; } +/** + * 判断是否是一个可用的url + */ +export function isValidUrl(str: unknown): str is string { + return typeof str == 'string' && urlRegex({ exact: true }).test(str); +} + /** * 检测一个地址是否是一个合法的资源地址 */ diff --git a/server/models/chat/inbox.ts b/server/models/chat/inbox.ts index e5356cf9..ed3f8043 100644 --- a/server/models/chat/inbox.ts +++ b/server/models/chat/inbox.ts @@ -12,28 +12,22 @@ import type { Types } from 'mongoose'; import { User } from '../user/user'; import { Message } from './message'; -class InboxMessage { +interface InboxMessage { /** * 消息所在群组Id */ - @prop() groupId?: string; /** * 消息所在会话Id */ - @prop() converseId: string; - @prop({ - ref: () => Message, - }) - messageId: Ref; + messageId: string; /** * 消息片段,用于消息的预览/发送通知 */ - @prop() messageSnippet: string; } @@ -60,11 +54,9 @@ export class Inbox extends TimeStamps implements Base { @prop({ type: () => String, }) - type: 'message'; + type: string; - @prop({ - type: () => InboxMessage, - }) + @prop() message?: InboxMessage; /** diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 3467a764..7e90147f 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -8,12 +8,10 @@ import { } from '@typegoose/typegoose'; import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'; import type { Types } from 'mongoose'; +import { UserType, userType } from 'tailchat-server-sdk'; type BaseUserInfo = Pick; -const userType = ['normalUser', 'pluginBot', 'openapiBot']; -type UserType = typeof userType[number]; - /** * 用户设置 */ diff --git a/server/package.json b/server/package.json index 0595a0cc..6e51e454 100644 --- a/server/package.json +++ b/server/package.json @@ -74,7 +74,8 @@ "socket.io-msgpack-parser": "^3.0.2", "tailchat-server-sdk": "workspace:*", "ts-node": "^10.0.0", - "typescript": "^4.3.3" + "typescript": "^4.3.3", + "url-regex": "^5.0.0" }, "devDependencies": { "@babel/helper-compilation-targets": "^7.18.2", diff --git a/server/packages/sdk/src/index.ts b/server/packages/sdk/src/index.ts index 2644f67d..ba0f26a6 100644 --- a/server/packages/sdk/src/index.ts +++ b/server/packages/sdk/src/index.ts @@ -30,6 +30,7 @@ export type { MessageStruct, MessageReactionStruct, MessageMetaStruct, + InboxStruct, } from './structs/chat'; export type { BuiltinEventMap } from './structs/events'; export type { @@ -38,7 +39,8 @@ export type { GroupPanelStruct, } from './structs/group'; export { GroupPanelType } from './structs/group'; -export type { UserStruct } from './structs/user'; +export { userType } from './structs/user'; +export type { UserStruct, UserType } from './structs/user'; // db export * as db from './db'; diff --git a/server/packages/sdk/src/services/lib/call.ts b/server/packages/sdk/src/services/lib/call.ts index 5e0467bd..aaeac4e9 100644 --- a/server/packages/sdk/src/services/lib/call.ts +++ b/server/packages/sdk/src/services/lib/call.ts @@ -2,11 +2,11 @@ import { GroupStruct, UserStruct, SYSTEM_USERID, - TcContext, PERMISSION, + TcPureContext, } from '../../index'; -export function call(ctx: TcContext) { +export function call(ctx: TcPureContext) { return { /** * 加入socketio房间 diff --git a/server/packages/sdk/src/structs/chat.ts b/server/packages/sdk/src/structs/chat.ts index 907a1111..e231d5cd 100644 --- a/server/packages/sdk/src/structs/chat.ts +++ b/server/packages/sdk/src/structs/chat.ts @@ -21,3 +21,42 @@ export interface MessageMetaStruct { content: string; }; } + +interface InboxMessageStruct { + /** + * 消息所在群组Id + */ + groupId?: string; + + /** + * 消息所在会话Id + */ + converseId: string; + + /** + * 消息ID + */ + messageId: string; + + /** + * 消息片段,用于消息的预览/发送通知 + */ + messageSnippet: string; +} + +export interface InboxStruct { + _id: string; + userId: string; + type: string; + message?: InboxMessageStruct; + + /** + * 信息体,没有类型 + */ + payload?: object; + + /** + * 是否已读 + */ + readed: boolean; +} diff --git a/server/packages/sdk/src/structs/events.ts b/server/packages/sdk/src/structs/events.ts index 9134b99f..fb804965 100644 --- a/server/packages/sdk/src/structs/events.ts +++ b/server/packages/sdk/src/structs/events.ts @@ -1,4 +1,4 @@ -import type { MessageMetaStruct } from './chat'; +import type { InboxStruct, MessageMetaStruct } from './chat'; /** * 默认服务的事件映射 @@ -22,4 +22,5 @@ export interface BuiltinEventMap { meta: MessageMetaStruct; }; 'config.updated': { config: Record }; + 'chat.inbox.append': InboxStruct; } diff --git a/server/packages/sdk/src/structs/user.ts b/server/packages/sdk/src/structs/user.ts index 1423f57b..af351a8f 100644 --- a/server/packages/sdk/src/structs/user.ts +++ b/server/packages/sdk/src/structs/user.ts @@ -1,5 +1,5 @@ -const userType = ['normalUser', 'pluginBot', 'openapiBot']; -type UserType = typeof userType[number]; +export const userType = ['normalUser', 'pluginBot', 'openapiBot'] as const; +export type UserType = typeof userType[number]; export interface UserStruct { /** @@ -39,7 +39,7 @@ export interface UserStruct { */ avatar?: string; - type: UserType[]; + type: UserType; emailVerified: boolean; } diff --git a/server/services/core/chat/inbox.service.ts b/server/services/core/chat/inbox.service.ts index 7d88b130..99776281 100644 --- a/server/services/core/chat/inbox.service.ts +++ b/server/services/core/chat/inbox.service.ts @@ -4,6 +4,7 @@ import { TcContext, TcDbService, TcPureContext, + InboxStruct, } from 'tailchat-server-sdk'; /** @@ -114,6 +115,7 @@ class InboxService extends TcService { const inboxItem = await this.transformDocuments(ctx, {}, doc); await this.notifyUsersInboxAppend(ctx, [userId], inboxItem); + await this.emitInboxAppendEvent(ctx, inboxItem); return true; } @@ -152,6 +154,7 @@ class InboxService extends TcService { const inboxItem = await this.transformDocuments(ctx, {}, doc); await this.notifyUsersInboxAppend(ctx, [userId], inboxItem); + await this.emitInboxAppendEvent(ctx, inboxItem); return true; } @@ -237,9 +240,7 @@ class InboxService extends TcService { } /** - * 发送通知群组信息有新的内容 - * - * 发送通知时会同时清空群组信息缓存 + * 通知用户收件箱追加了新的内容 */ private async notifyUsersInboxAppend( ctx: TcPureContext, @@ -250,9 +251,7 @@ class InboxService extends TcService { } /** - * 发送通知群组信息发生变更 - * - * 发送通知时会同时清空群组信息缓存 + * 通知用户收件箱有新的内容 */ private async notifyUsersInboxUpdate( ctx: TcPureContext, @@ -260,6 +259,16 @@ class InboxService extends TcService { ): Promise { await this.listcastNotify(ctx, userIds, 'updated', {}); } + + /** + * 向微服务通知有新的内容产生 + */ + private async emitInboxAppendEvent( + ctx: TcPureContext, + inboxItem: InboxStruct + ) { + await ctx.emit('chat.inbox.append', inboxItem); + } } export default InboxService; diff --git a/server/services/core/user/user.service.ts b/server/services/core/user/user.service.ts index 01d62e3a..d8652749 100644 --- a/server/services/core/user/user.service.ts +++ b/server/services/core/user/user.service.ts @@ -197,6 +197,11 @@ class UserService extends TcService { avatar: { type: 'string', optional: true }, }, }); + this.registerAction('findOpenapiBotId', this.findOpenapiBotId, { + params: { + email: 'string', + }, + }); this.registerAction('ensureOpenapiBot', this.ensureOpenapiBot, { params: { /** @@ -912,6 +917,13 @@ class UserService extends TcService { }; } + /** + * 根据用户邮件获取开放平台机器人id + */ + findOpenapiBotId(ctx: TcContext<{ email: string }>): string { + return this.parseOpenapiBotEmail(ctx.params.email); + } + async generateUserToken( ctx: TcContext<{ userId: string; @@ -1056,6 +1068,14 @@ class UserService extends TcService { return `${botId}@tailchat-openapi.com`; } + private parseOpenapiBotEmail(email: string): string | null { + if (email.endsWith('@tailchat-openapi.com')) { + return email.replace('@tailchat-openapi.com', ''); + } + + return null; + } + /** * 构建验证邮箱的缓存key */ diff --git a/server/services/openapi/bot.service.ts b/server/services/openapi/bot.service.ts index e4e2ff40..f7315f8b 100644 --- a/server/services/openapi/bot.service.ts +++ b/server/services/openapi/bot.service.ts @@ -1,5 +1,8 @@ -import { TcService, config, TcContext } from 'tailchat-server-sdk'; +import { TcService, config, TcContext, call } from 'tailchat-server-sdk'; +import { isValidStr, isValidUrl } from '../../lib/utils'; import type { OpenApp } from '../../models/openapi/app'; +import got from 'got'; +import _ from 'lodash'; class OpenBotService extends TcService { get serviceName(): string { @@ -11,6 +14,45 @@ class OpenBotService extends TcService { return; } + this.registerEventListener('chat.inbox.append', async (payload, ctx) => { + const userInfo = await call(ctx).getUserInfo(payload._id); + + if (userInfo.type !== 'openapiBot') { + return; + } + + // 开放平台机器人 + const botId: string | null = await ctx.call('user.findOpenapiBotId', { + email: userInfo.email, + }); + + if (!(isValidStr(botId) && botId.startsWith('open_'))) { + return; + } + + // 是合法的机器人id + + const appId = botId.replace('open_', ''); + const appInfo: OpenApp | null = await ctx.call('openapi.app.get', { + appId, + }); + const callbackUrl = _.get(appInfo, 'bot.callbackUrl'); + + if (!isValidUrl(callbackUrl)) { + this.logger.info('机器人回调地址不是一个可用的url, skip.'); + return; + } + + got + .post(callbackUrl) + .then((res) => { + this.logger.info('调用机器人通知接口回调成功', res); + }) + .catch((err) => { + this.logger.error('调用机器人通知接口回调失败:', err); + }); + }); + this.registerAction('login', this.login, { params: { appId: 'string', @@ -94,7 +136,7 @@ class OpenBotService extends TcService { avatar, }); - this.logger.info('Simple Notify Bot Id:', botUserId); + this.logger.info('[getOrCreateBotAccount] Bot Id:', botUserId); return { userId: String(botUserId),