Skip to content

Commit

Permalink
Drawer: Save user's avatar from a given URL
Browse files Browse the repository at this point in the history
* Better cache
* Pass always a buffer and not a file path
  • Loading branch information
nau7ilus committed Aug 21, 2024
1 parent a0134ce commit 6206295
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 35 deletions.
16 changes: 8 additions & 8 deletions drawer/lib/drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ const addNickname = (ctx, nickname, isGame = true) => {
ctx.fillText(isGame ? lines.join('\n') : nickname, x, y);
};

const addAvatar = async (ctx, avatarPath) => {
const addAvatar = async (ctx, avatar) => {
// Create circular clipping region
ctx.beginPath();
ctx.arc(...AVATAR_MASK_COORDS, 40, 0, Math.PI * 2);
ctx.clip();
// Draw avatar image inside
const avatarImage = await loadImage(avatarPath);
const avatarImage = await loadImage(avatar);
ctx.drawImage(avatarImage, ...AVATAR_COORDS, ...AVATAR_SIZE);
ctx.closePath();
};
Expand Down Expand Up @@ -170,14 +170,14 @@ const addWord = (ctx, word, guessed = guessed.toLowerCase(), isLose = false) =>
ctx.textAlign = 'left';
};

const createGameCard = async ({ mistakes, nickname, avatarPath, attemptsLeft, hangmanType, word, guessed, locale }) => {
const createGameCard = async ({ mistakes, nickname, avatar, attemptsLeft, hangmanType, word, guessed, locale }) => {
const { canvas, ctx } = await initCanvas('game', locale);
addMistakes(ctx, mistakes.join(' '));
addNickname(ctx, nickname);
setAttemptsLeft(ctx, attemptsLeft);
addHangman(ctx, hangmanType);
addWord(ctx, word, guessed);
await addAvatar(ctx, avatarPath);
await addAvatar(ctx, avatar);
return canvas;
};

Expand Down Expand Up @@ -215,23 +215,23 @@ const addRankInfo = (ctx, level, exp, nextLevelExp) => {
roundRect(ctx, barStartX, RANK_BAR_Y, barWidth, RANK_BAR_HEIGHT, RANK_BAR_RADII);
};

const createWinCard = async ({ nickname, avatarPath, locale, level, exp, nextLevelExp, points, word }) => {
const createWinCard = async ({ nickname, avatar, locale, level, exp, nextLevelExp, points, word }) => {
const { canvas, ctx } = await initCanvas('win', locale);
addRankInfo(ctx, level, exp, nextLevelExp);
addPoints(ctx, points);
addGuessedWord(ctx, word);
addNickname(ctx, nickname, false);
await addAvatar(ctx, avatarPath);
await addAvatar(ctx, avatar);
return canvas;
};

const createLoseCard = async ({ nickname, avatarPath, locale, level, exp, nextLevelExp, points, word, guessed }) => {
const createLoseCard = async ({ nickname, avatar, locale, level, exp, nextLevelExp, points, word, guessed }) => {
const { canvas, ctx } = await initCanvas('lose', locale);
addRankInfo(ctx, level, exp, nextLevelExp);
addPoints(ctx, points);
addNickname(ctx, nickname, false);
addWord(ctx, word, guessed, true);
await addAvatar(ctx, avatarPath);
await addAvatar(ctx, avatar);
return canvas;
};

Expand Down
45 changes: 30 additions & 15 deletions drawer/lib/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,43 @@

const fs = require('node:fs');
const fsPromises = require('node:fs/promises');
const path = require('node:path');

const DEFAULT_AVATARS_PATH = './lib/assets/defaultAvatars';
const CACHED_AVATARS_PATH = './.cache/avatars';
const CACHE_PATH = '.cache/avatars';

const buildAvatarUrl = (userId, avatarId) => `https://cdn.discordapp.com/avatars/${userId}/${avatarId}.png?size=64`;
// An avatar URL should look like this:
// https://cdn.discordapp.com/avatars/{USER_ID}/{AVATAR_ID}.png?size=64
// which can be easily created via
// discord.js::GuildMember.displayAvatarURL({ extension: 'png', size: 64 })
const downloadDiscordAvatar = async (avatarURL) => {
// For caching purpuses the avatar ID is being extracted from the provided URL
avatarURL = new URL(avatarURL);
const avatarId = path.basename(avatarURL.pathname, `.png`);

// TODO: Guild user avatars
const getAvatarPath = async (userId, avatarId) => {
if (typeof avatarId !== 'string') avatarId = avatarId.toString();
if (avatarId.length === 1) return fsPromises.readFile(`${DEFAULT_AVATARS_PATH}/${avatarId}.png`);
const avatarPath = `${CACHED_AVATARS_PATH}/${avatarId}.png`;
const isCached = await fs.existsSync(avatarPath);
const avatarUrl = buildAvatarUrl(userId, avatarId);
if (!isCached) await downloadImage(avatarUrl, avatarPath);
return avatarPath;
const cachedFilePath = path.join(CACHE_PATH, `${avatarId}.png`);
const isCached = fs.existsSync(cachedFilePath);

if (!isCached) {
await downloadImage(avatarURL, cachedFilePath);
}

return fsPromises.readFile(cachedFilePath);
};

const downloadImage = async (url, filePath) => {
const downloadImage = async (url, saveFilePath) => {
const res = await fetch(url);
const buffer = Buffer.from(await res.arrayBuffer());
if (filePath) await fsPromises.writeFile(filePath, buffer);

if (saveFilePath) {
const directory = path.dirname(saveFilePath);
if (!fs.existsSync(directory)) {
await fsPromises.mkdir(directory, { recursive: true });
}

await fsPromises.writeFile(saveFilePath, buffer);
}

return buffer;
};

module.exports = { downloadImage, getAvatarPath };
module.exports = { downloadImage, downloadDiscordAvatar };
7 changes: 3 additions & 4 deletions drawer/test/1-game-card.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use strict';

const path = require('node:path');
const { createGameCard, getAvatarPath, saveCanvasPNG } = require('../drawer');
const { createGameCard, saveCanvasPNG, downloadDiscordAvatar } = require('../drawer');

const USER_ID = null;
const AVATAR_ID = 2;
const AVATAR_URL = 'https://cdn.discordapp.com/avatars/876172866897448981/57695c6ec3d9f8f2ede0eb56d4704e6c.png?size=64';

(async () => {
const options = {
mistakes: ['k', 'c'],
nickname: 'HangmanDemoUser',
avatarPath: await getAvatarPath(USER_ID, AVATAR_ID),
avatar: await downloadDiscordAvatar(AVATAR_URL),
attemptsLeft: 3,
hangmanType: 4,
word: 'hangman',
Expand Down
7 changes: 3 additions & 4 deletions drawer/test/2-win-card.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
'use strict';

const path = require('node:path');
const { getAvatarPath, saveCanvasPNG, createWinCard } = require('../drawer');
const { downloadDiscordAvatar, saveCanvasPNG, createWinCard } = require('../drawer');

const USER_ID = null;
const AVATAR_ID = 3;
const AVATAR_URL = 'https://cdn.discordapp.com/avatars/876172866897448981/57695c6ec3d9f8f2ede0eb56d4704e6c.png?size=64';

(async () => {
const options = {
nickname: 'HangmanDemoUser',
avatarPath: await getAvatarPath(USER_ID, AVATAR_ID),
avatar: await downloadDiscordAvatar(AVATAR_URL),
locale: 'de',
level: 4,
exp: 80,
Expand Down
7 changes: 3 additions & 4 deletions drawer/test/3-lose-card.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
'use strict';

const path = require('node:path');
const { getAvatarPath, saveCanvasPNG, createLoseCard } = require('../drawer');
const { downloadDiscordAvatar, saveCanvasPNG, createLoseCard } = require('../drawer');

const USER_ID = null;
const AVATAR_ID = 3;
const AVATAR_URL = 'https://cdn.discordapp.com/avatars/876172866897448981/57695c6ec3d9f8f2ede0eb56d4704e6c.png?size=64';

(async () => {
const options = {
nickname: 'HangmanDemoUser',
avatarPath: await getAvatarPath(USER_ID, AVATAR_ID),
avatar: await downloadDiscordAvatar(AVATAR_URL),
locale: 'de',
level: 1,
exp: 30,
Expand Down
Binary file modified drawer/test/results/1-game-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified drawer/test/results/2-win-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified drawer/test/results/3-lose-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 6206295

Please sign in to comment.