Handle d2m events with empty guild/user avatars #106

Open
l1s5 wants to merge 3 commits from l1s5/out-of-your-element:handle-empty-guild-and-user-avatars into main
8 changed files with 74 additions and 6 deletions

View file

@ -106,6 +106,7 @@ async function ensureSimJoined(pkMessage, roomID) {
* Generate profile data based on webhook displayname and configured avatar.
* @param {Ty.PkMessage} pkMessage
* @param {Ty.WebhookAuthor} author
* @returns {Promise<Ty.Event.M_Room_Member>}
*/
async function memberToStateContent(pkMessage, author) {
// We prefer to use the member's avatar URL data since the image upload can be cached across channels,

View file

@ -111,6 +111,7 @@ async function ensureSimJoined(pluRalMessage, roomID) {
* Generate profile data based on webhook displayname and configured avatar.
* @param {Ty.PluRalWebhookMessage} pluRalMessage
* @param {Ty.WebhookAuthor} author
* @returns {Promise<Ty.Event.M_Room_Member>}
*/
async function memberToStateContent(pluRalMessage, author) {
// We prefer to use the member's avatar URL data since the image upload can be cached across channels,

View file

@ -102,8 +102,8 @@ async function userToGlobalProfile(user) {
globalProfile.displayname = user.username
if (user.global_name) globalProfile.displayname = user.global_name
if (user.avatar) {
const avatarPath = file.userAvatar(user) // the user avatar only
if (avatarPath) {
globalProfile.avatar_url = await file.uploadDiscordFileToMxc(avatarPath)
}
@ -113,6 +113,8 @@ async function userToGlobalProfile(user) {
/**
* @param {DiscordTypes.APIUser} user
* @param {Omit<DiscordTypes.APIGuildMember, "user"> | undefined} member
* @param {string} guildID
* @returns {Promise<Ty.Event.M_Room_Member>}
*/
async function memberToStateContent(user, member, guildID) {
let displayname = user.username
@ -132,9 +134,9 @@ async function memberToStateContent(user, member, guildID) {
}
}
if (member?.avatar || user.avatar) {
// const avatarPath = file.userAvatar(user) // the user avatar only
const avatarPath = file.memberAvatar(guildID, user, member) // the member avatar or the user avatar
if (avatarPath) {
content["moe.cadence.ooye.member"].avatar = avatarPath
content.avatar_url = await file.uploadDiscordFileToMxc(avatarPath)
}

View file

@ -87,6 +87,7 @@ async function ensureSimJoined(fakeUserID, author, roomID) {
/**
* Generate profile data based on webhook displayname and configured avatar.
* @param {Ty.WebhookAuthor} author
* @returns {Promise<Ty.Event.M_Room_Member>}
*/
async function authorToStateContent(author) {
const avatar = file.userAvatar(author)

View file

@ -1,5 +1,6 @@
// @ts-check
const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../passthrough")
const {reg, writeRegistration} = require("./read-registration.js")
const Ty = require("../types")
@ -138,14 +139,30 @@ async function _actuallyUploadDiscordFileToMxc(url) {
}
}
/**
* @param {DiscordTypes.APIGuild} guild
* @returns {string | null}
*/
function guildIcon(guild) {
if (!guild.icon) return null
return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}`
}
/**
* @param {DiscordTypes.APIUser | Ty.WebhookAuthor} user
* @returns {string | null}
*/
function userAvatar(user) {
if (!user.avatar) return null
return `/avatars/${user.id}/${user.avatar}.png?size=${IMAGE_SIZE}`
}
/**
* @param {string} guildID
* @param {DiscordTypes.APIUser} user
* @param {Omit<DiscordTypes.APIGuildMember, "user"> | undefined} member
* @returns {string}
*/
function memberAvatar(guildID, user, member) {
if (!member?.avatar) return userAvatar(user)
return `/guilds/${guildID}/users/${user.id}/avatars/${member?.avatar}.png?size=${IMAGE_SIZE}`

View file

@ -26,3 +26,33 @@ test("removeExpiryParams: works on media proxy and keeps quality", t => {
const result = file._removeExpiryParams(url)
t.equal(result, "https://media.discordapp.net/attachments/1160894080998461480/1548665030898094240/855064e9-bdae-487a-a497-c662bd510487.png?format=webp")
})
test("guildIcon: returns icon path", t => {
t.equal(
// @ts-ignore
file.guildIcon({id: "112760669178241024", icon: "a_f83622e09ead74f0c5c527fe241f8f8c"}),
"/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"
)
})
test("guildIcon: returns null if guild has no icon", t => {
t.equal(
// @ts-ignore
file.guildIcon({id: "112760669178241024", icon: null}),
null
)
})
test("userAvatar: returns avatar path", t => {
t.equal(
file.userAvatar({id: "113340068197859328", username: "kumaccino", avatar: "b48302623a12bc7c59a71328f72ccb39"}),
"/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024"
)
})
test("userAvatar: returns null if user has no avatar", t => {
t.equal(
file.userAvatar({id: "113340068197859328", username: "kumaccino", avatar: null}),
null
)
})

View file

@ -32,7 +32,11 @@ async function kstateUploadMxc(obj) {
for (const [k, v] of Object.entries(obj)) {
if (v == null || typeof v !== "object") continue
if (v.$url) {
if ("$url" in v) {
if (!v.$url) {
delete obj[k]
continue
}
promises.push(
file.uploadDiscordFileToMxc(v.$url)
.then(mxc => obj[k] = mxc)

View file

@ -21,6 +21,18 @@ test("kstate strip: keeps true conditions while removing $if", t => {
})
})
test("kstateUploadMxc: removes keys where $url is null", async t => {
const input = {
"m.room.avatar/": {
url: {$url: null}
}
}
await kstateUploadMxc(input)
t.deepEqual(input, {
"m.room.avatar/": {}
})
})
test("kstateUploadMxc: sets the mxc", async t => {
const input = {
"m.room.avatar/": {