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
10 changed files with 41 additions and 35 deletions
Showing only changes of commit 6118c7fb74 - Show all commits

fixup! Handle d2m events with empty guild/user avatars

l1s5 2026-11-10 09:08:28 +09:00
Signed by untrusted user: l1s5
GPG key ID: D0A90CC905B58D9B

View file

@ -181,7 +181,7 @@ async function channelToKState(channel, guild, di) {
network: { network: {
id: guild.id, id: guild.id,
displayname: guild.name, displayname: guild.name,
...(guild.icon ? {avatar_url: {$url: file.guildIcon(guild)}} : {}) avatar_url: {$url: file.guildIcon(guild)}
}, },
channel: { channel: {
id: channel.id, id: channel.id,

View file

@ -77,7 +77,7 @@ async function guildToKState(guild, privacyLevel) {
"m.room.name/": {name: guild.name}, "m.room.name/": {name: guild.name},
"m.room.avatar/": { "m.room.avatar/": {
$if: guild.icon, $if: guild.icon,
url: guild.icon ? {$url: file.guildIcon(guild)} : null url: {$url: file.guildIcon(guild)}
}, },
"m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
"m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.SPACE_HISTORY_VISIBILITY[privacyLevel]}, "m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.SPACE_HISTORY_VISIBILITY[privacyLevel]},

View file

@ -110,10 +110,7 @@ async function ensureSimJoined(pkMessage, roomID) {
async function memberToStateContent(pkMessage, author) { async function memberToStateContent(pkMessage, author) {
// We prefer to use the member's avatar URL data since the image upload can be cached across channels, // We prefer to use the member's avatar URL data since the image upload can be cached across channels,
// unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL. // unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL.
const avatar = pkMessage.member.avatar_url const avatar = pkMessage.member.avatar_url || pkMessage.member.webhook_avatar_url || pkMessage.system.avatar_url || file.userAvatar(author)
|| pkMessage.member.webhook_avatar_url
|| pkMessage.system.avatar_url
|| (author.avatar ? file.userAvatar(author) : null)
const color = pkMessage.member.color ?? pkMessage.system.color const color = pkMessage.member.color ?? pkMessage.system.color
const content = { const content = {

View file

@ -115,7 +115,7 @@ async function ensureSimJoined(pluRalMessage, roomID) {
async function memberToStateContent(pluRalMessage, author) { async function memberToStateContent(pluRalMessage, author) {
// We prefer to use the member's avatar URL data since the image upload can be cached across channels, // We prefer to use the member's avatar URL data since the image upload can be cached across channels,
// unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL. // unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL.
const avatar = pluRalMessage.member.avatar_url || (author.avatar ? file.userAvatar(author) : null) const avatar = pluRalMessage.member.avatar_url || file.userAvatar(author)
const content = { const content = {
displayname: author.username, displayname: author.username,

View file

@ -102,8 +102,8 @@ async function userToGlobalProfile(user) {
globalProfile.displayname = user.username globalProfile.displayname = user.username
if (user.global_name) globalProfile.displayname = user.global_name if (user.global_name) globalProfile.displayname = user.global_name
if (user.avatar) {
const avatarPath = file.userAvatar(user) // the user avatar only const avatarPath = file.userAvatar(user) // the user avatar only
if (avatarPath) {
globalProfile.avatar_url = await file.uploadDiscordFileToMxc(avatarPath) globalProfile.avatar_url = await file.uploadDiscordFileToMxc(avatarPath)
} }
@ -132,9 +132,9 @@ async function memberToStateContent(user, member, guildID) {
} }
} }
if (member?.avatar || user.avatar) {
// const avatarPath = file.userAvatar(user) // the user avatar only // const avatarPath = file.userAvatar(user) // the user avatar only
const avatarPath = file.memberAvatar(guildID, user, member) // the member avatar or the user avatar const avatarPath = file.memberAvatar(guildID, user, member) // the member avatar or the user avatar
if (avatarPath) {
content["moe.cadence.ooye.member"].avatar = avatarPath content["moe.cadence.ooye.member"].avatar = avatarPath
content.avatar_url = await file.uploadDiscordFileToMxc(avatarPath) content.avatar_url = await file.uploadDiscordFileToMxc(avatarPath)
} }

View file

@ -89,7 +89,7 @@ async function ensureSimJoined(fakeUserID, author, roomID) {
* @param {Ty.WebhookAuthor} author * @param {Ty.WebhookAuthor} author
*/ */
async function authorToStateContent(author) { async function authorToStateContent(author) {
const avatar = author.avatar ? file.userAvatar(author) : null const avatar = file.userAvatar(author)
const content = { const content = {
displayname: author.username, displayname: author.username,

View file

@ -1,6 +1,5 @@
// @ts-check // @ts-check
const assert = require("assert").strict
const DiscordTypes = require("discord-api-types/v10") const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../passthrough") const passthrough = require("../passthrough")
const {reg, writeRegistration} = require("./read-registration.js") const {reg, writeRegistration} = require("./read-registration.js")
@ -142,19 +141,19 @@ async function _actuallyUploadDiscordFileToMxc(url) {
/** /**
* @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuild} guild
* @returns {string} * @returns {string | null}
*/ */
function guildIcon(guild) { function guildIcon(guild) {
assert(guild.icon, `guild ${guild.id} has no icon`) if (!guild.icon) return null
return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}` return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}`
} }
/** /**
* @param {DiscordTypes.APIUser | Ty.WebhookAuthor} user * @param {DiscordTypes.APIUser | Ty.WebhookAuthor} user
* @returns {string} * @returns {string | null}
*/ */
function userAvatar(user) { function userAvatar(user) {
assert(user.avatar, `user ${user.id} has no avatar`) if (!user.avatar) return null
return `/avatars/${user.id}/${user.avatar}.png?size=${IMAGE_SIZE}` return `/avatars/${user.id}/${user.avatar}.png?size=${IMAGE_SIZE}`
} }

View file

@ -35,15 +35,12 @@ test("guildIcon: returns icon path", t => {
) )
}) })
test("guildIcon: throws if guild has no icon", t => { test("guildIcon: returns null if guild has no icon", t => {
let err t.equal(
try {
// @ts-ignore // @ts-ignore
file.guildIcon({id: "112760669178241024", icon: null}) file.guildIcon({id: "112760669178241024", icon: null}),
} catch (e) { null
err = e )
}
t.match(err?.message, /has no icon/)
}) })
test("userAvatar: returns avatar path", t => { test("userAvatar: returns avatar path", t => {
@ -53,12 +50,9 @@ test("userAvatar: returns avatar path", t => {
) )
}) })
test("userAvatar: throws if user has no avatar", t => { test("userAvatar: returns null if user has no avatar", t => {
let err t.equal(
try { file.userAvatar({id: "113340068197859328", username: "kumaccino", avatar: null}),
file.userAvatar({id: "113340068197859328", username: "kumaccino", avatar: null}) null
} catch (e) { )
err = e
}
t.match(err?.message, /has no avatar/)
}) })

View file

@ -32,7 +32,11 @@ async function kstateUploadMxc(obj) {
for (const [k, v] of Object.entries(obj)) { for (const [k, v] of Object.entries(obj)) {
if (v == null || typeof v !== "object") continue if (v == null || typeof v !== "object") continue
if (v.$url) { if ("$url" in v) {
if (!v.$url) {
delete obj[k]
continue
}
promises.push( promises.push(
file.uploadDiscordFileToMxc(v.$url) file.uploadDiscordFileToMxc(v.$url)
.then(mxc => obj[k] = mxc) .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 => { test("kstateUploadMxc: sets the mxc", async t => {
const input = { const input = {
"m.room.avatar/": { "m.room.avatar/": {