diff --git a/src/lib/tickets/archiver.js b/src/lib/tickets/archiver.js index 0b5dd3c15..f1e7c008b 100644 --- a/src/lib/tickets/archiver.js +++ b/src/lib/tickets/archiver.js @@ -1,4 +1,7 @@ const { reusable } = require('../threads'); +const { + getAvatarData, saveAvatar, +} = require('../users'); /** @@ -71,8 +74,13 @@ module.exports = class TicketArchiver { } for (const member of members) { + const avatar = getAvatarData(member); + + const savedAvatarFilename = await saveAvatar(avatar); + if(!savedAvatarFilename) this.client.log.warn(`Couldn't save user avatar at: ${avatar.url}`); + const data = { - avatar: member.avatar || member.user.avatar, // TODO: save avatar in user/avatars/ + avatar: avatar.hash, bot: member.user.bot, discriminator: member.user.discriminator, displayName: member.displayName ? await worker.encrypt(member.displayName) : null, diff --git a/src/lib/users.js b/src/lib/users.js index c3ce0cf7d..0f4cba3a2 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -1,3 +1,6 @@ +const fs = require('fs'); +const { Readable } = require('stream'); +const path = require('path'); const { PermissionsBitField } = require('discord.js'); /** @@ -70,3 +73,68 @@ module.exports.getPrivilegeLevel = async member => { else if (await this.isStaff(member.guild, member.id)) return 1; else return 0; }; + +/** + * + * @param {import("discord.js").GuildMember} member + * @returns {{hash: string, url: string, isAnimated: boolean} | false} + */ +module.exports.getAvatarData = member => { + if (!member?.user) return false; + + const { avatar: userAvatar } = member.user; + const memberAvatar = member.avatar; + + const avatarHash = memberAvatar || userAvatar; + if (!avatarHash) return false; + + const isAnimated = avatarHash.startsWith('a_'); + + const url = memberAvatar + ? member.avatarURL() + : member.user.avatarURL(); + + return { + hash: avatarHash, + isAnimated, + url, + }; +}; + +/** + * + * @param {{url: string, hash: string}} avatar + * @returns {Promise} - The filename of the saved avatar if successful, `false` if failed. + */ +module.exports.saveAvatar = async avatar => { + const avatarDir = path.join('user', 'avatars'); + + if (!avatar?.url || !avatar.hash) return false; + + const ext = path.extname(avatar.url); + const filename = `${avatar.hash}${ext}`; + const filePath = path.join(avatarDir, filename); + + try { + await fs.promises.access(filePath); + return filename; // Avatar already saved + } catch (err) { + if (err.code !== 'ENOENT') { + return false; + } + } + + const res = await fetch(avatar.url); + if (!res.ok) { + return false; + } + + const nodeReadable = Readable.fromWeb(res.body); + const writeStream = fs.createWriteStream(filePath); + + return new Promise((resolve, reject) => { + nodeReadable.pipe(writeStream); + writeStream.on('finish', () => resolve(filename)); + writeStream.on('error', () => reject(false)); + }); +};