Merge branch 'experimental' into develop

First 4.0.0-Pudding commit of develop
This commit is contained in:
Siwoo Jeon 2024-09-28 11:23:51 +09:00
commit 3ac32871f6
Signed by: migan
GPG key ID: 036E9A8C5E8E48DA
55 changed files with 2321 additions and 1696 deletions

5
.gitignore vendored
View file

@ -79,6 +79,7 @@ web_modules/
.env.test.local
.env.production.local
.env.local
docker.env
# parcel-bundler cache (https://parceljs.org/)
.cache
@ -138,3 +139,7 @@ db/
.idea/
./database/
.DS_Store
# Prisma
prisma/**/*.*
!prisma/schema.prisma

View file

@ -1,7 +1,7 @@
{
"recommendations": [
"arcanis.vscode-zipfs",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View file

@ -3,8 +3,8 @@
"**/.yarn": true,
"**/.pnp.*": true
},
"eslint.nodePath": ".yarn/sdks",
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.nodePath": ".yarn/sdks"
"typescript.enablePromptUseWorkspaceTsdk": true
}

File diff suppressed because one or more lines are too long

925
.yarn/releases/yarn-4.5.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint/bin/eslint.js your application uses
module.exports = absRequire(`eslint/bin/eslint.js`);
module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`));

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint your application uses
module.exports = absRequire(`eslint`);
module.exports = wrapWithUserWrapper(absRequire(`eslint`));

32
.yarn/sdks/eslint/lib/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint`));

View file

@ -0,0 +1,32 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/rules
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint/rules your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/rules`));

View file

@ -0,0 +1,32 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/universal
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint/universal your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/universal`));

View file

@ -0,0 +1,32 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/use-at-your-own-risk
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint/use-at-your-own-risk your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`));

32
.yarn/sdks/eslint/lib/universal.js vendored Normal file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/universal
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint/universal your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/universal`));

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real eslint/use-at-your-own-risk your application uses
module.exports = absRequire(`eslint/use-at-your-own-risk`);
module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`));

View file

@ -1,14 +1,27 @@
{
"name": "eslint",
"version": "9.5.0-sdk",
"version": "9.11.0-sdk",
"main": "./lib/api.js",
"type": "commonjs",
"bin": {
"eslint": "./bin/eslint.js"
},
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/api.js"
},
"./package.json": "./package.json",
".": "./lib/api.js",
"./use-at-your-own-risk": "./lib/unsupported-api.js"
"./use-at-your-own-risk": {
"types": "./lib/types/use-at-your-own-risk.d.ts",
"default": "./lib/unsupported-api.js"
},
"./rules": {
"types": "./lib/types/rules/index.d.ts"
},
"./universal": {
"types": "./lib/types/universal.d.ts",
"default": "./lib/universal.js"
}
}
}

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real prettier/bin/prettier.cjs your application uses
module.exports = absRequire(`prettier/bin/prettier.cjs`);
module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`));

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real prettier your application uses
module.exports = absRequire(`prettier`);
module.exports = wrapWithUserWrapper(absRequire(`prettier`));

View file

