import { TcService, config, TcDbService, TcContext, EntityError, NoPermissionError, } from 'tailchat-server-sdk'; import _ from 'lodash'; import { filterAvailableAppCapability, OpenApp, OpenAppBot, OpenAppDocument, OpenAppModel, OpenAppOAuth, } from '../../models/openapi/app'; import { Types } from 'mongoose'; import { nanoid } from 'nanoid'; import crypto from 'crypto'; interface OpenAppService extends TcService, TcDbService {} class OpenAppService extends TcService { get serviceName(): string { return 'openapi.app'; } onInit(): void { if (!config.enableOpenapi) { return; } this.registerLocalDb(require('../../models/openapi/app').default); this.registerAction('authToken', this.authToken, { params: { appId: 'string', token: 'string', capability: { type: 'array', items: 'string', optional: true }, }, cache: { keys: ['appId', 'token'], ttl: 60 * 60, // 1 hour }, }); this.registerAction('all', this.all); this.registerAction('get', this.get, { params: { appId: 'string', }, cache: { keys: ['appId'], ttl: 60 * 60, // 1 hour }, }); this.registerAction('create', this.create, { params: { appName: 'string', appDesc: 'string', appIcon: 'string', }, }); this.registerAction('delete', this.delete, { params: { appId: 'string', }, }); this.registerAction('setAppInfo', this.setAppInfo, { params: { appId: 'string', fieldName: 'string', fieldValue: 'string', }, }); this.registerAction('setAppCapability', this.setAppCapability, { params: { appId: 'string', capability: { type: 'array', items: 'string' }, }, }); this.registerAction('setAppOAuthInfo', this.setAppOAuthInfo, { params: { appId: 'string', fieldName: 'string', fieldValue: 'any', }, }); this.registerAction('setAppBotInfo', this.setAppBotInfo, { params: { appId: 'string', fieldName: 'string', fieldValue: 'any', }, }); } /** * 校验Token 返回true/false * * Token 生成方式: appId + appSecret 取md5 */ async authToken( ctx: TcContext<{ appId: string; token: string; capability?: OpenAppDocument['capability']; }> ): Promise { const { appId, token, capability } = ctx.params; const app = await this.adapter.model.findOne({ appId, }); if (!app) { // 没有找到应用 throw new Error('Not found open app:' + appId); } if (Array.isArray(capability)) { for (const item of capability) { if (!app.capability.includes(item)) { throw new Error('Open app not enabled capability:' + item); } } } const appSecret = app.appSecret; if ( token === crypto .createHash('md5') .update(appId + appSecret) .digest('hex') ) { return true; } return false; } /** * 获取用户参与的所有应用 */ async all(ctx: TcContext<{}>) { const apps = await this.adapter.model.find({ owner: ctx.meta.userId, }); return await this.transformDocuments(ctx, {}, apps); } /** * 获取应用信息 */ async get(ctx: TcContext<{ appId: string }>) { const appId = ctx.params.appId; const app = await this.adapter.model.findOne( { appId, }, { appSecret: false, } ); return await this.transformDocuments(ctx, {}, app); } /** * 创建一个第三方应用 */ async create( ctx: TcContext<{ appName: string; appDesc: string; appIcon: string; }> ) { const { appName, appDesc, appIcon } = ctx.params; const userId = ctx.meta.userId; if (!appName) { throw new EntityError(); } const doc = await this.adapter.model.create({ owner: String(userId), appId: `tc_${new Types.ObjectId().toString()}`, appSecret: nanoid(32), appName, appDesc, appIcon, }); return await this.transformDocuments(ctx, {}, doc); } /** * 删除开放平台应用 */ async delete( ctx: TcContext<{ appId: string; }> ) { const { appId } = ctx.params; const userId = ctx.meta.userId; const t = ctx.meta.t; const appInfo: OpenApp = await this.localCall('get', { appId, }); if (String(appInfo.owner) !== userId) { throw new NoPermissionError(t('没有操作权限')); } // 可能会出现ws机器人不会立即中断连接的问题,不重要暂时不处理 await this.adapter.model.remove({ appId, owner: userId, }); return true; } /** * 修改应用信息 */ async setAppInfo( ctx: TcContext<{ appId: string; fieldName: string; fieldValue: string; }> ) { const { appId, fieldName, fieldValue } = ctx.params; const userId = ctx.meta.userId; const t = ctx.meta.t; if (!['appName', 'appDesc', 'appIcon'].includes(fieldName)) { // 只允许修改以上字段 throw new EntityError(`${t('该数据不允许修改')}: ${fieldName}`); } const doc = await this.adapter.model .findOneAndUpdate( { appId, owner: userId, }, { [fieldName]: fieldValue, }, { new: true, } ) .exec(); this.cleanAppInfoCache(appId); return await this.transformDocuments(ctx, {}, doc); } /** * 设置应用开放的能力 */ async setAppCapability( ctx: TcContext<{ appId: string; capability: string[]; }> ) { const { appId, capability } = ctx.params; const { userId } = ctx.meta; const openapp = await this.adapter.model.findAppByIdAndOwner(appId, userId); if (!openapp) { throw new Error('Not found openapp'); } await openapp .updateOne({ capability: filterAvailableAppCapability(_.uniq(capability)), }) .exec(); await this.cleanAppInfoCache(appId); return true; } /** * 设置OAuth的设置信息 */ async setAppOAuthInfo( ctx: TcContext<{ appId: string; fieldName: T; fieldValue: OpenAppOAuth[T]; }> ) { const { appId, fieldName, fieldValue } = ctx.params; const { userId } = ctx.meta; if (!['redirectUrls'].includes(fieldName)) { throw new Error('Not allowed fields'); } if (fieldName === 'redirectUrls') { if (!Array.isArray(fieldValue)) { throw new Error('`redirectUrls` should be an array'); } } await this.adapter.model.findOneAndUpdate( { appId, owner: userId, }, { $set: { [`oauth.${fieldName}`]: fieldValue, }, } ); await this.cleanAppInfoCache(appId); } /** * 设置Bot的设置信息 */ async setAppBotInfo( ctx: TcContext<{ appId: string; fieldName: T; fieldValue: OpenAppBot[T]; }> ) { const { appId, fieldName, fieldValue } = ctx.params; const { userId } = ctx.meta; if (!['callbackUrl'].includes(fieldName)) { throw new Error('Not allowed fields'); } if (fieldName === 'callbackUrl') { if (typeof fieldValue !== 'string') { throw new Error('`callbackUrl` should be a string'); } } await this.adapter.model.findOneAndUpdate( { appId, owner: userId, }, { $set: { [`bot.${fieldName}`]: fieldValue, }, } ); await this.cleanAppInfoCache(appId); } /** * 清理获取开放平台应用的缓存 */ private async cleanAppInfoCache(appId: string) { await this.cleanActionCache('get', [String(appId)]); } } export default OpenAppService;