diff --git a/.gitignore b/.gitignore index 53e653a..cdd96a9 100644 --- a/.gitignore +++ b/.gitignore @@ -367,5 +367,8 @@ dist/ *.js *.js.map -# binaries will bot bundle -bin/ \ No newline at end of file +# binaries is not bundled +bin/ + +# Keep enviromental variables out of version control +*.env \ No newline at end of file diff --git a/README.md b/README.md index fa1527e..2fd54d4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Quetza - 2205.2 +# Quetza - 2205.3 > _**Higher perfomance - better design.**_ diff --git a/package-lock.json b/package-lock.json index aafe097..4f58257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "quetza-bot", - "version": "2205.1", + "version": "2205.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "quetza-bot", - "version": "2205.1", + "version": "2205.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@discordjs/opus": "^0.7.0", "@discordjs/voice": "^0.8.0", + "@prisma/client": "^3.14.0", "discord-ytdl-core": "^5.0.4", "discord.js": "^13.6.0", "tweetnacl": "^1.0.3", @@ -22,6 +23,7 @@ "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", "eslint": "^8.16.0", + "prisma": "^3.14.0", "typescript": "^4.5.5" }, "engines": { @@ -228,6 +230,38 @@ "node": ">= 8" } }, + "node_modules/@prisma/client": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.14.0.tgz", + "integrity": "sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + }, + "engines": { + "node": ">=12.6" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz", + "integrity": "sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz", + "integrity": "sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ==" + }, "node_modules/@sapphire/async-queue": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.1.tgz", @@ -1742,6 +1776,23 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.14.0.tgz", + "integrity": "sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=12.6" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -2363,6 +2414,25 @@ "fastq": "^1.6.0" } }, + "@prisma/client": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.14.0.tgz", + "integrity": "sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==", + "requires": { + "@prisma/engines-version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + } + }, + "@prisma/engines": { + "version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz", + "integrity": "sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz", + "integrity": "sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ==" + }, "@sapphire/async-queue": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.1.tgz", @@ -3447,6 +3517,15 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prisma": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.14.0.tgz", + "integrity": "sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw==", + "devOptional": true, + "requires": { + "@prisma/engines": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", diff --git a/package.json b/package.json index 4908dbc..b0ef856 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "node": "16.14.x", "npm": "*" }, - "version": "2205.1", + "version": "2205.3.0", "description": "Discord multifunctional bot", "author": { "name": "unknowableshade", @@ -25,11 +25,12 @@ "start": "node dist/index.js", "build": "tsc --build", "clean": "tsc --build --clean", - "postinstall": "npm run build" + "postinstall": "npm run build && npx prisma generate" }, "dependencies": { "@discordjs/opus": "^0.7.0", "@discordjs/voice": "^0.8.0", + "@prisma/client": "^3.14.0", "discord-ytdl-core": "^5.0.4", "discord.js": "^13.6.0", "tweetnacl": "^1.0.3", @@ -40,6 +41,7 @@ "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", "eslint": "^8.16.0", + "prisma": "^3.14.0", "typescript": "^4.5.5" } } diff --git a/prisma/migrations/20220608191808_init/migration.sql b/prisma/migrations/20220608191808_init/migration.sql new file mode 100644 index 0000000..851b7f5 --- /dev/null +++ b/prisma/migrations/20220608191808_init/migration.sql @@ -0,0 +1,44 @@ +-- CreateTable +CREATE TABLE "Guild" ( + "id" TEXT NOT NULL, + "lang" TEXT NOT NULL DEFAULT E'en', + + CONSTRAINT "Guild_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Member" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "xp" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "guildId" TEXT NOT NULL, + + CONSTRAINT "Member_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Queue" ( + "id" SERIAL NOT NULL, + "title" VARCHAR(32) NOT NULL, + "descriptrion" VARCHAR(128) NOT NULL, + "tracks" TEXT[], + "guildId" TEXT NOT NULL, + "authorId" INTEGER NOT NULL, + + CONSTRAINT "Queue_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Member_userId_guildId_key" ON "Member"("userId", "guildId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Queue_title_guildId_key" ON "Queue"("title", "guildId"); + +-- AddForeignKey +ALTER TABLE "Member" ADD CONSTRAINT "Member_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Queue" ADD CONSTRAINT "Queue_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Queue" ADD CONSTRAINT "Queue_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..7d582fa --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,40 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Guild { + id String @id + lang String @default("en") + members Member[] + queues Queue[] +} + +model Member { + id Int @id @default(autoincrement()) + userId String + xp Float @default(0.0) + guild Guild @relation(fields: [guildId], references: [id]) + guildId String + Queue Queue[] + @@unique([userId, guildId]) +} + +model Queue { + id Int @id @default(autoincrement()) + title String @db.VarChar(32) + descriptrion String @db.VarChar(128) + tracks String[] + author Member @relation(fields: [authorId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + guildId String + authorId Int + @@unique([title, guildId]) +} diff --git a/src/commands/ping.ts b/src/commands/ping.ts new file mode 100644 index 0000000..aaaf4aa --- /dev/null +++ b/src/commands/ping.ts @@ -0,0 +1,23 @@ +import { CommandInteraction } from "discord.js"; +import Client from "../lib/Client"; +import I8n from "../lib/I8n"; + +export async function run(client: Client, ctx: CommandInteraction) { + const start = Date.now(); + + if (!client.application) { + await ctx.reply({ embeds: [I8n.en.ping(undefined)] }); + return; + } + + await client.application.fetch(); + + await ctx.reply({ embeds: [I8n.en.ping(Date.now() - start)] }); +} + +const data = { + name: "ping", + description: "Try to ping me.", +}; + +export { data }; diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 572fc09..0e424b6 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -7,6 +7,7 @@ import path from "path"; import Music from "./Music"; import { command, event } from "./Types"; import config from "../config"; +import { Storage } from "./Storage"; export default class Client extends DiscordClient { private commands = new Map(); @@ -17,6 +18,7 @@ export default class Client extends DiscordClient { public readonly modules = { music: new Music(), + storage: new Storage() }; public constructor() { diff --git a/src/lib/QFetch.ts b/src/lib/Fetch.ts similarity index 97% rename from src/lib/QFetch.ts rename to src/lib/Fetch.ts index f4556f7..8dea95a 100644 --- a/src/lib/QFetch.ts +++ b/src/lib/Fetch.ts @@ -4,8 +4,6 @@ import { promisify } from "util"; import config from "../config"; import path from "path"; -import fetch from "node-fetch"; - const execFileAsync = promisify(execFile); function argumentsResolver(args: ytdlArgs): string[] { diff --git a/src/lib/I8n.ts b/src/lib/I8n.ts index 981ee54..197d4c4 100644 --- a/src/lib/I8n.ts +++ b/src/lib/I8n.ts @@ -47,7 +47,7 @@ export default class I8n { filtered: (filter?: string) => { return new MessageEmbed() .setColor("GREEN") - .setTitle(`**✅ ${filter} was applied to the player.**`); + .setTitle(filter ? `**✅ ${filter} was applied to the player.**` : "**✅ All filters were declined.**"); }, appended: (track?: track) => { return !track @@ -210,7 +210,7 @@ export default class I8n { } else { return new MessageEmbed() .setColor("BLUE") - .setTitle("💿 Insert the disk...") + .setTitle("💿 Insert the disk...") .setDescription(statusBarGenerator(0, 0)) .setURL( "https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley" @@ -267,7 +267,7 @@ export default class I8n { .setTitle( resource ? `${queue.Position + 1}. ${resource.metadata.title}` - : "💿 Insert the disk..." + : "💿 Insert the disk..." ) .setURL( resource @@ -303,7 +303,7 @@ export default class I8n { fullyAlone: () => { return new MessageEmbed() .setColor("LUMINOUS_VIVID_PINK") - .setTitle("🥬 I have decided to leave to conserve my energy.") + .setTitle("🥬 I have decided to leave to conserve my energy.") .setDescription( "_**Since there was nobody in the voice channel.**_" ); @@ -311,7 +311,30 @@ export default class I8n { reshuffle: () => { return new MessageEmbed() .setColor("GREEN") - .setTitle("✅ Successfuly reshuffled queue."); + .setTitle("**✅ Successfuly reshuffled queue.**"); + }, + savedQueue: (title: string) => { + return new MessageEmbed() + .setColor("GREEN") + .setTitle(`**✅ Successfuly saved ${title} to the server's storage.**`); + }, + savedExceed: (title: string) => { + return new MessageEmbed() + .setColor("RED") + .setTitle(`**🚫 Could not save ${title} to the storage.**`) + .setDescription("_**Delete your old one beforehand...**_"); + }, + ping: (ping?: number) => { + const title = ping ? `I have pinged ${ping} ms late.` : "There is no application."; + + const comment = !ping ? "How am I speaking then?" : ping > 250 ? "I'm kinda slowpoke ngl." : + ping > 200 ? "I can do better than this." : + "Dream's speed!"; + + return new MessageEmbed() + .setColor("RANDOM") + .setTitle(`**${title}**`) + .setDescription(`_**${comment}**_`); } }; } diff --git a/src/lib/Player.ts b/src/lib/Player.ts index ad1d163..3ab603f 100644 --- a/src/lib/Player.ts +++ b/src/lib/Player.ts @@ -22,7 +22,7 @@ import { import Music from "./Music"; import Filter from "./Filter"; import Queue from "./Queue"; -import { fetchStream } from "./QFetch"; +import { fetchStream } from "./Fetch"; import I8n from "./I8n"; import ytdl from "discord-ytdl-core"; @@ -30,8 +30,8 @@ import { track } from "./Types"; import { largestCommonSequence } from "./Misc"; export default class Player { - private guild: Guild; private music: Music; + private guild: Guild; private channel: TextBasedChannel; public get Channel(): TextBasedChannel { diff --git a/src/lib/Storage.ts b/src/lib/Storage.ts new file mode 100644 index 0000000..5fce282 --- /dev/null +++ b/src/lib/Storage.ts @@ -0,0 +1,83 @@ +import { PrismaClient } from "@prisma/client"; + +interface CustomNodeJsGlobal extends Global { + prisma: PrismaClient +} + +declare const global: CustomNodeJsGlobal; + +export class Storage { + private prisma = global.prisma || new PrismaClient(); + + public readonly member = { + get: async (userId: string, guildId: string) => { + return await this.prisma.member.findUnique({ + where: { + userId_guildId: { + userId: userId, + guildId: guildId + } + } + }) || await this.prisma.member.create({ + data: { + userId: userId, + guild: { + connectOrCreate: { + where: { + id: guildId + }, + create: { + id: guildId + } + } + } + } + }); + } + }; + + public queue = { + set: async (data: {title: string, description: string, tracks: string[], userId: string, guildId: string}) => { + const member = await this.member.get(data.userId, data.guildId); + + if ((await this.queue.get({ userId: data.userId, guildId: data.guildId })).length === 5) { + throw new Error("Exceeded amount of tracks."); + } + + await this.prisma.queue.create({ + data: { + title: data.title, + descriptrion: data.description, + tracks: data.tracks, + guildId: member.guildId, + authorId: member.id + } + }); + }, + get: async (where: { title?: string, userId?: string, guildId: string }) => { + return await this.prisma.queue.findMany({ + where: { + title: where.title, + guildId: where.guildId, + authorId: where.userId ? (await this.member.get(where.userId, where.guildId)).id : undefined + } + }); + }, + del: async (where: {title: string, guildId: string, userId: string}) => { + if ((await this.queue.get(where)).length === 0) return; + + await this.prisma.queue.delete({ + where: { + title_guildId: { + title: where.title, + guildId: where.guildId + } + } + }); + } + }; + + public constructor() { + if (process.env.NODE_ENV === "development") global.prisma = this.prisma; + } +} \ No newline at end of file