@ -1,6 +1,6 @@
{
"name": "prettier",
"version": "3.3.2-sdk",
"version": "3.3.3-sdk",
"main": "./index.cjs",
"type": "commonjs",
"bin": "./bin/prettier.cjs"

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`));

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`));

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);
module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`));

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,7 +24,15 @@ if (existsSync(absPnpApiPath)) {
}
}
const moduleWrapper = tsserver => {
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
const moduleWrapper = exports => {
return wrapWithUserWrapper(moduleWrapperFn(exports));
};
const moduleWrapperFn = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,7 +24,15 @@ if (existsSync(absPnpApiPath)) {
}
}
const moduleWrapper = tsserver => {
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
const moduleWrapper = exports => {
return wrapWithUserWrapper(moduleWrapperFn(exports));
};
const moduleWrapperFn = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}

View file

@ -8,6 +8,7 @@ const {pathToFileURL} = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
@ -23,5 +24,9 @@ if (existsSync(absPnpApiPath)) {
}
}
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;
// Defer to the real typescript your application uses
module.exports = absRequire(`typescript`);
module.exports = wrapWithUserWrapper(absRequire(`typescript`));

View file

@ -1,6 +1,6 @@
{
"name": "typescript",
"version": "5.5.2-sdk",
"version": "5.6.2-sdk",
"main": "./lib/typescript.js",
"type": "commonjs",
"bin": {

View file

@ -1 +1 @@
yarnPath: .yarn/releases/yarn-4.3.1.cjs
yarnPath: .yarn/releases/yarn-4.5.0.cjs

View file

@ -1,4 +1,4 @@
FROM node:18.20.3
FROM node:18.20.4
ENV DOCKERIZE_VERSION v0.2.0
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
@ -8,6 +8,7 @@ RUN mkdir app
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn db:push
RUN yarn build

View file

@ -7,7 +7,7 @@
### 종속성
- 이 프로젝트는 Node.JS을 사용하고, 패키지 매니저를 Yarn Berry로 사용합니다.
- 이 프로젝트는 MariaDB(또는 MySQL)를 사용합니다.
- 이 프로젝트는 MariaDB(또는 MySQL)와 Database ORM인 Prisma를 사용합니다.
#### 종속성 설치
@ -17,11 +17,25 @@ yarn install
### 설정 파일
- 이 프로젝트는 설정파일을 프로젝트 루트에 있는 `config.json`으로 하며, 그 내용은 `config.example.json`에서 확인할 수 있습니다.
- 이 프로젝트는 설정파일을 프로젝트 루트에 있는 `.env`으로 하며, 그 내용은 `example.env`에서 확인할 수 있습니다.
#### Docker 설정
- Docker로 실행할 경우, 해당 설정 파일을 `docker.env`으로 하며, 그 내용은 `example-docker.env`에서 확인할 수 있습니다.
### prisma 설정
- 해당 프로젝트는 Database ORM인 Prisma를 사용하여 해당 설정이 필요합니다.
1. 먼저 .env에 DATABASE_URL부분을 채워 줍니다.
- 예시: `mysql://username:user_password@hostname:port/database?schema=public`
2. 터미널 에서 `yarn db:push`를 합니다.
### 실행
위 두 과정을 정상적으로 따랐다면, 아래의 명령어로 봇을 실행할 수 있습니다.
위 과정을 정상적으로 따랐다면, 아래의 명령어로 봇을 실행할 수 있습니다.
#### 그냥 실행 (디버그용 로그 출력)
@ -29,7 +43,9 @@ yarn install
yarn dev
```
#### 빌드
#### 빌드 후 실행
##### 빌드
```sh
yarn build

View file

@ -1,17 +0,0 @@
{
"bot": {
"owner_ID": "",
"token": "",
"prefix": ""
},
"train": {
"user_ID": ""
},
"mysql": {
"user": "",
"host": "",
"password": "",
"database": "",
"port": 3306
}
}

View file

@ -9,7 +9,7 @@ services:
ports:
- "1502:3306"
env_file:
- "./.env"
- "./docker.env"
networks:
- muffin_ai
discord_bot:
@ -19,6 +19,8 @@ services:
- muffin_ai
depends_on:
- database
env_file:
- "./.env"
networks:
muffin_ai:

5
example-docker.env Normal file
View file

@ -0,0 +1,5 @@
# Docker configs (MariaDB container)
MYSQL_USER=
MYSQL_PASSWORD=
MYSQL_DATABASE=
MYSQL_ROOT_PASSWORD=

View file

@ -1,4 +1,13 @@
MYSQL_USER=
MYSQL_PASSWORD=
MYSQL_DATABASE=
MYSQL_ROOT_PASSWORD=
# Prisma configs
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL=
# Bot configs
BOT_TOKEN=
BOT_OWNER_ID=
BOT_PREFIX=
TRAIN_USER_ID=

