Commit 5977f881 authored by nanahira's avatar nanahira

first

parents
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
.git*
Dockerfile
.dockerignore
/ygopro-database
\ No newline at end of file
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
/ygopro-database
\ No newline at end of file
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
.build-image:
stage: build
script:
- docker build --pull -t $TARGET_IMAGE .
- docker push $TARGET_IMAGE
build-x86:
extends: .build-image
tags:
- docker
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
build-arm:
extends: .build-image
tags:
- docker-arm
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
.deploy:
stage: deploy
tags:
- docker
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
- docker manifest create $TARGET_IMAGE --amend $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86 --amend
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
- docker manifest push $TARGET_IMAGE
deploy_latest:
extends: .deploy
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:latest
only:
- master
deploy_branch:
extends: .deploy
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
/install-npm.sh
.git*
/data
/output
/config.yaml
.idea
.dockerignore
Dockerfile
/src
/ygopro-database
\ No newline at end of file
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
FROM node:lts-bullseye-slim as base
LABEL Author="Nanahira <nanahira@momobako.com>"
RUN apt update && \
apt -y install python3 build-essential git chromium libnss3 libfreetype6-dev libharfbuzz-bin libharfbuzz-dev ca-certificates fonts-freefont-otf fonts-freefont-ttf fonts-noto-cjk fonts-noto-cjk-extra libpq-dev && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/*
RUN useradd -d /app -m -s /bin/bash mycard
USER mycard
WORKDIR /app
COPY --chown=mycard:mycard ./package*.json ./
FROM base as builder
RUN npm ci && npm cache clean --force
COPY . ./
RUN npm run build
FROM base
ENV NODE_ENV production
RUN npm ci && npm cache clean --force
COPY --from=builder /app/dist ./dist
COPY --chown=mycard:mycard ./config.example.yaml ./config.yaml
ENV NODE_PG_FORCE_NATIVE=true
EXPOSE 3000
CMD [ "npm", "run", "start:prod" ]
This diff is collapsed.
# jdaw
决斗暗网微信公众号后端。
## Installation
```bash
$ npm install
```
## Config
Make a copy of `config.example.yaml` to `config.yaml`, or use env.
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## License
AGPLv3
host: '::'
port: 3000
DB_HOST: 'localhost' # 数据库
DB_PORT: 5432
DB_USER: 'mycard'
DB_PASS: 'mycard'
DB_NAME: 'mycard'
WX_ID: '' # 微信公众号,回调路径在 /wechat
WX_SECRET: ''
WX_TOKEN: ''
WX_AESKEY: ''
CDB_REPO: 'https://code.mycard.moe/mycard/ygopro-database.git'
CDB_BRANCH: 'master'
FEEDBACK_NOTIFY_URL: '' # 反馈推送消息。使用 koishi-plugin-apisend 搭建
FEEDBACK_NOTIFY_TOKEN: 'default'
\ No newline at end of file
#!/bin/bash
npm install --save typeorm @nestjs/typeorm pg pg-native nicot
#!/bin/bash
npm install --save \
class-validator \
class-transformer \
@nestjs/swagger \
@nestjs/config \
yaml
npm install --save-dev \
@types/express
npm i --save-exact --save-dev eslint@8.22.0
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/swagger"]
}
}
This diff is collapsed.
{
"name": "jdaw",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@koishijs/plugin-help": "^2.0.2",
"@nestjs/axios": "^1.0.1",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/schedule": "^2.1.0",
"@nestjs/swagger": "^6.1.4",
"@nestjs/typeorm": "^9.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"koishi": "^4.11.1",
"koishi-nestjs": "^6.0.16",
"koishi-plugin-adapter-wechat-official": "^1.0.2",
"koishi-plugin-puppeteer": "^3.3.1",
"koishi-plugin-ygocard": "^10.4.7",
"nicot": "^1.0.17",
"pg": "^8.8.0",
"pg-native": "^3.0.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"simple-git": "^3.15.1",
"typeorm": "^0.3.11",
"yaml": "^2.2.1"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.15",
"@types/jest": "28.1.8",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "8.22.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.8",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.0",
"typescript": "^4.9.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { loadConfig } from './utility/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { KoishiModule, PluginDef } from 'koishi-nestjs';
import { ReplyService } from './reply/reply.service';
import { CommandCountService } from './command-count/command-count.service';
import WechatBot from 'koishi-plugin-adapter-wechat-official';
import * as HelpPlugin from '@koishijs/plugin-help';
import { CommandCount } from './command-count/entities/command-count.entity';
import { CdbLoaderService } from './cdb-loader/cdb-loader.service';
import { ScheduleModule } from '@nestjs/schedule';
import { FeedbackService } from './feedback/feedback.service';
import PuppeteerPlugin from 'koishi-plugin-puppeteer';
import { HttpModule } from '@nestjs/axios';
import { Feedback } from './feedback/entities/feedback.entity';
@Module({
imports: [
ConfigModule.forRoot({
load: [loadConfig],
isGlobal: true,
ignoreEnvVars: true,
ignoreEnvFile: true,
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
type: 'postgres',
entities: [],
autoLoadEntities: true,
synchronize: !config.get('DB_NO_INIT'),
host: config.get('DB_HOST'),
port: parseInt(config.get('DB_PORT')) || 5432,
username: config.get('DB_USER'),
password: config.get('DB_PASS'),
database: config.get('DB_NAME'),
supportBigNumbers: true,
bigNumberStrings: false,
}),
}),
TypeOrmModule.forFeature([CommandCount, Feedback]),
ScheduleModule.forRoot(),
HttpModule,
KoishiModule.registerAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
useWs: true,
usePlugins: [
PluginDef(HelpPlugin),
PluginDef(PuppeteerPlugin),
PluginDef(WechatBot, {
appId: config.get('WX_ID'),
appSecret: config.get('WX_SECRET'),
token: config.get('WX_TOKEN'),
encodingAESKey: config.get('WX_AESKEY'),
path: '/wechat',
menus: [
{
type: 'parent',
name: '常用链接',
children: [
{ type: 'view', name: '玩家社区', url: 'https://ygobbs.com' },
{
type: 'view',
name: '决斗数据库',
url: 'https://mycard.moe/ygopro/arena',
},
{
type: 'click',
name: '游戏下载',
command: 'reply.download',
},
{
type: 'view',
name: '用户中心',
url: 'https://accounts.moecube.com/',
},
{
type: 'view',
name: 'MC周边店',
url: 'https://shop387046095.taobao.com/',
},
],
},
{
type: 'parent',
name: '用户服务',
children: [
{
type: 'click',
name: '服务器列表',
command: 'reply.servers',
},
{
type: 'click',
name: '如何更新',
command: 'reply.update',
},
{
type: 'click',
name: '常见疑难',
command: 'reply.faq',
},
{
type: 'click',
name: '娘化补丁',
command: 'reply.novelai',
},
],
},
{
type: 'parent',
name: '游戏功能',
children: [
{
type: 'click',
name: '随机抽卡',
command: 'card --random',
},
{
type: 'click',
name: '卡查',
command: 'card',
},
{
type: 'click',
name: '有奖调研',
command: 'reply.bonus',
},
{
type: 'click',
name: '意见反馈',
command: 'feedback',
},
{
type: 'click',
name: '周边反馈',
command: 'feedback.shop',
},
],
},
],
}),
],
}),
}),
],
providers: [
ReplyService,
CommandCountService,
CdbLoaderService,
FeedbackService,
],
})
export class AppModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { CdbLoaderService } from './cdb-loader.service';
describe('CdbLoaderService', () => {
let service: CdbLoaderService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CdbLoaderService],
}).compile();
service = module.get<CdbLoaderService>(CdbLoaderService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { PluginDef, UsePlugin } from 'koishi-nestjs';
import { ConfigService } from '@nestjs/config';
import path from 'path';
import fs from 'fs';
import simpleGit, { ResetMode, SimpleGit } from 'simple-git';
import YGOCardPlugin from 'koishi-plugin-ygocard';
import { Cron } from '@nestjs/schedule';
async function exists(path: string) {
try {
const res = await fs.promises.stat(path);
return res.isDirectory();
} catch (e) {
return false;
}
}
@Injectable()
export class CdbLoaderService extends ConsoleLogger {
private repoUrl = this.config.get<string>('CDB_REPO');
private repoPath = path.join(process.cwd(), 'ygopro-database');
private repoBranch = this.config.get<string>('CDB_BRANCH');
private git: SimpleGit;
constructor(private config: ConfigService) {
super('CdbLoaderService');
}
async initRepo() {
if (!(await exists(path.join(this.repoPath, '.git')))) {
this.log(`Cloning repo ${this.repoUrl} to ${this.repoPath}`);
await simpleGit().clone(this.repoUrl, this.repoPath, [
'--branch',
this.repoBranch,
]);
this.log('Repo cloned');
}
this.git = simpleGit(this.repoPath);
}
@UsePlugin()
async load() {
await this.initRepo();
return PluginDef(YGOCardPlugin, {
databasePaths: [path.join(this.repoPath, 'locales', 'zh-CN')],
matchCount: 20,
lang: 'cn',
usePuppeteer: true,
});
}
@Cron('0 0 3 * * *')
async updateRepo() {
this.log('Updating repo');
await this.git.fetch(['origin', this.repoBranch]);
await this.git.reset(ResetMode.HARD, ['origin/' + this.repoBranch]);
this.log('Repo updated');
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { CommandCountService } from './command-count.service';
describe('CommandCountService', () => {
let service: CommandCountService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CommandCountService],
}).compile();
service = module.get<CommandCountService>(CommandCountService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { CrudService } from 'nicot';
import { CommandCount } from './entities/command-count.entity';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { UseEvent } from 'koishi-nestjs';
import { Argv } from 'koishi';
@Injectable()
export class CommandCountService extends CrudService(CommandCount) {
constructor(@InjectRepository(CommandCount) repo: Repository<CommandCount>) {
super(repo);
}
@UseEvent('command/before-execute')
async onCommand(argv: Argv) {
const record = new CommandCount().fromArgv(argv);
await this.create(record);
}
}
import { Entity, Index, SelectQueryBuilder } from 'typeorm';
import { applyQueryProperty, StringColumn } from 'nicot';
import { Argv } from 'koishi';
import { UserBase } from '../../utility/user-base.entity';
@Entity()
export class CommandCount extends UserBase {
@Index()
@StringColumn(64, { required: true, description: '命令' })
command: string;
fromArgv(argv: Argv) {
this.command = argv.command.name;
return this.fromSession(argv.session);
}
applyQuery(qb: SelectQueryBuilder<CommandCount>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryProperty(this, qb, entityName, 'command');
}
}
// Blank for nicot use.
import { Entity, SelectQueryBuilder } from 'typeorm';
import { UserBase } from '../../utility/user-base.entity';
import {
applyQueryProperty,
applyQueryPropertySearch,
StringColumn,
} from 'nicot';
@Entity()
export class Feedback extends UserBase {
@StringColumn(50, { required: true, description: '反馈类型' })
category: string;
@StringColumn(10000, { required: true, description: '反馈内容' })
content: string;
applyQuery(qb: SelectQueryBuilder<Feedback>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryProperty(this, qb, entityName, 'category');
applyQueryPropertySearch(this, qb, entityName, 'content');
}
toNotifyText() {
return `收到 ${this.userId}${this.category} 反馈:\n${this.content}`;
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { FeedbackService } from './feedback.service';
describe('FeedbackService', () => {
let service: FeedbackService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [FeedbackService],
}).compile();
service = module.get<FeedbackService>(FeedbackService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import {
BadRequestException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { CrudService } from 'nicot';
import { Feedback } from './entities/feedback.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { Random, Session } from 'koishi';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';
import { PutSession, UseCommand } from 'koishi-nestjs';
@Injectable()
export class FeedbackService extends CrudService(Feedback) {
private notifyUrl = this.config.get<string>('FEEDBACK_NOTIFY_URL');
private notifyToken = this.config.get<string>('FEEDBACK_NOTIFY_TOKEN');
constructor(
@InjectRepository(Feedback) repo: Repository<Feedback>,
private config: ConfigService,
private http: HttpService,
) {
super(repo);
}
async notifyFeedback(feedback: Feedback) {
if (!this.notifyUrl) {
return;
}
try {
await lastValueFrom(
this.http.post(
this.notifyUrl,
{ content: feedback.toNotifyText() },
{
headers: {
Authorization: `Bearer ${this.notifyToken}`,
},
},
),
);
} catch (e) {
this.log.error(`Failed to notify feedback: ${e.message}`);
}
}
async createFeedback(session: Session, payload: Partial<Feedback>) {
if (!payload.content) {
throw new BadRequestException('');
}
if (payload.content.length > 10000) {
throw new BadRequestException('反馈内容过长。');
}
const feedback = new Feedback().fromSession(session);
Object.assign(feedback, payload);
this.notifyFeedback(feedback).then();
try {
await this.repo.save(feedback);
return await this.repo.count({
where: { category: payload.category === '周边' ? '周边' : Not('周边') },
});
} catch (e) {
const id = Random.id(10);
this.log.error(`Failed to save feedback id: ${e.message}`);
throw new InternalServerErrorException(
'反馈遇到问题,请提供下列编号与管理员取得联系: ' + id,
);
}
}
@UseCommand('feedback', '发送反馈。')
private async feedbackCommand(@PutSession() session: Session) {
await session.send(
'您的反馈种类是?(输入数字回复)\n' +
'1. BUG或举报玩家不当行为\n' +
'2. 游戏功能/改善 \n' +
'3. 游戏体验出问题(例如卡、掉线)\n' +
'4. 其他反馈',
);
let choice = 0;
while (true) {
const input = await session.prompt();
choice = parseInt(input.trim());
if (choice >= 1 && choice <= 4) {
break;
}
await session.send('请选择一个正确的选项(数字1~4)');
}
if (choice == 1) {
return (
'举报BUG需要提交对战录像或截图。\n' +
'举报玩家需提供充足的证据,例如骂人截图。\n' +
'因此请到 ygobbs.com 发帖举报。 感谢!'
);
}
const category = ['游戏功能/改善', '游戏体验问题', '其他'][choice - 2];
await session.send('您启动了反馈系统!您的下一段文字会作为反馈发给我们!');
const content = await session.prompt();
const count = await this.createFeedback(session, { category, content });
return `反馈成功!谢谢您的意见!
这是我们在决斗暗网服务号收到的第 ${count} 个反馈!`;
}
@UseCommand('feedback.shop', '发送周边反馈。')
private async shopFeedbackCommand(@PutSession() session: Session) {
await session.send(
'感谢进入周边反馈功能!周边对MC来说至关重要,MC需要周边来支持服务器维持。\n' +
'如果您有任何对MCPro游戏周边的反馈,请留言于此,感谢!\n' +
'我们会非常关心您对我们周边的意见和看法!\n' +
'为此,希望您可以尽可能清晰表达您的意见,以便我们更好理解您的需求。',
);
const content = await session.prompt();
const count = await this.createFeedback(session, {
category: '周边',
content,
});
return `我们收到了您的周边反馈。
这是我们在决斗暗网服务号收到的第 ${count} 个周边反馈!`;
}
}
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { KoishiWsAdapter } from 'koishi-nestjs';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useWebSocketAdapter(new KoishiWsAdapter(app));
app.enableCors();
app.set('trust proxy', ['172.16.0.0/12', 'loopback']);
const documentConfig = new DocumentBuilder()
.setTitle('app')
.setDescription('The app')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, documentConfig);
SwaggerModule.setup('docs', app, document);
const config = app.get(ConfigService);
await app.listen(
config.get<number>('port') || 3000,
config.get<string>('host') || '::',
);
}
bootstrap();
import { Test, TestingModule } from '@nestjs/testing';
import { ReplyService } from './reply.service';
describe('ReplyService', () => {
let service: ReplyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ReplyService],
}).compile();
service = module.get<ReplyService>(ReplyService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
@Injectable()
export class ReplyService implements OnModuleInit {
replies: { key: string; content: string }[] = [
{
key: 'download',
content:
'"下列都为游戏王YGOPro在各平台的软件,名字不同,但本质一样。MyCard平台会提供YGOPro和YGOPro2的下载与对战。\n' +
'安卓端YGOMobile下载:https://ygomobile.topIOS\n' +
'苹果端KoishiPro iOS下载:https://koishi.pro/download/\n' +
'电脑端(包含Win和Mac)MyCard萌卡平台下载:https://mycard.moe"',
},
{
key: 'servers',
content:
'1. MyCard服。电脑用户请使用MyCard客户端,安卓端用户点击【萌卡平台】,IOS端用户点击【MyCard】\n' +
'2. 233服。主机信息:s1.ygo233.com 端口:233。端口改为23333为先行卡服。\n' +
'3. Koishi服。主机信息:koishi.momobako.com 端口:7210。该服拥有断线重连。\n' +
'4. 轮抽2pick服。轮抽娱乐模式的服务器。 详情 https://ygobbs.com/t/87931\n' +
'5. 分级服。靠特殊卡表限制主流进场的服务器。详情 https://ygobbs.com/t/364147\n' +
'6. 游戏王Rush Duel服。遵循游戏王Rush Duel规则的服务器。详情 https://ygobbs.com/t/200563\n' +
'7. 高速决斗服。人物技能+游戏王5DS高速决斗模式。 详情 https://ygobbs.com/t/166971\n' +
'8. 诡异空间服。平衡的独立卡池,特殊规则服。详情 https://ygobbs.com/t/303983',
},
{
key: 'update',
content:
'安卓端YGOMobile:登录后会提示更新,手动更新请访问https://ygomobile.top\n' +
'苹果端KoishiPro2 iOS:用电脑使用爱思助手自签更新,教程请见 https://koishi.pro/download/\n' +
'电脑端MyCard会自动更新。若自动更新没触发,请在游戏大厅下方点击【校验完整性】更新。',
},
{
key: 'faq',
content:
'如果遇到了游戏安装、出BUG、下载等问题,请访问 https://ygobbs.com/t/108594\n' +
'如果遇到了游戏处理,规则类问题,请查看游戏王OCG规则修订书 https://ocg-rule.readthedocs.io/zh_CN/latest/ \n' +
'\n' +
'其他寻求帮助的方式:\n' +
'1. 在 ygobbs.com 搜索符合您问题描述的帖子,95%以上问题都有其他用户发帖询问过。\n' +
'2. 若没有,您可以在MC玩家社区 ygobbs.com 带上恰当的描述发帖寻求帮助,管理员会在36小时内回帖。\n' +
'3. 若问题很复杂,请加萌卡测试与 bug 反馈QQ群 553241437 询问。',
},
{
key: 'novelai',
content:
'由于AI绘图的普及,MC与各爱好贡献者们合作成立了YGOPro卡图娘化项目。\n' +
'现在为广大玩家们提供娘化补丁下载。\n' +
'详情与下载方式:https://ygobbs.com/t/396440',
},
{
key: 'bonus',
content:
'欢迎访问MC有奖调研功能。目前暂时无进行中的调研。我们一般每1-2个月都会公布有奖调研,欢迎下次再查询。',
},
];
constructor(@InjectContext() private ctx: Context) {}
onModuleInit() {
const base = this.ctx.command('reply', '默认回复。');
for (const reply of this.replies) {
base.subcommand(`.${reply.key}`).action(() => reply.content);
}
}
}
import yaml from 'yaml';
import * as fs from 'fs';
const defaultConfig = {
host: '::',
port: 3000,
DB_HOST: 'localhost',
DB_PORT: 5432,
DB_USER: 'mycard',
DB_PASS: 'mycard',
DB_NAME: 'mycard',
WX_ID: '',
WX_SECRET: '',
WX_TOKEN: '',
WX_AESKEY: '',
CDB_REPO: 'https://code.mycard.moe/mycard/ygopro-database.git',
CDB_BRANCH: 'master',
FEEDBACK_NOTIFY_URL: '',
FEEDBACK_NOTIFY_TOKEN: 'default',
};
export type Config = typeof defaultConfig;
export async function loadConfig(): Promise<Config> {
let readConfig: Partial<Config> = {};
try {
const configText = await fs.promises.readFile('./config.yaml', 'utf-8');
readConfig = yaml.parse(configText);
} catch (e) {
console.error(`Failed to read config: ${e.toString()}`);
}
return {
...defaultConfig,
...readConfig,
...process.env,
};
}
import { applyQueryProperty, DateColumn, IdBase, StringColumn } from 'nicot';
import { Index, SelectQueryBuilder } from 'typeorm';
import { Session } from 'koishi';
export class UserBase extends IdBase() {
@Index()
@StringColumn(128, { required: true, description: '用户' })
userId: string;
@Index()
@DateColumn({ required: true, description: '时间' })
date: Date;
fromSession(session: Session) {
this.userId = session.userId;
this.date = new Date();
return this;
}
applyQuery(qb: SelectQueryBuilder<UserBase>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryProperty(this, qb, entityName, 'userId', 'date');
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
/* it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
}); */
});
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"compileOnSave": true,
"allowJs": true
}
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