initial commit
Some checks failed
release-tag / release-image (push) Failing after 1m14s

This commit is contained in:
konchin
2024-10-11 19:49:58 +08:00
commit 2d7361e937
38 changed files with 4029 additions and 0 deletions

76
commands/contests/ac.ts Normal file
View File

@@ -0,0 +1,76 @@
import {
CommandInteraction,
CommandInteractionOptionResolver,
TextChannel,
SlashCommandBuilder,
SlashCommandStringOption,
PermissionFlagsBits,
} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {getContest, updateProblem} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
type CIOR = CommandInteractionOptionResolver;
class Ac extends Command{
get name(){return "ac";}
get description(){return "Add timestamp when you get ac";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
try{
// parse
const user = interaction.user;
let problemId = (interaction.options as CIOR).getString('problem');
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const contest = await getContest(channelId);
const time = new Date();
if(problemId === null){
logger.error('option error');
return;
}
problemId = problemId.toUpperCase();
if(contest === null){
await interaction.reply({
content: `The contest in this channel didn't start!`
});
logger.error(`Contest ${contestName} didn't start`);
return;
}
// update
await updateProblem(user, problemId, channelId, 'ac', time.valueOf());
await interaction.reply({
content: `Problem ${problemId} has ac-ed.`
});
logger.log(`Problem ${problemId} has ac-ed.`);
}catch(error: unknown){
logger.error(`Error occur while ac-ing problem`);
return;
}
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('problem')
.setDescription('The id of the problem.')
.setMinLength(1).setMaxLength(1)
.setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.SendMessages);
}
};
export const command = new Ac();

View File

@@ -0,0 +1,40 @@
import {
CommandInteraction,
TextChannel,
} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {getContest, clearContest} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
class Clear extends Command{
get name(){return "clear";}
get description(){return "clear contest.";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const contest = await getContest(contestName);
if(contest === null){
await interaction.reply({
content: `The contest in this channel didn't start!`
});
logger.error(`Contest ${contestName} didn't start`);
return;
}
await clearContest(channelId);
await interaction.reply({content: `Clear ${contestName} complete.`});
logger.log(`Command: clear ${contestName}/${channelId}`);
}
};
export const command = new Clear();

87
commands/contests/code.ts Normal file
View File

@@ -0,0 +1,87 @@
import {
CommandInteraction,
CommandInteractionOptionResolver,
TextChannel,
SlashCommandBuilder,
SlashCommandStringOption,
SlashCommandUserOption,
SlashCommandIntegerOption,
PermissionFlagsBits,
} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {getContest, putCode} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
type CIOR = CommandInteractionOptionResolver;
class Code extends Command{
get name(){return "code";}
get description(){return "Add timestamp to code a problem";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
try{
let problemId = (interaction.options as CIOR).getString('problem');
const estimate = (interaction.options as CIOR).getInteger('estimate') ?? -1;
const specifiedUser = (interaction.options as CIOR).getUser('user');
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const contest = await getContest(channelId);
const time = new Date();
if(problemId === null){
logger.error('option error');
return;
}
problemId = problemId.toUpperCase();
if(contest === null){
await interaction.reply({
content: `The contest in this channel didn't start!`
});
logger.error(`Contest ${contestName} didn't start`);
return;
}
const user = specifiedUser ?? interaction.user;
const sessionId: string = await putCode(
user, problemId, channelId, time.valueOf(), estimate*1000*60 + contest.startTime
);
const content = `Problem ${problemId} code by ${user.username}, estimate in ${estimate} min. (session id: ${sessionId})`;
await interaction.reply({content: logger.log(content)});
}catch(error: unknown){
logger.error(`Error occur while coding problem`);
return;
}
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('problem')
.setDescription('The id of the problem.')
.setMinLength(1).setMaxLength(1)
.setRequired(true))
.addIntegerOption((option: SlashCommandIntegerOption) =>
option
.setName('estimate')
.setDescription('The estimate coding time of the problem.')
.setRequired(true))
.addUserOption((option: SlashCommandUserOption) =>
option
.setName('user')
.setDescription('Who read the problem, default is yourself'))
.setDefaultMemberPermissions(PermissionFlagsBits.SendMessages);
}
};
export const command = new Code();