View file

@ -1,46 +1,52 @@
{
"name": "muffinbot",
"version": "3.1.0-cake.d240925a",
"version": "4.0.0-pudding.d240928a",
"main": "dist/index.js",
"private": true,
"dependencies": {
"@prisma/client": "^5.19.1",
"@sapphire/decorators": "^6.1.0",
"@sapphire/discord.js-utilities": "^7.3.0",
"@sapphire/framework": "^5.2.1",
"@sapphire/pieces": "^4.3.1",
"@sapphire/utilities": "^3.17.0",
"discord-api-types": "^0.37.93",
"discord.js": "^14.15.3",
"discord-api-types": "^0.37.100",
"discord.js": "^14.16.2",
"dokdo": "^0.6.2",
"mysql2": "^3.11.0",
"semver": "^7.6.3"
"dotenv": "^16.4.5",
"mysql2": "^3.11.3",
"semver": "^7.6.3",
"undici": "^6.19.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@eslint/js": "^9.11.0",
"@migan/prettier-config": "^1.2.0",
"@types/node": "^20.14.12",
"@types/semver": "^7",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@types/node": "^22.5.5",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"@yarnpkg/pnpify": "^4.1.2",
"cross-env": "^7.0.3",
"eslint": "^9.8.0",
"eslint": "^9.11.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"globals": "^15.8.0",
"globals": "^15.9.0",
"prettier": "^3.3.3",
"ts-node": "^10.9.2",
"tsup": "^8.2.3",
"typescript": "^5.5.4"
"prisma": "^5.19.1",
"tsup": "^8.3.0",
"typescript": "^5.6.2"
},
"scripts": {
"build": "tsup",
"dev": "cross-env NODE_ENV=development tsup --watch --onSuccess \"node --enable-source-maps dist\"",
"start": "cross-env NODE_ENV=production node dist"
"start": "cross-env NODE_ENV=production node dist",
"db:pull": "pnpify prisma db pull",
"db:push": "pnpify prisma db push"
},
"resolutions": {
"@types/ws": "^8.5.11",
"ws": "8.18.0"
},
"packageManager": "yarn@4.3.1"
"packageManager": "yarn@4.5.0"
}

35
prisma/schema.prisma Normal file
View file

@ -0,0 +1,35 @@
generator client {
provider = "prisma-client-js"
output = "./"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model learn {
id Int @id @default(autoincrement())
command String @db.VarChar(255)
result String @db.VarChar(255)
user_id String @db.VarChar(255)
created_at DateTime @default(now()) @db.DateTime(0)
}
model nsfw_content {
id Int @id @default(autoincrement())
text String @default("") @db.VarChar(255)
created_at DateTime? @default(now()) @db.DateTime(0)
persona String @default("") @db.VarChar(50)
}
model statement {
id Int @id @default(autoincrement())
text String @db.VarChar(255)
search_text String @default("") @db.VarChar(255)
conversation String @default("") @db.VarChar(32)
created_at DateTime? @default(now()) @db.DateTime(0)
in_response_to String? @db.VarChar(255)
search_in_response_to String @default("") @db.VarChar(255)
persona String @default("") @db.VarChar(50)
}

View file

@ -1,28 +0,0 @@
CREATE TABLE `statement` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` varchar(255) DEFAULT NOT NULL,
`search_text` varchar(255) NOT NULL DEFAULT '',
`conversation` varchar(32) NOT NULL DEFAULT '',
`created_at` datetime DEFAULT current_timestamp(),
`in_response_to` varchar(255) DEFAULT NULL,
`search_in_response_to` varchar(255) NOT NULL DEFAULT '',
`persona` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
);
CREATE TABLE `nsfw_content` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` varchar(255) NOT NULL DEFAULT '',
`created_at` datetime DEFAULT current_timestamp(),
`persona` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
);
CREATE TABLE `learn` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`command` varchar(255) NOT NULL,
`result` varchar(255) NOT NULL,
`user_id` varchar(255) NOT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
primary key (`id`)
);

