main
落雨楓 3 years ago
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

2289
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -17,6 +17,9 @@
"dependencies": { "dependencies": {
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"decoders": "^1.25.3", "decoders": "^1.25.3",
"handlebars": "^4.7.7",
"koa": "^2.13.4",
"koa-router": "^10.1.1",
"lua-runner": "^2.0.3", "lua-runner": "^2.0.3",
"micromatch": "^4.0.4", "micromatch": "^4.0.4",
"node-schedule": "^2.0.0", "node-schedule": "^2.0.0",
@ -28,6 +31,8 @@
"yaml": "^1.8.3" "yaml": "^1.8.3"
}, },
"devDependencies": { "devDependencies": {
"@types/koa": "^2.13.4",
"@types/koa-router": "^7.4.4",
"@types/micromatch": "^4.0.2", "@types/micromatch": "^4.0.2",
"@types/node": "^17.0.8", "@types/node": "^17.0.8",
"@types/request-promise": "^4.1.48", "@types/request-promise": "^4.1.48",

@ -6,6 +6,7 @@ import { BaseProvider, MultipleMessage } from './base/provider/BaseProvider';
import { ChannelManager } from './ChannelManager'; import { ChannelManager } from './ChannelManager';
import { ChannelConfig, Config } from './Config'; import { ChannelConfig, Config } from './Config';
import { ProviderManager } from './ProviderManager'; import { ProviderManager } from './ProviderManager';
import { RestfulApiManager } from './RestfulApiManager';
import { RobotManager } from './RobotManager'; import { RobotManager } from './RobotManager';
import { Service, ServiceManager } from './ServiceManager'; import { Service, ServiceManager } from './ServiceManager';
import { SubscribeManager, Target } from './SubscribeManager'; import { SubscribeManager, Target } from './SubscribeManager';
@ -19,6 +20,7 @@ export default class App {
public service!: ServiceManager; public service!: ServiceManager;
public subscribe!: SubscribeManager; public subscribe!: SubscribeManager;
public channel!: ChannelManager; public channel!: ChannelManager;
public restfulApi!: RestfulApiManager;
constructor(configFile: string) { constructor(configFile: string) {
this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' })); this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' }));
@ -71,6 +73,11 @@ export default class App {
await this.channel.initialize(); await this.channel.initialize();
} }
async initRestfulApiManager() {
this.restfulApi = new RestfulApiManager(this, this.config.http_api);
await this.restfulApi.initialize();
}
/** /**
* *
* @param serviceName * @param serviceName

@ -7,6 +7,7 @@ export type Config = {
debug: boolean; debug: boolean;
robot: { [key: string]: RobotConfig }; robot: { [key: string]: RobotConfig };
service: { [key: string]: ServiceConfig }; service: { [key: string]: ServiceConfig };
http_api: RestfulApiConfig;
}; };
export type RobotConfig = { export type RobotConfig = {
@ -14,6 +15,12 @@ export type RobotConfig = {
baseId: string; baseId: string;
}; };
export type RestfulApiConfig = {
host: string;
port: number;
tokens: { [type: string]: any };
};
export type ServiceConfig = { [name: string]: any }; export type ServiceConfig = { [name: string]: any };
export type ChannelConfig = any; export type ChannelConfig = any;

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

@ -1,3 +1,5 @@
import Handlebars from "handlebars";
import App from "../App"; import App from "../App";
import { MultipleMessage } from "../base/provider/BaseProvider"; import { MultipleMessage } from "../base/provider/BaseProvider";
import { ConfigCheckError } from "../error/ConfigCheckError"; import { ConfigCheckError } from "../error/ConfigCheckError";
@ -6,12 +8,11 @@ import { ConfigCheckError } from "../error/ConfigCheckError";
const { Utils } = require('../Utils'); const { Utils } = require('../Utils');
export type TemplateFilterConfig = { [key: string]: string }; export type TemplateFilterConfig = { [key: string]: string };
export type TemplateRenderFunction = (data: any) => string;
export class TemplateFilter { export class TemplateFilter {
private app: App; private app: App;
private config: TemplateFilterConfig; private config: TemplateFilterConfig;
private renderFunctionList: { [target: string]: TemplateRenderFunction }; private renderFunctionList: { [target: string]: HandlebarsTemplateDelegate<any> };
constructor(app: App, config: TemplateFilterConfig) { constructor(app: App, config: TemplateFilterConfig) {
this.app = app; this.app = app;
@ -28,7 +29,7 @@ export class TemplateFilter {
key = "base"; key = "base";
} }
if (typeof template === "string") { if (typeof template === "string") {
this.renderFunctionList[key] = this.buildTemplateCallback(template); this.renderFunctionList[key] = Handlebars.compile(template);
} }
} }
} }
@ -45,57 +46,6 @@ export class TemplateFilter {
} }
} }
/**
*
*/
parseTemplate(template: string): string {
template = template.replace(/\\/g, "\\\\").replace(/\r\n/g, "\n").replace(/\n/g, "\\n").replace(/'/g, "\\'");
template = template.replace(/\{\{(.*?)\}\}/g, (str, token) => {
if (token) {
return "' + (" + (token.replace(/\\'/g, "'")) + ") + '";
} else {
return str;
}
});
if(template.indexOf("' + (") == 0){ //开头是{{
template = template.substr(4);
} else {
template = "'" + template;
}
if(template.lastIndexOf(") + '") == template.length - 5){ //结尾是}}
template = template.substr(0, template.length - 4);
} else {
template = template + "'";
}
return template;
}
/**
* callback
* @param {string} template
* @returns {Function}
*/
buildTemplateCallback(template: string): TemplateRenderFunction {
const renderTpl = eval('(function(){ return ' + this.parseTemplate(template) + '; })')
return (data: any): string => {
let overridedKeys: string[] = [];
for (let key in data) {
if (!(key in global)) {
overridedKeys.push(key);
(global as any)[key] = data[key];
}
}
let result = renderTpl();
for (let key of overridedKeys) {
delete (global as any)[key];
}
return result;
};
}
async parse(data: any): Promise<MultipleMessage | null> { async parse(data: any): Promise<MultipleMessage | null> {
let result: MultipleMessage = {}; let result: MultipleMessage = {};
for (let target in this.renderFunctionList) { for (let target in this.renderFunctionList) {

@ -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'])); // 权限检测
}

@ -31,9 +31,9 @@ export default class QQRobot implements Robot {
/** /**
* *
* @param {int|int[]} user - QQ * @param user - QQ
* @param {string} message - * @param message -
* @returns {Promise<void>} * @returns
*/ */
async sendToUser(user: number|number[], message: string) { async sendToUser(user: number|number[], message: string) {
if(Array.isArray(user)){ //发送给多个用户的处理 if(Array.isArray(user)){ //发送给多个用户的处理

Loading…
Cancel
Save