View File

@@ -0,0 +1,90 @@
import {
CommandInteraction,
CommandInteractionOptionResolver,
TextChannel,
SlashCommandBuilder,
SlashCommandStringOption,
SlashCommandIntegerOption,
} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {getContest, updateSession} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
type CIOR = CommandInteractionOptionResolver;
class Code extends Command{
get name(){return "modify";}
get description(){return "Modify a session timestamp.";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
try{
// parse
const sessionId = (interaction.options as CIOR).getString('session');
const key = (interaction.options as CIOR).getString('key');
const channelId = interaction.channel.id;
const contest = await getContest(channelId);
let time = (interaction.options as CIOR).getInteger('time');
if(sessionId === null || key === null || contest === null){
logger.error(`Can't parse parameters in "modify"`);
throw Error();
}
if(time === null)
time = new Date().getTime();
else
time = time*1000*60 + contest.startTime;
// update
if (! await updateSession(sessionId, key, time)){
await interaction.reply({
content: `Session ${sessionId} doesn't exist`
});
return;
}
await interaction.reply({
content: `The value of key ${key} in session ${sessionId} has been updated.`
});
logger.log(`The value of key ${key} in session ${sessionId} has been updated.`);
}catch(error: unknown){
logger.error(`Error occur while coding problem`);
return;
}
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('session')
.setDescription('The id of the session.')
.setRequired(true)
)
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('key')
.setDescription('Which value should be modified.')
.setRequired(true)
.addChoices(
{name: 'start', value: 'start'},
{name: 'end', value: 'end'},
)
)
.addIntegerOption((option: SlashCommandIntegerOption) =>
option
.setName('time')
.setDescription('The time which being updated, default is now.')
)
}
};
export const command = new Code();

89
commands/contests/read.ts Normal file
View File

@@ -0,0 +1,89 @@
import {
CommandInteraction,
CommandInteractionOptionResolver,
TextChannel,
SlashCommandBuilder,
SlashCommandStringOption,
SlashCommandUserOption,
SlashCommandIntegerOption,
} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {getContest, putRead} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
type CIOR = CommandInteractionOptionResolver;
class Read extends Command{
get name(){return "read";}
get description(){return "Add timestamp to read a problem";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
try{
let problemId = (interaction.options as CIOR).getString('problem');
const estimate = (interaction.options as CIOR).getInteger('estimate') ?? -1;
const specifiedUser = (interaction.options as CIOR).getUser('user');
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const contest = await getContest(channelId);
const time = new Date();
if(problemId === null){
logger.error('Options error in "read".');
return;
}
const user = specifiedUser ?? interaction.user;
problemId = problemId.toUpperCase();
if(contest === null){
await interaction.reply({
content: `The contest in this channel didn't start!`
});
logger.error(`Contest ${contestName} didn't start`);
return;
}
const sessionId: string = await putRead(
user, problemId, channelId, time.valueOf(), estimate*1000*60 + contest.startTime
);
let content = `Problem ${problemId} read by ${user.username}`;
if(estimate !== -1) content += `, estimate in ${estimate} min`;
content += `. (session id: ${sessionId})`;
await interaction.reply({content: logger.log(content)});
}catch(error: unknown){
logger.error(`Error occur while reading problem`);
return;
}
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('problem')
.setDescription('The id of the problem.')
.setMinLength(1).setMaxLength(1)
.setRequired(true)
)
.addIntegerOption((option: SlashCommandIntegerOption) =>
option
.setName('estimate')
.setDescription('The estimate coding time of the problem.')
)
.addUserOption((option: SlashCommandUserOption) =>
option
.setName('user')
.setDescription('Who read the problem, default is yourself')
)
}
};
export const command = new Read();

