This commit is contained in:
49
.gitea/workflows/image.yaml
Normal file
49
.gitea/workflows/image.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
name: release-tag
|
||||
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
release-image:
|
||||
runs-on: imgbuilder
|
||||
env:
|
||||
CONTAINER_REGISTRY: gitea.konchin.com
|
||||
GITEA_TAG: latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
config-inline: |
|
||||
debug = true
|
||||
[registry."${{ env.CONTAINER_REGISTRY }}"]
|
||||
ca = ["rootca.pem"]
|
||||
|
||||
- name: Login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.CONTAINER_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Setup env
|
||||
run: |
|
||||
echo "GITEA_REPO=${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]' >> $GITHUB_ENV
|
||||
echo "GITEA_REF_NAME=${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.CONTAINER_REGISTRY }}/${{ env.GITEA_REPO }}:${{ env.GITEA_REF_NAME }}
|
||||
${{ env.CONTAINER_REGISTRY }}/${{ env.GITEA_REPO }}:${{ env.GITEA_TAG }}
|
||||
134
.gitignore
vendored
Normal file
134
.gitignore
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
files/md/*
|
||||
!files/md/example.md
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:22.4-slim AS build
|
||||
WORKDIR /work
|
||||
COPY ./package.json ./package-lock.json .
|
||||
RUN npm ci && npm run build
|
||||
|
||||
FROM gcr.io/distroless/nodejs22-debian12
|
||||
WORKDIR /work
|
||||
COPY --from=build /work /work
|
||||
COPY . /work
|
||||
|
||||
CMD ["index.js"]
|
||||
16
README.md
Normal file
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Mafuyu-Kirisu
|
||||
|
||||
A discord bot for ICPC training record.
|
||||
|
||||
## Installation
|
||||
|
||||
### Manually
|
||||
|
||||
1. Install npm, node, typescript, mongodb
|
||||
2. Start/enable mongodb
|
||||
3. Clone this repo
|
||||
4. Run `npm install` in the directory
|
||||
5. Move `mafuyu-kirisu.service` to `~/.config/systemd/user/`
|
||||
6. Modify the environment variables specified in the .service file
|
||||
7. Run `systemctl --user --daemon-reload`
|
||||
8. Run `systemctl --user enable --now mafuyu-kirisu`
|
||||
26
classes/command.ts
Normal file
26
classes/command.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
SlashCommandBuilder,
|
||||
CommandInteraction,
|
||||
PermissionFlagsBits,
|
||||
} from 'discord.js';
|
||||
|
||||
export interface Component{
|
||||
execute(interaction: unknown): Promise<void>;
|
||||
build(): unknown;
|
||||
};
|
||||
|
||||
export abstract class Command implements Component{
|
||||
abstract get name(): string;
|
||||
abstract get description(): string;
|
||||
abstract execute(interaction: CommandInteraction): Promise<void>;
|
||||
build():
|
||||
SlashCommandBuilder | Omit<
|
||||
SlashCommandBuilder,
|
||||
"addSubcommand" | "addSubcommandGroup"
|
||||
>{
|
||||
return new SlashCommandBuilder()
|
||||
.setName(this.name)
|
||||
.setDescription(this.description)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.SendMessages);
|
||||
}
|
||||
};
|
||||
22
classes/extendedclient.ts
Normal file
22
classes/extendedclient.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
Client,
|
||||
Collection,
|
||||
ClientOptions,
|
||||
} from 'discord.js';
|
||||
|
||||
import {Command} from './command'
|
||||
|
||||
export function isExtendedClient(client: Client): client is ExtendedClient{
|
||||
return (client as ExtendedClient).commands !== undefined;
|
||||
}
|
||||
|
||||
export class ExtendedClient extends Client{
|
||||
public commands: Collection<string, Command>;
|
||||
constructor(
|
||||
opts: ClientOptions,
|
||||
cmds = new Collection<string, Command>()
|
||||
){
|
||||
super(opts);
|
||||
this.commands = cmds;
|
||||
}
|
||||
};
|
||||
76
commands/contests/ac.ts
Normal file
76
commands/contests/ac.ts
Normal 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();
|
||||
40
commands/contests/clear.ts
Normal file
40
commands/contests/clear.ts
Normal 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
87
commands/contests/code.ts
Normal 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();
|
||||
90
commands/contests/modify.ts
Normal file
90
commands/contests/modify.ts
Normal 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
89
commands/contests/read.ts
Normal 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();
|
||||
21
commands/contests/reset.ts
Normal file
21
commands/contests/reset.ts
Normal 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
124
commands/contests/result.ts
Normal 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();
|
||||
37
commands/contests/virtual.ts
Normal file
37
commands/contests/virtual.ts
Normal 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
76
commands/contests/wa.ts
Normal 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
26
commands/tests/test.ts
Normal 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
19
commands/utils/help.ts
Normal 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
20
commands/utils/ping.ts
Normal 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();
|
||||
22
config.ts
Normal file
22
config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
token: process.env.DC_TOKEN!,
|
||||
clientId: process.env.DC_CLIENTID!,
|
||||
adminId: process.env.ADMIN_ID!,
|
||||
helpUrl: 'https://md.konchin.com/s/6zIFRGY_E',
|
||||
mdBaseDir: './files/md',
|
||||
nickname: '桐須 真冬',
|
||||
logger: {
|
||||
logFile: 'test.log',
|
||||
},
|
||||
mongodb: {
|
||||
host: process.env.MONGODB_HOST ?? '127.0.0.1',
|
||||
port: process.env.MONGODB_PORT ?? 27017,
|
||||
user: process.env.MONGODB_USER ?? 'mafuyu',
|
||||
pass: process.env.MONGODB_PASS ?? 'mafuyu',
|
||||
db: process.env.MONGODB_DB ?? 'mafuyu',
|
||||
},
|
||||
};
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
app:
|
||||
context: .
|
||||
working_dir: /home/node/app
|
||||
environment:
|
||||
- ADMIN_ID=${ADMIN_ID}
|
||||
- DC_CLIENTID=${DC_CLIENTID}
|
||||
- DC_TOKEN=${DC_TOKEN}
|
||||
- MONGODB_HOST=${MONGODB_HOST}
|
||||
- MONGODB_USER=${MONGODB_USER}
|
||||
- MONGODB_PASS=${MONGODB_PASS}
|
||||
restart: always
|
||||
10
eslint.config.mjs
Normal file
10
eslint.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
|
||||
export default [
|
||||
{languageOptions: { globals: globals.browser }},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
];
|
||||
39
events/commands.ts
Normal file
39
events/commands.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {Interaction} from 'discord.js';
|
||||
|
||||
import {isExtendedClient} from '../classes/extendedclient';
|
||||
import {logger} from '../logger';
|
||||
|
||||
export async function handleCommands(interaction: Interaction): Promise<void>{
|
||||
if(!interaction.isChatInputCommand()) return;
|
||||
if(!isExtendedClient(interaction.client)){
|
||||
logger.error(`Type Error in function "handleCommands"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = interaction.client.commands.get(interaction.commandName);
|
||||
if(!command){
|
||||
logger.error(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
if('execute' in command)
|
||||
await command.execute(interaction);
|
||||
else{
|
||||
logger.error('The command is missing a require "execute" function');
|
||||
return;
|
||||
}
|
||||
}catch(error){
|
||||
logger.error(`Execution error in function "handleCommands"`);
|
||||
if(interaction.replied || interaction.deferred)
|
||||
await interaction.followUp({
|
||||
content: 'There was an error while executing this command!',
|
||||
ephemeral: true
|
||||
});
|
||||
else
|
||||
await interaction.reply({
|
||||
content: 'There was an error while executing this command!',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
0
files/md/example.md
Normal file
0
files/md/example.md
Normal file
214
functions/database.ts
Normal file
214
functions/database.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {Types} from 'mongoose';
|
||||
import {User} from 'discord.js';
|
||||
import {logger} from '../logger';
|
||||
|
||||
import {Session, sessionModel} from '../models/sessions';
|
||||
import {Problem, problemModel} from '../models/problems';
|
||||
import {Contest, contestModel} from '../models/contests';
|
||||
|
||||
export async function clearContests(): Promise<void>{
|
||||
await contestModel.deleteMany({});
|
||||
}
|
||||
|
||||
export async function clearProblems(): Promise<void>{
|
||||
await problemModel.deleteMany({});
|
||||
}
|
||||
|
||||
export async function clearSessions(): Promise<void>{
|
||||
await sessionModel.deleteMany({});
|
||||
}
|
||||
|
||||
export async function clearContest(channelId: string): Promise<void>{
|
||||
await contestModel.deleteOne({channelId: channelId});
|
||||
}
|
||||
|
||||
export async function createContest(channelId: string, startTime: number): Promise<boolean>{
|
||||
const contest = await contestModel.findOne({channelId: channelId});
|
||||
if(contest){
|
||||
contest.startTime = startTime;
|
||||
await contest.save();
|
||||
return false;
|
||||
}
|
||||
const newContest = new contestModel({
|
||||
channelId: channelId,
|
||||
startTime: startTime,
|
||||
problems: [],
|
||||
});
|
||||
await newContest.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateProblem(
|
||||
user: User, problemId: string, channelId: string,
|
||||
action: string, time: number): Promise<void>{
|
||||
// find problem
|
||||
let contest = await contestModel.findOne(
|
||||
{channelId: channelId},
|
||||
{problems: 1, _id: {"$toString": "$_id"}},
|
||||
).populate({path: 'problems', select: 'problemId _id'});
|
||||
if(contest === null){
|
||||
logger.error(`contest didn't start`);
|
||||
throw Error();
|
||||
}
|
||||
if(contest.problems.every(p => p.problemId != problemId)){
|
||||
const newProblem = await new problemModel(
|
||||
{problemId: problemId}, {}, {new: true}
|
||||
).save();
|
||||
await contestModel.updateOne(
|
||||
{channelId: channelId},
|
||||
{$push: {problems: newProblem}},
|
||||
{new: true}
|
||||
);
|
||||
}
|
||||
contest = await contestModel.findOne(
|
||||
{channelId: channelId},
|
||||
).populate({path: 'problems', select: 'problemId _id'});
|
||||
const problem = (contest as Contest).problems.find(
|
||||
p => p.problemId == problemId);
|
||||
// update
|
||||
switch(action){
|
||||
case 'wa':{
|
||||
await problemModel.findByIdAndUpdate(
|
||||
(problem as Problem)._id,
|
||||
{$push: {wa: time}},
|
||||
{new: true}
|
||||
);
|
||||
}break;
|
||||
case 'ac':{
|
||||
await problemModel.findByIdAndUpdate(
|
||||
(problem as Problem)._id,
|
||||
{$set: {ac: time}},
|
||||
{new: true}
|
||||
);
|
||||
}break;
|
||||
}
|
||||
}
|
||||
|
||||
export async function putCode(
|
||||
user: User, problemId: string, channelId: string,
|
||||
time: number, estimate: number): Promise<string>{
|
||||
// find problem
|
||||
let contest = await contestModel.findOne(
|
||||
{channelId: channelId},
|
||||
{problems: 1, _id: {"$toString": "$_id"}},
|
||||
).populate({path: 'problems', select: 'problemId _id'});
|
||||
if(contest === null){
|
||||
logger.error(`contest didn't start`);
|
||||
throw Error();
|
||||
}
|
||||
if(contest.problems.every(p => p.problemId != problemId)){
|
||||
const newProblem = await new problemModel(
|
||||
{problemId: problemId}, {}, {new: true}
|
||||
).save();
|
||||
await contestModel.updateOne(
|
||||
{channelId: channelId},
|
||||
{$push: {problems: newProblem}},
|
||||
{new: true}
|
||||
);
|
||||
}
|
||||
contest = await contestModel.findOne(
|
||||
{channelId: channelId},
|
||||
).populate({path: 'problems', select: 'problemId _id'});
|
||||
const problem = (contest as Contest).problems.find(
|
||||
p => p.problemId == problemId);
|
||||
// update
|
||||
const newSession = await new sessionModel(
|
||||
{name: user.username, start: time, end: estimate},
|
||||
{}, {new: true, upsert: true}
|
||||
).save();
|
||||
await problemModel.findByIdAndUpdate(
|
||||
(problem as Problem)._id,
|
||||
{$push: {code: newSession}},
|
||||
{new: true}
|
||||
);
|
||||
return newSession._id.toString();
|
||||
}
|
||||
|
||||
export async function putRead(
|
||||
user: User, problemId: string, channelId: string,
|
||||
time: number, estimate: number): Promise<string>{
|
||||
// find problem
|
||||
let contest = await contestModel.findOne(
|
||||
{channelId: channelId},
|
||||
{problems: 1, _id: {"$toString": "$_id"}},
|
||||
).populate({path: 'problems', select: 'problemId _id'});
|
||||
if(contest === null){
|
||||
logger.error(`contest didn't start`);
|
||||
throw Error();
|
||||
}
|
||||
if(contest.problems.every(p => p.problemId != problemId)){
|
||||
const newProblem = await new problemModel(
|
||||
{problemId: problemId}, {}, {new: true}
|
||||
).save();
|
||||
await contestModel.updateOne(
|
||||
{channelId: channelId},
|
||||
{$push: {problems: newProblem}},
|
||||
{new: true}
|
||||
);
|
||||
}
|
||||
contest = await contestModel.findOne(
|
||||
{channelId: channelId},
|
||||
).populate({path: 'problems', select: 'problemId _id'});
|
||||
const problem = (contest as Contest).problems.find(
|
||||
p => p.problemId == problemId);
|
||||
// update
|
||||
const newSession = await new sessionModel(
|
||||
{name: user.username, start: time, end: estimate},
|
||||
{}, {new: true, upsert: true}
|
||||
).save();
|
||||
await problemModel.findByIdAndUpdate(
|
||||
(problem as Problem)._id,
|
||||
{$push: {read: newSession}},
|
||||
{new: true}
|
||||
);
|
||||
return newSession._id.toString();
|
||||
}
|
||||
|
||||
export async function updateSession(sessionId: string, key: string, time: number): Promise<boolean>{
|
||||
const session = await sessionModel.findOne({_id: sessionId});
|
||||
if(session === null){
|
||||
logger.error(`Session ${sessionId} not found.`);
|
||||
return false;
|
||||
}
|
||||
switch(key){
|
||||
case 'start':{
|
||||
await sessionModel.updateOne(
|
||||
{_id: sessionId}, {start: time}
|
||||
);
|
||||
}break;
|
||||
case 'end':{
|
||||
await sessionModel.updateOne(
|
||||
{_id: sessionId}, {end: time}
|
||||
);
|
||||
}break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getContest(channelId: string): Promise<Contest | null>{
|
||||
return await contestModel.findOne({channelId: channelId}).populate(
|
||||
{path: 'problems', select: 'problemId _id'}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getProblem(id: Types.ObjectId): Promise<Problem | null>{
|
||||
return await problemModel.findById(id).populate(
|
||||
{path: 'read code', select: 'name start end _id'}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSession(id: Types.ObjectId): Promise<Session | null>{
|
||||
return await sessionModel.findById(id).populate('name start end _id');
|
||||
}
|
||||
|
||||
export async function getAllSession(): Promise<Session[]>{
|
||||
return await sessionModel.find({});
|
||||
}
|
||||
|
||||
export async function getAllProblem(): Promise<Problem[]>{
|
||||
return await problemModel.find({});
|
||||
}
|
||||
|
||||
export async function getAllContest(): Promise<Contest[]>{
|
||||
return await contestModel.find({});
|
||||
}
|
||||
57
index.ts
Normal file
57
index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {Events, GatewayIntentBits, ActivityType} from 'discord.js';
|
||||
|
||||
// Global config
|
||||
import {config} from './config';
|
||||
|
||||
// Classes
|
||||
import {ExtendedClient} from './classes/extendedclient';
|
||||
|
||||
// Initialization functions
|
||||
import {setNickname} from './init/set-nickname';
|
||||
import {loadCommands} from './init/load-commands';
|
||||
import {registerCommands} from './init/register-commands';
|
||||
import {sendReadyDM} from './init/ready-dm';
|
||||
|
||||
// Event-handling functions
|
||||
import {handleCommands} from './events/commands';
|
||||
|
||||
// Sub-services
|
||||
import {logger} from './logger';
|
||||
import {runMongo} from './mongo';
|
||||
|
||||
logger.info(config.adminId);
|
||||
|
||||
const client = new ExtendedClient({ intents: [GatewayIntentBits.Guilds] });
|
||||
client.login(config.token);
|
||||
|
||||
client.on(Events.InteractionCreate, handleCommands);
|
||||
|
||||
client.once(Events.ClientReady, async c => {
|
||||
logger.info(`Logged in as ${c.user.tag}`);
|
||||
|
||||
if(client.user){
|
||||
client.user.setPresence({
|
||||
activities: [{
|
||||
name: 'ぼくたちは勉強ができない',
|
||||
type: ActivityType.Watching,
|
||||
}],
|
||||
status: 'online',
|
||||
});
|
||||
logger.info('Set status');
|
||||
}
|
||||
|
||||
await setNickname(client);
|
||||
logger.info(`Set nickname as ${config.nickname}`);
|
||||
|
||||
const commands = await loadCommands(client);
|
||||
logger.info(`${commands.length} commands loaded.`);
|
||||
|
||||
const regCmdCnt = await registerCommands(commands);
|
||||
logger.info(`${regCmdCnt} commands registered.`);
|
||||
|
||||
await runMongo();
|
||||
logger.info(`Database ready`);
|
||||
|
||||
logger.info(`Ready!`);
|
||||
await sendReadyDM(client, config.adminId);
|
||||
});
|
||||
25
init/load-commands.ts
Normal file
25
init/load-commands.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import path from 'path';
|
||||
import {readdirSync} from 'fs';
|
||||
|
||||
import {ExtendedClient} from '../classes/extendedclient';
|
||||
import {logger} from '../logger';
|
||||
|
||||
export async function loadCommands(client: ExtendedClient): Promise<Array<string>>{
|
||||
const foldersPath = path.join(__dirname, '../commands');
|
||||
const commandFolders = readdirSync(foldersPath);
|
||||
const commands: Array<string> = [];
|
||||
for(const folder of commandFolders){
|
||||
const commandsPath = path.join(foldersPath, folder);
|
||||
const commandsFiles = readdirSync(commandsPath).filter(file => file.endsWith('.ts'));
|
||||
for(const file of commandsFiles){
|
||||
const filePath = path.join(commandsPath, file);
|
||||
const data = await import(filePath);
|
||||
if(data.command !== undefined){
|
||||
client.commands.set(data.command.name, data.command);
|
||||
commands.push(data.command.build().toJSON());
|
||||
}else
|
||||
logger.warning(`The command at ${filePath} is missing required properties.`);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
11
init/ready-dm.ts
Normal file
11
init/ready-dm.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {logger} from '../logger';
|
||||
import {ExtendedClient} from '../classes/extendedclient';
|
||||
|
||||
export async function sendReadyDM(client: ExtendedClient, adminId: string): Promise<void>{
|
||||
try{
|
||||
await (await client.users.fetch(adminId)).send(`service up at ${new Date()}`);
|
||||
logger.log('Service up message sent');
|
||||
}catch(err: unknown){
|
||||
logger.warning('sendReadyDM failed.');
|
||||
}
|
||||
}
|
||||
23
init/register-commands.ts
Normal file
23
init/register-commands.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {REST, Routes} from 'discord.js';
|
||||
|
||||
import {config} from '../config';
|
||||
import {logger} from '../logger';
|
||||
|
||||
function isArray<T>(data: unknown): data is Array<T>{
|
||||
return (data as Array<T>).length !== undefined;
|
||||
}
|
||||
|
||||
export async function registerCommands(commands: Array<string>): Promise<number>{
|
||||
const rest = new REST().setToken(config.token);
|
||||
try{
|
||||
const data = await rest.put(
|
||||
Routes.applicationCommands(config.clientId),
|
||||
{body: commands},
|
||||
);
|
||||
if(!isArray(data)) throw Error();
|
||||
return data.length;
|
||||
}catch(error: unknown){
|
||||
logger.error(`Type error in function "registerCommands"`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
21
init/set-nickname.ts
Normal file
21
init/set-nickname.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {Guild} from 'discord.js';
|
||||
|
||||
import {ExtendedClient} from '../classes/extendedclient';
|
||||
import {config} from '../config';
|
||||
import {logger} from '../logger';
|
||||
|
||||
export async function setNickname(client: ExtendedClient): Promise<void>{
|
||||
await client.guilds.cache.forEach(async (guild: Guild): Promise<void> => {
|
||||
try{
|
||||
// console.log(guild.members);
|
||||
const self = await guild.members.fetch({user: config.clientId, force: true});
|
||||
if(self !== null){
|
||||
await self.setNickname(config.nickname);
|
||||
logger.log(`Nickname had changed in guild: ${guild.name}`);
|
||||
}else
|
||||
logger.error(`Execution error at changing nickname in guild: ${guild.name}`);
|
||||
}catch(error: unknown){
|
||||
logger.error(`Unknown execution error in function "setNickname".`);
|
||||
}
|
||||
});
|
||||
}
|
||||
45
logger.ts
Normal file
45
logger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {appendFileSync} from 'fs';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {config} from './config';
|
||||
|
||||
enum LogLevel{
|
||||
ERROR = 'ERROR',
|
||||
WARNING = 'WARNING',
|
||||
DEBUG = 'DEBUG',
|
||||
LOG = 'LOG',
|
||||
INFO = 'INFO',
|
||||
}
|
||||
|
||||
class Logger{
|
||||
constructor(readonly logFile?: string){
|
||||
this.debug('logger initialized');
|
||||
}
|
||||
private currentTime(): string{
|
||||
return '[' + moment().tz('Asia/Taipei').format('YYYY/MM/DD hh:mm:ss') + ']';
|
||||
}
|
||||
private writeLog(content: string, logLevel: LogLevel): void{
|
||||
const line = `${this.currentTime()} ${logLevel}: ${content}`;
|
||||
console.log(line);
|
||||
if(this.logFile !== undefined){
|
||||
appendFileSync(this.logFile, line + '\n');
|
||||
}
|
||||
}
|
||||
error(content: string): string{
|
||||
this.writeLog(content, LogLevel.ERROR); return content;
|
||||
}
|
||||
warning(content: string): string{
|
||||
this.writeLog(content, LogLevel.WARNING); return content;
|
||||
}
|
||||
debug(content: string): string{
|
||||
this.writeLog(content, LogLevel.DEBUG); return content;
|
||||
}
|
||||
log(content: string): string{
|
||||
this.writeLog(content, LogLevel.LOG); return content;
|
||||
}
|
||||
info(content: string): string{
|
||||
this.writeLog(content, LogLevel.INFO); return content;
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger(config.logger.logFile);
|
||||
18
models/contests.ts
Normal file
18
models/contests.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {Schema, model, Types} from 'mongoose';
|
||||
|
||||
import {Problem} from './problems';
|
||||
|
||||
export interface Contest{
|
||||
_id: Types.ObjectId;
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
problems: Problem[];
|
||||
}
|
||||
|
||||
const contestSchema = new Schema<Contest>({
|
||||
channelId: {type: String, required: true},
|
||||
startTime: {type: Number, required: true},
|
||||
problems: [{type: Schema.Types.ObjectId, ref: 'Problem'}],
|
||||
});
|
||||
|
||||
export const contestModel = model<Contest>('Contest', contestSchema);
|
||||
22
models/problems.ts
Normal file
22
models/problems.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {Schema, model, Types} from 'mongoose';
|
||||
|
||||
import {Session} from './sessions';
|
||||
|
||||
export interface Problem{
|
||||
_id: Types.ObjectId;
|
||||
problemId: string;
|
||||
read: Session[];
|
||||
code: Session[];
|
||||
wa: number[];
|
||||
ac: number;
|
||||
}
|
||||
|
||||
const problemSchema = new Schema<Problem>({
|
||||
problemId: {type: String, required: true},
|
||||
read: {type: [{type: Schema.Types.ObjectId, ref: 'Session'}], default: []},
|
||||
code: {type: [{type: Schema.Types.ObjectId, ref: 'Session'}], default: []},
|
||||
wa: {type: [Number], default: []},
|
||||
ac: {type: Number, default: -1},
|
||||
});
|
||||
|
||||
export const problemModel = model<Problem>('Problem', problemSchema);
|
||||
16
models/sessions.ts
Normal file
16
models/sessions.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {Schema, model, Types} from 'mongoose';
|
||||
|
||||
export interface Session{
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const sessionSchema = new Schema<Session>({
|
||||
name: {type: String, required: true},
|
||||
start: {type: Number, required: true},
|
||||
end: {type: Number, default: -1},
|
||||
});
|
||||
|
||||
export const sessionModel = model<Session>('Session', sessionSchema);
|
||||
8
mongo.setting
Normal file
8
mongo.setting
Normal file
@@ -0,0 +1,8 @@
|
||||
use mafuyu
|
||||
db.createUser(
|
||||
{
|
||||
user: "mafuyu",
|
||||
pwd: passwordPrompt(),
|
||||
roles: [ { role: "readWrite", db: "mafuyu" } ]
|
||||
}
|
||||
)
|
||||
38
mongo.ts
Normal file
38
mongo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import {config} from './config';
|
||||
|
||||
async function resetMongo(): Promise<void> {
|
||||
try{
|
||||
// await clearContests();
|
||||
// await clearProblems();
|
||||
// await clearSessions();
|
||||
}catch(err: unknown){
|
||||
throw new Error(`MongoDB reset failed. ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeMongo(): Promise<void> {
|
||||
try{
|
||||
await resetMongo();
|
||||
}catch(err: unknown){
|
||||
throw new Error(`MongoDB initialize failed. ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMongo(): Promise<void> {
|
||||
try {
|
||||
mongoose.set('strictQuery', false);
|
||||
const auth: string = `${config.mongodb.user}:${config.mongodb.pass}`;
|
||||
const server: string = `${config.mongodb.host}:${config.mongodb.port}`;
|
||||
const uri: string = `mongodb://${auth}@${server}/${config.mongodb.db}`;
|
||||
await mongoose.connect(uri);
|
||||
} catch(err: unknown) {
|
||||
throw new Error(`MongoDB connection failed. ${err}`);
|
||||
}
|
||||
try {
|
||||
await initializeMongo();
|
||||
} catch(err: unknown) {
|
||||
throw new Error(`Initialize MongoDB data failed. ${err}`);
|
||||
}
|
||||
}
|
||||
2336
package-lock.json
generated
Normal file
2336
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "kirisu-mafuyu",
|
||||
"version": "1.3.0",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "npx ts-node index.ts",
|
||||
"build": "npx tsc --build",
|
||||
"clean": "npx tsc --build --clean",
|
||||
"lint": "npx eslint *.ts models/**.ts init/**.ts events/**.ts commands/*/*.ts classes/**.ts functions/**.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.konchin.com/discord-bot/Mafuyu-Kirisu.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://git.konchin.com/discord-bot/Mafuyu-Kirisu/issues"
|
||||
},
|
||||
"homepage": "https://git.konchin.com/discord-bot/Mafuyu-Kirisu",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.3.2",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"fs": "^0.0.1-security",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mongodb": "^5.6.0",
|
||||
"mongoose": "^7.6.1",
|
||||
"path": "^0.12.7",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"globals": "^15.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript-eslint": "^7.9.0"
|
||||
}
|
||||
}
|
||||
109
tsconfig.json
Normal file
109
tsconfig.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonJS", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user