Compare commits
70 commits
3.0.1-Cake
...
main
Author | SHA1 | Date | |
---|---|---|---|
41fd07f4e7 | |||
67fde66b9f | |||
1e57b19a61 | |||
d874273465 | |||
6d177d34b0 | |||
3e88e85e7e | |||
eb2e6d9bdb | |||
8be73a2d7d | |||
2efe702309 | |||
9c3f371f0c | |||
dceac4b653 | |||
e3d3f1ce4f | |||
c2d0d905b8 | |||
111a0b6e7c | |||
1875de42b5 | |||
ef16debab0 | |||
bf970f5090 | |||
680a49c616 | |||
8e9855f6eb | |||
c8fec5b0c7 | |||
0fcff7207f | |||
87fd5f0ab9 | |||
84facc8e6f | |||
2d73662b37 | |||
cc3c627683 | |||
d4984c2df3 | |||
0f7f1d7272 | |||
11e784f128 | |||
0c334c6bd1 | |||
e0e82d44f8 | |||
db701a7756 | |||
9c1375ca7d | |||
789bfba759 | |||
ed782a7b96 | |||
a0a7adff79 | |||
926d2d2479 | |||
445cd6b44e | |||
1b29cc2336 | |||
50d1bc4528 | |||
33102d64dc | |||
355fd0b1f3 | |||
8f1c393ec4 | |||
4563104483 | |||
c1ba5e8dc0 | |||
f4b479a9ca | |||
68ff1429e1 | |||
aa8dc685ef | |||
4931390542 | |||
d7faeac384 | |||
9821bac66a | |||
115823f3a5 | |||
3ac32871f6 | |||
cbe8df23e1 | |||
71db89f4d5 | |||
9526aeb238 | |||
dfa6b75b32 | |||
1af47ed911 | |||
bbf0b0cd08 | |||
2b30c7b9a8 | |||
80223f62a6 | |||
96e9a66abd | |||
f7bd02adf2 | |||
9d84e7b608 | |||
81bfd775d3 | |||
0f664e97fb | |||
63864b073f | |||
7a4a724e14 | |||
abf62a00af | |||
fbeb259047 | |||
ef661dd3f7 |
69 changed files with 2566 additions and 6065 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -79,6 +79,7 @@ web_modules/
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
docker.env
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
.cache
|
.cache
|
||||||
|
@ -138,3 +139,7 @@ db/
|
||||||
.idea/
|
.idea/
|
||||||
./database/
|
./database/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Prisma
|
||||||
|
prisma/**/*.*
|
||||||
|
!prisma/schema.prisma
|
||||||
|
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"arcanis.vscode-zipfs",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
|
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"search.exclude": {
|
|
||||||
"**/.yarn": true,
|
|
||||||
"**/.pnp.*": true
|
|
||||||
},
|
|
||||||
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
|
||||||
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
|
||||||
"eslint.nodePath": ".yarn/sdks"
|
|
||||||
}
|
|
894
.yarn/releases/yarn-4.3.1.cjs
vendored
894
.yarn/releases/yarn-4.3.1.cjs
vendored
File diff suppressed because one or more lines are too long
27
.yarn/sdks/eslint/bin/eslint.js
vendored
27
.yarn/sdks/eslint/bin/eslint.js
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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/bin/eslint.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real eslint/bin/eslint.js your application uses
|
|
||||||
module.exports = absRequire(`eslint/bin/eslint.js`);
|
|
27
.yarn/sdks/eslint/lib/api.js
vendored
27
.yarn/sdks/eslint/lib/api.js
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real eslint your application uses
|
|
||||||
module.exports = absRequire(`eslint`);
|
|
27
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
27
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real eslint/use-at-your-own-risk your application uses
|
|
||||||
module.exports = absRequire(`eslint/use-at-your-own-risk`);
|
|
14
.yarn/sdks/eslint/package.json
vendored
14
.yarn/sdks/eslint/package.json
vendored
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"name": "eslint",
|
|
||||||
"version": "9.5.0-sdk",
|
|
||||||
"main": "./lib/api.js",
|
|
||||||
"type": "commonjs",
|
|
||||||
"bin": {
|
|
||||||
"eslint": "./bin/eslint.js"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
"./package.json": "./package.json",
|
|
||||||
".": "./lib/api.js",
|
|
||||||
"./use-at-your-own-risk": "./lib/unsupported-api.js"
|
|
||||||
}
|
|
||||||
}
|
|
5
.yarn/sdks/integrations.yml
vendored
5
.yarn/sdks/integrations.yml
vendored
|
@ -1,5 +0,0 @@
|
||||||
# This file is automatically generated by @yarnpkg/sdks.
|
|
||||||
# Manual changes might be lost!
|
|
||||||
|
|
||||||
integrations:
|
|
||||||
- vscode
|
|
27
.yarn/sdks/prettier/bin/prettier.cjs
vendored
27
.yarn/sdks/prettier/bin/prettier.cjs
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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 prettier/bin/prettier.cjs
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real prettier/bin/prettier.cjs your application uses
|
|
||||||
module.exports = absRequire(`prettier/bin/prettier.cjs`);
|
|
27
.yarn/sdks/prettier/index.cjs
vendored
27
.yarn/sdks/prettier/index.cjs
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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 prettier
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real prettier your application uses
|
|
||||||
module.exports = absRequire(`prettier`);
|
|
7
.yarn/sdks/prettier/package.json
vendored
7
.yarn/sdks/prettier/package.json
vendored
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"name": "prettier",
|
|
||||||
"version": "3.3.2-sdk",
|
|
||||||
"main": "./index.cjs",
|
|
||||||
"type": "commonjs",
|
|
||||||
"bin": "./bin/prettier.cjs"
|
|
||||||
}
|
|
27
.yarn/sdks/typescript/bin/tsc
vendored
27
.yarn/sdks/typescript/bin/tsc
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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 typescript/bin/tsc
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/bin/tsc your application uses
|
|
||||||
module.exports = absRequire(`typescript/bin/tsc`);
|
|
27
.yarn/sdks/typescript/bin/tsserver
vendored
27
.yarn/sdks/typescript/bin/tsserver
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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 typescript/bin/tsserver
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/bin/tsserver your application uses
|
|
||||||
module.exports = absRequire(`typescript/bin/tsserver`);
|
|
27
.yarn/sdks/typescript/lib/tsc.js
vendored
27
.yarn/sdks/typescript/lib/tsc.js
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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 typescript/lib/tsc.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/tsc.js your application uses
|
|
||||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
|
239
.yarn/sdks/typescript/lib/tsserver.js
vendored
239
.yarn/sdks/typescript/lib/tsserver.js
vendored
|
@ -1,239 +0,0 @@
|
||||||
#!/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 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 typescript/lib/tsserver.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleWrapper = tsserver => {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
return tsserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {isAbsolute} = require(`path`);
|
|
||||||
const pnpApi = require(`pnpapi`);
|
|
||||||
|
|
||||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
|
||||||
const isPortal = str => str.startsWith("portal:/");
|
|
||||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
|
||||||
|
|
||||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
|
||||||
return `${locator.name}@${locator.reference}`;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
|
||||||
// doesn't understand. This layer makes sure to remove the protocol
|
|
||||||
// before forwarding it to TS, and to add it back on all returned paths.
|
|
||||||
|
|
||||||
function toEditorPath(str) {
|
|
||||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
|
||||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
|
||||||
// We also take the opportunity to turn virtual paths into physical ones;
|
|
||||||
// this makes it much easier to work with workspaces that list peer
|
|
||||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
|
||||||
// file instances instead of the real ones.
|
|
||||||
//
|
|
||||||
// We only do this to modules owned by the the dependency tree roots.
|
|
||||||
// This avoids breaking the resolution when jumping inside a vendor
|
|
||||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
|
||||||
// errors on react).
|
|
||||||
//
|
|
||||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
|
||||||
if (resolved) {
|
|
||||||
const locator = pnpApi.findPackageLocator(resolved);
|
|
||||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
|
||||||
str = resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str = normalize(str);
|
|
||||||
|
|
||||||
if (str.match(/\.zip\//)) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
|
||||||
// VSCode only adds it automatically for supported schemes,
|
|
||||||
// so we have to do it manually for the `zip` scheme.
|
|
||||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
|
||||||
//
|
|
||||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
|
||||||
//
|
|
||||||
// 2021-10-08: VSCode changed the format in 1.61.
|
|
||||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-04-06: VSCode changed the format in 1.66.
|
|
||||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-05-06: VSCode changed the format in 1.68
|
|
||||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
case `vscode <1.61`: {
|
|
||||||
str = `^zip:${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.66`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.68`: {
|
|
||||||
str = `^/zip${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// To make "go to definition" work,
|
|
||||||
// We have to resolve the actual file system path from virtual path
|
|
||||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = resolve(`zipfile:${str}`);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
|
||||||
// We have to resolve the actual file system path from virtual path,
|
|
||||||
// everything else is up to neovim
|
|
||||||
case `neovim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = `zipfile://${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
str = `zip:${str}`;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromEditorPath(str) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
|
||||||
// So in order to convert it back, we use .* to match all the thing
|
|
||||||
// before `zipfile:`
|
|
||||||
return process.platform === `win32`
|
|
||||||
? str.replace(/^.*zipfile:\//, ``)
|
|
||||||
: str.replace(/^.*zipfile:/, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `neovim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
|
||||||
return str.replace(/^zipfile:\/\//, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`:
|
|
||||||
default: {
|
|
||||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force enable 'allowLocalPluginLoads'
|
|
||||||
// TypeScript tries to resolve plugins using a path relative to itself
|
|
||||||
// which doesn't work when using the global cache
|
|
||||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
|
||||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
|
||||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
|
||||||
// https://github.com/microsoft/vscode/issues/45856
|
|
||||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
|
||||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
|
||||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
|
||||||
this.projectService.allowLocalPluginLoads = true;
|
|
||||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// And here is the point where we hijack the VSCode <-> TS communications
|
|
||||||
// by adding ourselves in the middle. We locate everything that looks
|
|
||||||
// like an absolute path of ours and normalize it.
|
|
||||||
|
|
||||||
const Session = tsserver.server.Session;
|
|
||||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
|
||||||
let hostInfo = `unknown`;
|
|
||||||
|
|
||||||
Object.assign(Session.prototype, {
|
|
||||||
onMessage(/** @type {string | object} */ message) {
|
|
||||||
const isStringMessage = typeof message === 'string';
|
|
||||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
parsedMessage != null &&
|
|
||||||
typeof parsedMessage === `object` &&
|
|
||||||
parsedMessage.arguments &&
|
|
||||||
typeof parsedMessage.arguments.hostInfo === `string`
|
|
||||||
) {
|
|
||||||
hostInfo = parsedMessage.arguments.hostInfo;
|
|
||||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
|
||||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
|
||||||
// The RegExp from https://semver.org/ but without the caret at the start
|
|
||||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
|
||||||
) ?? []).map(Number)
|
|
||||||
|
|
||||||
if (major === 1) {
|
|
||||||
if (minor < 61) {
|
|
||||||
hostInfo += ` <1.61`;
|
|
||||||
} else if (minor < 66) {
|
|
||||||
hostInfo += ` <1.66`;
|
|
||||||
} else if (minor < 68) {
|
|
||||||
hostInfo += ` <1.68`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
|
||||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return originalOnMessage.call(
|
|
||||||
this,
|
|
||||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
send(/** @type {any} */ msg) {
|
|
||||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
|
||||||
return typeof value === `string` ? toEditorPath(value) : value;
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tsserver;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10));
|
|
||||||
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
|
|
||||||
// Ref https://github.com/microsoft/TypeScript/pull/55326
|
|
||||||
if (major > 5 || (major === 5 && minor >= 5)) {
|
|
||||||
moduleWrapper(absRequire(`typescript`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
|
||||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
|
239
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
239
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
|
@ -1,239 +0,0 @@
|
||||||
#!/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 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 typescript/lib/tsserverlibrary.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleWrapper = tsserver => {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
return tsserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {isAbsolute} = require(`path`);
|
|
||||||
const pnpApi = require(`pnpapi`);
|
|
||||||
|
|
||||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
|
||||||
const isPortal = str => str.startsWith("portal:/");
|
|
||||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
|
||||||
|
|
||||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
|
||||||
return `${locator.name}@${locator.reference}`;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
|
||||||
// doesn't understand. This layer makes sure to remove the protocol
|
|
||||||
// before forwarding it to TS, and to add it back on all returned paths.
|
|
||||||
|
|
||||||
function toEditorPath(str) {
|
|
||||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
|
||||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
|
||||||
// We also take the opportunity to turn virtual paths into physical ones;
|
|
||||||
// this makes it much easier to work with workspaces that list peer
|
|
||||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
|
||||||
// file instances instead of the real ones.
|
|
||||||
//
|
|
||||||
// We only do this to modules owned by the the dependency tree roots.
|
|
||||||
// This avoids breaking the resolution when jumping inside a vendor
|
|
||||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
|
||||||
// errors on react).
|
|
||||||
//
|
|
||||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
|
||||||
if (resolved) {
|
|
||||||
const locator = pnpApi.findPackageLocator(resolved);
|
|
||||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
|
||||||
str = resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str = normalize(str);
|
|
||||||
|
|
||||||
if (str.match(/\.zip\//)) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
|
||||||
// VSCode only adds it automatically for supported schemes,
|
|
||||||
// so we have to do it manually for the `zip` scheme.
|
|
||||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
|
||||||
//
|
|
||||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
|
||||||
//
|
|
||||||
// 2021-10-08: VSCode changed the format in 1.61.
|
|
||||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-04-06: VSCode changed the format in 1.66.
|
|
||||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-05-06: VSCode changed the format in 1.68
|
|
||||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
case `vscode <1.61`: {
|
|
||||||
str = `^zip:${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.66`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.68`: {
|
|
||||||
str = `^/zip${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// To make "go to definition" work,
|
|
||||||
// We have to resolve the actual file system path from virtual path
|
|
||||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = resolve(`zipfile:${str}`);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
|
||||||
// We have to resolve the actual file system path from virtual path,
|
|
||||||
// everything else is up to neovim
|
|
||||||
case `neovim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = `zipfile://${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
str = `zip:${str}`;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromEditorPath(str) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
|
||||||
// So in order to convert it back, we use .* to match all the thing
|
|
||||||
// before `zipfile:`
|
|
||||||
return process.platform === `win32`
|
|
||||||
? str.replace(/^.*zipfile:\//, ``)
|
|
||||||
: str.replace(/^.*zipfile:/, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `neovim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
|
||||||
return str.replace(/^zipfile:\/\//, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`:
|
|
||||||
default: {
|
|
||||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force enable 'allowLocalPluginLoads'
|
|
||||||
// TypeScript tries to resolve plugins using a path relative to itself
|
|
||||||
// which doesn't work when using the global cache
|
|
||||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
|
||||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
|
||||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
|
||||||
// https://github.com/microsoft/vscode/issues/45856
|
|
||||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
|
||||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
|
||||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
|
||||||
this.projectService.allowLocalPluginLoads = true;
|
|
||||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// And here is the point where we hijack the VSCode <-> TS communications
|
|
||||||
// by adding ourselves in the middle. We locate everything that looks
|
|
||||||
// like an absolute path of ours and normalize it.
|
|
||||||
|
|
||||||
const Session = tsserver.server.Session;
|
|
||||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
|
||||||
let hostInfo = `unknown`;
|
|
||||||
|
|
||||||
Object.assign(Session.prototype, {
|
|
||||||
onMessage(/** @type {string | object} */ message) {
|
|
||||||
const isStringMessage = typeof message === 'string';
|
|
||||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
parsedMessage != null &&
|
|
||||||
typeof parsedMessage === `object` &&
|
|
||||||
parsedMessage.arguments &&
|
|
||||||
typeof parsedMessage.arguments.hostInfo === `string`
|
|
||||||
) {
|
|
||||||
hostInfo = parsedMessage.arguments.hostInfo;
|
|
||||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
|
||||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
|
||||||
// The RegExp from https://semver.org/ but without the caret at the start
|
|
||||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
|
||||||
) ?? []).map(Number)
|
|
||||||
|
|
||||||
if (major === 1) {
|
|
||||||
if (minor < 61) {
|
|
||||||
hostInfo += ` <1.61`;
|
|
||||||
} else if (minor < 66) {
|
|
||||||
hostInfo += ` <1.66`;
|
|
||||||
} else if (minor < 68) {
|
|
||||||
hostInfo += ` <1.68`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
|
||||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return originalOnMessage.call(
|
|
||||||
this,
|
|
||||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
send(/** @type {any} */ msg) {
|
|
||||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
|
||||||
return typeof value === `string` ? toEditorPath(value) : value;
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tsserver;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10));
|
|
||||||
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
|
|
||||||
// Ref https://github.com/microsoft/TypeScript/pull/55326
|
|
||||||
if (major > 5 || (major === 5 && minor >= 5)) {
|
|
||||||
moduleWrapper(absRequire(`typescript`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
|
||||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
|
27
.yarn/sdks/typescript/lib/typescript.js
vendored
27
.yarn/sdks/typescript/lib/typescript.js
vendored
|
@ -1,27 +0,0 @@
|
||||||
#!/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 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 typescript
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
if (isPnpLoaderEnabled && register) {
|
|
||||||
register(pathToFileURL(absPnpLoaderPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript your application uses
|
|
||||||
module.exports = absRequire(`typescript`);
|
|
10
.yarn/sdks/typescript/package.json
vendored
10
.yarn/sdks/typescript/package.json
vendored
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"name": "typescript",
|
|
||||||
"version": "5.5.2-sdk",
|
|
||||||
"main": "./lib/typescript.js",
|
|
||||||
"type": "commonjs",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "./bin/tsc",
|
|
||||||
"tsserver": "./bin/tsserver"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
|
|
@ -1,14 +1,16 @@
|
||||||
FROM node:18.20.3
|
FROM node:18.20.4
|
||||||
|
|
||||||
ENV DOCKERIZE_VERSION v0.2.0
|
ENV DOCKERIZE_VERSION v0.2.0
|
||||||
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
||||||
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
|
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
|
||||||
RUN mkdir app
|
RUN mkdir app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN yarn install
|
RUN pnpm install
|
||||||
RUN yarn build
|
RUN pnpm db:generate
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
|
||||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
34
README.md
34
README.md
|
@ -6,37 +6,53 @@
|
||||||
|
|
||||||
### 종속성
|
### 종속성
|
||||||
|
|
||||||
- 이 프로젝트는 Node.JS을 사용하고, 패키지 매니저를 Yarn Berry로 사용합니다.
|
- 이 프로젝트는 Node.JS을 사용하고, 패키지 매니저를 PNPM을 사용합니다.
|
||||||
- 이 프로젝트는 MariaDB(또는 MySQL)를 사용합니다.
|
- 이 프로젝트는 MariaDB(또는 MySQL)와 Database ORM인 Prisma를 사용합니다.
|
||||||
|
|
||||||
#### 종속성 설치
|
#### 종속성 설치
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn install
|
pnpm 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`
|
||||||
|
|
||||||
|
2. 터미널 에서 `pnpm db:push`를 합니다.
|
||||||
|
|
||||||
### 실행
|
### 실행
|
||||||
|
|
||||||
위 두 과정을 정상적으로 따랐다면, 아래의 명령어로 봇을 실행할 수 있습니다.
|
위 과정들을 정상적으로 따랐다면, 아래의 명령어로 봇을 실행할 수 있습니다.
|
||||||
|
|
||||||
#### 그냥 실행 (디버그용 로그 출력)
|
#### 그냥 실행 (디버그용 로그 출력)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 빌드
|
#### 빌드 후 실행
|
||||||
|
|
||||||
|
##### 빌드
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 실행
|
##### 실행
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn start
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"bot": {
|
|
||||||
"owner_ID": "",
|
|
||||||
"token": "",
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"train": {
|
|
||||||
"user_ID": ""
|
|
||||||
},
|
|
||||||
"mysql": {
|
|
||||||
"user": "",
|
|
||||||
"host": "",
|
|
||||||
"password": "",
|
|
||||||
"database": "",
|
|
||||||
"port": 3306
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "1502:3306"
|
- "1502:3306"
|
||||||
env_file:
|
env_file:
|
||||||
- "./.env"
|
- "./docker.env"
|
||||||
networks:
|
networks:
|
||||||
- muffin_ai
|
- muffin_ai
|
||||||
discord_bot:
|
discord_bot:
|
||||||
|
@ -19,6 +19,8 @@ services:
|
||||||
- muffin_ai
|
- muffin_ai
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
|
env_file:
|
||||||
|
- "./.env"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
muffin_ai:
|
muffin_ai:
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
dockerize -wait tcp://database:1502 -timeout 20s
|
dockerize -wait tcp://database:1502 -timeout 20s
|
||||||
yarn start
|
pnpm start
|
|
@ -17,14 +17,7 @@ const compat = new FlatCompat({
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: ['**/dist/', '**/.vscode/', '**/.idea/', '**/node_modules/'],
|
||||||
'**/.yarn/',
|
|
||||||
'**/.pnp.*',
|
|
||||||
'**/dist/',
|
|
||||||
'**/.vscode/',
|
|
||||||
'**/.idea/',
|
|
||||||
'**/tsup.config.ts',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
...compat.extends('plugin:@typescript-eslint/recommended', 'prettier'),
|
...compat.extends('plugin:@typescript-eslint/recommended', 'prettier'),
|
||||||
{
|
{
|
||||||
|
|
5
example-docker.env
Normal file
5
example-docker.env
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Docker configs (MariaDB container)
|
||||||
|
MYSQL_USER=
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=
|
||||||
|
MYSQL_ROOT_PASSWORD=
|
17
example.env
17
example.env
|
@ -1,4 +1,13 @@
|
||||||
MYSQL_USER=
|
# Prisma configs
|
||||||
MYSQL_PASSWORD=
|
# Environment variables declared in this file are automatically made available to Prisma.
|
||||||
MYSQL_DATABASE=
|
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||||
MYSQL_ROOT_PASSWORD=
|
|
||||||
|
# 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=
|
||||||
|
|
5
nodemon.json
Normal file
5
nodemon.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"ext": "ts,json",
|
||||||
|
"exec": "tsc --sourcemap && cross-env NODE_ENV=development node --enable-source-maps dist/src",
|
||||||
|
"ignore": ["dist/**/*.*"]
|
||||||
|
}
|
58
package.json
58
package.json
|
@ -1,47 +1,49 @@
|
||||||
{
|
{
|
||||||
"name": "muffinbot",
|
"name": "muffinbot",
|
||||||
"version": "3.0.1-cake.r240815b",
|
"version": "4.1.0-pudding.r241119a",
|
||||||
"main": "dist/index.js",
|
"main": "dist/src/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sapphire/decorators": "^6.1.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@sapphire/discord.js-utilities": "^7.3.0",
|
"@sapphire/decorators": "^6.1.1",
|
||||||
"@sapphire/framework": "^5.2.1",
|
"@sapphire/discord.js-utilities": "^7.3.1",
|
||||||
|
"@sapphire/framework": "^5.3.1",
|
||||||
"@sapphire/pieces": "^4.3.1",
|
"@sapphire/pieces": "^4.3.1",
|
||||||
"@sapphire/utilities": "^3.17.0",
|
"@sapphire/utilities": "^3.18.1",
|
||||||
"discord-api-types": "^0.37.93",
|
"discord-api-types": "^0.37.105",
|
||||||
"discord.js": "^14.15.3",
|
"discord.js": "^14.16.3",
|
||||||
"dokdo": "^0.6.2",
|
"dokdo": "^1.0.1",
|
||||||
"mysql2": "^3.11.0",
|
"dotenv": "^16.4.5",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3"
|
||||||
"undici": "^6.19.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.15.0",
|
||||||
"@migan/prettier-config": "^1.2.0",
|
"@migan/prettier-config": "^1.2.0",
|
||||||
"@types/node": "^20.14.12",
|
"@types/node": "^22.9.0",
|
||||||
"@types/semver": "^7",
|
"@types/semver": "^7.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^7.17.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"globals": "^15.8.0",
|
"globals": "^15.12.0",
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"ts-node": "^10.9.2",
|
"prisma": "^5.22.0",
|
||||||
"tsup": "^8.2.3",
|
"typescript": "^5.6.3"
|
||||||
"typescript": "^5.5.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsc",
|
||||||
"dev": "cross-env NODE_ENV=development tsup --watch --onSuccess \"node --enable-source-maps dist\"",
|
"dev": "cross-env NODE_ENV=development nodemon",
|
||||||
"start": "cross-env NODE_ENV=production node dist"
|
"start": "cross-env NODE_ENV=production node dist/src",
|
||||||
|
"db:pull": "prisma db pull",
|
||||||
|
"db:push": "prisma db push",
|
||||||
|
"db:generate": "prisma generate"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/ws": "^8.5.11",
|
"@types/ws": "^8.5.11",
|
||||||
"ws": "8.18.0"
|
"ws": "8.18.0"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@4.3.1"
|
|
||||||
}
|
}
|
||||||
|
|
1685
pnpm-lock.yaml
Normal file
1685
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
34
prisma/schema.prisma
Normal file
34
prisma/schema.prisma
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
CREATE TABLE
|
|
||||||
`statement` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`text` varchar(255) DEFAULT 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`)
|
|
||||||
);
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { SapphireClient, container, LogLevel } from '@sapphire/framework'
|
|
||||||
import { GatewayIntentBits, Partials, type Snowflake } from 'discord.js'
|
|
||||||
import { ChatBot, NODE_ENV, MaaDatabase } from './modules'
|
|
||||||
import { version } from '../package.json'
|
|
||||||
import config from '../config.json'
|
|
||||||
import semver from 'semver'
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
if (release.startsWith('d')) {
|
|
||||||
container.release = 'DEV'
|
|
||||||
} else if (release.startsWith('p')) {
|
|
||||||
container.release = 'PRE-RELEASE'
|
|
||||||
} else {
|
|
||||||
container.release = 'RELEASE'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MuffinBot extends SapphireClient {
|
|
||||||
public constructor() {
|
|
||||||
super({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
],
|
|
||||||
loadMessageCommandListeners: true,
|
|
||||||
defaultPrefix: container.prefix,
|
|
||||||
logger: {
|
|
||||||
level: NODE_ENV === 'development' ? LogLevel.Debug : LogLevel.Info,
|
|
||||||
},
|
|
||||||
allowedMentions: {
|
|
||||||
users: [],
|
|
||||||
roles: [],
|
|
||||||
repliedUser: true,
|
|
||||||
},
|
|
||||||
partials: [Partials.Message, Partials.ThreadMember],
|
|
||||||
baseUserDirectory: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async login(): Promise<string> {
|
|
||||||
await container.chatBot.train(this)
|
|
||||||
return super.login(config.bot.token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@sapphire/framework' {
|
|
||||||
interface Container {
|
|
||||||
database: MaaDatabase
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
release: 'DEV' | 'PRE-RELEASE' | 'RELEASE'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@sapphire/framework' {
|
|
||||||
interface DetailedDescriptionCommandObject {
|
|
||||||
usage: string
|
|
||||||
examples?: string[]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import './learning_data'
|
|
||||||
import './deleteLearn'
|
|
||||||
import './learn'
|
|
||||||
import './help'
|
|
||||||
import './list'
|
|
|
@ -1,86 +0,0 @@
|
||||||
import {
|
|
||||||
Args,
|
|
||||||
Command,
|
|
||||||
container,
|
|
||||||
DetailedDescriptionCommandObject,
|
|
||||||
} from '@sapphire/framework'
|
|
||||||
import {
|
|
||||||
type SelectMenuComponentOptionData,
|
|
||||||
type Message,
|
|
||||||
ComponentType,
|
|
||||||
codeBlock,
|
|
||||||
} from 'discord.js'
|
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
|
||||||
import { type LearnData } from '../modules'
|
|
||||||
|
|
||||||
@ApplyOptions<Command.Options>({
|
|
||||||
name: '삭제',
|
|
||||||
aliases: ['지워', '잊어'],
|
|
||||||
description: '배운 단어를 삭ㅈ제해요.',
|
|
||||||
detailedDescription: {
|
|
||||||
usage: '머핀아 삭제 (삭제할 단어)',
|
|
||||||
examples: ['머핀아 삭제 머핀'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
class DeleteLearnCommand extends Command {
|
|
||||||
public async messageRun(msg: Message, args: Args) {
|
|
||||||
const command = await args.rest('string').catch(() => null)
|
|
||||||
const options: SelectMenuComponentOptionData[] = []
|
|
||||||
const db = this.container.database
|
|
||||||
const [datas] = await db.database.execute<LearnData[]>(
|
|
||||||
'SELECT * FROM learn WHERE command = ? AND user_id = ?;',
|
|
||||||
[command, msg.author.id],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!command) {
|
|
||||||
return await msg.channel.send(
|
|
||||||
`사용법: \n\`\`\`${(this.detailedDescription as DetailedDescriptionCommandObject).usage}\`\`\``,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!datas) {
|
|
||||||
return await msg.channel.send('해당하는 걸 찾ㅈ을 수 없어요.')
|
|
||||||
}
|
|
||||||
|
|
||||||
datas.forEach(data => {
|
|
||||||
console.log(data)
|
|
||||||
options.push({
|
|
||||||
label: `${data.id}번`,
|
|
||||||
value: `maa$deleteLearn-${data.id}`,
|
|
||||||
description: data.result.slice(0, 100),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await msg.reply({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: '삭제',
|
|
||||||
description: `${codeBlock(
|
|
||||||
'md',
|
|
||||||
datas.map(data => `${data.id}. ${data.result}`).join('\n'),
|
|
||||||
)}`,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: ComponentType.ActionRow,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: ComponentType.StringSelect,
|
|
||||||
customId: 'maa$deleteLearn',
|
|
||||||
placeholder: '지울 데이터를 선택해ㅈ주세요',
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: DeleteLearnCommand,
|
|
||||||
name: 'delete',
|
|
||||||
store: 'commands',
|
|
||||||
})
|
|
|
@ -1,82 +0,0 @@
|
||||||
import { type Args, Command, container } from '@sapphire/framework'
|
|
||||||
import { codeBlock, type Message } from 'discord.js'
|
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
|
||||||
|
|
||||||
@ApplyOptions<Command.Options>({
|
|
||||||
name: '배워',
|
|
||||||
aliases: ['공부'],
|
|
||||||
description: '단어를 가르치는 명령ㅇ어에요.',
|
|
||||||
detailedDescription: {
|
|
||||||
usage: '머핀아 배워 (등록할 단어) (대답)',
|
|
||||||
examples: [
|
|
||||||
'머핀아 배워 안녕 안녕!',
|
|
||||||
'머핀아 배워 "야 죽을래?" "아니요 ㅠㅠㅠ"',
|
|
||||||
'머핀아 배워 미간은_누구야? 이봇의_개발자요',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
class LearnCommand extends Command {
|
|
||||||
public async messageRun(msg: Message, args: Args) {
|
|
||||||
if (typeof this.detailedDescription === 'string') return
|
|
||||||
const config = this.container.config
|
|
||||||
const command = (await args.pick('string').catch(() => null))?.replaceAll(
|
|
||||||
'_',
|
|
||||||
' ',
|
|
||||||
)
|
|
||||||
const result = (await args.pick('string').catch(() => null))?.replaceAll(
|
|
||||||
'_',
|
|
||||||
' ',
|
|
||||||
)
|
|
||||||
if (!command || !result) {
|
|
||||||
return await msg.reply(
|
|
||||||
codeBlock(
|
|
||||||
'md',
|
|
||||||
`사용법: ${this.detailedDescription}
|
|
||||||
예시: ${this.detailedDescription.examples?.map(example => example).join('\n')}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const commands: string[] = []
|
|
||||||
const aliases: string[] = []
|
|
||||||
this.container.stores.get('commands').forEach(module => {
|
|
||||||
commands.push(module.name)
|
|
||||||
module.aliases.forEach(alias => aliases.push(alias))
|
|
||||||
})
|
|
||||||
const ignore = [
|
|
||||||
...commands,
|
|
||||||
...aliases,
|
|
||||||
...this.container.dokdoAliases,
|
|
||||||
'미간',
|
|
||||||
'Migan',
|
|
||||||
'migan',
|
|
||||||
'간미',
|
|
||||||
]
|
|
||||||
const disallowed = ['@everyone', '@here', `<@${config.bot.owner_ID}>`]
|
|
||||||
const db = this.container.database
|
|
||||||
|
|
||||||
for (const ig of ignore) {
|
|
||||||
if (command.includes(ig)) {
|
|
||||||
return msg.reply('해ㄷ당 단어는 배울ㄹ 수 없어요.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const di of disallowed) {
|
|
||||||
if (result.includes(di)) {
|
|
||||||
return msg.reply('해당 단ㅇ어는 개발자님이 특별히 금지하였ㅇ어요.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.learn.insert({
|
|
||||||
user_id: msg.author.id,
|
|
||||||
command,
|
|
||||||
result,
|
|
||||||
})
|
|
||||||
await msg.reply(`${command}을/를 배웠ㅇ어요.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: LearnCommand,
|
|
||||||
name: 'learn',
|
|
||||||
store: 'commands',
|
|
||||||
})
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
|
||||||
import { type ResponseData } from '../modules'
|
|
||||||
import { Command, container } from '@sapphire/framework'
|
|
||||||
import { type Message } from 'discord.js'
|
|
||||||
|
|
||||||
@ApplyOptions<Command.Options>({
|
|
||||||
name: '데이터학습량',
|
|
||||||
aliases: ['학습데이터량', '데이터량'],
|
|
||||||
description: '봇이 학습한 데ㅇ이터량을 보여줘요.',
|
|
||||||
detailedDescription: {
|
|
||||||
usage: '머핀아 학습데이터량',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
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[] = []
|
|
||||||
data.forEach(row => {
|
|
||||||
if (row.persona === 'muffin') muffin.push(row)
|
|
||||||
else return
|
|
||||||
})
|
|
||||||
|
|
||||||
await msg.reply(`머핀 데이터: ${muffin.length}개
|
|
||||||
nsfw 데이터: ${nsfwData.length}개
|
|
||||||
지금까지 배운 단어: ${learnData.length}개
|
|
||||||
${msg.author.username}님이 가르쳐준 단어: ${userData.length}개`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: LearnDataCommand,
|
|
||||||
name: 'learn_data',
|
|
||||||
store: 'commands',
|
|
||||||
})
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
|
||||||
import { Message, codeBlock } from 'discord.js'
|
|
||||||
import { Command, container } from '@sapphire/framework'
|
|
||||||
|
|
||||||
@ApplyOptions<Command.Options>({
|
|
||||||
name: '리스트',
|
|
||||||
aliases: ['list', '목록'],
|
|
||||||
description: '당신이 가ㄹ르쳐준 단어를 나열해요.',
|
|
||||||
detailedDescription: {
|
|
||||||
usage: '머핀아 리스트',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
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 list: string[] = []
|
|
||||||
|
|
||||||
if (!data[0]) {
|
|
||||||
return await msg.channel.send(
|
|
||||||
'당신ㄴ은 단어를 가르쳐준 기억이 없ㅅ는데요.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const listData of data) {
|
|
||||||
list.push(listData.command)
|
|
||||||
}
|
|
||||||
|
|
||||||
await msg.reply({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: `${msg.author.username}님의 지식`,
|
|
||||||
description: `총합: ${data.length}개\n${codeBlock(
|
|
||||||
'md',
|
|
||||||
list.map(item => `- ${item}`).join('\n'),
|
|
||||||
)}`,
|
|
||||||
color: 0x0000ff,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: ListCommand,
|
|
||||||
name: 'list',
|
|
||||||
store: 'commands',
|
|
||||||
})
|
|
107
src/client.ts
Normal file
107
src/client.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { SapphireClient, container, LogLevel } from '@sapphire/framework'
|
||||||
|
import { GatewayIntentBits, Partials } from 'discord.js'
|
||||||
|
import { ChatBot, Config, NODE_ENV } from './modules'
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { version } from '../package.json'
|
||||||
|
import semver from 'semver'
|
||||||
|
|
||||||
|
const config = new Config()
|
||||||
|
|
||||||
|
const release = version
|
||||||
|
.slice((semver.coerce(version)?.toString() + '-').length)
|
||||||
|
.split('.')[1]
|
||||||
|
|
||||||
|
function getLastUpdated() {
|
||||||
|
const updated = release.match(/[0-9]/g)!.join('')
|
||||||
|
const year = updated.slice(0, 2)
|
||||||
|
const month = updated.slice(2, 4)
|
||||||
|
const day = updated.slice(4, 6)
|
||||||
|
return `20${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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(getLastUpdated())
|
||||||
|
container.embedColors = {
|
||||||
|
default: 0xaddb87,
|
||||||
|
fail: 0xff0000,
|
||||||
|
success: 0x00ff00,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (release.startsWith('e')) {
|
||||||
|
container.channel = 'EXPERIMENTAL'
|
||||||
|
} else if (release.startsWith('d')) {
|
||||||
|
container.channel = 'DEV'
|
||||||
|
} else if (release.startsWith('p')) {
|
||||||
|
container.channel = 'PREVIEW'
|
||||||
|
} else {
|
||||||
|
container.channel = 'RELEASE'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MuffinBot extends SapphireClient {
|
||||||
|
public constructor() {
|
||||||
|
super({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
],
|
||||||
|
loadMessageCommandListeners: true,
|
||||||
|
defaultPrefix: container.prefix,
|
||||||
|
logger: {
|
||||||
|
level: NODE_ENV === 'development' ? LogLevel.Debug : LogLevel.Info,
|
||||||
|
},
|
||||||
|
allowedMentions: {
|
||||||
|
users: [],
|
||||||
|
roles: [],
|
||||||
|
repliedUser: true,
|
||||||
|
},
|
||||||
|
partials: [Partials.Message, Partials.ThreadMember],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async login(): Promise<string> {
|
||||||
|
if (container.channel === 'RELEASE') {
|
||||||
|
if (!config.train.user_ID)
|
||||||
|
container.logger.info(
|
||||||
|
'[MuffinBot] .env파일에 TRAIN_USER_ID값이 없어서 학습 기능이 꺼졌어요.',
|
||||||
|
)
|
||||||
|
else await container.chatBot.train(this)
|
||||||
|
} else {
|
||||||
|
container.logger.info(
|
||||||
|
'[MuffinBot] 해당 채널은 RELEASE 채널이 아니라서 학습 기능이 꺼졌습니다.',
|
||||||
|
)
|
||||||
|
container.logger.info(`[MuffinBot] 현재 채널: ${container.channel}`)
|
||||||
|
}
|
||||||
|
return super.login(config.bot.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/framework' {
|
||||||
|
interface Container {
|
||||||
|
database: PrismaClient
|
||||||
|
chatBot: ChatBot
|
||||||
|
prefix: string
|
||||||
|
version: string
|
||||||
|
dokdoAliases: string[]
|
||||||
|
config: Config
|
||||||
|
channel: 'EXPERIMENTAL' | 'DEV' | 'PREVIEW' | 'RELEASE'
|
||||||
|
lastUpdated: Date
|
||||||
|
embedColors: {
|
||||||
|
default: number
|
||||||
|
fail: number
|
||||||
|
success: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@sapphire/framework' {
|
||||||
|
interface DetailedDescriptionCommandObject {
|
||||||
|
usage: string
|
||||||
|
examples?: string[]
|
||||||
|
}
|
||||||
|
}
|
130
src/commands/deleteLearn.ts
Normal file
130
src/commands/deleteLearn.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import {
|
||||||
|
type SelectMenuComponentOptionData,
|
||||||
|
type User,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
ComponentType,
|
||||||
|
codeBlock,
|
||||||
|
Message,
|
||||||
|
ButtonStyle,
|
||||||
|
} from 'discord.js'
|
||||||
|
import {
|
||||||
|
DetailedDescriptionCommandObject,
|
||||||
|
Command,
|
||||||
|
Args,
|
||||||
|
} from '@sapphire/framework'
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
name: '삭제',
|
||||||
|
aliases: ['지워', '잊어'],
|
||||||
|
description: '배운 단어를 삭ㅈ제해요.',
|
||||||
|
detailedDescription: {
|
||||||
|
usage: '머핀아 삭제 (삭제할 단어)',
|
||||||
|
examples: ['머핀아 삭제 머핀'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class DeleteLearnCommand extends Command {
|
||||||
|
public registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand(builder =>
|
||||||
|
builder
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option
|
||||||
|
.setRequired(true)
|
||||||
|
.setName('단어')
|
||||||
|
.setDescription('삭제할 단어를 입력해주세요.'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _run(ctx: Message | ChatInputCommandInteraction, args?: Args) {
|
||||||
|
let command: string | null
|
||||||
|
let user: User
|
||||||
|
if (ctx instanceof Message) {
|
||||||
|
command = await args!.rest('string').catch(() => null)
|
||||||
|
user = ctx.author
|
||||||
|
} else {
|
||||||
|
command = ctx.options.getString('단어', true)
|
||||||
|
user = ctx.user
|
||||||
|
}
|
||||||
|
|
||||||
|
const ephemeral =
|
||||||
|
ctx instanceof ChatInputCommandInteraction ? { ephemeral: true } : null
|
||||||
|
const CUSTOM_ID = 'maa$deleteLearn'
|
||||||
|
const options: SelectMenuComponentOptionData[] = []
|
||||||
|
const deleteDataList: string[] = []
|
||||||
|
if (!command) {
|
||||||
|
return await ctx.reply(
|
||||||
|
`사용법: \n\`\`\`${(this.detailedDescription as DetailedDescriptionCommandObject).usage}\`\`\``,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDatas = await this.container.database.learn.findMany({
|
||||||
|
where: {
|
||||||
|
command,
|
||||||
|
user_id: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (deleteDatas.length === 0) {
|
||||||
|
return await ctx.reply({
|
||||||
|
...ephemeral,
|
||||||
|
content: '해당하는 걸 찾ㅈ을 수 없어요.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= deleteDatas.length; i++) {
|
||||||
|
deleteDataList.push(`${i}. ${deleteDatas[i - 1].result}`)
|
||||||
|
options.push({
|
||||||
|
label: `${i}번 지식`,
|
||||||
|
value: `${CUSTOM_ID}-${deleteDatas[i - 1].id}`,
|
||||||
|
description: deleteDatas[i - 1].result.slice(0, 100),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.reply({
|
||||||
|
...ephemeral,
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: '삭제',
|
||||||
|
description: `${codeBlock('md', deleteDataList.join('\n'))}`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
color: this.container.embedColors.default,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.StringSelect,
|
||||||
|
customId: `${CUSTOM_ID}@${user.id}`,
|
||||||
|
placeholder: '지울 데이터를 선택해ㅈ주세요',
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
customId: `${CUSTOM_ID}-cancel`,
|
||||||
|
label: '취소',
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async messageRun(msg: Message, args: Args) {
|
||||||
|
await this._run(msg, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
|
await this._run(interaction)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
import { Args, Command, container } from '@sapphire/framework'
|
|
||||||
import { codeBlock, type Message } from 'discord.js'
|
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import { Args, Command } from '@sapphire/framework'
|
||||||
|
import {
|
||||||
|
type ChatInputCommandInteraction,
|
||||||
|
codeBlock,
|
||||||
|
Message,
|
||||||
|
} from 'discord.js'
|
||||||
|
|
||||||
@ApplyOptions<Command.Options>({
|
@ApplyOptions<Command.Options>({
|
||||||
name: '도움말',
|
name: '도움말',
|
||||||
|
@ -11,9 +15,33 @@ import { ApplyOptions } from '@sapphire/decorators'
|
||||||
examples: ['머핀아 도움말', '머핀아 도움말 배워'],
|
examples: ['머핀아 도움말', '머핀아 도움말 배워'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
class HelpCommand extends Command {
|
export default class HelpCommand extends Command {
|
||||||
public async messageRun(msg: Message, args: Args) {
|
public registerApplicationCommands(registry: Command.Registry) {
|
||||||
const commandName = await args.pick('string').catch(() => null)
|
const commands = this.container.stores.get('commands').map(command => {
|
||||||
|
return {
|
||||||
|
name: command.name,
|
||||||
|
value: command.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
registry.registerChatInputCommand(builder =>
|
||||||
|
builder
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option
|
||||||
|
.setName('명령어')
|
||||||
|
.setDescription('해당 명령어에 대ㅎ한 도움말을 볼 수 있어요.')
|
||||||
|
.addChoices(commands),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private async _run(ctx: Message | ChatInputCommandInteraction, args?: Args) {
|
||||||
|
let commandName: string | null
|
||||||
|
if (ctx instanceof Message) {
|
||||||
|
commandName = await args!.pick('string').catch(() => null)
|
||||||
|
} else {
|
||||||
|
commandName = ctx.options.getString('명령어')
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!commandName ||
|
!commandName ||
|
||||||
!this.container.stores.get('commands').get(commandName)
|
!this.container.stores.get('commands').get(commandName)
|
||||||
|
@ -24,7 +52,7 @@ class HelpCommand extends Command {
|
||||||
commandList.push(`${module.name} - ${module.description}`)
|
commandList.push(`${module.name} - ${module.description}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
await msg.reply({
|
await ctx.reply({
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: `${this.container.client.user?.username}의 도움말`,
|
title: `${this.container.client.user?.username}의 도움말`,
|
||||||
|
@ -35,6 +63,7 @@ class HelpCommand extends Command {
|
||||||
footer: {
|
footer: {
|
||||||
text: `머핀봇 버전: ${this.container.version}`,
|
text: `머핀봇 버전: ${this.container.version}`,
|
||||||
},
|
},
|
||||||
|
color: this.container.embedColors.default,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -44,7 +73,7 @@ class HelpCommand extends Command {
|
||||||
this.container.stores.get('commands').get(commandName)!
|
this.container.stores.get('commands').get(commandName)!
|
||||||
if (typeof detailedDescription === 'string') return
|
if (typeof detailedDescription === 'string') return
|
||||||
|
|
||||||
await msg.reply({
|
await ctx.reply({
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: `${this.container.client.user?.username}의 도움말`,
|
title: `${this.container.client.user?.username}의 도움말`,
|
||||||
|
@ -81,15 +110,17 @@ class HelpCommand extends Command {
|
||||||
text: `머핀봇 버전: ${this.container.version}`,
|
text: `머핀봇 버전: ${this.container.version}`,
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
color: this.container.embedColors.default,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async messageRun(msg: Message, args: Args) {
|
||||||
|
await this._run(msg, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
public async chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
piece: HelpCommand,
|
await this._run(interaction)
|
||||||
name: 'help',
|
}
|
||||||
store: 'commands',
|
}
|
||||||
})
|
|
76
src/commands/information.ts
Normal file
76
src/commands/information.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import type { ChatInputCommandInteraction, Message } from 'discord.js'
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import { Command } from '@sapphire/framework'
|
||||||
|
import { platform, arch } from 'os'
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
name: '정보',
|
||||||
|
description: '머핀봇의 정보를 알ㄹ려줘요.',
|
||||||
|
detailedDescription: {
|
||||||
|
usage: '머핀아 정보',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class InformationCommand extends Command {
|
||||||
|
public registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand(builder =>
|
||||||
|
builder.setName(this.name).setDescription(this.description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _run(ctx: Message | ChatInputCommandInteraction) {
|
||||||
|
await ctx.reply({
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: `${this.container.client.user?.username}의 정ㅂ보`,
|
||||||
|
color: this.container.embedColors.default,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: '구동ㅎ환경',
|
||||||
|
value: `\`${platform()} ${arch()}\``,
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '버ㅈ전',
|
||||||
|
value: `\`${this.container.version}\``,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '채ㄴ널',
|
||||||
|
value: `\`${this.container.channel.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()!,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async messageRun(msg: Message) {
|
||||||
|
await this._run(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
|
await this._run(interaction)
|
||||||
|
}
|
||||||
|
}
|
128
src/commands/learn.ts
Normal file
128
src/commands/learn.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import { ChatInputCommandInteraction, codeBlock, Message } from 'discord.js'
|
||||||
|
import { type Args, Command } from '@sapphire/framework'
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
name: '배워',
|
||||||
|
aliases: ['공부'],
|
||||||
|
description: '단어를 가르치는 명령ㅇ어에요.',
|
||||||
|
detailedDescription: {
|
||||||
|
usage: '머핀아 배워 (등록할 단어) (대답)',
|
||||||
|
examples: [
|
||||||
|
'머핀아 배워 안녕 안녕!',
|
||||||
|
'머핀아 배워 "야 죽을래?" "아니요 ㅠㅠㅠ"',
|
||||||
|
'머핀아 배워 미간은_누구야? 이봇의_개발자요',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class LearnCommand extends Command {
|
||||||
|
public registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand(builder =>
|
||||||
|
builder
|
||||||
|
.setName(this.name)
|
||||||
|
.setDescription(this.description)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option
|
||||||
|
.setRequired(true)
|
||||||
|
.setName('단어')
|
||||||
|
.setDescription('등록할 단어를 입력해주세요.'),
|
||||||
|
)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option
|
||||||
|
.setRequired(true)
|
||||||
|
.setName('대답')
|
||||||
|
.setDescription('해당 단어의 대답을 입력해주세요.'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _run(ctx: Message | ChatInputCommandInteraction, args?: Args) {
|
||||||
|
if (ctx instanceof ChatInputCommandInteraction) await ctx.deferReply()
|
||||||
|
if (typeof this.detailedDescription === 'string') return
|
||||||
|
|
||||||
|
const config = this.container.config
|
||||||
|
const IG_MSG = '해ㄷ당 단어는 배울ㄹ 수 없어요.'
|
||||||
|
const DI_MSG = '해당 단ㅇ어는 개발자님이 특별히 금지하였ㅇ어요.'
|
||||||
|
const SUCCESS_MSG = '을/를 배웠ㅇ어요.'
|
||||||
|
|
||||||
|
let command: string | undefined
|
||||||
|
let result: string | undefined
|
||||||
|
|
||||||
|
if (ctx instanceof Message) {
|
||||||
|
command = (await args?.pick('string').catch(() => null))?.replaceAll(
|
||||||
|
'_',
|
||||||
|
' ',
|
||||||
|
)
|
||||||
|
result = (await args?.pick('string').catch(() => null))?.replaceAll(
|
||||||
|
'_',
|
||||||
|
' ',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
command = ctx.options.getString('단어', true)
|
||||||
|
result = ctx.options.getString('대답', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command || !result)
|
||||||
|
return await ctx.reply(
|
||||||
|
codeBlock(
|
||||||
|
'md',
|
||||||
|
`사용법: ${this.detailedDescription.usage}\n` +
|
||||||
|
`예시: ${this.detailedDescription.examples?.map(example => example).join('\n')}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const commands: string[] = []
|
||||||
|
const aliases: string[] = []
|
||||||
|
|
||||||
|
this.container.stores.get('commands').forEach(module => {
|
||||||
|
commands.push(module.name)
|
||||||
|
module.aliases.forEach(alias => aliases.push(alias))
|
||||||
|
})
|
||||||
|
|
||||||
|
const ignore = [
|
||||||
|
...commands,
|
||||||
|
...aliases,
|
||||||
|
...this.container.dokdoAliases,
|
||||||
|
'미간',
|
||||||
|
'Migan',
|
||||||
|
'migan',
|
||||||
|
'간미',
|
||||||
|
]
|
||||||
|
const disallowed = ['@everyone', '@here', `<@${config.bot.owner_ID}>`]
|
||||||
|
const user = ctx instanceof Message ? ctx.author : ctx.user
|
||||||
|
|
||||||
|
for (const ig of ignore) {
|
||||||
|
if (command.includes(ig))
|
||||||
|
return ctx instanceof Message
|
||||||
|
? await ctx.reply(IG_MSG)
|
||||||
|
: await ctx.editReply(IG_MSG)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const di of disallowed) {
|
||||||
|
if (result.includes(di))
|
||||||
|
return ctx instanceof Message
|
||||||
|
? await ctx.reply(DI_MSG)
|
||||||
|
: await ctx.editReply(DI_MSG)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.container.database.learn.create({
|
||||||
|
data: {
|
||||||
|
user_id: user.id,
|
||||||
|
command,
|
||||||
|
result,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx instanceof Message
|
||||||
|
? await ctx.reply(command + SUCCESS_MSG)
|
||||||
|
: await ctx.editReply(command + SUCCESS_MSG)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async messageRun(msg: Message, args: Args) {
|
||||||
|
await this._run(msg, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
|
await this._run(interaction)
|
||||||
|
}
|
||||||
|
}
|
50
src/commands/learnedData.ts
Normal file
50
src/commands/learnedData.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { ChatInputCommandInteraction, Message } from 'discord.js'
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import { Command } from '@sapphire/framework'
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
name: '데이터학습량',
|
||||||
|
aliases: ['학습데이터량', '데이터량'],
|
||||||
|
description: '봇이 학습한 데ㅇ이터량을 보여줘요.',
|
||||||
|
detailedDescription: {
|
||||||
|
usage: '머핀아 학습데이터량',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class LearnedDataCommand extends Command {
|
||||||
|
public registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand(builder =>
|
||||||
|
builder.setName(this.name).setDescription(this.description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _run(ctx: Message | ChatInputCommandInteraction) {
|
||||||
|
const user = ctx instanceof Message ? ctx.author : ctx.user
|
||||||
|
const db = this.container.database
|
||||||
|
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: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const muffin: any[] = []
|
||||||
|
data.forEach(row => {
|
||||||
|
if (row.persona === 'muffin') muffin.push(row)
|
||||||
|
else return
|
||||||
|
})
|
||||||
|
|
||||||
|
await ctx.reply(`머핀 데이터: ${muffin.length}개
|
||||||
|
nsfw 데이터: ${nsfwData.length}개
|
||||||
|
지금까지 배운 단어: ${learnData.length}개
|
||||||
|
${user.username}님이 가르쳐준 단어: ${userData.length}개`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async messageRun(msg: Message) {
|
||||||
|
await this._run(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
|
await this._run(interaction)
|
||||||
|
}
|
||||||
|
}
|
65
src/commands/list.ts
Normal file
65
src/commands/list.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { ChatInputCommandInteraction, Message, codeBlock } from 'discord.js'
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import { Command } from '@sapphire/framework'
|
||||||
|
|
||||||
|
@ApplyOptions<Command.Options>({
|
||||||
|
name: '리스트',
|
||||||
|
aliases: ['list', '목록'],
|
||||||
|
description: '당신이 가ㄹ르쳐준 단어를 나열해요.',
|
||||||
|
detailedDescription: {
|
||||||
|
usage: '머핀아 리스트',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class ListCommand extends Command {
|
||||||
|
public registerApplicationCommands(registry: Command.Registry) {
|
||||||
|
registry.registerChatInputCommand(builder =>
|
||||||
|
builder.setName(this.name).setDescription(this.description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _run(ctx: Message | ChatInputCommandInteraction) {
|
||||||
|
const user = ctx instanceof Message ? ctx.author : ctx.user
|
||||||
|
const ephemeral =
|
||||||
|
ctx instanceof ChatInputCommandInteraction ? { ephemeral: true } : null
|
||||||
|
const db = this.container.database
|
||||||
|
const data = await db.learn.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const list: string[] = []
|
||||||
|
|
||||||
|
if (!data[0]) {
|
||||||
|
return await ctx.reply({
|
||||||
|
...ephemeral,
|
||||||
|
content: '당신ㄴ은 단어를 가르쳐준 기억이 없ㅅ는데요.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listData of data) {
|
||||||
|
list.push(listData.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.reply({
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: `${user.username}님의 지식`,
|
||||||
|
description: `총합: ${data.length}개\n${codeBlock(
|
||||||
|
'md',
|
||||||
|
list.map(item => `- ${item}`).join('\n'),
|
||||||
|
)}`,
|
||||||
|
color: this.container.embedColors.default,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async messageRun(msg: Message<boolean>) {
|
||||||
|
await this._run(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async chatInputRun(interaction: ChatInputCommandInteraction) {
|
||||||
|
await this._run(interaction)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
import('./Client').then(a => new a.default().login())
|
import('./client').then(a => new a.default().login())
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
import './deleteLearn'
|
|
|
@ -1,43 +1,56 @@
|
||||||
import {
|
|
||||||
container,
|
|
||||||
InteractionHandler,
|
|
||||||
InteractionHandlerTypes,
|
|
||||||
} from '@sapphire/framework'
|
|
||||||
import { type StringSelectMenuInteraction } from 'discord.js'
|
import { type StringSelectMenuInteraction } from 'discord.js'
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import {
|
||||||
|
InteractionHandlerTypes,
|
||||||
|
InteractionHandler,
|
||||||
|
} from '@sapphire/framework'
|
||||||
|
|
||||||
@ApplyOptions<InteractionHandler.Options>({
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
interactionHandlerType: InteractionHandlerTypes.SelectMenu,
|
interactionHandlerType: InteractionHandlerTypes.SelectMenu,
|
||||||
})
|
})
|
||||||
class DeleteLearnHandler extends InteractionHandler {
|
export default class DeleteLearnHandler extends InteractionHandler {
|
||||||
|
private readonly _CUSTOM_ID = 'maa$deleteLearn'
|
||||||
|
|
||||||
public async parse(interaction: StringSelectMenuInteraction) {
|
public async parse(interaction: StringSelectMenuInteraction) {
|
||||||
if (interaction.customId !== 'maa$deleteLearn') return this.none()
|
if (!interaction.customId.startsWith(this._CUSTOM_ID)) return this.none()
|
||||||
|
const userId = interaction.customId.slice(`${this._CUSTOM_ID}@`.length)
|
||||||
|
if (interaction.user.id !== userId) {
|
||||||
|
await interaction.reply({
|
||||||
|
ephemeral: true,
|
||||||
|
content: '당신은 이 지ㅅ식을 안 가르쳐 주셨어요.',
|
||||||
|
})
|
||||||
|
return this.none()
|
||||||
|
}
|
||||||
return this.some()
|
return this.some()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(interaction: StringSelectMenuInteraction) {
|
public async run(interaction: StringSelectMenuInteraction) {
|
||||||
await interaction.deferUpdate()
|
await interaction.deferUpdate()
|
||||||
|
|
||||||
const id = interaction.values[0].slice('maa$deleteLearn-'.length)
|
const id = interaction.values[0].slice(`${this._CUSTOM_ID}-`.length)
|
||||||
const db = this.container.database
|
const db = this.container.database
|
||||||
|
const decimalRegexp = /^[0-9]/g
|
||||||
|
|
||||||
await db.learn.delete(id)
|
const itemId = interaction.component.options.map(item =>
|
||||||
|
item.value.endsWith(`${id}`) ? item.label.match(decimalRegexp)![0] : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.learn.delete({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: '삭제',
|
title: '삭제',
|
||||||
description: `${id}번을 정상적으로 삭제하ㅇ였어요.`,
|
description: `${Number(itemId[0]!)}번을 정상적으로 삭제하ㅇ였어요.`,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
color: this.container.embedColors.success,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
components: [],
|
components: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: DeleteLearnHandler,
|
|
||||||
name: 'deleteLearn',
|
|
||||||
store: 'interaction-handlers',
|
|
||||||
})
|
|
||||||
|
|
31
src/interaction-handlers/deleteLearnCancel.ts
Normal file
31
src/interaction-handlers/deleteLearnCancel.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
|
import {
|
||||||
|
InteractionHandler,
|
||||||
|
InteractionHandlerTypes,
|
||||||
|
} from '@sapphire/framework'
|
||||||
|
import { ButtonInteraction } from 'discord.js'
|
||||||
|
|
||||||
|
@ApplyOptions<InteractionHandler.Options>({
|
||||||
|
interactionHandlerType: InteractionHandlerTypes.Button,
|
||||||
|
})
|
||||||
|
export default class DeleteLearnCancelHandler extends InteractionHandler {
|
||||||
|
private _CUSTOM_ID = 'maa$deleteLearn'
|
||||||
|
|
||||||
|
public async parse(interaction: ButtonInteraction) {
|
||||||
|
if (interaction.customId !== `${this._CUSTOM_ID}-cancel`) return this.none()
|
||||||
|
return this.some()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(interaction: ButtonInteraction) {
|
||||||
|
return await interaction.update({
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: '삭제',
|
||||||
|
description: '아무것도 삭제하지 않았어요.',
|
||||||
|
color: this.container.embedColors.fail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
import './messageCreate'
|
|
||||||
import './debug'
|
|
||||||
import './ready'
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { Listener, container } from '@sapphire/framework'
|
import { Listener } from '@sapphire/framework'
|
||||||
|
|
||||||
class DebugListener extends Listener {
|
export default class DebugListener extends Listener {
|
||||||
public async run(debug: string) {
|
public async run(debug: string) {
|
||||||
this.container.logger.debug(`[MuffinBot] ${debug}`)
|
this.container.logger.debug(`[MuffinBot] ${debug}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: DebugListener,
|
|
||||||
name: 'debug',
|
|
||||||
store: 'listeners',
|
|
||||||
})
|
|
||||||
|
|
12
src/listeners/interactionCreate.ts
Normal file
12
src/listeners/interactionCreate.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { ChatInputCommandInteraction } from 'discord.js'
|
||||||
|
import { Listener } from '@sapphire/framework'
|
||||||
|
import { previewWarning } from '../modules'
|
||||||
|
|
||||||
|
export default class InteractionCreateListener extends Listener {
|
||||||
|
public async run(interaction: ChatInputCommandInteraction<'cached'>) {
|
||||||
|
if (interaction.isChatInputCommand()) {
|
||||||
|
if (this.container.channel !== 'RELEASE')
|
||||||
|
await previewWarning(interaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,36 +1,22 @@
|
||||||
import { Listener, container } from '@sapphire/framework'
|
import { noPerm, previewWarning } from '../modules'
|
||||||
|
import { Listener } from '@sapphire/framework'
|
||||||
import { type Message } from 'discord.js'
|
import { type Message } from 'discord.js'
|
||||||
import { noPerm } from '../modules'
|
import { Client } from 'dokdo'
|
||||||
import Dokdo from 'dokdo'
|
|
||||||
|
|
||||||
class MessageCreateListener extends Listener {
|
export default class MessageCreateListener extends Listener {
|
||||||
public async run(msg: Message) {
|
public async run(msg: Message<true>) {
|
||||||
const prefix = this.container.prefix
|
const prefix = this.container.prefix
|
||||||
const dokdo = new Dokdo(this.container.client, {
|
const dokdo = new Client(this.container.client, {
|
||||||
aliases: ['dokdo', 'dok'],
|
aliases: this.container.dokdoAliases,
|
||||||
owners: [this.container.config.bot.owner_ID],
|
owners: [this.container.config.bot.owner_ID],
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
|
secrets: [process.env.DATABASE_URL!],
|
||||||
noPerm,
|
noPerm,
|
||||||
})
|
})
|
||||||
if (msg.author.bot) return
|
if (msg.author.bot) return
|
||||||
if (msg.content.startsWith(prefix)) {
|
if (msg.content.startsWith(prefix)) {
|
||||||
if (this.container.release === 'PRE-RELEASE') {
|
if (this.container.channel !== 'RELEASE') await previewWarning(msg)
|
||||||
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: {
|
|
||||||
text: `현재 브랜치: ${this.container.release.toLowerCase()} 버전: ${this.container.version}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const args = msg.content.slice(prefix.length).trim().split(/ +/g)
|
const args = msg.content.slice(prefix.length).trim().split(/ +/g)
|
||||||
|
|
||||||
this.container.logger.debug(`[ChatBot] command: ${args.join(' ')}`)
|
this.container.logger.debug(`[ChatBot] command: ${args.join(' ')}`)
|
||||||
|
@ -47,9 +33,3 @@ class MessageCreateListener extends Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: MessageCreateListener,
|
|
||||||
name: 'messageCreate',
|
|
||||||
store: 'listeners',
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { container, Listener } from '@sapphire/framework'
|
|
||||||
import { ApplyOptions } from '@sapphire/decorators'
|
import { ApplyOptions } from '@sapphire/decorators'
|
||||||
import { ActivityType, Client } from 'discord.js'
|
import { ActivityType, Client } from 'discord.js'
|
||||||
|
import { Listener } from '@sapphire/framework'
|
||||||
|
|
||||||
@ApplyOptions<Listener.Options>({ once: true })
|
@ApplyOptions<Listener.Options>({ once: true })
|
||||||
class ClientReadyListener extends Listener {
|
export default class ClientReadyListener extends Listener {
|
||||||
public async run(client: Client<true>) {
|
public async run(client: Client<true>) {
|
||||||
function setStatus() {
|
function setStatus() {
|
||||||
client.user.setActivity({
|
client.user.setActivity({
|
||||||
|
@ -18,9 +18,3 @@ class ClientReadyListener extends Listener {
|
||||||
this.container.logger.info(`[MuffinBot] 먹힐 준ㅂ비 완료`)
|
this.container.logger.info(`[MuffinBot] 먹힐 준ㅂ비 완료`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void container.stores.loadPiece({
|
|
||||||
piece: ClientReadyListener,
|
|
||||||
name: 'ready',
|
|
||||||
store: 'listeners',
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import type { Client, Message, TextChannel } from 'discord.js'
|
import type { Client, Message, TextChannel } from 'discord.js'
|
||||||
|
import type { PrismaClient } from '@prisma/client'
|
||||||
import { container } from '@sapphire/framework'
|
import { container } from '@sapphire/framework'
|
||||||
import type { MaaDatabase } from './database'
|
|
||||||
|
|
||||||
export default class ChatBot {
|
export default class ChatBot {
|
||||||
public constructor(public db: MaaDatabase) {
|
public constructor(public db: PrismaClient) {}
|
||||||
setInterval(async () => {
|
|
||||||
this.db.ping()
|
|
||||||
}, 60000)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getResponse(msg: Message): Promise<string> {
|
public async getResponse(msg: Message): Promise<string> {
|
||||||
const prefix = container.prefix
|
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 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 learnData = learn[Math.floor(Math.random() * learn.length)]
|
||||||
const randomNumber = Math.round(Math.random() * (2 - 1) + 1)
|
const randomNumber = Math.round(Math.random() * (2 - 1) + 1)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export default class ChatBot {
|
||||||
|
|
||||||
let response: string
|
let response: string
|
||||||
if ((msg.channel as TextChannel).nsfw) {
|
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]
|
const dataList = [...data, ...NSFWData]
|
||||||
response = dataList[Math.floor(Math.random() * dataList.length)].text
|
response = dataList[Math.floor(Math.random() * dataList.length)].text
|
||||||
} else {
|
} else {
|
||||||
|
@ -50,19 +50,23 @@ export default class ChatBot {
|
||||||
if (msg.author.bot) return
|
if (msg.author.bot) return
|
||||||
if (msg.author.id === container.config.train.user_ID) {
|
if (msg.author.id === container.config.train.user_ID) {
|
||||||
const response = await this.getResponse(msg)
|
const response = await this.getResponse(msg)
|
||||||
await this.db.statement.insert({
|
await this.db.statement.create({
|
||||||
|
data: {
|
||||||
text: msg.content,
|
text: msg.content,
|
||||||
persona: 'muffin',
|
persona: 'muffin',
|
||||||
in_response_to: response,
|
in_response_to: response,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (!(msg.channel as TextChannel).nsfw) return
|
if (!(msg.channel as TextChannel).nsfw) return
|
||||||
if (!msg.content.startsWith(prefix)) return
|
if (!msg.content.startsWith(prefix)) return
|
||||||
const persona = `user:${msg.author.username.slice(0, 50).toLowerCase()}`
|
const persona = `user:${msg.author.username.slice(0, 50).toLowerCase()}`
|
||||||
const text = msg.content.replace(prefix, '')
|
const text = msg.content.replace(prefix, '')
|
||||||
await this.db.nsfwContent.insert({
|
await this.db.nsfw_content.create({
|
||||||
|
data: {
|
||||||
text,
|
text,
|
||||||
persona,
|
persona,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
22
src/modules/config.ts
Normal file
22
src/modules/config.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
function getConfigValue(
|
||||||
|
value: 'BOT_TOKEN' | 'BOT_OWNER_ID' | 'BOT_PREFIX',
|
||||||
|
) {
|
||||||
|
const configValue = process.env[value]
|
||||||
|
if (!configValue)
|
||||||
|
throw new Error(`.env 파일에서 ${value}값을 찾을 수 없어요.`)
|
||||||
|
return configValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MAAConfig {
|
||||||
|
public readonly bot = {
|
||||||
|
token: getConfigValue('BOT_TOKEN'),
|
||||||
|
owner_ID: getConfigValue('BOT_OWNER_ID'),
|
||||||
|
prefix: getConfigValue('BOT_PREFIX'),
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly train = {
|
||||||
|
user_ID: process.env.TRAIN_USER_ID,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from './database'
|
|
||||||
export * from './type'
|
|
||||||
export * from './model'
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from './statement'
|
|
||||||
export * from './learn'
|
|
||||||
export * from './nsfwContent'
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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[]>
|
|
||||||
}
|
|
|
@ -1,14 +1,7 @@
|
||||||
import { ResponseData, NSFWData, LearnData, MaaDatabase } from './database'
|
import previewWarning from './previewWarning'
|
||||||
import { NODE_ENV } from './env'
|
import { NODE_ENV } from './env'
|
||||||
import ChatBot from './ChatBot'
|
import ChatBot from './chatBot'
|
||||||
|
import Config from './config'
|
||||||
import noPerm from './noPerm'
|
import noPerm from './noPerm'
|
||||||
|
|
||||||
export {
|
export { NODE_ENV, ChatBot, noPerm, Config, previewWarning }
|
||||||
ResponseData,
|
|
||||||
MaaDatabase,
|
|
||||||
LearnData,
|
|
||||||
NODE_ENV,
|
|
||||||
ChatBot,
|
|
||||||
NSFWData,
|
|
||||||
noPerm,
|
|
||||||
}
|
|
||||||
|
|
21
src/modules/previewWarning.ts
Normal file
21
src/modules/previewWarning.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import type { ChatInputCommandInteraction, Message } from 'discord.js'
|
||||||
|
import { container } from '@sapphire/framework'
|
||||||
|
|
||||||
|
export default async function previewWarning(
|
||||||
|
ctx: Message<true> | ChatInputCommandInteraction<'cached'>,
|
||||||
|
) {
|
||||||
|
await ctx.channel!.send({
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: '정식 출시 이전 버전 사용안내',
|
||||||
|
description:
|
||||||
|
`현재 이 버전의 ${container.client.user?.username}은 정식출시 되기 이전이라 많이 불안정할 수 있어요.\n` +
|
||||||
|
`만약 오류가 발견되면 ${(await container.client.users.fetch(container.config.bot.owner_ID)).username}님에게 알려주세요.\n`,
|
||||||
|
color: 0xff0000,
|
||||||
|
footer: {
|
||||||
|
text: `현재 채널: ${container.channel.toLowerCase()} 버전: ${container.version}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
import { defineConfig } from 'tsup'
|
|
||||||
|
|
||||||
const sourcemap = process.env.NODE_ENV === 'development' ? true : false
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
clean: true,
|
|
||||||
format: ['cjs'],
|
|
||||||
entry: ['src/index.ts'],
|
|
||||||
minify: true,
|
|
||||||
sourcemap,
|
|
||||||
})
|
|
Loading…
Reference in a new issue