|
|
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;
|