Commit 842c9c98 authored by nanahira's avatar nanahira

half done

parent 11290e84
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppLogger } from './app.logger';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeormConfig } from './config';
import { BotService } from './bot/bot.service';
import { BotLogger } from './bot/bot.logger';
import { BotController } from './bot/bot.controller';
@Module({
imports: [TypeOrmModule.forRoot(typeormConfig())],
controllers: [AppController, BotController],
providers: [AppService, AppLogger, BotService, BotLogger],
})
export class AppModule {}
const reasonString = `{{#reason}}因为 {{reason}} {{/reason}}`;
export const DefaultRollText = export const DefaultRollText =
'{{name}} {{#reason}}因为 {{reason}} {{/reason}}而投掷了 {{count}} 个 {{size}} 面骰子,投掷出了 {{result}} 点。\n{{formula}}={{result}}'; '{{&name}} {{#reason}}因为 {{&reason}} {{/reason}}而投掷了 {{count}} 个 {{size}} 面骰子,投掷出了 {{result}} 点。\n{{&formula}}={{result}}';
export const TooMuchCountText = export const TooMuchCountText =
'{{name}} {{#reason}}因为 {{reason}} {{/reason}}而投掷了 {{count}} 个 {{size}} 面骰子。\n骰子滚落了一地,找不到了。'; '{{&name}} {{#reason}}因为 {{&reason}} {{/reason}}而投掷了 {{count}} 个 {{size}} 面骰子。\n骰子滚落了一地,找不到了。';
export const TooMuchSizeText = export const TooMuchSizeText =
'{{name}} {{#reason}}因为 {{reason}} {{/reason}}而投掷了 {{count}} 个 {{size}} 面骰子。\n丢了个球。丢个球啊!'; '{{&name}} {{#reason}}因为 {{&reason}} {{/reason}}而投掷了 {{count}} 个 {{size}} 面骰子。\n丢了个球。丢个球啊!';
export const BadUserText = '非法用户。';
export const defaultTemplateMap = new Map<string, string>(); export const defaultTemplateMap = new Map<string, string>();
defaultTemplateMap.set('roll', DefaultRollText); defaultTemplateMap.set('roll', DefaultRollText);
defaultTemplateMap.set('too_much_count', TooMuchCountText); defaultTemplateMap.set('too_much_count', TooMuchCountText);
defaultTemplateMap.set('too_much_size', TooMuchSizeText); defaultTemplateMap.set('too_much_size', TooMuchSizeText);
defaultTemplateMap.set('bad_user', BadUserText);
import { Controller, Get } from '@nestjs/common'; import { Body, Controller, Get, Post, Query, Headers } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { HttpServerService } from './http-server/http-server.service';
@Controller() @Controller('api')
export class AppController { export class AppController {
constructor(private readonly appService: AppService) {} constructor(private readonly httpServerService: HttpServerService) {}
@Get('user')
async getUser(
@Headers('Authorization') token,
@Query('id') id,
@Query('name') name,
) {
this.httpServerService.checkAccess(token);
const data = await this.httpServerService.getUser(id, name);
return { success: true, data };
}
@Get() @Post('user')
getHello(): string { async setUser(
return this.appService.getHello(); @Headers('Authorization') token,
@Body('id') id,
@Body('name') name,
@Body('permissions') permissions,
) {
this.httpServerService.checkAccess(token);
await this.httpServerService.setUser(id, name, parseInt(permissions));
return { success: true };
} }
} }
...@@ -4,10 +4,22 @@ import { AppService } from './app.service'; ...@@ -4,10 +4,22 @@ import { AppService } from './app.service';
import { AppLogger } from './app.logger'; import { AppLogger } from './app.logger';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { typeormConfig } from './config'; import { typeormConfig } from './config';
import { HttpServerService } from './http-server/http-server.service';
import { BotController } from './bot/bot.controller';
import { HttpServerLogger } from './http-server/http-server.logger';
import { BotService } from './bot/bot.service';
import { BotLogger } from './bot/bot.logger';
@Module({ @Module({
imports: [TypeOrmModule.forRoot(typeormConfig())], imports: [TypeOrmModule.forRoot(typeormConfig())],
controllers: [AppController], controllers: [AppController, BotController],
providers: [AppService, AppLogger], providers: [
AppService,
AppLogger,
HttpServerService,
HttpServerLogger,
BotService,
BotLogger,
],
}) })
export class AppModule {} export class AppModule {}
import { Injectable } from '@nestjs/common'; import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { AppLogger } from './app.logger'; import { AppLogger } from './app.logger';
import { Connection } from 'typeorm'; import { Connection } from 'typeorm';
import { InjectConnection } from '@nestjs/typeorm'; import { InjectConnection } from '@nestjs/typeorm';
import { User } from './entities/User';
import { BotService } from './bot/bot.service';
import { UserPermissions } from './constants';
import { diceConfig, DiceConfig } from './config';
import _ from 'lodash';
import { TextTemplate } from './entities/TextTemplate';
import { GroupTemplate } from './entities/GroupTemplate';
import { DefaultTemplate } from './entities/DefaultTemplate';
import { defaultTemplateMap } from './DefaultTemplate';
import Mustache from 'mustache';
export interface RollResult {
name: string;
reason?: string;
count: number;
size: number;
result?: number;
formula?: string;
results?: number[];
}
@Injectable() @Injectable()
export class AppService { export class AppService {
config: DiceConfig;
constructor( constructor(
@InjectConnection('app') @InjectConnection('app')
private db: Connection, private db: Connection,
private log: AppLogger, private log: AppLogger,
private botService: BotService,
) { ) {
this.log.setContext('app'); this.log.setContext('app');
this.config = diceConfig();
}
static rollProcess(rollResult: RollResult) {
rollResult.results = _.range(rollResult.count).map(
() => Math.floor(Math.random() * rollResult.size) + 1,
);
rollResult.formula = rollResult.results.join('+');
rollResult.result = _.sum(rollResult.results);
}
async checkJoinGroup(userId: string) {
const user = await this.botService.findOrCreateUser(userId);
if (user.checkPermissions(UserPermissions.inviteBot)) {
return true;
} else if (user.isBanned) {
return false;
}
return undefined;
}
private logReturn(msg: string) {
this.log.log(`msg: ${msg}`);
return msg;
}
private async renderTemplate(key: string, data: any, groupId?: string) {
let template: TextTemplate;
if (groupId) {
template = await this.db
.getRepository(GroupTemplate)
.createQueryBuilder('template')
.where('template.key = :key', { key })
.andWhere('template.groupId = :groupId', { groupId })
.getOne();
if (template) {
return template.render(data);
}
}
template = await this.db
.getRepository(DefaultTemplate)
.findOne({ where: { key } });
if (template) {
return template.render(data);
}
return Mustache.render(defaultTemplateMap.get(key), data) || key;
}
private async checkUserAndGroup(
userId: string,
username: string,
groupId: string,
) {
const user = await this.botService.findOrCreateUser(userId, username);
if (user.isBanned) {
return false;
}
const group = await this.botService.findOrCreateGroup(groupId);
if (group.isBanned) {
return false;
}
return true;
} }
getHello(): string { async rollDice(
return 'Hello World!'; rollResult: RollResult,
userId: string,
groupId: string,
): Promise<string> {
if (!(await this.checkUserAndGroup(userId, rollResult.name, groupId))) {
return await this.renderTemplate('bad_user', {});
}
if (rollResult.count > this.config.maxDiceCount) {
return await this.renderTemplate('too_much_count', rollResult, groupId);
}
if (rollResult.size > this.config.maxDiceSize) {
return await this.renderTemplate('too_much_size', rollResult, groupId);
}
AppService.rollProcess(rollResult);
return await this.renderTemplate('roll', rollResult, groupId);
} }
} }
import { Controller } from '@nestjs/common'; import { Controller } from '@nestjs/common';
import { AppService } from '../app.service'; import { AppService, RollResult } from '../app.service';
import { BotService } from './bot.service'; import { BotService } from './bot.service';
import { App } from 'koishi'; import { App } from 'koishi';
import * as koishiCommonPlugin from 'koishi-plugin-common'; import * as koishiCommonPlugin from 'koishi-plugin-common';
...@@ -23,12 +23,67 @@ export class BotController { ...@@ -23,12 +23,67 @@ export class BotController {
token: process.env.CQ_TOKEN, token: process.env.CQ_TOKEN,
prefix: process.env.CQ_PREFIX || '.', prefix: process.env.CQ_PREFIX || '.',
}); });
this.bot.plugin(koishiCommonPlugin); this.bot.plugin(koishiCommonPlugin, {
onFriendRequest: true,
onGroupRequest: async (session) => {
const userId = session.userId;
return await this.appService.checkJoinGroup(userId);
},
});
this.loadBotRouters(); this.loadBotRouters();
await this.bot.start(); await this.bot.start();
this.botService.log.log(`Bot started.`); this.botService.log.log(`Bot started.`);
} }
loadBotRouters() { loadBotRouters() {
// all middlewares should be here. // all middlewares should be here.
this.bot.command('echo <msg:text>').action((argv, msg) => {
return msg;
});
const groupCtx = this.bot.group(); // change here
groupCtx.middleware(async (session, next) => {
const content = session.content.trim();
const rollResult: RollResult = {
name: session.username,
count: 1,
size: 6,
};
const rollMatch = content.match(/^\.r(\d*)d(\d+)( .+)?$/);
if (rollMatch) {
rollResult.count = parseInt(rollMatch[1]) || 1;
rollResult.size = parseInt(rollMatch[2]) || 6;
rollResult.reason = rollMatch[3] ? rollMatch[3].trim() : null;
await session.send(
await this.appService.rollDice(
rollResult,
session.userId,
session.groupId,
),
);
}
return next();
});
groupCtx
.command('rolldice', '投掷骰子')
.option('count', '-c <count:posint> 骰子数量', { fallback: 1 })
.option('size', '-s <count:posint> 骰子面数', { fallback: 6 })
.option('reason', '-r <reason:text> 骰子说明')
.alias('rd', 'roll')
.usage('也支持 .rd<> 和 .r<count>d<size> [reason] 这样的传统语法。')
.example('.rolldice -c 2 -s 6 -r "行动判定"')
.action(async (argv, args) => {
const session = argv.session;
const rollResult = {
name: session.username,
count: 1,
size: 6,
reason: null,
...argv.options,
};
return await this.appService.rollDice(
rollResult,
session.userId,
session.groupId,
);
});
} }
} }
...@@ -19,7 +19,12 @@ export class BotService { ...@@ -19,7 +19,12 @@ export class BotService {
const repo = this.db.getRepository(User); const repo = this.db.getRepository(User);
let ent = await repo.findOne({ where: { id } }); let ent = await repo.findOne({ where: { id } });
if (ent) { if (ent) {
return ent; if (!ent.name && name) {
ent.name = name;
return await repo.save(ent);
} else {
return ent;
}
} }
ent = new User(); ent = new User();
ent.id = id; ent.id = id;
......
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { User } from './entities/User'; import { User } from './entities/User';
import { Group } from './entities/Group';
import { DefaultTemplate } from './entities/DefaultTemplate';
import { GroupTemplate } from './entities/GroupTemplate';
export function dbConfig() { export function dbConfig() {
return { return {
...@@ -14,8 +17,19 @@ export function typeormConfig(): TypeOrmModuleOptions { ...@@ -14,8 +17,19 @@ export function typeormConfig(): TypeOrmModuleOptions {
return { return {
name: 'app', name: 'app',
type: 'mysql', type: 'mysql',
entities: [User], // entities here entities: [User, Group, DefaultTemplate, GroupTemplate], // entities here
synchronize: true, synchronize: true,
...dbConfig(), ...dbConfig(),
}; };
} }
export interface DiceConfig {
maxDiceCount: number;
maxDiceSize: number;
}
export function diceConfig(): DiceConfig {
return {
maxDiceCount: parseInt(process.env.DICE_MAX_COUNT) || 1000,
maxDiceSize: parseInt(process.env.DICE_MAX_SIZE) || 1000,
};
}
export const UserPermissions = {
// read
UserRead: 0x1,
GroupRead: 0x2,
TemplateRead: 0x4,
// write
UserWrite: 0x100,
GroupWrite: 0x200,
TemplateWrite: 0x400,
// others
inviteBot: 0x10000,
GroupCheck: 0x20000,
};
...@@ -3,7 +3,7 @@ import { TextTemplate } from './TextTemplate'; ...@@ -3,7 +3,7 @@ import { TextTemplate } from './TextTemplate';
import { Group } from './Group'; import { Group } from './Group';
@Entity() @Entity()
export class GroupTemplate extends TextTemplate { export class DefaultTemplate extends TextTemplate {
@PrimaryColumn('varchar', { length: 32 }) @PrimaryColumn('varchar', { length: 32 })
key: string; key: string;
} }
import { QQIDBase } from './QQIDBase'; import { QQIDBase } from './QQIDBase';
import { Entity, OneToMany } from 'typeorm'; import { Column, Entity, OneToMany } from 'typeorm';
import { GroupTemplate } from './GroupTemplate'; import { GroupTemplate } from './GroupTemplate';
@Entity() @Entity()
......
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import {
Column,
Entity,
Index,
ManyToOne,
PrimaryGeneratedColumn,
RelationId,
} from 'typeorm';
import { TextTemplate } from './TextTemplate'; import { TextTemplate } from './TextTemplate';
import { Group } from './Group'; import { Group } from './Group';
...@@ -6,8 +13,14 @@ import { Group } from './Group'; ...@@ -6,8 +13,14 @@ import { Group } from './Group';
export class GroupTemplate extends TextTemplate { export class GroupTemplate extends TextTemplate {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column('varchar', { length: 32 }) @Column('varchar', { length: 32 })
@Index()
key: string; key: string;
@ManyToOne((type) => Group, (group) => group.templates) @ManyToOne((type) => Group, (group) => group.templates)
group: Group; group: Group;
/*@RelationId((template: GroupTemplate) => template.group)
groupId: string;*/
} }
...@@ -4,4 +4,7 @@ import { Column, Index, PrimaryColumn } from 'typeorm'; ...@@ -4,4 +4,7 @@ import { Column, Index, PrimaryColumn } from 'typeorm';
export class QQIDBase extends TimeBase { export class QQIDBase extends TimeBase {
@PrimaryColumn('varchar', { length: 12 }) @PrimaryColumn('varchar', { length: 12 })
id: string; id: string;
@Column('tinyint', { default: 0 })
isBanned: number;
} }
import { TimeBase } from './TimeBase'; import { TimeBase } from './TimeBase';
import { Column, PrimaryGeneratedColumn } from 'typeorm'; import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { import Mustache from 'mustache';
DefaultRollText,
TooMuchCountText,
TooMuchSizeText,
} from '../DefaultTemplate';
import * as Mustache from 'mustache';
import _ from 'lodash';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Mustache.escape = (text) => {
return text;
};
export class TextTemplate extends TimeBase { export class TextTemplate extends TimeBase {
key: string; // column differs key: string; // column differs
@Column('text') @Column('text')
content: string; content: string;
changeContent(content: string) {
this.content = Buffer.from(content, 'utf-8').toString('base64');
}
render(data: any) { render(data: any) {
return Mustache.render(this.content, data); return Mustache.render(
Buffer.from(this.content, 'base64').toString('utf-8'),
data,
);
} }
} }
...@@ -6,4 +6,11 @@ export class User extends QQIDBase { ...@@ -6,4 +6,11 @@ export class User extends QQIDBase {
@Index() @Index()
@Column('varchar', { length: 32, nullable: true }) @Column('varchar', { length: 32, nullable: true })
name: string; name: string;
@Column('int', { default: 0, unsigned: true }) // default with all read permissions
permissions: number;
checkPermissions(permissionNeeded: number) {
return !!(this.permissions & permissionNeeded);
}
} }
import { Injectable, Scope, Logger } from '@nestjs/common';
@Injectable()
export class HttpServerLogger extends Logger {}
import { Test, TestingModule } from '@nestjs/testing';
import { HttpServerService } from './http-server.service';
describe('HttpServerService', () => {
let service: HttpServerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HttpServerService],
}).compile();
service = module.get<HttpServerService>(HttpServerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { AppLogger } from '../app.logger';
import { BotService } from '../bot/bot.service';
import { User } from '../entities/User';
import { HttpServerLogger } from './http-server.logger';
@Injectable()
export class HttpServerService {
adminToken: string;
constructor(
@InjectConnection('app')
private db: Connection,
private log: HttpServerLogger,
private botService: BotService,
) {
this.log.setContext('http-api');
this.adminToken = process.env.ADMIN_TOKEN;
}
checkAccess(token: string) {
if (this.adminToken && token !== this.adminToken) {
throw new ForbiddenException({ success: false, message: 'Forbidden.' });
}
}
async getUser(id: string, name: string) {
const query = this.db.getRepository(User).createQueryBuilder().where('1');
if (id) {
query.andWhere('id = :id', { id });
}
if (name) {
query.andWhere('name = :name', { name });
}
try {
const user = await query.getMany();
return user;
} catch (e) {
throw new NotFoundException({
success: false,
message: `Database fail: ${e.toString()}`,
});
}
}
async setUser(id: string, name: string, permissions: number) {
try {
const user = await this.botService.findOrCreateUser(id);
if (name) {
user.name = name;
}
if (permissions || permissions === 0) {
if (permissions < 0) {
throw new BadRequestException({
success: false,
message: `Permission cannot be less than zero: ${permissions}`,
});
}
user.permissions = permissions;
}
await this.db.getRepository(User).save(user);
} catch (e) {
throw new NotFoundException({
success: false,
message: `Database fail: ${e.toString()}`,
});
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment