Commit a470da9c authored by nanahira's avatar nanahira

finish

parent 3cc4eb3f
......@@ -17,17 +17,20 @@
"@aws-sdk/util-format-url": "^3.38.0",
"class-transformer": "^0.4.0",
"koishi-utils-schemagen": "^1.1.9",
"moment": "^2.29.1",
"source-map-support": "^0.5.20"
},
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"axios": "^0.24.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"koishi": "^4.0.0-alpha.12",
"prettier": "^2.4.1",
"proxy-agent": "^5.0.0",
"raw-loader": "^4.0.2",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4",
......@@ -2140,6 +2143,15 @@
"websocket-stream": "^5.5.2"
}
},
"node_modules/aws-crt/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
......@@ -2156,11 +2168,12 @@
"peer": true
},
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.0"
"follow-redirects": "^1.14.4"
}
},
"node_modules/balanced-match": {
......@@ -4771,6 +4784,14 @@
"reflect-metadata": "^0.1.13"
}
},
"node_modules/koishi/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
......@@ -5044,6 +5065,14 @@
"node": ">=10"
}
},
"node_modules/moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"engines": {
"node": "*"
}
},
"node_modules/mqtt": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.8.tgz",
......@@ -8892,6 +8921,17 @@
"mqtt": "^4.2.8",
"tar": "^6.1.11",
"websocket-stream": "^5.5.2"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"peer": true,
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"aws-sign2": {
......@@ -8907,11 +8947,12 @@
"peer": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.0"
"follow-redirects": "^1.14.4"
}
},
"balanced-match": {
......@@ -10870,6 +10911,16 @@
"parseurl": "^1.3.3",
"path-to-regexp": "^6.2.0",
"proxy-agent": "^5.0.0"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"koishi-utils-schemagen": {
......@@ -11099,6 +11150,11 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"peer": true
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"mqtt": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.8.tgz",
......
......@@ -11,13 +11,11 @@ import { Context, Schema } from 'koishi';
import {
GetObjectCommand,
ListObjectsCommand,
S3,
S3Client,
S3ClientConfig,
} from '@aws-sdk/client-s3';
import { Tournament } from './def/challonge';
import { Type } from 'class-transformer';
import { SRVProRoomInfo } from './def/srvpro';
import { Selection } from './def/selection';
const credentialsSchema: Schema<Credentials> = Schema.object(
{
......@@ -106,6 +104,16 @@ export class TournamentConfig {
})
challongeCacheTTL: number;
@DefineSchema({
desc: 'Challonge URL 前缀',
default: 'https://api.challonge.com/v1/tournaments/',
})
challongeUrlPrefix: string;
getChallongeUrl() {
return `${this.challongeUrlPrefix}${this.challongeTournamentId}`;
}
async fetchRooms(ctx: Context) {
return ctx.http.get<Partial<SRVProRoomInfo>>(
`${this.endpoint}/api/getrooms`,
......@@ -115,9 +123,25 @@ export class TournamentConfig {
},
);
}
async kickRoom(ctx: Context, search: string) {
return ctx.http.get(`${this.endpoint}/api/message`, {
username: this.username,
pass: this.password,
kick: search,
});
}
}
export class YGOTournamentPluginConfig {
@DefineSchema({
type: 'object',
allowUnknown: true,
desc: '裁判接口作用域',
required: true,
})
judgeSelection: Selection;
@DefineSchema()
tournament: TournamentConfig;
......@@ -134,6 +158,7 @@ export class YGOTournamentPluginConfig {
}
export interface YGOTournamentPluginConfigLike {
judgeSelection: Selection;
tournament?: Partial<TournamentConfig>;
deckFetch?: Partial<DeckFetchConfig>;
}
......@@ -35,7 +35,7 @@ export class MatchWrapper {
isClean() {
return (
this.match.state !== 'complete' &&
(this.match.scores_csv?.length && 0) < 3
(this.match.scores_csv?.length || 0) < 3
);
}
getPlayerIds() {
......@@ -91,6 +91,10 @@ export class ParticipantWrapper {
}
return { userId: matching[1], userName: matching[2] };
}
getDisplayName() {
return `${this.participant.name}(${this.participant.id})`;
}
}
export class Tournament {
......
import { MaybeArray } from 'koishi';
const selectors = [
'user',
'guild',
'channel',
'self',
'private',
'platform',
] as const;
type SelectorType = typeof selectors[number];
type SelectorValue = boolean | MaybeArray<string | number>;
type BaseSelection = { [K in SelectorType as `$${K}`]?: SelectorValue };
export interface Selection extends BaseSelection {
$and?: Selection[];
$or?: Selection[];
$not?: Selection;
}
import 'source-map-support/register';
import { Context, Schema } from 'koishi';
import { Context, Schema, Quester } from 'koishi';
import {
YGOTournamentPluginConfig,
YGOTournamentPluginConfigLike,
......@@ -10,12 +10,21 @@ import { Tournament, TournamentWrapper } from './def/challonge';
import { S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from './presign';
import { Room, SRVProRoomInfo } from './def/srvpro';
import moment from 'moment';
import axios, { AxiosRequestConfig } from 'axios';
import ProxyAgent from 'proxy-agent';
export interface FromAndToTimeUnix {
fromTime: number;
toTime: number;
}
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cache {
interface Tables {
challongeData: any;
lateDeclarationTime: FromAndToTimeUnix;
}
}
}
......@@ -55,7 +64,7 @@ export class YGOTournamentPlugin {
return cached;
}
const plain = await this.ctx.http.get(
`https://api.challonge.com/v1/tournaments/${this.config.tournament.challongeTournamentId}.json`,
`${this.config.tournament.getChallongeUrl()}.json`,
{
api_key: this.config.tournament.challongeKey,
include_participants: 1,
......@@ -69,7 +78,6 @@ export class YGOTournamentPlugin {
private async onUserQuit(userId: string) {
const tournament = await this.fetchChallongeData();
const { matches } = tournament.tournament;
const participant = tournament.getParticipantFromId(userId);
if (!participant) {
return '未找到您的参赛信息。';
......@@ -77,7 +85,7 @@ export class YGOTournamentPlugin {
// const participantName = participant.getNameMatching().userName;
this.ctx
.logger('challonge')
.info(`Player ${participant.participant.name} requested for quit.`);
.info(`Player ${participant.getDisplayName()} requested for quit.`);
const currentMatch = tournament.getCurrentMatchFromId(
participant.participant.id,
);
......@@ -86,19 +94,21 @@ export class YGOTournamentPlugin {
participant.participant.id === currentMatch.match.player2_id
? '2--1'
: '-1-2';
const winner_id = currentMatch
.getPlayerIds()
.find((id) => id !== participant.participant.id);
this.ctx
.logger('challonge')
.info(
`Cleaning remaining match for player ${participant.participant.name}.`,
`Cleaning remaining match for player ${participant.getDisplayName()}.`,
);
await this.ctx.http.put(
`https://api.challonge.com/v1/tournaments/${this.config.tournament.challongeTournamentId}/matches/${currentMatch.match.id}.json`,
`${this.config.tournament.getChallongeUrl()}/matches/${
currentMatch.match.id
}.json`,
{
api_key: this.config.tournament.challongeKey,
scores_csv,
winner_id: currentMatch
.getPlayerIds()
.find((id) => id !== participant.participant.id),
match: { scores_csv, winner_id },
},
);
this.ctx
......@@ -107,23 +117,33 @@ export class YGOTournamentPlugin {
`Cleaned remaining match for player ${participant.participant.name}.`,
);
await this.ctx.http.delete(
`https://api.challonge.com/v1/tournaments/${
this.config.tournament.challongeTournamentId
}/participants/${
`${this.config.tournament.getChallongeUrl()}/participants/${
participant.participant.id
}.json?api_key=${encodeURIComponent(
this.config.tournament.challongeKey,
)}`,
}.json`,
{
api_key: this.config.tournament.challongeKey,
},
);
this.ctx
.logger('challonge')
.info(`Player ${participant.participant.name} quitted.`);
.info(`Player ${participant.getDisplayName()} quited.`);
await this.deleteChallongeCache();
return '退赛成功。';
}
}
private async onUserDeclareLate(userId: string) {
const timeUnix = await this.ctx.cache.get('lateDeclarationTime', 'current');
this.ctx.logger('test').warn(JSON.stringify(timeUnix));
if (
!timeUnix ||
!moment().isBetween(
moment.unix(timeUnix.fromTime),
moment.unix(timeUnix.toTime),
)
) {
return '现在不是允许迟到杀的时间。';
}
const tournament = await this.fetchChallongeData();
const participant = tournament.getParticipantFromId(userId);
if (!participant) {
......@@ -157,27 +177,39 @@ export class YGOTournamentPlugin {
participant.participant.id === currentMatch.match.player1_id
? '2--1'
: '-1-2';
const rivalId = currentMatch
.getPlayerIds()
.find((id) => id !== participant.participant.id);
const rival = tournament.tournament.participants.find(
(p) => p.participant.id === rivalId,
);
this.ctx
.logger('challonge')
.info(
`Player ${participant.participant.name} requested for a late declaration.`,
`Player ${participant.getDisplayName()} requested for a late declaration against ${rival?.getDisplayName()}.`,
);
const result = await this.ctx.http.put(
`https://api.challonge.com/v1/tournaments/${this.config.tournament.challongeTournamentId}/matches/${currentMatch.match.id}.json`,
`${this.config.tournament.getChallongeUrl()}/matches/${
currentMatch.match.id
}.json`,
{
api_key: this.config.tournament.challongeKey,
scores_csv,
winner_id: participant.participant.id,
match: { scores_csv, winner_id: participant.participant.id },
},
);
this.ctx
.logger('challonge')
.info(
`Result of late declaration for player ${
participant.participant.name
}: ${JSON.stringify(result)}`,
`Player ${participant.getDisplayName()} declared late against ${rival?.getDisplayName()}.`,
);
await this.deleteChallongeCache();
await Promise.all([
this.deleteChallongeCache(),
...roomsWithPlayer.map((r) =>
this.config.tournament.kickRoom(this.ctx, r.roomid),
),
]);
return '迟到杀成功。';
}
......@@ -220,12 +252,116 @@ export class YGOTournamentPlugin {
});
}
// WTF fix for missing ctx.http.delete
private workaroundQuester(quester: Quester) {
const config = quester.config;
const options: AxiosRequestConfig = {
timeout: config.timeout,
headers: config.headers,
};
if (config.proxyAgent) {
const agent = new ProxyAgent(config.proxyAgent);
options.httpAgent = agent;
options.httpsAgent = agent;
}
quester.delete = async (url, params, headers) => {
const { data } = await axios.delete(url, {
...options,
...config,
params,
headers: {
...options.headers,
...headers,
},
});
return data;
};
}
initializeTournament() {
if (!this.config.isTournamentEnabled()) {
return;
}
this.ctx.cache.table('challongeData', {
maxAge: this.config.tournament.challongeCacheTTL,
this.workaroundQuester(this.ctx.http);
this.ctx.cache.table('lateDeclarationTime', { maxAge: 3600 * 1000 });
this.ctx
.command('tournament/late', '迟到杀')
.shortcut('迟到杀')
.usage('迟到杀需要选手在房间内,且只有1人才可以迟到杀。')
.action(async ({ session }) => {
try {
return await this.onUserDeclareLate(session.userId);
} catch (e) {
this.ctx
.logger('challonge')
.error(`Failed to quit user ${session.userId}: ${e.toString()}`);
return '迟到杀出现了一些问题,请与技术人员联系。';
}
});
this.ctx
.command('tournament/quit', '退赛')
.shortcut('退赛')
.usage('退赛需要慎重,会取消后续的比赛资格。')
.action(async ({ session }) => {
await session.send('确定要退赛吗?输入 yes 以确认。');
const reply = await session.prompt();
if (reply !== 'yes') {
return '退赛被取消,接下来要继续努力哦。';
}
try {
return await this.onUserQuit(session.userId);
} catch (e) {
this.ctx
.logger('challonge')
.error(`Failed to quit user ${session.userId}: ${e.toString()}`);
return '退赛出现了一些问题,请与技术人员联系。';
}
});
const judgeCommand = this.ctx
.select(this.config.judgeSelection)
.command('tournament/judge', '裁判操作')
.usage('需要有裁判权限才能执行这些操作。');
judgeCommand
.subcommand('.enablelate', '开启迟到杀')
.usage('允许选手使用指令来进行迟到杀。')
.option('delay', '-d <time:posint> 在指定分钟后开启迟到杀。', {
fallback: 0,
})
.option('duration', '-t <time:posint> 迟到杀允许的分钟数。', {
fallback: 30,
})
.action(async ({ session, options }) => {
const fromTime = moment().add(options.delay, 'minutes');
const toTime = fromTime.clone().add(options.duration, 'minutes');
await this.ctx.cache.set(
'lateDeclarationTime',
'current',
{
fromTime: fromTime.unix(),
toTime: toTime.unix(),
},
(options.delay + options.duration) * 60000,
);
return `设置成功。将允许在 ${fromTime.format(
'HH:mm:ss',
)}${toTime.format('HH:mm:ss')} 期间允许迟到杀。`;
});
judgeCommand
.subcommand('.disablelate', '关闭迟到杀')
.usage('不再允许选手进行迟到杀操作。')
.action(async () => {
await this.ctx.cache.del('lateDeclarationTime', 'current');
return '设置成功。选手不再允许进行迟到杀操作。';
});
judgeCommand
.subcommand('.refresh', '清理 Challonge 缓存')
.action(async () => {
await this.ctx.cache.del(
'challongeData',
this.config.tournament.challongeTournamentId,
);
return '清理缓存成功。';
});
}
......
......@@ -29,6 +29,8 @@ module.exports = {
},
externals: {
koishi: 'koishi',
'proxy-agent': 'proxy-agent',
axios: 'axios',
...(packAll
? {}
: {
......@@ -37,8 +39,10 @@ module.exports = {
'@aws-sdk/protocol-http': '@aws-sdk/protocol-http',
'@aws-sdk/smithy-client': '@aws-sdk/smithy-client',
'@aws-sdk/util-format-url': '@aws-sdk/util-format-url',
'@aws-sdk/types': '@aws-sdk/types',
'koishi-utils-schemagen': 'koishi-utils-schemagen',
'class-transformer': 'class-transformer',
moment: 'moment',
}),
},
};
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