View File

@@ -0,0 +1,21 @@
/* DEPRECATED
import {CommandInteraction} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {clearContests, clearProblems, clearSessions} from '../../functions/database';
class Reset extends Command{
get name(){return "reset";}
get description(){return "Reset database.";}
async execute(interaction: CommandInteraction): Promise<void>{
await clearContests();
await clearProblems();
await clearSessions();
await interaction.reply({content: `Reset complete.`});
logger.log(`Command: reset database.`);
}
};
export const command = new Reset();
*/

124
commands/contests/result.ts Normal file
View File

@@ -0,0 +1,124 @@
import {
CommandInteraction,
TextChannel,
SlashCommandBuilder,
SlashCommandBooleanOption,
CommandInteractionOptionResolver,
} from 'discord.js';
import {writeFileSync} from 'fs';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {config} from '../../config';
import {getContest, getProblem, getSession} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
type CIOR = CommandInteractionOptionResolver;
class Result extends Command{
get name(){return "result";}
get description(){return "See the result of a contest.";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const markdown = (interaction.options as CIOR).getBoolean('markdown') ?? false;
const contest = await getContest(channelId);
if(contest === null){
await interaction.reply({
content: `The contest in this channel didn't start!`
});
logger.error(`Contest ${contestName} didn't start`);
return;
}
let content: string = '';
content += `# ${contestName}\n\n`
contest.problems.sort(
(a, b) => {
if(a.problemId > b.problemId)
return 1;
if(a.problemId < b.problemId)
return -1;
return 0;
}
);
const getTime = (time: number) => {
if(time === -1) return -1;
return Math.floor((time-contest.startTime)/(1000*60));
};
for(const problem of contest.problems){
const p = await getProblem(problem._id);
if(p === null) continue;
content += `## p${p.problemId}\n`;
if(getTime(p.ac) === -1)
content += "*Problem unsolved!*\n";
else
content += `**AC** at ${getTime(p.ac)} min\n`;
if(p.wa.length !== 0){
content += `**WA** at`;
let isFirst: boolean = true;
for(const wa of p.wa.sort()){
if(isFirst) isFirst = false;
else content += ',';
content += ` ${getTime(wa)}`;
}
content += ' min\n';
}
if(p.read.length === 0)
content += "*Problem unread!*\n";
else{
content += `**Read**:\n`;
for(const session of p.read){
const s = await getSession(session._id);
if(s === null) continue;
content += `- \`${s.name}\` at ${getTime(s.start)} min`;
if(getTime(s.end) !== -1)
content += ` (estimate: ${getTime(s.end)} min)`;
content += '\n';
}
}
if(p.code.length === 0)
content += "*No one have attempted*\n";
else{
content += `**Code**:\n`;
for(const session of p.code){
const s = await getSession(session._id);
if(s === null) continue;
content += `- \`${s.name}\` at ${getTime(s.start)} min`
content += ` (estimate: ${getTime(s.end)} min)\n`;
}
}
content += '\n';
}
if(markdown){
const file = `${config.mdBaseDir}/${channelId}.md`;
logger.log(`Output to ${file}`);
await writeFileSync(file, content, {encoding: 'utf8'});
await interaction.reply({files:[file]});
}else
await interaction.reply({content: content});
logger.log(`Command: result of ${contestName}/${channelId}`);
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addBooleanOption((option: SlashCommandBooleanOption) =>
option
.setName('markdown')
.setDescription('If true then upload markdown file instead.')
)
}
};
export const command = new Result();

View File

@@ -0,0 +1,37 @@
import {CommandInteraction, TextChannel} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {createContest} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
class Virtual extends Command{
get name(){return "virtual";}
get description(){return "Start a virtual contest.";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
try{
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const startTime = new Date();
await createContest(channelId, startTime.valueOf());
await interaction.reply({
content: `Contest archive ${contestName} created on ${startTime.toString()}.`
});
logger.log(`Contest archive ${contestName} created.`);
}catch(error: unknown){
logger.error(`Error occur while initializing contest`);
return;
}
}
};
export const command = new Virtual();

