Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/xp system #19

Merged
merged 4 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
version: 9.0.0

- name: Setup Node.js
uses: actions/setup-node@v4
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ WORKDIR /app

COPY package.json ./
COPY pnpm-lock.yaml ./
COPY prisma ./prisma

RUN corepack enable

Expand All @@ -20,11 +21,12 @@ WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma

RUN corepack enable

RUN pnpm install --prod

VOLUME /data

CMD ["pnpm", "start"]
CMD ["pnpm", "start:prod"]
12 changes: 12 additions & 0 deletions compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
bot:
env_file:
- path: ./.env
required: false
build:
context: .
dockerfile: Dockerfile

volumes:
eery-data:
external: false
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
"main": "dist/index.js",
"type": "module",
"license": "MIT",
"packageManager": "pnpm@9.0.0",
"scripts": {
"clean": "rimraf dist",
"start": "node .",
"start:env": "node --env-file=.env .",
"start:prod": "prisma migrate deploy && pnpm start",
"build": "pnpm run clean && tsc && tsc-alias",
"dev": "nodemon --exec \"pnpm run build && pnpm run start:env\" --watch src --ext ts",
"lint": "biome check src",
"lint:fix": "biome check --apply src",
"generate": "prisma generate",
"prepare": "husky || true"
},
"devDependencies": {
Expand All @@ -22,12 +25,12 @@
"@types/node": "^20.12.12",
"husky": "^9.0.11",
"nodemon": "^3.1.1",
"prisma": "^5.14.0",
"rimraf": "^5.0.7",
"tsc-alias": "^1.8.10",
"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",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Events" (
"id" TEXT NOT NULL PRIMARY KEY,
"description" TEXT NOT NULL,
"date" DATETIME 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", "date", "description", "guild_id", "id", "repeat", "updated_at") SELECT "created_at", "created_by", "date", "description", "guild_id", "id", "repeat", "updated_at" FROM "Events";
DROP TABLE "Events";
ALTER TABLE "new_Events" RENAME TO "Events";
CREATE TABLE "new_User" (
"id" TEXT NOT NULL PRIMARY KEY,
"discord_id" TEXT NOT NULL,
"xp" INTEGER NOT NULL DEFAULT 0,
"level" INTEGER NOT NULL DEFAULT 1,
"birthday" DATETIME,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL
);
INSERT INTO "new_User" ("birthday", "created_at", "discord_id", "id", "level", "updated_at", "xp") SELECT "birthday", "created_at", "discord_id", "id", "level", "updated_at", "xp" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_discord_id_key" ON "User"("discord_id");
CREATE TABLE "new_Guild" (
"id" TEXT NOT NULL PRIMARY KEY,
"discord_id" TEXT NOT NULL,
"main_channel" TEXT,
"daily_id" TEXT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL,
CONSTRAINT "Guild_daily_id_fkey" FOREIGN KEY ("daily_id") REFERENCES "Daily" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Guild" ("created_at", "daily_id", "discord_id", "id", "main_channel", "updated_at") SELECT "created_at", "daily_id", "discord_id", "id", "main_channel", "updated_at" FROM "Guild";
DROP TABLE "Guild";
ALTER TABLE "new_Guild" RENAME TO "Guild";
CREATE UNIQUE INDEX "Guild_discord_id_key" ON "Guild"("discord_id");
PRAGMA foreign_key_check("Events");
PRAGMA foreign_key_check("User");
PRAGMA foreign_key_check("Guild");
PRAGMA foreign_keys=ON;
68 changes: 34 additions & 34 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,53 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
provider = "sqlite"
url = env("DATABASE_URL")
}

model User {
id String @id @default(cuid()) @map("id")
discordId String @unique @map("discord_id")
xp Int @default(0) @map("xp")
level Int @default(0) @map("level")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
birthday DateTime? @map("birthday")
Events Events[] @relation("UserEvents")
id String @id @default(cuid()) @map("id")
discordId String @unique @map("discord_id")
xp Int @default(0) @map("xp")
level Int @default(1) @map("level")
birthday DateTime? @map("birthday")
Events Events[] @relation("UserEvents")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
}

model Events {
id String @id @default(cuid()) @map("id")
description String @map("description")
date DateTime @map("date")
repeat Boolean @default(false) @map("repeat")
guildId String? @map("guild_id")
Guild Guild? @relation("GuildEvents", fields: [guildId], references: [id])
createdBy String? @map("created_by")
User User? @relation("UserEvents", fields: [createdBy], references: [discordId])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
id String @id @default(cuid()) @map("id")
description String @map("description")
date DateTime @map("date")
repeat Boolean @default(false) @map("repeat")
guildId String? @map("guild_id")
Guild Guild? @relation("GuildEvents", fields: [guildId], references: [id])
createdBy String? @map("created_by")
User User? @relation("UserEvents", fields: [createdBy], references: [discordId])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
}

model Daily {
id String @id @default(cuid()) @map("id")
day Int @default(0) @map("day")
afternoon Int @default(0) @map("afternoon")
night Int @default(0) @map("night")
Guild Guild[]
id String @id @default(cuid()) @map("id")
day Int @default(0) @map("day")
afternoon Int @default(0) @map("afternoon")
night Int @default(0) @map("night")
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")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")
Daily Daily? @relation(fields: [dailyId], references: [id])
dailyId String? @map("daily_id")
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])
dailyId String? @map("daily_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
}
7 changes: 7 additions & 0 deletions src/Client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ExpHandler from "@/structures/xpHandler.js";
import { PrismaClient } from "@prisma/client";
import { SapphireClient, container } from "@sapphire/framework";
import type { ClientOptions } from "discord.js";

