openvibers/openvibe.live Commits

Avatar sync with the Network both ways (internal user-avatar route, explicit change reports from both avatar pickers)

OP OpenVibers committed 18 Sept 2026 · 3e5d4562a3 · 4 files, +43 −2


Changes

ddiff --git a/server/auth/routes.js b/server/auth/routes.js
iindex 532d3c89..73a1038b 100644
---- a/server/auth/routes.js
++++ b/server/auth/routes.js
@@@ -233,7 +233,7 @@ router.post('/avatar', requireAuth, avatarUpload.single('avatar'), async (req, r
db.updateUserAvatar(req.user.id, screenshotUrl, paste.id || null);
const updated = db.getUserById(req.user.id);
-- try { require('../utils/notify').reportLinkedAccount(updated); } catch { /* the Network learns the new picture on the next sign-in */ }
++ try { require('../utils/notify').reportAvatarChange(updated); } catch { /* the Network learns the new picture on the next sign-in */ }
res.json({ user: sanitizeUser(updated), avatar_url: screenshotUrl });
} catch (err) {
if (req.file) try { fs.unlinkSync(req.file.path); } catch { }
ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex 795d9d10..540fcb22 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -61,6 +61,33 @@ router.post('/url-registry/refresh', async (req, res) => {
// role change propagates to the local user record immediately — instead of
// waiting on the user's next token (up to 24h) and without letting a stale token
// downgrade them. Matches by openvibe.network account link first, then username.
++// ── The account's avatar changed on the Network (or on another site) ─────────
++// The avatar is a network-wide property (OpenVibe.Network server/profile/avatar.js). Live keeps a copy on its
++// own user row because every stream card, chat line and profile reads it locally.
++router.post('/user-avatar', (req, res) => {
++ try {
++ const { username, openvibenetwork_id, avatar_url } = req.body || {};
++ let url = null;
++ if (avatar_url) {
++ let u; try { u = new URL(String(avatar_url)); } catch { return res.status(400).json({ ok: false, error: 'bad url' }); }
++ if (u.protocol !== 'https:' || u.hostname !== 'openvibe.media' || String(avatar_url).length > 500) return res.status(422).json({ ok: false, error: 'avatars live on openvibe.media' });
++ url = u.toString();
++ }
++ let user = null;
++ if (openvibenetwork_id != null) {
++ const linked = db.getDb().prepare("SELECT user_id FROM linked_accounts WHERE service = 'network' AND service_user_id = ?").get(String(openvibenetwork_id));
++ if (linked) user = db.getUserById(linked.user_id);
++ }
++ if (!user && username) user = db.getUserByUsername(username);
++ if (!user) return res.status(404).json({ ok: false, error: 'user not found' });
++ if ((user.avatar_url || null) !== url) db.updateUserAvatar(user.id, url, null);
++ return res.json({ ok: true, id: user.id, changed: (user.avatar_url || null) !== url });
++ } catch (err) {
++ console.error('[Internal] user-avatar error:', err.message);
++ return res.status(500).json({ ok: false, error: err.message });
++ }
++});
++
router.post('/user-role', (req, res) => {
try {
const { username, openvibenetwork_id, role } = req.body || {};
ddiff --git a/server/media-proxy/pastes.js b/server/media-proxy/pastes.js
iindex 8adb2804..8fb88a04 100644
---- a/server/media-proxy/pastes.js
++++ b/server/media-proxy/pastes.js
@@@ -118,6 +118,8 @@ router.post('/:slug/set-avatar', requireAuth, async (req, res) => {
const avatarUrl = media.publicUrl(paste.screenshot_url)
|| (paste.screenshot_path ? media.screenshotUrl(require('path').basename(paste.screenshot_path)) : media.pasteRawUrl(paste.slug));
db.updateUserAvatar(req.user.id, avatarUrl, paste.id || null);
++ // The avatar belongs to the network account: every other OpenVibe site shows it too.
++ try { require('../utils/notify').reportAvatarChange({ id: req.user.id, avatar_url: avatarUrl }); } catch { /* picked up at the next sign-in */ }
res.json({ success: true, avatar_url: avatarUrl });
} catch (err) {
console.warn('[Pastes proxy] set-avatar:', err.message);
ddiff --git a/server/utils/notify.js b/server/utils/notify.js
iindex a88af333..888519c9 100644
---- a/server/utils/notify.js
++++ b/server/utils/notify.js
@@@ -86,6 +86,18 @@ function reportLinkedAccount(user) {
}).catch(() => { _linkedReported.delete(user.id); });
}
++/**
++ * The user picked a new avatar here. The avatar belongs to the network account, so say so explicitly: unlike the
++ * sign-in report above (which only fills an empty picture), this one replaces whatever the Network had.
++ */
++function reportAvatarChange(user) {
++ if (!user?.id || !INTERNAL_API_KEY) return;
++ const networkId = toNetworkId(user.id);
++ if (!networkId) return;
++ _post('/internal/user-avatar', { user_id: networkId, avatar_url: user.avatar_url || null, origin: 'live' })
++ .then(r => { if (r && !r.ok) console.warn(`[Notify] avatar sync refused (${r.status})`); }).catch(() => {});
++}
++
/**
* Push the same notification to many LIVE users (translated, deduped, chunked to the
* network's 1000-per-call limit).
@@@ -126,4 +138,4 @@ function markNotificationsRead(userId, type, urlPattern) {
.catch(err => console.warn('[Notify] Mark-read error:', err.message));
}
--module.exports = { pushNotification, pushBulkNotification, actorInfo, markNotificationsRead, reportLinkedAccount, toNetworkId, toNetworkIds, OV_NETWORK_INTERNAL_URL, INTERNAL_API_KEY };
++module.exports = { reportAvatarChange, pushNotification, pushBulkNotification, actorInfo, markNotificationsRead, reportLinkedAccount, toNetworkId, toNetworkIds, OV_NETWORK_INTERNAL_URL, INTERNAL_API_KEY };