76
commands/contests/wa.ts Normal file
View File

@@ -0,0 +1,76 @@
import {
CommandInteraction,
CommandInteractionOptionResolver,
TextChannel,
SlashCommandBuilder,
SlashCommandStringOption,
PermissionFlagsBits,
} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger'
import {getContest, updateProblem} from '../../functions/database';
function isTextChannel(data: unknown): data is TextChannel{
return (data as TextChannel).name !== undefined;
}
type CIOR = CommandInteractionOptionResolver;
class Wa extends Command{
get name(){return "wa";}
get description(){return "Add timestamp when you get wa";}
async execute(interaction: CommandInteraction): Promise<void>{
if(!isTextChannel(interaction.channel)){
await interaction.reply({
content: `Channel name doesn't exist!`
});
logger.error(`Channel name doesn't exist`);
return;
}
try{
// parse
const user = interaction.user;
let problemId = (interaction.options as CIOR).getString('problem');
const contestName = interaction.channel.name;
const channelId = interaction.channel.id;
const contest = await getContest(channelId);
const time = new Date();
if(problemId === null){
logger.error('option error');
return;
}
problemId = problemId.toUpperCase();
if(contest === null){
await interaction.reply({
content: `The contest in this channel didn't start!`
});
logger.error(`Contest ${contestName} didn't start`);
return;
}
// update
await updateProblem(user, problemId, channelId, 'wa', time.valueOf());
await interaction.reply({
content: `Problem ${problemId} has wa-ed.`
});
logger.log(`Problem ${problemId} has wa-ed.`);
}catch(error: unknown){
logger.error(`Error occur while wa-ing problem`);
return;
}
}
override build(): SlashCommandBuilder |
Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">{
return new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.addStringOption((option: SlashCommandStringOption) =>
option
.setName('problem')
.setDescription('The id of the problem.')
.setMinLength(1).setMaxLength(1)
.setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.SendMessages);
}
};
export const command = new Wa();

26
commands/tests/test.ts Normal file
View File

@@ -0,0 +1,26 @@
/* DEPRECATED
import {CommandInteraction} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {getAllContest, getAllProblem, getAllSession} from '../../functions/database';
class Test extends Command{
get name(){return "test";}
get description(){return "Testing some features.";}
async execute(interaction: CommandInteraction): Promise<void>{
try{
console.log(await getAllContest());
console.log(await getAllProblem());
console.log(await getAllSession());
interaction.reply({
content: `test ok`
});
}catch(error: unknown){
logger.error(`error get all contests`);
}
}
};
export const command = new Test();
*/

19
commands/utils/help.ts Normal file
View File

@@ -0,0 +1,19 @@
import {CommandInteraction} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
import {config} from '../../config';
class Help extends Command{
get name(){return "help";}
get description(){return "Help messages.";}
async execute(interaction: CommandInteraction): Promise<void>{
await interaction.reply({content:
"# [Help](https://hello.konchin.com)\n"+
`Read the documentation at [this link](${config.helpUrl})\n`
});
logger.log(`Command: help`);
}
};
export const command = new Help();

20
commands/utils/ping.ts Normal file
View File

@@ -0,0 +1,20 @@
import {CommandInteraction} from 'discord.js';
import {Command} from '../../classes/command';
import {logger} from '../../logger';
class Ping extends Command{
get name(){return "ping";}
get description(){return "Reply with the RTT of this bot.";}
async execute(interaction: CommandInteraction): Promise<void>{
const sent = await interaction.reply({
content: "Pinging...",
ephemeral: true,
fetchReply: true,
});
const rtt = sent.createdTimestamp - interaction.createdTimestamp;
await interaction.editReply(`Roundtrip latency: ${rtt}ms`);
logger.log(`ping with rtt: ${rtt}`);
}
};
export const command = new Ping();