Expand All @@ -21,11 +23,16 @@ export class Client extends SapphireClient {
throw new Error(`Failed to fetch: ${response.statusText}`);
return response.json();
};

container.db = new PrismaClient();
container.expHandler = new ExpHandler();
}
}

declare module "@sapphire/pieces" {
interface Container {
fetch: <T>(endpoint: string, options?: Partial<RequestInit>) => Promise<T>;
db: PrismaClient;
expHandler: ExpHandler;
}
}
62 changes: 62 additions & 0 deletions src/commands/guild/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
EmbedColors,
leaderboardEmojis,
leaderboardIcon,
} from "@/utils/contants.js";
import { Command } from "@sapphire/framework";
import { EmbedBuilder } from "discord.js";

export class LeaderboardCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, { ...options });
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName("leaderboard")
.setDescription("Mostrar o ranking de nivel dos usuários"),
);
}

public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const guild = interaction.guild;

if (!guild) return;

const leaderboard = await this.container.expHandler.getLeaderboard();
const levels = leaderboard.map((user) => `Nível ${user.level}`);
const users = leaderboard.map((user, index) => {
const nickname =
guild.members.cache.get(user.discordId)?.displayName ??
"Não Encontrado";

return `${leaderboardEmojis[index] ?? `${index + 1}.`} ${nickname}`;
});

const embed = new EmbedBuilder()
.setAuthor({
name: `Top ${users.length} Níveis`,
iconURL: leaderboardIcon,
})
.setColor(EmbedColors.default)
.addFields([
{
name: "Usuário",
value: users.join("\n"),
inline: true,
},
{
name: "Nível",
value: levels.join("\n"),
inline: true,
},
]);

await interaction.reply({
embeds: [embed],
ephemeral: false,
fetchReply: false,
});
}
}
65 changes: 65 additions & 0 deletions src/commands/user/level.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Command } from "@sapphire/framework";
import {
EmbedBuilder,
type GuildMember,
type ImageURLOptions,
} from "discord.js";

export class LevelCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, { ...options });
}

public override registerApplicationCommands(registry: Command.Registry) {
registry.registerChatInputCommand((builder) =>
builder
.setName("level")
.setDescription("Mostrar o nivel atual do usuário")
.addUserOption((option) => {
option.setName("user");
option.setDescription("Usuário que deseja ver o nivel");
option.setRequired(false);
return option;
}),
);
}

public async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const guild = interaction.guild;

if (!guild) return;

const member =
guild?.members.cache.get(interaction.options.getUser("user")?.id ?? "") ??
(interaction.member as GuildMember);

if (!member) return;

const nickname = member.displayName;
const color = member.displayColor;
const config: ImageURLOptions = {
extension: "png",
size: 4096,
forceStatic: false,
};

const stats = await this.container.expHandler.getStats(member, guild);
const progress = this.container.expHandler.progressBar(
stats.exp,
stats.level,
15,
);

const icon = member.displayAvatarURL(config);
const embed = new EmbedBuilder()
.setAuthor({ name: nickname, iconURL: icon })
.setDescription(`***Nivel ${stats.level}***\n**${progress}**`)
.setColor(color);

await interaction.reply({
embeds: [embed],
ephemeral: false,
fetchReply: false,
});
}
}
11 changes: 11 additions & 0 deletions src/events/giveXp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { container } from "@sapphire/pieces";
import type { Message } from "discord.js";

export default function giveXp(message: Message) {
const member = message.member;
const guild = message.guild;

if (!member || !guild) return;

container.expHandler.addExp(member, guild);
}
Loading