Update
parent
87a3012897
commit
f2258cca35
@ -1,9 +1,18 @@
|
|||||||
robot:
|
robot:
|
||||||
host: "192.168.0.14:5700"
|
qq:
|
||||||
pusher:
|
type: "qq"
|
||||||
app_id: ""
|
user: 123456789
|
||||||
key: ""
|
host: "127.0.0.1:8094"
|
||||||
secret: ""
|
service:
|
||||||
cluster: "ap1"
|
pusher:
|
||||||
|
app_id: ""
|
||||||
|
key: ""
|
||||||
|
secret: ""
|
||||||
|
cluster: "ap1"
|
||||||
|
http_api:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
port: 8902
|
||||||
|
tokens:
|
||||||
|
test: abc
|
||||||
channel_config_path: "./channels"
|
channel_config_path: "./channels"
|
||||||
debug: false
|
debug: false
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,138 @@
|
|||||||
|
import App from "./App";
|
||||||
|
import Koa from 'koa';
|
||||||
|
import { RestfulApiConfig } from "./Config";
|
||||||
|
import Router from "koa-router";
|
||||||
|
import { makeRoutes } from "./restful/routes";
|
||||||
|
|
||||||
|
export interface RestfulContext {
|
||||||
|
app: App
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FullRestfulContext = RestfulContext & Koa.BaseContext;
|
||||||
|
|
||||||
|
export class RestfulApiManager {
|
||||||
|
private app: App;
|
||||||
|
private config: RestfulApiConfig;
|
||||||
|
|
||||||
|
private koa: Koa;
|
||||||
|
private router: Router<any, RestfulContext>;
|
||||||
|
|
||||||
|
constructor(app: App, config: Pick<RestfulApiConfig, any>) {
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 8082,
|
||||||
|
...config
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
this.koa = new Koa();
|
||||||
|
this.router = new Router<any, RestfulContext>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialize(): Promise<void> {
|
||||||
|
makeRoutes(this.router, this, this.app);
|
||||||
|
|
||||||
|
this.koa.use(this.globalMiddleware);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.koa.use(this.router.routes());
|
||||||
|
this.koa.listen(this.config.port, () => {
|
||||||
|
console.log(`Restful API 启动于:${this.config.port}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局变量中间件
|
||||||
|
* @param ctx
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
public globalMiddleware = async (ctx: FullRestfulContext, next: () => Promise<any>) => {
|
||||||
|
ctx.app = this.app; // 注入全局app
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造token检测中间件
|
||||||
|
* @param tokenType
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public tokenMiddlewareFactory(tokenType: string | string[]) {
|
||||||
|
if (typeof tokenType === "string") {
|
||||||
|
tokenType = [tokenType];
|
||||||
|
}
|
||||||
|
tokenType.push('root'); // 永远允许Root Token
|
||||||
|
|
||||||
|
let allowedTokens = [];
|
||||||
|
tokenType.forEach((tokenName) => {
|
||||||
|
let token = this.config.tokens[tokenName];
|
||||||
|
if (token) {
|
||||||
|
allowedTokens.push(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (allowedTokens.length === 0) { // 无Token校验
|
||||||
|
return async (ctx: FullRestfulContext, next: Koa.Next) => {
|
||||||
|
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return async (ctx: FullRestfulContext, next: Koa.Next) => {
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测请求token
|
||||||
|
* @param token
|
||||||
|
* @param tokenType 允许的Token类型
|
||||||
|
*/
|
||||||
|
public verifyToken(token: string, tokenType: string | string[]) {
|
||||||
|
if (typeof tokenType === "string") {
|
||||||
|
tokenType = [tokenType];
|
||||||
|
}
|
||||||
|
tokenType.push('root'); // 永远允许Root Token
|
||||||
|
|
||||||
|
let allowedTokens = [];
|
||||||
|
tokenType.forEach((tokenName) => {
|
||||||
|
let token = this.config.tokens[tokenName];
|
||||||
|
if (token) {
|
||||||
|
allowedTokens.push(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyTokenByTokenList(ctx: FullRestfulContext, tokenList: string[]): boolean
|
||||||
|
public verifyTokenByTokenList(token: string, tokenList: string[]): boolean
|
||||||
|
public verifyTokenByTokenList(ctx: FullRestfulContext | string, tokenList: string[]): boolean {
|
||||||
|
let verifyType: "token" | "token-hash" = "token";
|
||||||
|
let token: string | undefined;
|
||||||
|
if (typeof ctx === "string") {
|
||||||
|
token = ctx;
|
||||||
|
} else {
|
||||||
|
let authHeader = ctx.headers.authorization;
|
||||||
|
if (!authHeader) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let [authMode, authInfo] = authHeader.split(" ");
|
||||||
|
switch (authMode.toLowerCase()) {
|
||||||
|
case "token":
|
||||||
|
token = authInfo;
|
||||||
|
break;
|
||||||
|
case "token-hash":
|
||||||
|
verifyType = "token-hash";
|
||||||
|
token = authInfo;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifyType === "token") {
|
||||||
|
return tokenList.includes(token);
|
||||||
|
} else if (verifyType === "token-hash") {
|
||||||
|
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
var Channel = require('./Channel');
|
||||||
|
|
||||||
|
class BroadcastChannel extends Channel {
|
||||||
|
constructor(app){
|
||||||
|
super(app, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(){
|
||||||
|
this.channelName = 'broadcast';
|
||||||
|
this.baseTemplate = '{{data.message}}';
|
||||||
|
this.parseTemplate = this.buildTemplateCallback(this.baseTemplate);
|
||||||
|
|
||||||
|
this.initPush();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(data){
|
||||||
|
try {
|
||||||
|
let finalMessage = this.parseMessage(data);
|
||||||
|
if(data.target.group){
|
||||||
|
this.app.robot.sendToGroup(data.target.group, finalMessage);
|
||||||
|
}
|
||||||
|
if(data.target.user){
|
||||||
|
this.app.robot.sendToUser(data.target.group, finalMessage);
|
||||||
|
}
|
||||||
|
} catch(ex){
|
||||||
|
console.log(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BroadcastChannel;
|
@ -0,0 +1,176 @@
|
|||||||
|
var utils = require('../Utils');
|
||||||
|
|
||||||
|
class Channel {
|
||||||
|
constructor(app, config){
|
||||||
|
this.app = app;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static checkConfig(data){
|
||||||
|
if(typeof data !== 'object') return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(data){
|
||||||
|
this.config = data;
|
||||||
|
|
||||||
|
this.channelName = data.channel;
|
||||||
|
this.baseTemplates = data.templates;
|
||||||
|
this.prepareFileList = data.files;
|
||||||
|
this.receiver = data.receiver;
|
||||||
|
|
||||||
|
this.initTemplates();
|
||||||
|
this.initReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(){
|
||||||
|
this.channel = this.app.pusher.subscribe(this.channelName);
|
||||||
|
this.channel.bind_global(this.onPush.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind(){
|
||||||
|
this.channel.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
initTemplates(){
|
||||||
|
this.template = {};
|
||||||
|
for(let key in this.baseTemplates){
|
||||||
|
let one = this.baseTemplates[key];
|
||||||
|
this.template[key] = this.buildTemplateCallback(one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initReceiver(){
|
||||||
|
this.getReceiver = this.buildGetReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
initPrepareFileList(){
|
||||||
|
this.prepareFileCallback = {};
|
||||||
|
for(let key in this.prepareFileList){
|
||||||
|
let one = this.prepareFileList[key];
|
||||||
|
this.prepareFileCallback[key] = this.buildPrepareFileCallback(one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destory(){
|
||||||
|
this.app.pusher.unsubscribe(this.channelName);
|
||||||
|
this.channel.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTemplate(template){
|
||||||
|
template = template.replace(/\\/g, "\\\\").replace(/\r\n/g, "\n").replace(/\n/g, "\\n").replace(/'/g, "\\'");
|
||||||
|
if(template.indexOf('{{') == 0){ //开头是{{
|
||||||
|
template = template.substr(2);
|
||||||
|
} else {
|
||||||
|
template = "'" + template;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(template.indexOf('}}') == template.length - 2){ //结尾是}}
|
||||||
|
template = template.substr(0, template.length - 2);
|
||||||
|
} else {
|
||||||
|
template = template + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
template = template.replace(/\{\{/g, "' + ").replace(/\}\}/g, " + '");
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTemplateCallback(template){
|
||||||
|
return eval('(function(data){ return ' + this.parseTemplate(template) + '; })').bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPrepareFileCallback(cond){
|
||||||
|
return eval('(function(data){ return ' + cond + '; })').bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseMessage(data){
|
||||||
|
try {
|
||||||
|
return this.parseTemplate(data);
|
||||||
|
} catch(ex){
|
||||||
|
return this.baseTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataVal(data, key, defaultVal = undefined){
|
||||||
|
let keyList = key.split('.');
|
||||||
|
let finded = data;
|
||||||
|
for(let key of keyList){
|
||||||
|
if(typeof finded === 'object' && key in finded){
|
||||||
|
finded = finded[key];
|
||||||
|
} else {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finded;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildGetReceiver(){
|
||||||
|
if(typeof this.receiver === 'string'){
|
||||||
|
return (data) => {
|
||||||
|
return this.getDataVal(data, this.receiver);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let resultFunc = {};
|
||||||
|
for(let type of ['group', 'user']){
|
||||||
|
if(type in this.receiver){
|
||||||
|
if(typeof this.receiver[type] === 'string'){
|
||||||
|
resultFunc[type] = (data) => {
|
||||||
|
return this.getDataVal(data, this.receiver[type]);
|
||||||
|
};
|
||||||
|
} else if(Array.isArray(this.receiver[type])) {
|
||||||
|
let staticTargets = [];
|
||||||
|
let paramTargets = [];
|
||||||
|
for(let val of this.receiver[type]){
|
||||||
|
if(typeof val === "number"){
|
||||||
|
staticTargets.push(val);
|
||||||
|
} else {
|
||||||
|
paramTargets.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultFunc[type] = (data) => {
|
||||||
|
let targets = staticTargets.slice();
|
||||||
|
for(let key of paramTargets){
|
||||||
|
targets.push(this.getDataVal(data, key))
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (data) => {
|
||||||
|
let ret = {};
|
||||||
|
if('group' in resultFunc){
|
||||||
|
ret.group = resultFunc.group();
|
||||||
|
}
|
||||||
|
if('user' in resultFunc){
|
||||||
|
ret.user = resultFunc.user();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPush(type, data){
|
||||||
|
try {
|
||||||
|
if(type.indexOf('pusher:') == 0 || !this.template[type]){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalMessage = this.template[type](data);
|
||||||
|
let receiver = this.getReceiver();
|
||||||
|
if(typeof receiver === 'object'){
|
||||||
|
if('group' in receiver){
|
||||||
|
this.app.robot.sendToGroup(receiver.group, finalMessage);
|
||||||
|
}
|
||||||
|
if('user' in receiver){
|
||||||
|
this.app.robot.sendToUser(receiver.user, finalMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(ex){
|
||||||
|
console.log(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Channel;
|
@ -0,0 +1,8 @@
|
|||||||
|
import koa from "koa";
|
||||||
|
import { FullRestfulContext } from "../../RestfulApiManager";
|
||||||
|
|
||||||
|
export class IndexController {
|
||||||
|
public static async index(ctx: FullRestfulContext, next: koa.Next) {
|
||||||
|
ctx.body = "Isekai Feedbot endpoint.";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import Koa from 'koa';
|
||||||
|
import { FullRestfulContext } from '../../RestfulApiManager';
|
||||||
|
|
||||||
|
export class SubscribeController {
|
||||||
|
static async getTargetList(ctx: FullRestfulContext, next: Koa.Next) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTargetSubscribeList(ctx: FullRestfulContext, next: Koa.Next) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import Router from "koa-router";
|
||||||
|
import App from "../App";
|
||||||
|
import { RestfulApiManager, RestfulContext } from "../RestfulApiManager";
|
||||||
|
import { SubscribeController } from "./controller/SubscribeController";
|
||||||
|
|
||||||
|
export function makeRoutes(routes: Router<any, RestfulContext>, manager: RestfulApiManager, app: App) {
|
||||||
|
// 订阅管理
|
||||||
|
routes.all('/subscribe', manager.tokenMiddlewareFactory(['subscribe'])); // 权限检测
|
||||||
|
routes.get('/subscribe', SubscribeController.getTargetList); // 获取订阅目标列表
|
||||||
|
routes.get('/subscribe/:robot/:targetType/:targetId', SubscribeController.getTargetSubscribeList); // 获取订阅列表
|
||||||
|
|
||||||
|
// 推送消息
|
||||||
|
routes.all('/push', manager.tokenMiddlewareFactory(['push'])); // 权限检测
|
||||||
|
}
|
Loading…
Reference in New Issue