View file

@ -1,25 +1,29 @@
import { SapphireClient, container, LogLevel } from '@sapphire/framework'
import { GatewayIntentBits, Partials, type Snowflake } from 'discord.js'
import { ChatBot, NODE_ENV, MaaDatabase } from './modules'
import { GatewayIntentBits, Partials } from 'discord.js'
import { ChatBot, Config, NODE_ENV } from './modules'
import { version } from '../package.json'
import config from '../config.json'
import { PrismaClient } from '../prisma'
import semver from 'semver'
const config = new Config()
// Load pieces
import './interaction-handlers/_load'
import './listeners/_load'
import './Commands/_load'
container.config = config
container.prefix = config.bot.prefix
container.version = version
container.database = new MaaDatabase()
container.dokdoAliases = ['dokdo', 'dok', 'Dokdo', 'Dok', '테스트']
container.chatBot = new ChatBot(container.database)
const release = version
.slice((semver.coerce(version)?.toString() + '-').length)
.split('.')[1]
container.config = config
container.prefix = config.bot.prefix
container.version = version
container.database = new PrismaClient()
container.dokdoAliases = ['dokdo', 'dok', 'Dokdo', 'Dok', '테스트']
container.chatBot = new ChatBot(container.database)
container.lastUpdated = new Date('2024-09-28')
if (release.startsWith('e')) {
container.release = 'EXPERIMENTAL'
} else if (release.startsWith('d')) {
@ -61,28 +65,14 @@ export default class MuffinBot extends SapphireClient {
declare module '@sapphire/framework' {
interface Container {
database: MaaDatabase
database: PrismaClient
chatBot: ChatBot
prefix: string
version: string
dokdoAliases: string[]
config: {
bot: {
owner_ID: Snowflake
token: string
}
train: {
user_ID: Snowflake
}
mysql: {
user: string
host: string
password: string
database: string
port: number
}
}
config: Config
release: 'EXPERIMENTAL' | 'DEV' | 'PREVIEW' | 'RELEASE'
lastUpdated: Date
}
}

View file

@ -1,5 +1,6 @@
import './learning_data'
import './deleteLearn'
import './information'
import './learn'
import './help'
import './list'

View file

@ -11,7 +11,6 @@ import {
codeBlock,
} from 'discord.js'
import { ApplyOptions } from '@sapphire/decorators'
import { type LearnData } from '../modules'
@ApplyOptions<Command.Options>({
name: '삭제',
@ -27,21 +26,22 @@ class DeleteLearnCommand extends Command {
const CUSTOM_ID = 'maa$deleteLearn'
const command = await args.rest('string').catch(() => null)
const options: SelectMenuComponentOptionData[] = []
const db = this.container.database
const deleteDataList: string[] = []
const [deleteDatas] = await db.database.execute<LearnData[]>(
'SELECT * FROM learn WHERE command = ? AND user_id = ?;',
[command, msg.author.id],
)
if (!command) {
return await msg.channel.send(
return await msg.reply(
`사용법: \n\`\`\`${(this.detailedDescription as DetailedDescriptionCommandObject).usage}\`\`\``,
)
}
const deleteDatas = await this.container.database.learn.findMany({
where: {
command,
user_id: msg.author.id,
},
})
if (!deleteDatas) {
return await msg.channel.send('해당하는 걸 찾ㅈ을 수 없어요.')
return await msg.reply('해당하는 걸 찾ㅈ을 수 없어요.')
}
for (let i = 1; i <= deleteDatas.length; i++) {

View file

@ -0,0 +1,65 @@
import { ApplyOptions } from '@sapphire/decorators'
import { Command, container } from '@sapphire/framework'
import { Message } from 'discord.js'
import { platform, arch } from 'os'
@ApplyOptions<Command.Options>({
name: '정보',
description: '머핀봇의 정보를 알ㄹ려줘요.',
detailedDescription: {
usage: '머핀아 정보',
},
})
class InformationCommand extends Command {
public async messageRun(msg: Message) {
await msg.reply({
embeds: [
{
title: `${this.container.client.user?.username}의 정ㅂ보`,
fields: [
{
name: '구동ㅎ환경',
value: `${platform()} ${arch()}`,
inline: false,
},
{
name: '버ㅈ전',
value: this.container.version,
inline: true,
},
{
name: '채ㄴ널',
value: this.container.release.toLowerCase(),
inline: true,
},
{
name: '최근 업ㄷ데이트 날짜',
value: this.container.lastUpdated.toLocaleDateString('ko', {
dateStyle: 'long',
}),
inline: true,
},
{
name: '개ㅂ발자',
value: (
await this.container.client.users.fetch(
this.container.config.bot.owner_ID,
)
).username,
inline: false,
},
],
thumbnail: {
url: this.container.client.user!.displayAvatarURL()!,
},
},
],
})
}
}
void container.stores.loadPiece({
piece: InformationCommand,
name: 'information',
store: 'commands',
})

View file

@ -52,7 +52,6 @@ class LearnCommand extends Command {
'간미',
]
const disallowed = ['@everyone', '@here', `<@${config.bot.owner_ID}>`]
const db = this.container.database
for (const ig of ignore) {
if (command.includes(ig)) {
@ -66,10 +65,12 @@ class LearnCommand extends Command {
}
}
await db.learn.insert({
user_id: msg.author.id,
command,
result,
await this.container.database.learn.create({
data: {
user_id: msg.author.id,
command,
result,
},
})
await msg.reply(`${command}을/를 배웠ㅇ어요.`)
}

View file

@ -1,6 +1,5 @@
import { ApplyOptions } from '@sapphire/decorators'
import { type ResponseData } from '../modules'
import { Command, container } from '@sapphire/framework'
import { ApplyOptions } from '@sapphire/decorators'
import { type Message } from 'discord.js'
@ApplyOptions<Command.Options>({
@ -14,11 +13,15 @@ import { type Message } from 'discord.js'
class LearnDataCommand extends Command {
public async messageRun(msg: Message<true>) {
const db = this.container.database
const data = await db.statement.all()
const nsfwData = await db.nsfwContent.all()
const learnData = await db.learn.all()
const userData = await db.learn.findOneAnotherKey('user_id', msg.author.id)
const muffin: ResponseData[] = []
const data = await db.statement.findMany()
const nsfwData = await db.nsfw_content.findMany()
const learnData = await db.learn.findMany()
const userData = await db.learn.findMany({
where: {
user_id: msg.author.id,
},
})
const muffin: any[] = []
data.forEach(row => {
if (row.persona === 'muffin') muffin.push(row)
else return

View file

@ -13,13 +13,15 @@ import { Command, container } from '@sapphire/framework'
class ListCommand extends Command {
public async messageRun(msg: Message<boolean>) {
const db = this.container.database
const data = await db.learn.findOneAnotherKey('user_id', msg.author.id)
const data = await db.learn.findMany({
where: {
user_id: msg.author.id,
},
})
const list: string[] = []
if (!data[0]) {
return await msg.channel.send(
'당신ㄴ은 단어를 가르쳐준 기억이 없ㅅ는데요.',
)
return await msg.reply('당신ㄴ은 단어를 가르쳐준 기억이 없ㅅ는데요.')
}
for (const listData of data) {

View file

@ -28,7 +28,7 @@ class DeleteLearnHandler extends InteractionHandler {
public async run(interaction: StringSelectMenuInteraction) {
await interaction.deferUpdate()
const id = interaction.values[0].slice(`${this._CUSTOM_ID}-`.length)
const id = Number(interaction.values[0].slice(`${this._CUSTOM_ID}-`.length))
const db = this.container.database
const decimalRegexp = /^[0-9]/g
@ -36,7 +36,11 @@ class DeleteLearnHandler extends InteractionHandler {
item.value.endsWith(id) ? item.label.match(decimalRegexp)![0] : null,
)
await db.learn.delete(id)
await db.learn.delete({
where: {
id,
},
})
await interaction.editReply({
embeds: [

View file

@ -4,7 +4,7 @@ import { noPerm } from '../modules'
import Dokdo from 'dokdo'
class MessageCreateListener extends Listener {
public async run(msg: Message) {
public async run(msg: Message<true>) {
const prefix = this.container.prefix
const dokdo = new Dokdo(this.container.client, {
aliases: ['dokdo', 'dok'],
@ -14,14 +14,13 @@ class MessageCreateListener extends Listener {
})
if (msg.author.bot) return
if (msg.content.startsWith(prefix)) {
if (this.container.release === 'PRE-RELEASE') {
if (this.container.release !== 'RELEASE') {
await msg.reply({
embeds: [
{
title: '정식 출시 이전 버전 사용안내',
description:
`현재 이 버전의 ${this.container.client.user?.username}은 정식출시 되기 이전이라 많이 불안정할 수 있어요.\n` +
`또한 이 버전의 ${this.container.client.user?.username}의 데이터는 정식버전과 연동이 안돼요.\n` +
`만약 오류가 발견되면 ${(await this.container.client.users.fetch(this.container.config.bot.owner_ID)).username}님에게 알려주세요.\n`,
color: 0xff0000,
footer: {

View file

@ -1,19 +1,19 @@
import type { Client, Message, TextChannel } from 'discord.js'
import type { PrismaClient } from '../../prisma'
import { container } from '@sapphire/framework'
import type { MaaDatabase } from './database'
export default class ChatBot {
public constructor(public db: MaaDatabase) {
setInterval(async () => {
this.db.ping()
}, 60000)
}
public constructor(public db: PrismaClient) {}
public async getResponse(msg: Message): Promise<string> {
const prefix = container.prefix
const data = await this.db.statement.all()
const data = await this.db.statement.findMany()
const args = msg.content.slice(prefix.length).trim().split(/ +/g).join(' ')
const learn = await this.db.learn.findOne(args)
const learn = await this.db.learn.findMany({
where: {
command: args,
},
})
const learnData = learn[Math.floor(Math.random() * learn.length)]
const randomNumber = Math.round(Math.random() * (2 - 1) + 1)
@ -30,7 +30,7 @@ export default class ChatBot {
let response: string
if ((msg.channel as TextChannel).nsfw) {
const NSFWData = await this.db.nsfwContent.all()
const NSFWData = await this.db.nsfw_content.findMany()
const dataList = [...data, ...NSFWData]
response = dataList[Math.floor(Math.random() * dataList.length)].text
} else {
@ -50,19 +50,23 @@ export default class ChatBot {
if (msg.author.bot) return
if (msg.author.id === container.config.train.user_ID) {
const response = await this.getResponse(msg)
await this.db.statement.insert({
text: msg.content,
persona: 'muffin',
in_response_to: response,
await this.db.statement.create({
data: {
text: msg.content,
persona: 'muffin',
in_response_to: response,
},
})
} else {
if (!(msg.channel as TextChannel).nsfw) return
if (!msg.content.startsWith(prefix)) return
const persona = `user:${msg.author.username.slice(0, 50).toLowerCase()}`
const text = msg.content.replace(prefix, '')
await this.db.nsfwContent.insert({
text,
persona,
await this.db.nsfw_content.create({
data: {
text,
persona,
},
})
}
})

13
src/modules/config.ts Normal file
View file

@ -0,0 +1,13 @@
import 'dotenv/config'
export default class MAAConfig {
public readonly bot = {
token: process.env.BOT_TOKEN!,
owner_ID: process.env.BOT_OWNER_ID!,
prefix: process.env.BOT_PREFIX!,
}
public readonly train = {
user_id: process.env.TRAIN_USER_ID!,
}
}

View file

@ -1,27 +0,0 @@
import { LearnTable, NSFWContentTable, StatementTable } from './model'
import { container } from '@sapphire/framework'
import { createPool } from 'mysql2/promise'
export class MaaDatabase {
public readonly database = createPool({
...container.config.mysql,
keepAliveInitialDelay: 10000,
enableKeepAlive: true,
})
.on('release', conn => {
container.logger.debug(`[MaaDatabase] ${conn.threadId} Released.`)
})
.on('connection', conn => {
container.logger.debug(`[MaaDatabase] ${conn.threadId} Connected.`)
})
public statement = new StatementTable(this.database)
public nsfwContent = new NSFWContentTable(this.database)
public learn = new LearnTable(this.database)
public ping() {
this.database.getConnection().then(conn => {
conn.ping()
conn.release()
})
}
}

View file

@ -1,3 +0,0 @@
export * from './database'
export * from './type'
export * from './model'

View file

@ -1,3 +0,0 @@
export * from './statement'
export * from './learn'
export * from './nsfwContent'

View file

@ -1,72 +0,0 @@
import type { BaseTable, LearnData } from '../type'
import { type Pool } from 'mysql2/promise'
import { Snowflake } from 'discord.js'
import run from '../run'
export class LearnTable implements BaseTable<LearnData, string> {
public readonly name = 'learn'
public constructor(private _database: Pool) {}
public async all(): Promise<LearnData[]> {
const [rows] = await this._database.execute<LearnData[]>(
`SELECT * FROM ${this.name};`,
)
return rows
}
public async findOne(key: string): Promise<LearnData[]> {
const [rows] = await this._database.execute<LearnData[]>(
`SELECT * FROM ${this.name} WHERE command = ?;`,
[key],
)
return rows
}
public async insert(data: {
command: string
result: string
user_id: Snowflake
}): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(
`INSERT INTO ${this.name} (command, result, user_id) VALUES (?, ?, ?);`,
[data.command, data.result, data.user_id],
)
})
}
public async update(data: {
command: string
result: string
}): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(
`UPDATE ${this.name} SET result = ? WHERE command = ?;`,
[data.command, data.result],
)
})
}
public async delete(key: string): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(`DELETE FROM ${this.name} WHERE id = ?;`, [key])
})
}
public async findOneAnotherKey(
key: 'id' | 'command' | 'result' | 'user_id' | 'created_at',
data: any,
): Promise<LearnData[]> {
const [rows] = await this._database.execute<LearnData[]>(
`SELECT * FROM ${this.name} WHERE ${key} = ?;`,
[data],
)
return rows
}
}

View file

@ -1,66 +0,0 @@
import type { BaseTable, NSFWData } from '../type'
import { type Pool } from 'mysql2/promise'
import run from '../run'
export class NSFWContentTable implements BaseTable<NSFWData, number> {
public readonly name = 'nsfw_content'
public constructor(private _database: Pool) {}
public async all(): Promise<NSFWData[]> {
const [rows] = await this._database.execute<NSFWData[]>(
`SELECT * FROM ${this.name};`,
)
return rows
}
public async findOne(key: number): Promise<NSFWData[]> {
const [rows] = await this._database.execute<NSFWData[]>(
`SELECT * FROM ${this.name} WHERE id = ?;`,
[key],
)
return rows
}
public async insert(data: { text: string; persona: string }): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(
`INSERT INTO ${this.name} (text, persona) VALUES (?, ?);`,
[data.text, data.persona],
)
})
}
public async update(data: { id: number; text: string }): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(`UPDATE ${this.name} SET text = ? WHERE id = ?;`, [
data.text,
data.id,
])
})
}
public async delete(key: number): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(`DELETE FROM ${this} WHERE id = ?;`, [key])
})
}
public async findOneAnotherKey(
key: 'id' | 'text' | 'persona' | 'created_at',
data: any,
): Promise<NSFWData[]> {
const [rows] = await this._database.execute<NSFWData[]>(
`SELECT *
FROM ${this.name}
WHERE ${key} = ?;`,
[data],
)
return rows
}
}

View file

@ -1,76 +0,0 @@
import type { BaseTable, ResponseData } from '../type'
import { type Pool } from 'mysql2/promise'
import run from '../run'
export class StatementTable implements BaseTable<ResponseData, number> {
public readonly name = 'statement'
public constructor(private _database: Pool) {}
public async all(): Promise<ResponseData[]> {
const [rows] = await this._database.execute<ResponseData[]>(
`SELECT * FROM ${this.name};`,
)
return rows
}
public async findOne(key: number): Promise<ResponseData[]> {
const [rows] = await this._database.execute<ResponseData[]>(
`SELECT * FROM ${this.name} WHERE id = ?;`,
[key],
)
return rows
}
public async insert(data: {
text: string
persona: string
in_response_to: string
}): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(
`INSERT INTO ${this.name} (text, persona, in_response_to) VALUES (?, ?, ?);`,
[data.text, data.persona, data.in_response_to],
)
})
}
public async update(data: { id: number; text: string }): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(`UPDATE ${this.name} SET text = ? WHERE id = ?;`, [
data.text,
data.id,
])
})
}
public async delete(key: number): Promise<void> {
const db = await this._database.getConnection()
await run(db, async () => {
await db.execute(`DELETE FROM ${this.name} WHERE id = ?;`, [key])
})
}
public async findOneAnotherKey(
key:
| 'id'
| 'text'
| 'persona'
| 'created_at'
| 'search_text'
| 'conversation'
| 'in_response_to'
| 'search_in_response_to',
data: any,
): Promise<ResponseData[]> {
const [rows] = await this._database.execute<ResponseData[]>(
`SELECT * FROM ${this.name} WHERE ${key} = ?;`,
[data],
)
return rows
}
}

View file

@ -1,14 +0,0 @@
import { type PoolConnection } from 'mysql2/promise'
export default async function run(db: PoolConnection, fn: () => Promise<any>) {
try {
await db.beginTransaction()
await fn()
await db.commit()
} catch (err) {
console.error(err)
await db.rollback()
} finally {
db.release()
}
}

View file

@ -1,36 +0,0 @@
import type { RowDataPacket } from 'mysql2/promise'
import type { Snowflake } from 'discord.js'
export interface BaseData extends RowDataPacket {
id: number
text: string
created_at: string
persona: string
}
export interface ResponseData extends BaseData {
search_text: string
conversation: string
in_response_to: string | null
search_in_response_to: string
}
export interface LearnData extends RowDataPacket {
id: number
command: string
result: string
user_id: Snowflake
created_at: string
}
export { BaseData as NSFWData }
export interface BaseTable<T, V> {
name: string
all(): Promise<T[]>
findOne(key: V): Promise<T[]>
insert(data: any): Promise<void>
update(data: any): Promise<void>
delete(key: V): Promise<void>
findOneAnotherKey(key: string, data: any): Promise<T[]>
}

View file

@ -1,14 +1,6 @@
import { ResponseData, NSFWData, LearnData, MaaDatabase } from './database'
import { NODE_ENV } from './env'
import ChatBot from './ChatBot'
import Config from './config'
import noPerm from './noPerm'
export {
ResponseData,
MaaDatabase,
LearnData,
NODE_ENV,
ChatBot,
NSFWData,
noPerm,
}
export { NODE_ENV, ChatBot, noPerm, Config }

1205
yarn.lock

File diff suppressed because it is too large Load diff