You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/server/services/openapi/app.service.ts

384 lines
7.9 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<OpenAppDocument, OpenAppModel> {}
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<boolean> {
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<T extends keyof OpenAppOAuth>(
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<T extends keyof OpenAppBot>(
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;