diff --git a/src/events/ready/utils/playerTracking.ts b/src/events/ready/utils/playerTracking.ts index 3e42e4b1..382de992 100644 --- a/src/events/ready/utils/playerTracking.ts +++ b/src/events/ready/utils/playerTracking.ts @@ -50,17 +50,14 @@ export const run: EventUtil["run"] = async (client) => { break; } - const embed = await EmbedCreator.createRecentPlayEmbed( - score, - DroidHelper.getAvatarURL(trackedPlayer.uid), - 8311585, - ); - channel.send({ + ...(await EmbedCreator.createRecentPlayEmbed( + score, + 8311585, + )), content: MessageCreator.createAccept( `Recent play for ${player.username}:`, ), - embeds: [embed], }); } } diff --git a/src/interactions/buttons/Onboarding/onboardingShowMostRecentPlay.ts b/src/interactions/buttons/Onboarding/onboardingShowMostRecentPlay.ts index 305727c8..e3a984e0 100644 --- a/src/interactions/buttons/Onboarding/onboardingShowMostRecentPlay.ts +++ b/src/interactions/buttons/Onboarding/onboardingShowMostRecentPlay.ts @@ -95,23 +95,18 @@ export const run: ButtonCommand["run"] = async (_, interaction) => { PPCalculationMethod.live, ); - const embed = await EmbedCreator.createRecentPlayEmbed( - score, - player instanceof Player - ? player.avatarUrl - : DroidHelper.getAvatarURL(player.id), - interaction.member.displayColor, - scoreAttribs?.attributes, - undefined, - localization.language, - ); - const options: InteractionReplyOptions = { content: MessageCreator.createAccept( localization.getTranslation("recentPlayDisplay"), player.username, ), - embeds: [embed], + ...(await EmbedCreator.createRecentPlayEmbed( + score, + interaction.member.displayColor, + scoreAttribs?.attributes, + undefined, + localization.language, + )), ephemeral: true, }; diff --git a/src/interactions/commands/osu! and osu!droid/compare/compare.ts b/src/interactions/commands/osu! and osu!droid/compare/compare.ts index f7b2c4e2..4d512edc 100644 --- a/src/interactions/commands/osu! and osu!droid/compare/compare.ts +++ b/src/interactions/commands/osu! and osu!droid/compare/compare.ts @@ -160,23 +160,18 @@ export const run: SlashCommand["run"] = async (_, interaction) => { PPCalculationMethod.live, ); - const embed = await EmbedCreator.createRecentPlayEmbed( - score, - player instanceof Player - ? player.avatarUrl - : DroidHelper.getAvatarURL(player.id), - (interaction.member)?.displayColor, - scoreAttribs?.attributes, - undefined, - localization.language, - ); - const options: InteractionReplyOptions = { + ...(await EmbedCreator.createRecentPlayEmbed( + score, + (interaction.member)?.displayColor, + scoreAttribs?.attributes, + undefined, + localization.language, + )), content: MessageCreator.createAccept( localization.getTranslation("comparePlayDisplay"), player.username, ), - embeds: [embed], }; const replay = await ReplayHelper.analyzeReplay(score); diff --git a/src/interactions/commands/osu! and osu!droid/profile/subcommands/profile-bindinfo.ts b/src/interactions/commands/osu! and osu!droid/profile/subcommands/profile-bindinfo.ts index a2555102..d52a243f 100644 --- a/src/interactions/commands/osu! and osu!droid/profile/subcommands/profile-bindinfo.ts +++ b/src/interactions/commands/osu! and osu!droid/profile/subcommands/profile-bindinfo.ts @@ -1,4 +1,4 @@ -import { bold, GuildMember } from "discord.js"; +import { AttachmentBuilder, bold, GuildMember } from "discord.js"; import { DatabaseManager } from "@database/DatabaseManager"; import { SlashSubcommand } from "structures/core/SlashSubcommand"; import { MessageCreator } from "@utils/creators/MessageCreator"; @@ -131,6 +131,9 @@ export const run: SlashSubcommand["run"] = async (_, interaction) => { ? player.rank : ((await DroidHelper.getPlayerPPRank(player.id)) ?? 0); + const avatar = await DroidHelper.getAvatar(player.id); + const embedAvatarURL = "attachment://avatar.png"; + embed .setAuthor({ name: StringHelper.formatString( @@ -140,11 +143,7 @@ export const run: SlashSubcommand["run"] = async (_, interaction) => { iconURL: interaction.user.avatarURL()!, url: ProfileManager.getProfileLink(player.id).toString(), }) - .setThumbnail( - player instanceof Player - ? player.avatarUrl - : DroidHelper.getAvatarURL(player.id), - ) + .setThumbnail(embedAvatarURL) .setDescription( `[${localization.getTranslation("avatarLink")}](${ player instanceof Player @@ -176,6 +175,9 @@ export const run: SlashSubcommand["run"] = async (_, interaction) => { InteractionHelper.reply(interaction, { embeds: [embed], + files: avatar + ? [new AttachmentBuilder(avatar, { name: "avatar.png" })] + : [], }); }; diff --git a/src/interactions/commands/osu! and osu!droid/recent/recent.ts b/src/interactions/commands/osu! and osu!droid/recent/recent.ts index 4b30aff6..9fbc146f 100644 --- a/src/interactions/commands/osu! and osu!droid/recent/recent.ts +++ b/src/interactions/commands/osu! and osu!droid/recent/recent.ts @@ -213,23 +213,20 @@ export const run: SlashCommand["run"] = async (_, interaction) => { ) )?.attributes; - const embed = await EmbedCreator.createRecentPlayEmbed( - score, - player instanceof Player - ? player.avatarUrl - : DroidHelper.getAvatarURL(player.id), - (interaction.member)?.displayColor, - scoreAttribs, - score instanceof RecentPlay ? (score.osuAttribs ?? null) : undefined, - localization.language, - ); - const options: InteractionReplyOptions = { + ...(await EmbedCreator.createRecentPlayEmbed( + score, + (interaction.member)?.displayColor, + scoreAttribs, + score instanceof RecentPlay + ? (score.osuAttribs ?? null) + : undefined, + localization.language, + )), content: MessageCreator.createAccept( localization.getTranslation("recentPlayDisplay"), player.username, ), - embeds: [embed], }; const replay = await ReplayHelper.analyzeReplay(score); diff --git a/src/interactions/commands/osu! and osu!droid/simulate/simulate.ts b/src/interactions/commands/osu! and osu!droid/simulate/simulate.ts index 1763cdc7..d8d0d9e8 100644 --- a/src/interactions/commands/osu! and osu!droid/simulate/simulate.ts +++ b/src/interactions/commands/osu! and osu!droid/simulate/simulate.ts @@ -658,22 +658,17 @@ export const run: SlashCommand["run"] = async (_, interaction) => { BeatmapManager.setChannelLatestBeatmap(interaction.channelId, score.hash); - const embed = await EmbedCreator.createRecentPlayEmbed( - score, - player instanceof Player - ? player.avatarUrl - : DroidHelper.getAvatarURL(player.id), - (interaction.member)?.displayColor, - droidAttribs?.attributes, - osuAttribs?.attributes, - ); - InteractionHelper.reply(interaction, { + ...(await EmbedCreator.createRecentPlayEmbed( + score, + (interaction.member)?.displayColor, + droidAttribs?.attributes, + osuAttribs?.attributes, + )), content: MessageCreator.createAccept( localization.getTranslation("simulatedPlayDisplay"), player.username, ), - embeds: [embed], }); }; diff --git a/src/interactions/contextmenus/message/compareScore.ts b/src/interactions/contextmenus/message/compareScore.ts index 522e497c..86107c37 100644 --- a/src/interactions/contextmenus/message/compareScore.ts +++ b/src/interactions/contextmenus/message/compareScore.ts @@ -9,7 +9,6 @@ import { CommandHelper } from "@utils/helpers/CommandHelper"; import { InteractionHelper } from "@utils/helpers/InteractionHelper"; import { BeatmapManager } from "@utils/managers/BeatmapManager"; import { Modes } from "@rian8337/osu-base"; -import { Player } from "@rian8337/osu-droid-utilities"; import { GuildMember, InteractionReplyOptions } from "discord.js"; import { MessageButtonCreator } from "@utils/creators/MessageButtonCreator"; import { PPCalculationMethod } from "@enums/utils/PPCalculationMethod"; @@ -121,23 +120,18 @@ export const run: MessageContextMenuCommand["run"] = async (_, interaction) => { PPCalculationMethod.live, ); - const embed = await EmbedCreator.createRecentPlayEmbed( - score, - player instanceof Player - ? player.avatarUrl - : DroidHelper.getAvatarURL(player.id), - (interaction.member)?.displayColor, - scoreAttribs?.attributes, - undefined, - localization.language, - ); - const options: InteractionReplyOptions = { + ...(await EmbedCreator.createRecentPlayEmbed( + score, + (interaction.member)?.displayColor, + scoreAttribs?.attributes, + undefined, + localization.language, + )), content: MessageCreator.createAccept( localization.getTranslation("comparePlayDisplay"), player.username, ), - embeds: [embed], }; const replay = await ReplayHelper.analyzeReplay(score); diff --git a/src/utils/creators/EmbedCreator.ts b/src/utils/creators/EmbedCreator.ts index a7630617..6ffec92d 100644 --- a/src/utils/creators/EmbedCreator.ts +++ b/src/utils/creators/EmbedCreator.ts @@ -14,6 +14,7 @@ import { underscore, channelMention, hyperlink, + AttachmentBuilder, } from "discord.js"; import { Config } from "@core/Config"; import { BeatmapManager } from "@utils/managers/BeatmapManager"; @@ -514,7 +515,6 @@ export abstract class EmbedCreator { > | Score | RecentPlay, - playerAvatarURL: string, embedColor?: ColorResolvable, droidAttribs?: CompleteCalculationAttributes< DroidDifficultyAttributes, @@ -525,7 +525,7 @@ export abstract class EmbedCreator { OsuPerformanceAttributes > | null, language: Language = "en", - ): Promise { + ): Promise { const localization = this.getLocalization(language); const BCP47 = LocaleHelper.convertToBCP47(language); const arrow = Symbols.rightArrowSmall; @@ -543,9 +543,19 @@ export abstract class EmbedCreator { ? score.completeModString : DroidHelper.getCompleteModString(score.mode); + const avatar = await DroidHelper.getAvatar(score.uid); + const avatarURL = "attachment://avatar.png"; + + const options: BaseMessageOptions = { + embeds: [embed], + files: avatar + ? [new AttachmentBuilder(avatar, { name: avatarURL })] + : [], + }; + embed.setAuthor({ name: `${score instanceof Score || score instanceof RecentPlay ? score.title : DroidHelper.cleanupFilename(score.filename)} ${modString}`, - iconURL: playerAvatarURL, + iconURL: avatarURL, }); if (droidAttribs === undefined && osuAttribs !== null) { @@ -602,7 +612,7 @@ export abstract class EmbedCreator { }/${accuracy.nmiss}]`; embed.setDescription(beatmapInformation); - return embed; + return options; } const beatmap: MapInfo = (await BeatmapManager.getBeatmap(score.hash, { @@ -616,7 +626,7 @@ export abstract class EmbedCreator { } | ${osuAttribs.difficulty.starRating.toFixed(2)}${ Symbols.star }]`, - iconURL: playerAvatarURL, + iconURL: avatarURL, url: beatmap.beatmapLink, }) .setThumbnail( @@ -796,7 +806,7 @@ export abstract class EmbedCreator { embed.setDescription(beatmapInformation); - return embed; + return options; } /** diff --git a/src/utils/creators/ProfileCardCreator.ts b/src/utils/creators/ProfileCardCreator.ts index 31e24083..723b34e8 100644 --- a/src/utils/creators/ProfileCardCreator.ts +++ b/src/utils/creators/ProfileCardCreator.ts @@ -174,7 +174,7 @@ export class ProfileCardCreator { this.drawPlayerLevel(); } - await this.writePlayerProfile(); + this.writePlayerProfile(); } /** @@ -215,12 +215,13 @@ export class ProfileCardCreator { private async drawPlayerAvatar(): Promise { this.context.save(); - const avatar = await loadImage( - this.player instanceof Player - ? this.player.avatarUrl - : DroidHelper.getAvatarURL(this.player.id), - ); - this.context.drawImage(avatar, 9, 9, 150, 150); + const avatar = await DroidHelper.getAvatar(this.player.id); + + if (!avatar) { + return; + } + + this.context.drawImage(await loadImage(avatar), 9, 9, 150, 150); this.context.restore(); } @@ -371,7 +372,7 @@ export class ProfileCardCreator { /** * Writes the details of the player's profile. */ - private async writePlayerProfile(): Promise { + private writePlayerProfile(): void { this.context.save(); const x = 169; diff --git a/src/utils/helpers/DroidHelper.ts b/src/utils/helpers/DroidHelper.ts index 2e5eb989..9ec1ece9 100644 --- a/src/utils/helpers/DroidHelper.ts +++ b/src/utils/helpers/DroidHelper.ts @@ -11,6 +11,7 @@ import { DroidAPIRequestBuilder, ModUtil } from "@rian8337/osu-base"; import { APIScore, Player, Score } from "@rian8337/osu-droid-utilities"; import { RowDataPacket } from "mysql2"; import { OnlinePlayerRank } from "@structures/utils/OnlinePlayerRank"; +import { readFile, stat } from "fs/promises"; /** * A helper for osu!droid related requests. @@ -552,6 +553,35 @@ export abstract class DroidHelper { return `https://osudroid.moe/user/avatar?id=${uid}`; } + /** + * Obtains the avatar of a player. + * + * In debug mode, the avatar will be obtained by requesting the osu!droid server. + * Otherwise, the avatar will be obtained via the file system. + * + * @param uid The uid of the player. + * @returns The avatar of the player, `null` if the avatar is not found or the osu!droid server cannot be requested. + */ + static async getAvatar(uid: number): Promise { + if (Config.isDebug) { + return fetch(this.getAvatarURL(uid)) + .then((res) => res.arrayBuffer()) + .then(Buffer.from) + .catch(() => null); + } + + const avatarBasePath = "/DroidData/osudroid/zip/avatar/"; + let avatarPath = `${avatarBasePath}${uid}.png`; + + const avatarStats = await stat(avatarPath); + + if (!avatarStats.isFile()) { + avatarPath = `${avatarBasePath}0.png`; + } + + return readFile(avatarPath).catch(() => null); + } + /** * Cleans up filenames received from the score table to a proper title. *