Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

(Partially) Implement Levels #150

Closed
wants to merge 18 commits into from
Closed
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"prettier": "^2.6.2",
"prisma": "^3.14.0",
"ts-jest": "^28.0.2",
"ts-node-dev": "^1.1.8",
"ts-node-dev": "^2.0.0",
"typescript": "^4.6.4"
},
"engines": {
Expand Down
11 changes: 11 additions & 0 deletions prisma/migrations/20220601060604_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "Setting" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"starboardThreshold" INTEGER NOT NULL,

CONSTRAINT "Setting_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Setting_guildId_key" ON "Setting"("guildId");
16 changes: 16 additions & 0 deletions prisma/migrations/20220601190849_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "Starboard" (
"guildId" TEXT NOT NULL,
"messageId" TEXT NOT NULL,
"starboardMessageID" TEXT NOT NULL,
"stars" INTEGER NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Starboard_guildId_key" ON "Starboard"("guildId");

-- CreateIndex
CREATE UNIQUE INDEX "Starboard_messageId_key" ON "Starboard"("messageId");

-- CreateIndex
CREATE UNIQUE INDEX "Starboard_starboardMessageID_key" ON "Starboard"("starboardMessageID");
21 changes: 21 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ model SpecialRole {
id String @id @default(uuid())
}

model Setting {
id String @id @default(uuid())
guildId String @unique
starboardThreshold Int
}

model ModmailMessage {
guildId String
channelId String?
Expand Down Expand Up @@ -88,3 +94,18 @@ model Modmail {
closed Boolean @default(false)
id String @id @default(uuid())
}

model LevelEntry {
id String @id @default(uuid())
memberId String @unique
level Int
exp Int
lastTime DateTime
}

model Starboard {
guildId String @unique
messageId String @unique
starboardMessageID String @unique
stars Int
}
25 changes: 25 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SpecialRole,
} from "../database";
import { BotCommand } from "../structures";
import { setSetting } from "../database/settings";

const badges: APIApplicationCommandOptionChoice<string>[] = Object.keys(
DEFAULT_BADGES
Expand All @@ -39,12 +40,17 @@ const specChannels: APIApplicationCommandOptionChoice<string>[] = [
"roles",
"appeals",
"modmail",
"starboard",
"help",
].map((v) => ({
name: v,
value: v,
}));

const settings: APIApplicationCommandOptionChoice<string>[] = [
"starboardThreshold",
].map((v) => ({ name: v, value: v }));

class Config extends BotCommand {
constructor() {
super(
Expand Down Expand Up @@ -106,6 +112,17 @@ class Config extends BotCommand {
.setRequired(true)
)
)
.addSubcommand((sub) =>
sub
.setName("starboardthreshold")
.setDescription("Set the starboard threshold.")
.addNumberOption((opt) =>
opt
.setName("value")
.setDescription("The starboard threshold.")
.setRequired(true)
)
)
.toJSON(),
{ requiredPerms: ["ADMINISTRATOR"] }
);
Expand Down Expand Up @@ -149,6 +166,14 @@ class Config extends BotCommand {
case "setrole":
await Config.setRole(guildId, interaction);
break;
case "starboardthreshold":
await setSetting(guildId, {
starboardThreshold: interaction.options.getNumber(
"value",
true
),
});
break;
default:
await interaction.reply("How did we get here?");
return;
Expand Down
2 changes: 1 addition & 1 deletion src/database/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type SpecialChannel =
| "roles"
| "appeals"
| "modmail"
| "warnings"
| "starboard"
| "help";

/**
Expand Down
17 changes: 17 additions & 0 deletions src/database/levels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getClient } from "./index";

export type LevelEntry = {
level: number;
exp: number;
lastTime: Date;
};

export async function getLevelEntry(
memberId: string
): Promise<LevelEntry | null> {
const client = getClient();
return client.levelEntry.findFirst({
select: { level: true, exp: true, lastTime: true },
where: { memberId },
});
}
48 changes: 48 additions & 0 deletions src/database/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Prisma, Setting } from "@prisma/client";
import { getClient } from ".";

/**
* Get all settings for a specific guild
* @param {string} guildId
* @param {Setting} setting
*/
export const getSettings = async (guildId: string): Promise<Setting | null> => {
const client = getClient();
const result = await client.setting.findFirst({
where: { guildId },
});

if (!result) return null;
return result;
};

/**
* Set a specific setting in a guild
* @param {string} guildId
* @param {Prisma.SettingCreateInput} settings
*/
export const setSetting = async (
guildId: string,
settings: Partial<Prisma.SettingCreateInput>
): Promise<void> => {
const client = getClient();
const result = await client.setting.findFirst({
where: { guildId },
});

if (result === null) {
await client.setting.create({
data: {
...(settings as Prisma.SettingCreateInput),
guildId,
},
});
} else {
await client.setting.update({
data: settings,
where: {
guildId,
},
});
}
};
54 changes: 54 additions & 0 deletions src/database/starboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Message, MessageEmbed, TextChannel } from "discord.js";
import { getClient } from ".";
import { getSpecialChannel } from "./channels";

export const addStarboard = async (
guildId: string,
message: Message
): Promise<void> => {
const channel = (await getSpecialChannel(
guildId,
"starboard"
)) as TextChannel | null;

if (!channel) return;

const starboardEmbed = new MessageEmbed()
.setAuthor({
iconURL:
message.member?.user.avatarURL() ||
message.member?.user.defaultAvatarURL,
name: message.member?.user.tag as string,
})
.setDescription(message.content as string)
.setColor("ORANGE");

const starboardMessage = await channel.send({ embeds: [starboardEmbed] });

const client = getClient();
await client.starboard.create({
data: {
messageId: message.id,
starboardMessageID: starboardMessage.id,
stars: 0,
guildId,
},
});
};

export const removeStarboard = async (guildId: string, messageId: string) => {
const client = getClient();
const deletedStarboard = await client.starboard.delete({
where: { messageId },
});

const channel = (await getSpecialChannel(
guildId,
"starboard"
)) as TextChannel | null;

const message = await channel?.messages.fetch(
deletedStarboard.starboardMessageID
);
message?.delete();
};
5 changes: 5 additions & 0 deletions src/events/messageCreate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Message } from "discord.js";

import { updateLevels } from "../services/levels";
import { Message, TextChannel } from "discord.js";
import { v4 as uuid } from "uuid";
import { getSpecialChannel } from "../database";
Expand Down Expand Up @@ -34,5 +37,7 @@ export default TypedEvent({
) {
await message.member?.timeout(600_000, "Mass mentions");
}

await updateLevels(message.author.id);
},
});
24 changes: 24 additions & 0 deletions src/events/messageReactionAdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Message } from "discord.js";
import { getSettings } from "../database/settings";
import { addStarboard } from "../database/starboard";
import { TypedEvent } from "../types";

