feat(activity): show the current activity on the profile card
The activity pipeline was already complete end to end — Activity type, store, WS broadcast, server validation, presence relay, and an ActivityCard used by four list surfaces — but the profile card rendered none of it, which is the 'Listening to Spotify' block the design calls for. Add ProfileActivity: richer than ActivityCard because the card has room for artwork, track and artist, so it reads details/state/assets. All optional, so it degrades to the bare name that today's process-based detector supplies. Also scheme-check activity image assets server-side. activity.url was already restricted to http(s) but assets.largeImage/smallImage were only length-checked — an asymmetry that was harmless while nothing rendered them, and is not once they become <img src>: a client could point them at a host it controls and harvest the IP of everyone opening that profile.
This commit is contained in:
@@ -473,9 +473,16 @@ function validateActivities(raw: unknown): Activity[] | null {
|
||||
if (obj.assets && typeof obj.assets === 'object') {
|
||||
const aObj = obj.assets as Record<string, unknown>;
|
||||
const assets: ActivityAssets = {};
|
||||
if (typeof aObj.largeImage === 'string' && aObj.largeImage.length <= ACTIVITY_LIMITS.MAX_URL_LENGTH) assets.largeImage = aObj.largeImage;
|
||||
// Image assets are rendered as <img src> by clients, so they get the same
|
||||
// scheme check `url` above already has. Without it a client could point
|
||||
// them at a host it controls and harvest the IP of everyone who opens
|
||||
// that profile — and data: URIs would smuggle payloads through a field
|
||||
// only length-checked.
|
||||
if (typeof aObj.largeImage === 'string' && aObj.largeImage.length <= ACTIVITY_LIMITS.MAX_URL_LENGTH
|
||||
&& isHttpUrl(aObj.largeImage)) assets.largeImage = aObj.largeImage;
|
||||
if (typeof aObj.largeText === 'string' && aObj.largeText.length <= ACTIVITY_LIMITS.MAX_ASSET_TEXT_LENGTH) assets.largeText = aObj.largeText;
|
||||
if (typeof aObj.smallImage === 'string' && aObj.smallImage.length <= ACTIVITY_LIMITS.MAX_URL_LENGTH) assets.smallImage = aObj.smallImage;
|
||||
if (typeof aObj.smallImage === 'string' && aObj.smallImage.length <= ACTIVITY_LIMITS.MAX_URL_LENGTH
|
||||
&& isHttpUrl(aObj.smallImage)) assets.smallImage = aObj.smallImage;
|
||||
if (typeof aObj.smallText === 'string' && aObj.smallText.length <= ACTIVITY_LIMITS.MAX_ASSET_TEXT_LENGTH) assets.smallText = aObj.smallText;
|
||||
if (Object.keys(assets).length > 0) activity.assets = assets;
|
||||
}
|
||||
@@ -485,6 +492,10 @@ function validateActivities(raw: unknown): Activity[] | null {
|
||||
return validated;
|
||||
}
|
||||
|
||||
function isHttpUrl(value: string): boolean {
|
||||
return value.startsWith('https://') || value.startsWith('http://');
|
||||
}
|
||||
|
||||
function handlePresenceUpdate(event: Record<string, unknown>, userId: string): void {
|
||||
const status = event.status as string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user