refactor: move all input note normalization into helper method, have assertPrivate mock a message object (with said normalization) before sending message

This commit is contained in:
Julian Lam
2025-03-04 13:54:36 -05:00
parent 6c26d9f4a3
commit 4ec7552cfb
2 changed files with 114 additions and 85 deletions

View File

@@ -40,6 +40,95 @@ const sanitizeConfig = {
}, },
}; };
Mocks._normalize = async (object) => {
// Normalized incoming AP objects into expected types for easier mocking
let { attributedTo, url, image, content, source } = object;
switch (true) { // non-string attributedTo handling
case Array.isArray(attributedTo): {
attributedTo = attributedTo.reduce((valid, cur) => {
if (typeof cur === 'string') {
valid.push(cur);
} else if (typeof cur === 'object') {
if (cur.type === 'Person' && cur.id) {
valid.push(cur.id);
}
}
return valid;
}, []);
attributedTo = attributedTo.shift(); // take first valid uid
break;
}
case typeof attributedTo === 'object' && attributedTo.hasOwnProperty('id'): {
attributedTo = attributedTo.id;
}
}
let sourceContent = source && source.mediaType === 'text/markdown' ? source.content : undefined;
if (sourceContent) {
content = null;
sourceContent = await activitypub.helpers.remoteAnchorToLocalProfile(sourceContent, true);
} else if (content && content.length) {
content = sanitize(content, sanitizeConfig);
content = await activitypub.helpers.remoteAnchorToLocalProfile(content);
} else {
content = '<em>This post did not contain any content.</em>';
}
switch (true) {
case image && image.hasOwnProperty('url') && !!image.url: {
image = image.url;
break;
}
case image && typeof image === 'string': {
// no change
break;
}
default: {
image = null;
}
}
if (image) {
const parsed = new URL(image);
if (!mime.getType(parsed.pathname).startsWith('image/')) {
activitypub.helpers.log(`[activitypub/mocks.post] Received image not identified as image due to MIME type: ${image}`);
image = null;
}
}
if (url) { // Handle url array
if (Array.isArray(url)) {
url = url.reduce((valid, cur) => {
if (typeof cur === 'string') {
valid.push(cur);
} else if (typeof cur === 'object') {
if (cur.type === 'Link' && cur.href) {
if (!cur.mediaType || (cur.mediaType && cur.mediaType === 'text/html')) {
valid.push(cur.href);
}
}
}
return valid;
}, []);
url = url.shift(); // take first valid url
}
}
return {
...object,
attributedTo,
content,
sourceContent,
image,
url,
};
};
Mocks.profile = async (actors, hostMap) => { Mocks.profile = async (actors, hostMap) => {
// Should only ever be called by activitypub.actors.assert // Should only ever be called by activitypub.actors.assert
const profiles = await Promise.all(actors.map(async (actor) => { const profiles = await Promise.all(actors.map(async (actor) => {
@@ -154,6 +243,8 @@ Mocks.post = async (objects) => {
} }
const posts = await Promise.all(objects.map(async (object) => { const posts = await Promise.all(objects.map(async (object) => {
object = await Mocks._normalize(object);
if ( if (
!activitypub._constants.acceptedPostTypes.includes(object.type) || !activitypub._constants.acceptedPostTypes.includes(object.type) ||
!activitypub.helpers.isUri(object.id) // sanity-check the id !activitypub.helpers.isUri(object.id) // sanity-check the id
@@ -166,31 +257,10 @@ Mocks.post = async (objects) => {
url, url,
attributedTo: uid, attributedTo: uid,
inReplyTo: toPid, inReplyTo: toPid,
published, updated, name, content, source, published, updated, name, content, sourceContent,
type, to, cc, audience, attachment, tag, image, type, to, cc, audience, attachment, tag, image,
} = object; } = object;
switch (true) { // non-string attributedTo handling
case Array.isArray(uid): {
uid = uid.reduce((valid, cur) => {
if (typeof cur === 'string') {
valid.push(cur);
} else if (typeof cur === 'object') {
if (cur.type === 'Person' && cur.id) {
valid.push(cur.id);
}
}
return valid;
}, []);
uid = uid.shift(); // take first valid uid
break;
}
case typeof uid === 'object' && uid.hasOwnProperty('id'): {
uid = uid.id;
}
}
await activitypub.actors.assert(uid); await activitypub.actors.assert(uid);
const resolved = await activitypub.helpers.resolveLocalId(toPid); const resolved = await activitypub.helpers.resolveLocalId(toPid);
@@ -202,59 +272,6 @@ Mocks.post = async (objects) => {
let edited = new Date(updated); let edited = new Date(updated);
edited = Number.isNaN(edited.valueOf()) ? undefined : edited; edited = Number.isNaN(edited.valueOf()) ? undefined : edited;
let sourceContent = source && source.mediaType === 'text/markdown' ? source.content : undefined;
if (sourceContent) {
content = null;
sourceContent = await activitypub.helpers.remoteAnchorToLocalProfile(sourceContent, true);
} else if (content && content.length) {
content = sanitize(content, sanitizeConfig);
content = await activitypub.helpers.remoteAnchorToLocalProfile(content);
} else {
content = '<em>This post did not contain any content.</em>';
}
switch (true) {
case image && image.hasOwnProperty('url') && !!image.url: {
image = image.url;
break;
}
case image && typeof image === 'string': {
// no change
break;
}
default: {
image = null;
}
}
if (image) {
const parsed = new URL(image);
if (!mime.getType(parsed.pathname).startsWith('image/')) {
activitypub.helpers.log(`[activitypub/mocks.post] Received image not identified as image due to MIME type: ${image}`);
image = null;
}
}
if (url) { // Handle url array
if (Array.isArray(url)) {
url = url.reduce((valid, cur) => {
if (typeof cur === 'string') {
valid.push(cur);
} else if (typeof cur === 'object') {
if (cur.type === 'Link' && cur.href) {
if (!cur.mediaType || (cur.mediaType && cur.mediaType === 'text/html')) {
valid.push(cur.href);
}
}
}
return valid;
}, []);
url = url.shift(); // take first valid url
}
}
if (type === 'Video') { if (type === 'Video') {
attachment = attachment || []; attachment = attachment || [];
attachment.push({ url }); attachment.push({ url });
@@ -281,6 +298,19 @@ Mocks.post = async (objects) => {
return single ? posts.pop() : posts; return single ? posts.pop() : posts;
}; };
Mocks.message = async (object) => {
object = await Mocks._normalize(object);
const message = {
mid: object.id,
uid: object.attributedTo,
content: object.content,
// ip: caller.ip,
};
return message;
};
Mocks.actors = {}; Mocks.actors = {};
Mocks.actors.user = async (uid) => { Mocks.actors.user = async (uid) => {

View File

@@ -286,31 +286,30 @@ Notes.assertPrivate = async (object) => {
timestamp = Date.now(); timestamp = Date.now();
} }
const payload = await activitypub.mocks.message(object);
if (!roomId) { if (!roomId) {
roomId = await messaging.newRoom(object.attributedTo, { uids: [...recipients] }); roomId = await messaging.newRoom(payload.uid, { uids: [...recipients] });
} }
// Add any new members to the chat // Add any new members to the chat
const added = Array.from(recipients).filter(uid => !participantUids.includes(uid)); const added = Array.from(recipients).filter(uid => !participantUids.includes(uid));
const assertion = await activitypub.actors.assert(added); const assertion = await activitypub.actors.assert(added);
if (assertion) { if (assertion) {
await messaging.addUsersToRoom(object.attributedTo, added, roomId); await messaging.addUsersToRoom(payload.uid, added, roomId);
} }
// Add message to room // Add message to room
const message = await messaging.sendMessage({ const message = await messaging.sendMessage({
mid: object.id, ...payload,
uid: object.attributedTo,
roomId: roomId,
content: object.content,
toMid: toMid,
timestamp: Date.now(), timestamp: Date.now(),
// ip: caller.ip, roomId: roomId,
toMid: toMid,
}); });
messaging.notifyUsersInRoom(object.attributedTo, roomId, message); messaging.notifyUsersInRoom(payload.uid, roomId, message);
// Set real timestamp back so that the message shows even though it predates room joining // Set real timestamp back so that the message shows even though it predates room joining
await messaging.setMessageField(object.id, 'timestamp', timestamp); await messaging.setMessageField(payload.mid, 'timestamp', timestamp);
return { roomId }; return { roomId };
}; };