openvibers/openvibe.live Commits
Avatar sync with the Network both ways (internal user-avatar route, explicit change reports from both avatar pickers)
Changes
@@@ -233,7 +233,7 @@ router.post('/avatar', requireAuth, avatarUpload.single('avatar'), async (req, rdb.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 { }@@@ -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 || {};@@@ -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);@@@ -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 };
@@ -233,7 +233,7 @@ router.post('/avatar', requireAuth, avatarUpload.single('avatar'), async (req, r
@@ -233,7 +233,7 @@ router.post('/avatar', requireAuth, avatarUpload.single('avatar'), async (req, r
db.updateUserAvatar(req.user.id, screenshotUrl, paste.id || null);
db.updateUserAvatar(req.user.id, screenshotUrl, paste.id || null);
const updated = db.getUserById(req.user.id);
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 });
res.json({ user: sanitizeUser(updated), avatar_url: screenshotUrl });
} catch (err) {
} catch (err) {
if (req.file) try { fs.unlinkSync(req.file.path); } catch { }
if (req.file) try { fs.unlinkSync(req.file.path); } catch { }
@@ -61,6 +61,33 @@ router.post('/url-registry/refresh', async (req, res) => {
@@ -61,6 +61,33 @@ router.post('/url-registry/refresh', async (req, res) => {
// role change propagates to the local user record immediately — instead of
// 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
// 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.
// 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) => {
router.post('/user-role', (req, res) => {
try {
try {
const { username, openvibenetwork_id, role } = req.body || {};
const { username, openvibenetwork_id, role } = req.body || {};
@@ -118,6 +118,8 @@ router.post('/:slug/set-avatar', requireAuth, async (req, res) => {
@@ -118,6 +118,8 @@ router.post('/:slug/set-avatar', requireAuth, async (req, res) => {
const avatarUrl = media.publicUrl(paste.screenshot_url)
const avatarUrl = media.publicUrl(paste.screenshot_url)
|| (paste.screenshot_path ? media.screenshotUrl(require('path').basename(paste.screenshot_path)) : media.pasteRawUrl(paste.slug));
|| (paste.screenshot_path ? media.screenshotUrl(require('path').basename(paste.screenshot_path)) : media.pasteRawUrl(paste.slug));
db.updateUserAvatar(req.user.id, avatarUrl, paste.id || null);
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 });
res.json({ success: true, avatar_url: avatarUrl });
} catch (err) {
} catch (err) {
console.warn('[Pastes proxy] set-avatar:', err.message);
console.warn('[Pastes proxy] set-avatar:', err.message);
@@ -86,6 +86,18 @@ function reportLinkedAccount(user) {
@@ -86,6 +86,18 @@ function reportLinkedAccount(user) {
}).catch(() => { _linkedReported.delete(user.id); });
}).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
* Push the same notification to many LIVE users (translated, deduped, chunked to the
* network's 1000-per-call limit).
* network's 1000-per-call limit).
@@ -126,4 +138,4 @@ function markNotificationsRead(userId, type, urlPattern) {
@@ -126,4 +138,4 @@ function markNotificationsRead(userId, type, urlPattern) {
.catch(err => console.warn('[Notify] Mark-read error:', err.message));
.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 };