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