diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7aa3c09..35c9722 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -13,10 +13,10 @@ jobs: - name: Checkout repository uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - name: Install Node v16 + - name: Install Node v18 uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3 with: - node-version: 16 + node-version: 18 cache: yarn - name: Install Dependencies diff --git a/package.json b/package.json index 74fbc12..98e7c70 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,17 @@ "start:dev": "NODE_ENV=development nodemon ./dist", "start": "node ./dist", "lint": "eslint src", - "postinstall": "is-ci || husky install" + "postinstall": "is-ci || husky install", + "prisma": "dotenv -e data/.env -- prisma" + }, + "dependencies": { + "@prisma/client": "^4.7.1", + "@snowcrystals/iglo": "1.2.0", + "add": "^2.0.6", + "discord.js": "^14.7.1", + "dotenv": "^16.0.3", + "ms": "^2.1.3", + "yarn": "^1.22.19" }, "devDependencies": { "@commitlint/cli": "^17.3.0", @@ -26,6 +36,7 @@ "@types/node": "^18.11.13", "@typescript-eslint/eslint-plugin": "^5.46.0", "@typescript-eslint/parser": "^5.46.0", + "dotenv-cli": "^6.0.0", "eslint": "^8.29.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", @@ -34,18 +45,11 @@ "lint-staged": "^13.1.0", "nodemon": "^2.0.20", "prettier": "^2.8.1", + "prisma": "^4.7.1", "typescript": "4.9.4" }, "engines": { "node": ">= v18.12.1" }, - "packageManager": "yarn@3.3.0", - "dependencies": { - "@snowcrystals/iglo": "1.2.0", - "add": "^2.0.6", - "discord.js": "^14.7.1", - "dotenv": "^16.0.3", - "ms": "^2.1.3", - "yarn": "^1.22.19" - } + "packageManager": "yarn@3.3.0" } diff --git a/prisma/migrations/20221214162812_backup_model_init/migration.sql b/prisma/migrations/20221214162812_backup_model_init/migration.sql new file mode 100644 index 0000000..ef5eb57 --- /dev/null +++ b/prisma/migrations/20221214162812_backup_model_init/migration.sql @@ -0,0 +1,87 @@ +-- CreateTable +CREATE TABLE "guild_backup" ( + "backup_id" TEXT NOT NULL, + "backup_creation_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guild_name" TEXT NOT NULL, + "guild_id" TEXT, + "guild_icon" TEXT, + "inactive_channel_id" TEXT, + "inactive_channel_name" TEXT, + "inactive_timeout" INTEGER NOT NULL, + "system_channel_name" TEXT, + "system_channel_id" TEXT, + "system_enable_join" BOOLEAN NOT NULL, + "system_enable_sticker" BOOLEAN NOT NULL, + "system_enable_boost" BOOLEAN NOT NULL, + "system_enable_tips" BOOLEAN NOT NULL, + "nitro_progress_enabled" BOOLEAN NOT NULL, + "invite_banner" TEXT, + "guild_banner" TEXT, + "widget_enabled" BOOLEAN NOT NULL, + "widget_channel_name" TEXT, + "widget_channel_id" TEXT, + + CONSTRAINT "guild_backup_pkey" PRIMARY KEY ("backup_id") +); + +-- CreateTable +CREATE TABLE "BackUpRole" ( + "backup_id" TEXT NOT NULL, + "role_id" TEXT, + "role_name" TEXT NOT NULL, + "role_position" INTEGER NOT NULL, + "role_icon" TEXT NOT NULL, + "role_color" TEXT NOT NULL, + "role_mention_everyone" BOOLEAN NOT NULL, + "role_display_separate" BOOLEAN NOT NULL, + "role_permissions" BIGINT NOT NULL, + "role_members" TEXT[], + "backUpBackUpId" TEXT, + + CONSTRAINT "BackUpRole_pkey" PRIMARY KEY ("backup_id") +); + +-- CreateTable +CREATE TABLE "BackUpBan" ( + "backup_id" TEXT NOT NULL, + "ban_user_id" TEXT NOT NULL, + "ban_reason" TEXT NOT NULL, + "backUpBackUpId" TEXT, + + CONSTRAINT "BackUpBan_pkey" PRIMARY KEY ("backup_id") +); + +-- CreateTable +CREATE TABLE "BackUpEmoji" ( + "backup_id" TEXT NOT NULL, + "emoji_id" TEXT NOT NULL, + "emoji_hash" TEXT NOT NULL, + "backUpBackUpId" TEXT, + + CONSTRAINT "BackUpEmoji_pkey" PRIMARY KEY ("backup_id") +); + +-- CreateTable +CREATE TABLE "BackUpSticker" ( + "backup_id" TEXT NOT NULL, + "sticker_id" TEXT, + "sticker_name" TEXT NOT NULL, + "sticker_related_emoji" TEXT NOT NULL, + "sticker_description" TEXT NOT NULL, + "sticker_hash" TEXT NOT NULL, + "backUpBackUpId" TEXT, + + CONSTRAINT "BackUpSticker_pkey" PRIMARY KEY ("backup_id") +); + +-- AddForeignKey +ALTER TABLE "BackUpRole" ADD CONSTRAINT "BackUpRole_backUpBackUpId_fkey" FOREIGN KEY ("backUpBackUpId") REFERENCES "guild_backup"("backup_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BackUpBan" ADD CONSTRAINT "BackUpBan_backUpBackUpId_fkey" FOREIGN KEY ("backUpBackUpId") REFERENCES "guild_backup"("backup_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BackUpEmoji" ADD CONSTRAINT "BackUpEmoji_backUpBackUpId_fkey" FOREIGN KEY ("backUpBackUpId") REFERENCES "guild_backup"("backup_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BackUpSticker" ADD CONSTRAINT "BackUpSticker_backUpBackUpId_fkey" FOREIGN KEY ("backUpBackUpId") REFERENCES "guild_backup"("backup_id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20221214194729_backup_secret_add/migration.sql b/prisma/migrations/20221214194729_backup_secret_add/migration.sql new file mode 100644 index 0000000..cffe5bb --- /dev/null +++ b/prisma/migrations/20221214194729_backup_secret_add/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `backup_secret` to the `guild_backup` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "guild_backup" ADD COLUMN "backup_secret" TEXT NOT NULL; 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..0f350c3 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,104 @@ +// 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") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") +} + +model BackUp { + backUpId String @id @map("backup_id") + secret String @map("backup_secret") + + createdAt DateTime @default(now()) @map("backup_creation_date") + + guildName String @map("guild_name") + guildId String? @map("guild_id") + guildIcon String? @map("guild_icon") // guildIconHash + + inactiveChannelId String? @map("inactive_channel_id") + inactiveChannelName String? @map("inactive_channel_name") // fallback for when data is imported from one server to another or when channel is deleted + inactiveTimeout Int @map("inactive_timeout") + + systemChannelName String? @map("system_channel_name") + systemChannelId String? @map("system_channel_id") + systemEnableJoin Boolean @map("system_enable_join") + systemEnableSticker Boolean @map("system_enable_sticker") + systemEnableBoost Boolean @map("system_enable_boost") + systemEnableTips Boolean @map("system_enable_tips") + + nitroProgressEnabled Boolean @map("nitro_progress_enabled") + inviteBackground String? @map("invite_banner") // imageHash + guildBanner String? @map("guild_banner") // imageHash + + widgetEnabled Boolean @map("widget_enabled") + widgetChannelName String? @map("widget_channel_name") + widgetChannelId String? @map("widget_channel_id") // Both can be undefined if no invite is chosen + + roles BackUpRole[] + bans BackUpBan[] + emojis BackUpEmoji[] + stickers BackUpSticker[] + // TODO: Add Automod & Community data fields + + @@map("guild_backup") +} + +model BackUpRole { + backUpId String @id @map("backup_id") + + id String? @map("role_id") + name String @map("role_name") + position Int @map("role_position") + + icon String @map("role_icon") // imageHash + color String @map("role_color") + + mentionEveryone Boolean @map("role_mention_everyone") + displaySeparate Boolean @map("role_display_separate") + + permissions BigInt @map("role_permissions") + members String[] @map("role_members") // list of members with the role (ID) + + BackUp BackUp? @relation(fields: [backUpBackUpId], references: [backUpId]) + backUpBackUpId String? +} + +model BackUpBan { + backUpId String @id @map("backup_id") + + userId String @map("ban_user_id") + reason String @map("ban_reason") + + BackUp BackUp? @relation(fields: [backUpBackUpId], references: [backUpId]) + backUpBackUpId String? +} + +model BackUpEmoji { + backUpId String @id @map("backup_id") + + name String @map("emoji_id") + emoji String @map("emoji_hash") + + BackUp BackUp? @relation(fields: [backUpBackUpId], references: [backUpId]) + backUpBackUpId String? +} + +model BackUpSticker { + backUpId String @id @map("backup_id") + + id String? @map("sticker_id") + name String @map("sticker_name") + + emoji String @map("sticker_related_emoji") + description String @map("sticker_description") + sticker String @map("sticker_hash") + + BackUp BackUp? @relation(fields: [backUpBackUpId], references: [backUpId]) + backUpBackUpId String? +} diff --git a/src/lib/structures/BackItUpGuild.ts b/src/lib/structures/BackItUpGuild.ts new file mode 100644 index 0000000..e6a1593 --- /dev/null +++ b/src/lib/structures/BackItUpGuild.ts @@ -0,0 +1,10 @@ +import type BackItUpClient from "../Client.js"; +import type { BackItUpSettings } from "./type.js"; + +export class BackItUpGuild { + public backUpIds: string[]; + + public constructor(public client: BackItUpClient, data: BackItUpSettings) { + this.backUpIds = data.backUpIds; + } +} diff --git a/src/lib/structures/BackUp.ts b/src/lib/structures/BackUp.ts new file mode 100644 index 0000000..9f820a6 --- /dev/null +++ b/src/lib/structures/BackUp.ts @@ -0,0 +1,80 @@ +import type { Guild } from "discord.js"; +import type BackItUpClient from "../Client.js"; +import type { BackUpBan, BackUpData, BackUpEmoji, RawBackUp, BackUpSticker, BackUpRole } from "./type.js"; + +export default class BackUp { + public guild!: Guild; + public id: string; + + public constructor(public client: BackItUpClient, public data: BackUpData) { + this.id = data.guild.id; + } + + public loadGuild(): boolean { + const guild = this.data.guild.id + ? this.client.guilds.cache.get(this.data.guild.id) + : this.client.guilds.cache.find((g) => g.name === this.data.guild.name); + if (!guild) return false; + + this.guild = guild; + return true; + } + + public static parse(client: BackItUpClient, data: RawBackUp) { + const bans = data.bans.map((ban) => ({ member: ban.userId, reason: ban.reason })); + const emojis = data.emojis.map((emoji) => ({ name: emoji.name, image: emoji.emoji })); + const stickers = data.stickers.map((sticker) => ({ + name: sticker.name, + emoji: sticker.emoji, + description: sticker.description, + image: sticker.sticker + })); + const roles = data.roles.map((role) => ({ + name: role.name, + id: role.id ?? undefined, + postion: role.position, + permissions: role.permissions, + color: role.color, + display: role.displaySeparate, + members: role.members, + mention: role.mentionEveryone, + icon: role.icon ?? undefined + })); + + const backUp: BackUpData = { + guild: { + id: data.backUpId, + name: data.guildName, + nitroProgress: data.nitroProgressEnabled, + banner: data.guildBanner ?? undefined, + icon: data.guildIcon ?? undefined, + guildId: data.guildId ?? undefined, + inviteBackground: data.inviteBackground ?? undefined, + inactive: { + channelId: data.inactiveChannelId ?? undefined, + channelName: data.inactiveChannelName ?? undefined, + timeout: data.inactiveTimeout + }, + system: { + channelId: data.systemChannelId ?? undefined, + channelName: data.systemChannelName ?? undefined, + join: data.systemEnableJoin, + boost: data.systemEnableBoost, + tips: data.systemEnableTips, + stickerButton: data.systemEnableSticker + } + }, + widget: { + enabled: data.widgetEnabled, + channelId: data.widgetChannelId ?? undefined, + channelName: data.widgetChannelName ?? undefined + }, + bans, + emojis, + roles, + stickers + }; + + return new BackUp(client, backUp); + } +} diff --git a/src/lib/structures/type.ts b/src/lib/structures/type.ts new file mode 100644 index 0000000..167c51f --- /dev/null +++ b/src/lib/structures/type.ts @@ -0,0 +1,101 @@ +import type { + BackUp as RawBackUpType, + BackUpBan as RawBackUpBan, + BackUpEmoji as RawBackUpEmoji, + BackUpRole as RawBackUpRole, + BackUpSticker as RawBackUpSticker +} from "@prisma/client"; + +interface RawBackUpComplete { + bans: RawBackUpBan[]; + emojis: RawBackUpEmoji[]; + roles: RawBackUpRole[]; + stickers: RawBackUpSticker[]; +} + +export type RawBackUp = RawBackUpType & RawBackUpComplete; + +export interface BackUpData { + guild: BackUpGuild; + widget: BackUpWidget; + + // automod stuff + // community stuff + + roles: BackUpRole[]; + bans: BackUpBan[]; + + emojis: BackUpEmoji[]; + stickers: BackUpSticker[]; +} + +export interface BackUpGuild { + name: string; + guildId?: string; + id: string; // secret identifier for retrieving data from from other server (only ) + icon?: ImageHash; + inactive: { + channelId?: string; + channelName?: string; + timeout: number; + }; + system: { + channelId?: string; + channelName?: string; + join: boolean; + stickerButton: boolean; + boost: boolean; + tips: boolean; + }; + nitroProgress: boolean; + inviteBackground?: ImageHash; // todo image type + banner?: ImageHash; +} + +export interface BackUpWidget { + enabled: boolean; + channelId?: string; + channelName?: string; +} + +export interface BackUpRole { + name: string; + id?: string; + postion: number; + + icon?: ImageHash; + color: string; + + mention: boolean; + display: boolean; + + permissions: bigint; + members: string[]; +} + +export interface BackUpBan { + member: string; + reason: string; +} + +export interface BackUpEmoji { + name: string; + image: ImageHash; +} + +export interface BackUpSticker { + name: string; + emoji: string; + description?: string; + image: ImageHash; +} + +export type ImageHash = string; + +export interface BackItUpSettings { + backUpTime: number; // min 4 hours, max 1 week (premium: starting at every hour) + backUpHistory: number; // default = 2 days, premium: 7 days, 31 days + backUpAmount: number; // max 16 for default, premium = unlimited + manualTriggerAmount: number; // default 2, premium 4 (all per day) + backUpIds: string[]; +} diff --git a/yarn.lock b/yarn.lock index 0bb4480..85d3dbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -444,6 +444,34 @@ __metadata: languageName: node linkType: hard +"@prisma/client@npm:^4.7.1": + version: 4.7.1 + resolution: "@prisma/client@npm:4.7.1" + dependencies: + "@prisma/engines-version": 4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c + peerDependencies: + prisma: "*" + peerDependenciesMeta: + prisma: + optional: true + checksum: 01ce4c7382d80a1cca256e532a773d982ada9b7f64acd6bb590415634d04305413cee700048287dbf94f09ccf31e19db03a4a6501bfd0a01509f80498ee58121 + languageName: node + linkType: hard + +"@prisma/engines-version@npm:4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c": + version: 4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c + resolution: "@prisma/engines-version@npm:4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c" + checksum: 66221f1805ce700a4f9f0b6751a291d7114afff9d8b61fd03beb9bbe8b45b037fc67ee0425f45bbad57b7bf95cf649a490d5caaf75a4f3a7e17b3187ea8a6a15 + languageName: node + linkType: hard + +"@prisma/engines@npm:4.7.1": + version: 4.7.1 + resolution: "@prisma/engines@npm:4.7.1" + checksum: 1660f1d5a6188a9cb9aa5186111910f37b1bc02f09be10336a41e0ec61a18a4193e0ff424077dafb1bf1392a435fc89cec47526a58784a7bdc6c8278adf147a4 + languageName: node + linkType: hard + "@sapphire/async-queue@npm:^1.5.0": version: 1.5.0 resolution: "@sapphire/async-queue@npm:1.5.0" @@ -1125,6 +1153,7 @@ __metadata: dependencies: "@commitlint/cli": ^17.3.0 "@commitlint/config-angular": ^17.3.0 + "@prisma/client": ^4.7.1 "@sapphire/eslint-config": ^4.3.8 "@sapphire/prettier-config": ^1.4.4 "@sapphire/ts-config": ^3.3.4 @@ -1137,6 +1166,7 @@ __metadata: add: ^2.0.6 discord.js: ^14.7.1 dotenv: ^16.0.3 + dotenv-cli: ^6.0.0 eslint: ^8.29.0 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 @@ -1146,6 +1176,7 @@ __metadata: ms: ^2.1.3 nodemon: ^2.0.20 prettier: ^2.8.1 + prisma: ^4.7.1 typescript: 4.9.4 yarn: ^1.22.19 languageName: unknown @@ -1629,7 +1660,28 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.3": +"dotenv-cli@npm:^6.0.0": + version: 6.0.0 + resolution: "dotenv-cli@npm:6.0.0" + dependencies: + cross-spawn: ^7.0.3 + dotenv: ^16.0.0 + dotenv-expand: ^8.0.1 + minimist: ^1.2.5 + bin: + dotenv: cli.js + checksum: 3db5a363eedd24d428001a956d9b8c72094983bfe0e0e722cda7803b614daf85dde9c9beb5d9fa28a139a5c594533ecd8f9d8430a90c244175e81c9a40abc5b1 + languageName: node + linkType: hard + +"dotenv-expand@npm:^8.0.1": + version: 8.0.3 + resolution: "dotenv-expand@npm:8.0.3" + checksum: 128ce90ac825b543de3ece0154a51b056ab0dc36bb26d97a68cd0b8707327ecd3c182fb6ac63b26a0fcdfa85064419906a1065cb634f1f9dc08ad311375f1fc0 + languageName: node + linkType: hard + +"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3": version: 16.0.3 resolution: "dotenv@npm:16.0.3" checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 @@ -3049,6 +3101,13 @@ __metadata: languageName: node linkType: hard +"minimist@npm:^1.2.5": + version: 1.2.7 + resolution: "minimist@npm:1.2.7" + checksum: 7346574a1038ca23c32e02252f603801f09384dd1d78b69a943a4e8c2c28730b80e96193882d3d3b22a063445f460e48316b29b8a25addca2d7e5e8f75478bec + languageName: node + linkType: hard + "minimist@npm:^1.2.6": version: 1.2.6 resolution: "minimist@npm:1.2.6" @@ -3513,6 +3572,18 @@ __metadata: languageName: node linkType: hard +"prisma@npm:^4.7.1": + version: 4.7.1 + resolution: "prisma@npm:4.7.1" + dependencies: + "@prisma/engines": 4.7.1 + bin: + prisma: build/index.js + prisma2: build/index.js + checksum: eb7046a5928f33e484bb2a100f49b613a37bdd83d72aa7e87a4edb38a235b7c44bb9c89bc9116af60950554da06d6ef7c581dd0c637ba2798810485d04da29d4 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1"