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

View 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
View 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
View 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
View 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
View 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
View 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
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();

22
config.ts Normal file
View 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
View 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
View 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
View 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
View File

214
functions/database.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
use mafuyu
db.createUser(
{
user: "mafuyu",
pwd: passwordPrompt(),
roles: [ { role: "readWrite", db: "mafuyu" } ]
}
)

38
mongo.ts Normal file
View 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

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View 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
View 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. */
}
}