export default TypedEvent({
eventName: "messageReactionAdd",
run: async (_, reaction) => {
if (
reaction.emoji.name !== "⭐" ||
!reaction.message.guild ||
!reaction.message.member ||
reaction.message.author?.bot
)
return;

const guildId = reaction.message.guild.id;
const guildSettings = await getSettings(guildId);

if (reaction.count !== guildSettings?.starboardThreshold) return;

await addStarboard(guildId, reaction.message as Message);
},
});
28 changes: 28 additions & 0 deletions src/events/messageReactionRemove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Setting } from "@prisma/client";
import { getSettings } from "../database/settings";
import { removeStarboard } from "../database/starboard";
import { TypedEvent } from "../types";

export default TypedEvent({
eventName: "messageReactionRemove",
run: async (_, reaction) => {
if (
reaction.emoji.name !== "⭐" ||
!reaction.message.guild ||
!reaction.message.member ||
reaction.message.author?.bot
)
return;

const guildId = reaction.message.guild.id;
const guildSettings = (await getSettings(guildId)) as Setting;

if (
reaction.count !== null &&
reaction.count >= guildSettings?.starboardThreshold
)
return;

await removeStarboard(reaction.message.guild.id, reaction.message.id);
},
});
16 changes: 16 additions & 0 deletions src/services/levels/commands/rank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { CommandInteraction } from "discord.js";
import { BotCommand } from "../../../structures";

export default class RankCommand extends BotCommand {
constructor() {
super(
new SlashCommandBuilder()
.setName("rank")
.setDescription("Check your ranking in Levels.")
.toJSON()
);
}

public async execute(interaction: CommandInteraction): Promise<void> {}
}
40 changes: 40 additions & 0 deletions src/services/levels/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getClient } from "../../database";
import { getLevelEntry } from "../../database/levels";

export async function isLevelCooldown(memberId: string): Promise<boolean> {
const result = await getLevelEntry(memberId);

if (result === null) return false;

const now = new Date();
return now.getSeconds() - result.lastTime.getSeconds() <= 60;
}

export async function upsertLevelEntry(
memberId: string,
exp: number
): Promise<void> {
const client = getClient();
const result = await getLevelEntry(memberId);
if (result === null) {
await client.levelEntry.create({
data: {
memberId,
level: 0,
exp,
lastTime: new Date(),
},
});
} else {
const newExp = result.exp + exp;
const requiredExp = 5 * result.level ** 2 + 50 * result.level + 100;

await client.levelEntry.update({
data: {
level: newExp >= requiredExp ? result.level + 1 : result.level,
exp: newExp >= requiredExp ? newExp - requiredExp : newExp,
},
where: { memberId: memberId },
});
}
}
Loading