diff --git a/src/namespaces/coins.ts b/src/namespaces/coins.ts index 2a1e1915..80d3fc99 100644 --- a/src/namespaces/coins.ts +++ b/src/namespaces/coins.ts @@ -1,19 +1,25 @@ import userTable from "#tables/user.ts" +import guildTable from "#tables/guild.ts" import pointTable from "#tables/point.ts" import noteTable from "#tables/rating.ts" import messageTable from "#tables/message.ts" +import activeTable from "#tables/active.ts" +import { ResponseCache } from "#database" + +export interface FullUser { + _id: number + id: string + coins: number // sum(coins) + points: number // sum(amount) rel(to_id) + rating: number // avg(value) rel(to_id) + rateOthers: number // count(rating.*) rel(from_id) + active: boolean // exists? in guild_id rel(user_id) + messages: number // count(message.*) in guild_id rel(author_id) +} export async function giveHourlyCoins() { // get all users with points and ratings from a join query. - const users: { - _id: number - coins: number - points: `${number}` - rating: `${number}` - givenNotes: `${number}` - active: `${string}` | null - messages: `${number}` - }[] = await userTable.query + const users: FullUser[] = await userTable.query .leftJoin( pointTable.query .select("to_id") @@ -56,7 +62,7 @@ export async function giveHourlyCoins() { "user.coins", "point_totals.points", "notes.rating", - "given_notes.total as givenNotes", + "given_notes.total as rateOthers", "active.user_id as active", "message_totals.messages", ) @@ -84,16 +90,105 @@ export async function giveHourlyCoins() { .insert( users.map((user) => ({ _id: user._id, - coins: Math.ceil( - user.coins + - +user.points * Math.max(1, +user.rating) + - +user.givenNotes * 5 + - (user.active - ? Math.max(10, Math.floor(+user.messages * 0.001)) - : 0), - ), + coins: Math.ceil(user.coins + getUserHourlyCoins(user)), })), ) .onConflict("_id") .merge(["coins"]) } + +export function getUserHourlyCoins(user: FullUser): number { + return ( + +user.points * Math.max(1, +user.rating) + + +user.rateOthers * 5 + + (user.active ? Math.max(10, Math.floor(+user.messages * 0.001)) : 0) + ) +} + +const fullUserCache = new ResponseCache( + async (userId: number, guildId: number) => { + // 1. Récupérer les informations de base de l'utilisateur + const user = await userTable.query + .select("_id", "id", "coins") + .where("_id", userId) + .first() + + if (!user) { + return null // Si l'utilisateur n'existe pas + } + + // 2. Calculer la somme des points reçus par l'utilisateur (table `point`) + const points = await pointTable.query + .sum({ points: "amount" }) + .where("to_id", userId) + .first() + + // 3. Calculer la moyenne des évaluations reçues (table `rating`) dans la guilde + const rating = await noteTable.query + .avg({ rating: "value" }) + .where("to_id", userId) + .andWhere("guild_id", guildId) + .first() + + // 4. Compter le nombre d'évaluations faites par l'utilisateur (table `rating`) + const rateOthers = await noteTable.query + .count({ rateOthers: "*" }) + .where("from_id", userId) + .first() + + // 5. Vérifier si l'utilisateur est actif dans la guilde (table `active`) + const active = await activeTable.query + .select("user_id") + .where("user_id", userId) + .andWhere("guild_id", guildId) + .first() + + // 6. Compter le nombre de messages envoyés par l'utilisateur dans la guilde (table `message`) + const messages = await messageTable.query + .count({ messages: "*" }) + .where("author_id", userId) + .andWhere("guild_id", guildId) + .first() + + // Assemblage du résultat final + return { + _id: user._id, + id: user.id, + coins: Number(user.coins), + points: Number(points?.points || 0), // Valeur par défaut à 0 si pas de points + rating: Number(rating?.rating || 0), // Valeur par défaut à 0 si pas d'évaluation + rateOthers: Number(rateOthers?.rateOthers || 0), // Valeur par défaut à 0 + active: !!active, // Convertit en booléen + messages: Number(messages?.messages || 0), // Valeur par défaut à 0 si pas de messages + } + }, + 6_000_000, +) + +export async function getFullUser(user: { id: string }, guild: { id: string }) { + const userId = await userTable.cache.get( + `user.id.${user.id}`, + async (query) => { + return query + .select("_id") + .where("id", user.id) + .first() + .then((user) => user?._id) + }, + ) + + const guildId = await guildTable.cache.get( + `guild.id.${guild.id}`, + async (query) => { + return query + .select("_id") + .where("id", guild.id) + .first() + .then((guild) => guild?._id) + }, + ) + + if (!userId || !guildId) return null + + return fullUserCache.get(user.id, userId, guildId) +} diff --git a/src/namespaces/rating.ts b/src/namespaces/rating.ts index c4e63abf..4ba24cb0 100644 --- a/src/namespaces/rating.ts +++ b/src/namespaces/rating.ts @@ -11,19 +11,19 @@ export interface RatingLadderLine { const mineRatingCount = 2 -export function renderNoteValue(value: number) { +export function renderRatingValue(value: number) { return `**${value.toFixed(2).replace(/\.?0+$/, "")}**` } -export function renderNoteBar(value?: number) { +export function renderRatingBar(value?: number) { const full = "▰" const empty = "▱" const round = Math.round(value ?? 0) return full.repeat(round) + empty.repeat(5 - round) } -export function renderNoteLine(value: number, count: number) { - return `${renderNoteBar(value)} ${renderNoteValue(value)} / 5 (*x${count}*)` +export function renderRatingLine(value: number, count: number) { + return `${renderRatingBar(value)} ${renderRatingValue(value)} / 5 (*x${count}*)` } export const ratingLadder = (guild_id?: number) => @@ -63,7 +63,7 @@ export const ratingLadder = (guild_id?: number) => ) }, formatLine(line) { - return `${app.formatRank(line.rank)} ${renderNoteLine( + return `${app.formatRank(line.rank)} ${renderRatingLine( line.score, line.rating_count, )} <@${line.target}>` @@ -128,7 +128,7 @@ export async function ratingEmbed(target: app.GuildMember) { const fields: app.EmbedField[] = [ { name: "Global rating", - value: renderNoteLine(globalRating.avg, globalRating.count), + value: renderRatingLine(globalRating.avg, globalRating.count), inline: false, }, ] @@ -140,7 +140,7 @@ export async function ratingEmbed(target: app.GuildMember) { ) { fields.push({ name: target.guild.name, - value: renderNoteLine(guildRating.avg, guildRating.count), + value: renderRatingLine(guildRating.avg, guildRating.count), inline: false, }) } @@ -151,7 +151,7 @@ export async function ratingEmbed(target: app.GuildMember) { value: externalRating .map( ({ rating, guild }) => - `${renderNoteLine(rating.avg, rating.count)} - **${guild.name}**`, + `${renderRatingLine(rating.avg, rating.count)} - **${guild.name}**`, ) .join("\n"), inline: false, diff --git a/src/namespaces/tools.ts b/src/namespaces/tools.ts index aaa792a2..5413cb21 100644 --- a/src/namespaces/tools.ts +++ b/src/namespaces/tools.ts @@ -35,7 +35,7 @@ export async function sendLog( const userCache = new ResponseCache((id: string) => { return users.query.where("id", id).first() -}, 60_000) +}, 600_000) export async function getUser(user: { id: string }): Promise export async function getUser(user: { id: string }, force: true): Promise @@ -59,7 +59,7 @@ export async function getUser(user: { id: string }, force?: true) { const guildCache = new ResponseCache((id: string) => { return guilds.query.where("id", id).first() -}, 60_000) +}, 600_000) export async function getGuild(guild: { id: string diff --git a/src/slash/profile.ts b/src/slash/profile.ts new file mode 100644 index 00000000..ab6cfa9a --- /dev/null +++ b/src/slash/profile.ts @@ -0,0 +1,55 @@ +import * as app from "#app" + +import messageTable from "#tables/message.ts" + +export default new app.SlashCommand({ + name: "profile", + description: "View your profile", + guildOnly: true, + channelType: "guild", + async run(interaction) { + const user = await app.getFullUser(interaction.user, interaction.guild) + + if (!user) return interaction.reply("You don't have a profile yet.") + + const guild = await app.getGuild(interaction.guild) + + if (!guild) + return interaction.reply("This guild is not registered in the database.") + + const pointRank = await app.getPointRank(interaction.user) + + return interaction.reply({ + embeds: [ + new app.EmbedBuilder() + .setTitle(`Profile of ${interaction.user.tag}`) + .setThumbnail(interaction.user.displayAvatarURL()) + .setDescription( + (pointRank + ? `Helper rank: **#${pointRank.rank}** (**${user.points}** pts)\n` + : "") + + `Total money: **${user.coins}** 🪙\n` + + `Hourly money: **${Math.floor(app.getUserHourlyCoins(user))}** 🪙\n` + + `Messages sent: **${await messageTable.cache.count( + `author_id = ${user._id} AND guild_id = ${guild._id}`, + )}**`, + ) + .setFields( + { + name: `Rating: ${app.renderRatingValue(user.rating)}`, + value: app.renderRatingBar(user.rating), + inline: true, + }, + { + name: "Prestige", + value: + user.active && guild.active_role_id + ? app.roleMention(guild.active_role_id) + : "`nope`", + inline: true, + }, + ), + ], + }) + }, +})