diff --git a/src/commands/colonThree.ts b/src/commands/colonThree.ts new file mode 100644 index 0000000..3349b07 --- /dev/null +++ b/src/commands/colonThree.ts @@ -0,0 +1,241 @@ +import { Discord, Slash, SlashGroup, SlashOption } from "discordx"; +import { ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, GuildMember, MessageFlags } from "discord.js"; + +import db from "../db"; +import { colonTable, type ColonThreeType } from "../db/schema"; + +import { eq, desc } from "drizzle-orm"; + +@Discord() +export class ColonThreeInit { + @Slash({ description: "Init all users for :3 Leaderboard", name: "init_colonthree" }) + async init_colon(inter: CommandInteraction) { + if (Bun.env.OWNER != inter.user.id) { + await inter.reply({ content: "You're not allowed to run this.", flags: MessageFlags.Ephemeral }) + return; + } + + for (const userObject of await inter.guild!.members.cache) { + const user = userObject[1]; + if (user.user.bot) { + continue; + } + + const check = await db.select().from(colonTable).where(eq(colonTable.user, user.id)); + + if (check.length >= 1) { + continue; + } + + await db.insert(colonTable).values({ + user: user.id, + amount: 0, + messages_count: 0 + }); + } + + await inter.reply({ content: "All users have been initalized", flags: MessageFlags.Ephemeral }); + } +} + +@Discord() +@SlashGroup({ + description: "The :3 Commands", + name: "colonthree" +}) +@SlashGroup("colonthree") +export class ColonThree { + @Slash({ description: "Stats" }) + async stats( + @SlashOption({ + description: "Get stats from user", + name: "user", + required: false, + type: ApplicationCommandOptionType.User, + }) + user: GuildMember, + inter: CommandInteraction + ) { + const statsUser = ( + await db + .select() + .from(colonTable) + .where(eq(colonTable.user, (user?.id || inter.user.id))) + )[0]; + + if (!statsUser) { + await inter.reply({ content: `Failed to get <@${user?.id || inter.user.id}>'s stats.`, flags: MessageFlags.Ephemeral }); + return; + } + + const userObject = inter.client.users.cache.get(statsUser.user); + if (!userObject) { + await inter.reply({ content: "Something went wrong...", flags: MessageFlags.Ephemeral }); + return; + } + + const embed = new EmbedBuilder() + .setTitle(`${userObject.username}'s Stats`) + .setAuthor({ name: userObject.username, iconURL: userObject.avatarURL()! }) + .addFields( + { + name: '**Total :3 Sent**', + value: `โ–บ **${statsUser.amount}** times`, + inline: true + }, + { + name: '**Average :3 per Message**', + value: `โ–บ **${(statsUser.amount / statsUser.messages_count).toFixed(2)}**`, + inline: true + }, + { + name: '**Messages Count**', + value: `โ–บ **${statsUser.messages_count}**`, + inline: true + } + ); + + await inter.reply( { embeds: [embed], flags: MessageFlags.Ephemeral }); + } + + @Slash({ description: "Compare" }) + async compare( + @SlashOption({ + description: "Get stats from user", + name: "x", + required: true, + type: ApplicationCommandOptionType.User, + }) + x: GuildMember, + @SlashOption({ + description: "Get stats from user", + name: "y", + required: true, + type: ApplicationCommandOptionType.User, + }) + y: GuildMember, + inter: CommandInteraction + ) { + const xStats = ( + await db + .select() + .from(colonTable) + .where(eq(colonTable.user, x.id)) + )[0]; + + if (!xStats) { + await inter.reply({ content: `Failed to get <@${x.id}>'s stats.`, flags: MessageFlags.Ephemeral }); + return; + } + + const yStats = ( + await db + .select() + .from(colonTable) + .where(eq(colonTable.user, y.id)) + )[0]; + + if (!yStats) { + await inter.reply({ content: `Failed to get <@${y.id}>'s stats.`, flags: MessageFlags.Ephemeral }); + return; + } + + const winner = xStats.amount > yStats.amount ? x : y; + + const embed = new EmbedBuilder() + .setTitle(`๐ŸŽ‰ ${winner.user.username} is using :3 more!`) + .addFields( + { + name: `๐Ÿ“Š ${x.user.username}'s Stats`, + value: `โ–บ Sent **${xStats.amount}** :3\nโ–บ Avg: **${(xStats.amount / xStats.messages_count).toFixed(2)}** :3 per message`, + inline: true + }, + { + name: `๐Ÿ“Š ${y.user.username}'s Stats`, + value: `โ–บ Sent **${yStats.amount}** :3\nโ–บ Avg: **${(yStats.amount / yStats.messages_count).toFixed(2)}** :3 per message`, + inline: true + } + ) + + await inter.reply({ embeds: [embed] }); + } + + @Slash({ description: "Leaderboard" }) + async board(inter: CommandInteraction) { + const theColonThreeLeaders = await db.select() + .from(colonTable) + .orderBy(desc(colonTable.amount)); + + const topTen = theColonThreeLeaders + .slice(0, 10) + .filter(user => user.amount >= 1); + + const leaderboardText = topTen.map((user, index) => { + const rank = index + 1; + const avg = (user.amount / user.messages_count).toFixed(2); + + const medal = rank === 1 ? "๐Ÿฅ‡" : rank === 2 ? "๐Ÿฅˆ" : rank === 3 ? "๐Ÿฅ‰" : `**${rank}.**`; + + return `${medal} <@${user.user}> ยป **${user.amount}** :3 (avg: **${avg}**)`; + }); + + const embed = new EmbedBuilder() + .setTitle(`๐Ÿ† :3 Leaderboard`) + .setDescription(leaderboardText.join("\n") || "*No data yet!*") + .addFields({ + name: 'Stats', + value: `โ–บ **Tracking users:** ${theColonThreeLeaders.length}`, + inline: false + }) + .setFooter({ text: 'Keep using :3!!!!!!' }) + .setTimestamp(); + + await inter.reply({ embeds: [embed] }); + } + + @Slash({ description: "Stop tracking and delete my data" }) + async delete( + @SlashOption({ + description: "Type confirm to delete", + name: "confirmation", + required: true, + type: ApplicationCommandOptionType.String, + }) + confirmation: string, + inter: CommandInteraction + ) { + if (confirmation === "confirm") { + await db.delete(colonTable).where(eq(colonTable.user, inter.user.id)); + await inter.reply({ content: "All of data was deleted, you will not be tracked again.", flags: MessageFlags.Ephemeral }); + return; + } + + await inter.reply({ content: 'You need to type "confirm" to delete.', flags: MessageFlags.Ephemeral }); + } + + @Slash({ description: "Start tracking" }) + async start( + inter: CommandInteraction + ) { + const check = ( + await db + .select() + .from(colonTable) + .where(eq(colonTable.user, inter.user.id) + ) + )[0]; + + if (check) { + await inter.reply({ content: "Scythe already tracks your :3 data.", flags: MessageFlags.Ephemeral }); + return; + } + + await db.insert(colonTable).values({ + user: inter.user.id, + amount: 0, + messages_count: 0 + }); + await inter.reply({ content: "Scythe starts tracking your :3 data again.", flags: MessageFlags.Ephemeral }); + } +} + diff --git a/src/db/migrations/0001_warm_ego.sql b/src/db/migrations/0001_warm_ego.sql new file mode 100644 index 0000000..6a3d7fe --- /dev/null +++ b/src/db/migrations/0001_warm_ego.sql @@ -0,0 +1,5 @@ +CREATE TABLE `colonthree` ( + `user` text PRIMARY KEY NOT NULL, + `amount` integer NOT NULL, + `messages_count` integer NOT NULL +); diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..4e7b605 --- /dev/null +++ b/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,73 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a3e18d09-b7d4-48e3-8920-1fa55c43ee70", + "prevId": "fa98c7d2-c794-4f03-887b-f6f5c1b3891d", + "tables": { + "colonthree": { + "name": "colonthree", + "columns": { + "user": { + "name": "user", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages_count": { + "name": "messages_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tickets": { + "name": "tickets", + "columns": { + "user": { + "name": "user", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index ec51bed..0e72227 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1742570954158, "tag": "0000_nervous_komodo", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1742844574438, + "tag": "0001_warm_ego", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 95ac626..a6c3532 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,6 +1,16 @@ -import { text, sqliteTable } from "drizzle-orm/sqlite-core"; +import { text, sqliteTable, int } from "drizzle-orm/sqlite-core"; + +import type { InferSelectModel } from "drizzle-orm"; export const ticketsTable = sqliteTable("tickets", { user: text().primaryKey().notNull(), channel: text().notNull() -}) \ No newline at end of file +}) + +export const colonTable = sqliteTable("colonthree", { + user: text().primaryKey().notNull(), + amount: int().notNull(), + messages_count: int().notNull() +}) + +export type ColonThreeType = InferSelectModel; \ No newline at end of file diff --git a/src/events/members.ts b/src/events/members.ts index aca216d..cd0eb45 100644 --- a/src/events/members.ts +++ b/src/events/members.ts @@ -1,6 +1,10 @@ import type { TextChannel } from "discord.js"; import { Client, Discord, On, type ArgsOf } from "discordx"; +import db from "../db"; +import { colonTable } from "../db/schema"; +import { eq } from "drizzle-orm"; + @Discord() export class MemberEvents { @On({ event: "guildMemberAdd" }) @@ -14,13 +18,25 @@ export class MemberEvents { if (botRole) { await member.roles.add(botRole); } + } else { + await db.insert(colonTable).values({ + user: member.id, + amount: 0, + messages_count: 0 + }); } } + @On({ event: "guildMemberRemove" }) async memberRemove([member]: ArgsOf<"guildMemberRemove">, client: Client) { const channel = client.channels.cache.get(Bun.env.GOODBYE!) as TextChannel; await channel.send(`We're sad to see you go, ${member.user.username}.`); + + await db + .delete(colonTable) + .where(eq(colonTable.user, member.id)); } + @On({ event: "guildMemberUpdate" }) async memberUpdate([_, newM]: ArgsOf<"guildMemberUpdate">) { if (newM.roles.cache.get(Bun.env.BAD_ROLE!)) { diff --git a/src/events/messages.ts b/src/events/messages.ts index 821d7a8..e97de8f 100644 --- a/src/events/messages.ts +++ b/src/events/messages.ts @@ -3,6 +3,10 @@ import { snipeObject } from ".."; import { sleep } from "../utils/underage"; import { bumpRemind } from "../utils/bump"; +import db from "../db"; +import { colonTable } from "../db/schema"; +import { eq, sql } from "drizzle-orm"; + @Discord() export class MessageEvents { @On({ event: "messageDelete" }) @@ -10,9 +14,11 @@ export class MessageEvents { snipeObject.author = msg.author?.username ?? "unknown"; snipeObject.content = msg.content; } + @On({ event: "messageCreate" }) async messageCreate([msg]: ArgsOf<"messageCreate">, client: Client) { if (msg.author.id == msg.client.user.id) return; + const linkRegex = /instagram\.com\/([^\s?]+)/; if (linkRegex.test(msg.content)) { let fixedLink = msg.content.match(linkRegex); @@ -24,6 +30,7 @@ export class MessageEvents { await msg.suppressEmbeds(); } } + if (msg.embeds.length > 0) { if (msg.embeds[0].description?.includes("Bump done!")) { await msg.channel.send( @@ -34,5 +41,19 @@ export class MessageEvents { bumpRemind(client); } } + + const cThreeRegex = /:3/g; + if (cThreeRegex.test(msg.content)) { + let colonThrees = msg.content.match(cThreeRegex); + + await db.update(colonTable).set({ + amount: sql`${colonTable.amount} + ${colonThrees?.length}`, + messages_count: sql`${colonTable.messages_count} + 1`, + }).where(eq(colonTable.user, msg.author.id)); + } else { + await db.update(colonTable).set({ + messages_count: sql`${colonTable.messages_count} + 1`, + }).where(eq(colonTable.user, msg.author.id)); + } } }