From 83b292dcd5abfe6cab1aa5c1857a412128486120 Mon Sep 17 00:00:00 2001 From: Gabriel Tavares <bieltmd@outlook.com> Date: Sat, 8 Jun 2024 13:40:55 -0400 Subject: [PATCH 1/4] feat: announce when user levels up --- src/events/giveXp.ts | 7 ++++--- src/events/supressUrlEmbeds.ts | 2 +- src/structures/xpHandler.ts | 7 +++++-- src/utils/dateFormat.ts | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/events/giveXp.ts b/src/events/giveXp.ts index 34c980b..5ecc098 100644 --- a/src/events/giveXp.ts +++ b/src/events/giveXp.ts @@ -1,11 +1,12 @@ import { container } from "@sapphire/pieces"; -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; export default function giveXp(message: Message) { const member = message.member; const guild = message.guild; + const channel = message.channel as TextChannel; - if (!member || !guild) return; + if (!member || !guild || !channel) return; - container.expHandler.addExp(member, guild); + container.expHandler.addExp(member, guild, channel); } diff --git a/src/events/supressUrlEmbeds.ts b/src/events/supressUrlEmbeds.ts index 95e8ec6..2ea2657 100644 --- a/src/events/supressUrlEmbeds.ts +++ b/src/events/supressUrlEmbeds.ts @@ -4,7 +4,7 @@ import type { Message } from "discord.js"; export default async function suppressUrlEmbeds(message: Message) { const content = message.content; - if (!message.embeds.length) return; + if (message.embeds.length <= 0) return; for (const url in urlFixers) { if (content.includes(url)) { diff --git a/src/structures/xpHandler.ts b/src/structures/xpHandler.ts index 3f9991e..eb9c6c6 100644 --- a/src/structures/xpHandler.ts +++ b/src/structures/xpHandler.ts @@ -1,6 +1,6 @@ import { ExpValues } from "@/utils/contants.js"; import { container } from "@sapphire/pieces"; -import type { Guild, GuildMember } from "discord.js"; +import type { Guild, GuildMember, TextChannel } from "discord.js"; export type ExpStats = { exp: number; @@ -53,7 +53,7 @@ export default class ExpHandler { }; } - public async addExp(member: GuildMember, guild: Guild) { + public async addExp(member: GuildMember, guild: Guild, channel: TextChannel) { const expStats = await this.getStats(member, guild); if (!expStats?.canGetExp) return; @@ -67,6 +67,9 @@ export default class ExpHandler { if (expStats.exp >= xpRequired) { expStats.level++; expStats.exp = Math.max(newXp - xpRequired, 0); + channel.send( + `${member.toString()} subiu para o nível ${expStats.level}!`, + ); } await container.db.user.upsert({ diff --git a/src/utils/dateFormat.ts b/src/utils/dateFormat.ts index 2476ff1..90f4eee 100644 --- a/src/utils/dateFormat.ts +++ b/src/utils/dateFormat.ts @@ -20,9 +20,9 @@ export function dateToCron(dateStr: string) { export function formatDate(date: DateObject): string { const parts = [ - String(date.day).padStart(2, "0"), - String(date.month).padStart(2, "0"), - String(date.year), + date.day ? String(date.day).padStart(2, "0") : undefined, + date.month ? String(date.month).padStart(2, "0") : undefined, + date.year ? String(date.year) : undefined, ].filter((part) => part); return parts.join("/"); From cb7f8da3b6f2f5d29bc56be33ab6eb7999d51056 Mon Sep 17 00:00:00 2001 From: Gabriel Tavares <bieltmd@outlook.com> Date: Sun, 9 Jun 2024 21:56:36 -0400 Subject: [PATCH 2/4] feat: scheduled events Co-authored-by: Psykka <Psykka@users.noreply.github.com> --- docker/Dockerfile | 5 ++ docker/Dockerfile.backup | 5 +- package.json | 5 +- pnpm-lock.yaml | 22 ++++++++ .../migration.sql | 28 ++++++++++ .../20240610010812_event_type/migration.sql | 27 +++++++++ prisma/schema.prisma | 16 +++--- src/commands/user/avatar.ts | 3 - src/events/announceScheduledEvent.ts | 56 +++++++++++++++++++ src/events/startTimers.ts | 15 +++++ src/listeners/ready.ts | 5 +- 11 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20240610005557_update_date_and_fields/migration.sql create mode 100644 prisma/migrations/20240610010812_event_type/migration.sql create mode 100644 src/events/announceScheduledEvent.ts create mode 100644 src/events/startTimers.ts diff --git a/docker/Dockerfile b/docker/Dockerfile index 2ce0258..a822e33 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,6 +16,11 @@ RUN pnpm build FROM node:20-alpine as runner +ENV TZ America/Sao_Paulo + +RUN apk add --no-cache tzdata +RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime + WORKDIR /app COPY --from=builder /app/package.json ./ diff --git a/docker/Dockerfile.backup b/docker/Dockerfile.backup index 7713ba6..37f571e 100644 --- a/docker/Dockerfile.backup +++ b/docker/Dockerfile.backup @@ -1,8 +1,9 @@ FROM alpine:latest -RUN apk add --no-cache sqlite curl tzdata tar +ENV TZ America/Sao_Paulo -ENV TZ=America/Sao_Paulo +RUN apk add --no-cache sqlite curl tzdata tar +RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime COPY scripts/backup.sh /usr/local/bin/backup.sh COPY scripts/crontab /etc/crontabs/root diff --git a/package.json b/package.json index 3e3f065..ef7cde0 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,13 @@ "typescript": "^5.4.5" }, "dependencies": { - "prisma": "^5.14.0", "@prisma/client": "5.14.0", "@sapphire/discord.js-utilities": "^7.2.1", "@sapphire/framework": "^5.2.1", "@sapphire/pieces": "^4.2.2", + "cron": "^3.1.7", "dayjs": "^1.11.11", - "discord.js": "14.x" + "discord.js": "14.x", + "prisma": "^5.14.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cabfa52..40744b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@sapphire/pieces': specifier: ^4.2.2 version: 4.2.2 + cron: + specifier: ^3.1.7 + version: 3.1.7 dayjs: specifier: ^1.11.11 version: 1.11.11 @@ -347,6 +350,9 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/node@20.12.12': resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} @@ -453,6 +459,9 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + cron@3.1.7: + resolution: {integrity: sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==} + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -693,6 +702,10 @@ packages: lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + magic-bytes.js@1.10.0: resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} @@ -1345,6 +1358,8 @@ snapshots: dependencies: '@types/node': 20.12.12 + '@types/luxon@3.4.2': {} + '@types/node@20.12.12': dependencies: undici-types: 5.26.5 @@ -1455,6 +1470,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + cron@3.1.7: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.4.4 + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -1709,6 +1729,8 @@ snapshots: pseudomap: 1.0.2 yallist: 2.1.2 + luxon@3.4.4: {} + magic-bytes.js@1.10.0: {} merge-stream@2.0.0: {} diff --git a/prisma/migrations/20240610005557_update_date_and_fields/migration.sql b/prisma/migrations/20240610005557_update_date_and_fields/migration.sql new file mode 100644 index 0000000..9c31f14 --- /dev/null +++ b/prisma/migrations/20240610005557_update_date_and_fields/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - You are about to drop the column `date` on the `Events` table. All the data in the column will be lost. + - Added the required column `day` to the `Events` table without a default value. This is not possible if the table is not empty. + - Added the required column `month` to the `Events` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Events" ( + "id" TEXT NOT NULL PRIMARY KEY, + "description" TEXT NOT NULL, + "month" INTEGER NOT NULL, + "day" INTEGER NOT NULL, + "repeat" BOOLEAN NOT NULL DEFAULT false, + "guild_id" TEXT, + "created_by" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "Events_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Events_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "User" ("discord_id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Events" ("created_at", "created_by", "description", "guild_id", "id", "repeat", "updated_at") SELECT "created_at", "created_by", "description", "guild_id", "id", "repeat", "updated_at" FROM "Events"; +DROP TABLE "Events"; +ALTER TABLE "new_Events" RENAME TO "Events"; +PRAGMA foreign_key_check("Events"); +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20240610010812_event_type/migration.sql b/prisma/migrations/20240610010812_event_type/migration.sql new file mode 100644 index 0000000..495c838 --- /dev/null +++ b/prisma/migrations/20240610010812_event_type/migration.sql @@ -0,0 +1,27 @@ +/* + Warnings: + + - Added the required column `type` to the `Events` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Events" ( + "id" TEXT NOT NULL PRIMARY KEY, + "description" TEXT NOT NULL, + "month" INTEGER NOT NULL, + "day" INTEGER NOT NULL, + "type" TEXT NOT NULL, + "repeat" BOOLEAN NOT NULL DEFAULT false, + "guild_id" TEXT, + "created_by" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "Events_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Events_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "User" ("discord_id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Events" ("created_at", "created_by", "day", "description", "guild_id", "id", "month", "repeat", "updated_at") SELECT "created_at", "created_by", "day", "description", "guild_id", "id", "month", "repeat", "updated_at" FROM "Events"; +DROP TABLE "Events"; +ALTER TABLE "new_Events" RENAME TO "Events"; +PRAGMA foreign_key_check("Events"); +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ef1708..6571c69 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,7 +16,7 @@ model User { xp Int @default(0) @map("xp") level Int @default(1) @map("level") birthday DateTime? @map("birthday") - Events Events[] @relation("UserEvents") + events Events[] @relation("user_events") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") } @@ -24,12 +24,14 @@ model User { model Events { id String @id @default(cuid()) @map("id") description String @map("description") - date DateTime @map("date") + month Int @map("month") + day Int @map("day") + type String @map("type") repeat Boolean @default(false) @map("repeat") guildId String? @map("guild_id") - Guild Guild? @relation("GuildEvents", fields: [guildId], references: [id]) + guild Guild? @relation("guild_events", fields: [guildId], references: [id]) createdBy String? @map("created_by") - User User? @relation("UserEvents", fields: [createdBy], references: [discordId]) + user User? @relation("user_events", fields: [createdBy], references: [discordId]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") } @@ -39,15 +41,15 @@ model Daily { day Int @default(0) @map("day") afternoon Int @default(0) @map("afternoon") night Int @default(0) @map("night") - Guild Guild[] + guild Guild[] } model Guild { id String @id @default(cuid()) @map("id") discordId String @unique @map("discord_id") mainChannel String? @map("main_channel") - Events Events[] @relation("GuildEvents") - Daily Daily? @relation(fields: [dailyId], references: [id]) + events Events[] @relation("guild_events") + daily Daily? @relation(fields: [dailyId], references: [id]) dailyId String? @map("daily_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/src/commands/user/avatar.ts b/src/commands/user/avatar.ts index 8f5771b..123a375 100644 --- a/src/commands/user/avatar.ts +++ b/src/commands/user/avatar.ts @@ -58,8 +58,6 @@ export class AvatarCommand extends Command { }; const userType = interaction.options.getString("type"); - const avatarType = - userType === "user" || !member.avatar ? "Usuário" : "Servidor"; const avatar = userType === "user" ? member.user.displayAvatarURL(config) @@ -68,7 +66,6 @@ export class AvatarCommand extends Command { const embed = new EmbedBuilder() .setTitle(nickname) .setImage(avatar) - .setFooter({ text: avatarType }) .setColor(color); await interaction.reply({ diff --git a/src/events/announceScheduledEvent.ts b/src/events/announceScheduledEvent.ts new file mode 100644 index 0000000..d17fb87 --- /dev/null +++ b/src/events/announceScheduledEvent.ts @@ -0,0 +1,56 @@ +import { container } from "@sapphire/pieces"; +import type { TextChannel } from "discord.js"; + +type ChannelMessage = { + channel: TextChannel; + message: string[]; +}; + +export default async function announceScheduledEvent() { + const today = new Date(); + const events = await container.db.events.findMany({ + where: { + month: today.getMonth() + 1, + day: today.getDate(), + type: "DEFAULT", + }, + include: { + guild: true, + }, + }); + + if (!events || events.length <= 0) return; + + const guilds = container.client.guilds.cache; + const channelMessages = new Map<string, ChannelMessage>(); + + for (const event of events) { + if (!event.guildId || !event.guild?.mainChannel) continue; + + const guild = guilds.get(event.guild.discordId); + const channel = guild?.channels.cache.get(event.guild.mainChannel) as + | TextChannel + | undefined; + + if (!channel || !guild) continue; + + if (channelMessages.has(channel.id)) { + const messages = channelMessages.get(channel.id)?.message; + messages?.push(`- ${event.description}`); + } else { + channelMessages.set(channel.id, { + channel, + message: [`- ${event.description}`], + }); + } + + if (!event.repeat) { + await container.db.events.delete({ where: { id: event.id } }); + } + } + + for (const channelMessage of channelMessages.values()) { + const channel = channelMessage.channel; + channel.send(`# Eventos\n${channelMessage.message.join("\n")}`); + } +} diff --git a/src/events/startTimers.ts b/src/events/startTimers.ts new file mode 100644 index 0000000..56fb5a2 --- /dev/null +++ b/src/events/startTimers.ts @@ -0,0 +1,15 @@ +import { CronJob } from "cron"; +import announceScheduledEvent from "./announceScheduledEvent.js"; +import { defaultTimeZone } from "@/utils/contants.js"; + +export default function startTimers() { + new CronJob( + "* * * * *", + () => { + announceScheduledEvent(); + }, + null, + true, + defaultTimeZone, + ); +} diff --git a/src/listeners/ready.ts b/src/listeners/ready.ts index fc0b1fe..ba7552d 100644 --- a/src/listeners/ready.ts +++ b/src/listeners/ready.ts @@ -1,3 +1,4 @@ +import startTimers from "@/events/startTimers.js"; import { Listener } from "@sapphire/framework"; import type { Client } from "discord.js"; @@ -13,5 +14,7 @@ export class ReadyListener extends Listener { }); } - public run(client: Client) {} + public run(client: Client) { + startTimers(); + } } From 169f61f2a259ff4bd09f614e9f88f0a005d21868 Mon Sep 17 00:00:00 2001 From: Gabriel Tavares <bieltmd@outlook.com> Date: Sun, 9 Jun 2024 21:59:21 -0400 Subject: [PATCH 3/4] fix: imports --- src/events/startTimers.ts | 2 +- src/utils/contants.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/events/startTimers.ts b/src/events/startTimers.ts index 56fb5a2..73b1782 100644 --- a/src/events/startTimers.ts +++ b/src/events/startTimers.ts @@ -1,6 +1,6 @@ +import { defaultTimeZone } from "@/utils/contants.js"; import { CronJob } from "cron"; import announceScheduledEvent from "./announceScheduledEvent.js"; -import { defaultTimeZone } from "@/utils/contants.js"; export default function startTimers() { new CronJob( diff --git a/src/utils/contants.ts b/src/utils/contants.ts index a0c0ab0..104de60 100644 --- a/src/utils/contants.ts +++ b/src/utils/contants.ts @@ -3,6 +3,8 @@ export const EmbedColors = { anilist: 0x3db4f2, }; +export const defaultTimeZone = "America/Sao_Paulo"; + export const leaderboardIcon = "https://i.imgur.com/qpb2q9S.png"; export const leaderboardEmojis = ["🥇", "🥈", "🥉"]; From c860b48623be04392bd6c01da0671693daed96db Mon Sep 17 00:00:00 2001 From: Gabriel Tavares <bieltmd@outlook.com> Date: Sun, 9 Jun 2024 22:17:04 -0400 Subject: [PATCH 4/4] chore: message formatting --- src/events/announceScheduledEvent.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/events/announceScheduledEvent.ts b/src/events/announceScheduledEvent.ts index d17fb87..1a60dd3 100644 --- a/src/events/announceScheduledEvent.ts +++ b/src/events/announceScheduledEvent.ts @@ -36,11 +36,11 @@ export default async function announceScheduledEvent() { if (channelMessages.has(channel.id)) { const messages = channelMessages.get(channel.id)?.message; - messages?.push(`- ${event.description}`); + messages?.push(event.description); } else { channelMessages.set(channel.id, { channel, - message: [`- ${event.description}`], + message: [event.description], }); } @@ -51,6 +51,8 @@ export default async function announceScheduledEvent() { for (const channelMessage of channelMessages.values()) { const channel = channelMessage.channel; - channel.send(`# Eventos\n${channelMessage.message.join("\n")}`); + channel.send( + `# Eventos\n${channelMessage.message.map((m) => `- ${m}`).join("\n")}`, + ); } }