mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: #12822, send all outgoing links to /ap first to see if it's activitypub enabled, parse if able, otherwise redirect as normal
This commit is contained in:
@@ -609,6 +609,9 @@ $(document).ready(function () {
|
||||
ajaxify.go('outgoing?url=' + encodeURIComponent(href));
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
ajaxify.go(`ap?resource=${encodeURIComponent(this.href)}`);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ const requestCache = ttl({
|
||||
max: 5000,
|
||||
ttl: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
const probeCache = ttl({
|
||||
max: 500,
|
||||
ttl: 1000 * 60 * 60, // 1 hour
|
||||
});
|
||||
|
||||
const ActivityPub = module.exports;
|
||||
|
||||
@@ -443,3 +447,72 @@ ActivityPub.buildRecipients = async function (object, { pid, uid, cid }) {
|
||||
targets,
|
||||
};
|
||||
};
|
||||
|
||||
ActivityPub.probe = async ({ uid, url }) => {
|
||||
/**
|
||||
* Checks whether a passed-in id or URL is an ActivityPub object and can be mapped to a local representation
|
||||
* - `uid` is optional (links to private messages won't match without uid)
|
||||
* - Returns a relative path if already available, true if not, and false otherwise.
|
||||
*/
|
||||
|
||||
// Known resources
|
||||
const [isNote, isMessage, isActor] = await Promise.all([
|
||||
posts.exists(url),
|
||||
messaging.messageExists(url),
|
||||
db.isObjectField('remoteUrl:uid', url),
|
||||
]);
|
||||
switch (true) {
|
||||
case isNote: {
|
||||
return `/post/${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
case isMessage: {
|
||||
if (uid) {
|
||||
const { roomId } = await messaging.getMessageFields(url, ['roomId']);
|
||||
const canView = await messaging.canViewMessage(url, roomId, uid);
|
||||
if (canView) {
|
||||
return `/message/${encodeURIComponent(url)}`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case isActor: {
|
||||
const uid = await db.getObjectField('remoteUrl:uid', url);
|
||||
const slug = await user.getUserField(uid, 'userslug');
|
||||
return `/user/${slug}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Cached result
|
||||
if (probeCache.has(url)) {
|
||||
return probeCache.get(url);
|
||||
}
|
||||
|
||||
// Opportunistic HEAD
|
||||
const { response } = await request.head(url);
|
||||
try {
|
||||
const { headers } = response;
|
||||
if (headers && headers.link) {
|
||||
let parts = headers.link.split(';');
|
||||
parts.shift();
|
||||
parts = parts
|
||||
.map(p => p.trim())
|
||||
.reduce((memo, cur) => {
|
||||
cur = cur.split('=');
|
||||
memo[cur[0]] = cur[1].slice(1, -1);
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
if (parts.rel === 'alternate' && parts.type === 'application/activity+json') {
|
||||
probeCache.set(url, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ...
|
||||
}
|
||||
|
||||
probeCache.set(url, false);
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,48 @@ const Controller = module.exports;
|
||||
Controller.actors = require('./actors');
|
||||
Controller.topics = require('./topics');
|
||||
|
||||
Controller.fetch = async (req, res, next) => {
|
||||
// Given a `resource` query parameter, attempts to retrieve and parse it
|
||||
if (!req.query.resource) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
url = new URL(req.query.resource);
|
||||
const result = await activitypub.probe({
|
||||
uid: req.uid,
|
||||
url: url.href,
|
||||
});
|
||||
|
||||
if (typeof result === 'string') {
|
||||
return helpers.redirect(res, result);
|
||||
} else if (result) {
|
||||
const { id, type } = await activitypub.get('uid', req.uid || 0, url.href);
|
||||
switch (true) {
|
||||
case activitypub._constants.acceptedPostTypes.includes(type): {
|
||||
return helpers.redirect(res, `/post/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
case activitypub._constants.acceptableActorTypes.has(type): {
|
||||
await activitypub.actors.assert(id);
|
||||
const userslug = await user.getUserField(id, 'userslug');
|
||||
return helpers.redirect(res, `/user/${userslug}`);
|
||||
}
|
||||
|
||||
default:
|
||||
return next();
|
||||
// return helpers.redirect(res, result);
|
||||
}
|
||||
}
|
||||
|
||||
helpers.redirect(res, url.href, false);
|
||||
} catch (e) {
|
||||
activitypub.helpers.log(`[activitypub/fetch] Invalid URL received: ${url}`);
|
||||
return next();
|
||||
}
|
||||
};
|
||||
|
||||
Controller.getFollowing = async (req, res) => {
|
||||
const { followingCount, followingRemoteCount } = await user.getUserFields(req.params.uid, ['followingCount', 'followingRemoteCount']);
|
||||
const totalItems = parseInt(followingCount || 0, 10) + parseInt(followingRemoteCount || 0, 10);
|
||||
|
||||
@@ -166,7 +166,7 @@ helpers.redirect = function (res, url, permanent) {
|
||||
// this is used by sso plugins to redirect to the auth route
|
||||
// { external: '/auth/sso' } or { external: 'https://domain/auth/sso' }
|
||||
if (url.hasOwnProperty('external')) {
|
||||
const redirectUrl = encodeURI(prependRelativePath(url.external));
|
||||
const redirectUrl = prependRelativePath(url.external);
|
||||
if (res.locals.isAPI) {
|
||||
res.set('X-Redirect', redirectUrl).status(200).json({ external: redirectUrl });
|
||||
} else {
|
||||
@@ -176,10 +176,9 @@ helpers.redirect = function (res, url, permanent) {
|
||||
}
|
||||
|
||||
if (res.locals.isAPI) {
|
||||
url = encodeURI(url);
|
||||
res.set('X-Redirect', url).status(200).json(url);
|
||||
} else {
|
||||
res.redirect(permanent ? 308 : 307, encodeURI(prependRelativePath(url)));
|
||||
res.redirect(permanent ? 308 : 307, prependRelativePath(url));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ const helpers = require('./helpers');
|
||||
|
||||
module.exports = function (app, middleware, controllers) {
|
||||
helpers.setupPageRoute(app, '/world', [middleware.activitypub.enabled], controllers.activitypub.topics.list);
|
||||
helpers.setupPageRoute(app, '/ap', [middleware.activitypub.enabled], controllers.activitypub.fetch);
|
||||
|
||||
/**
|
||||
* These controllers only respond if the sender is making an json+activitypub style call (i.e. S2S-only)
|
||||
* The following controllers only respond if the sender is making an json+activitypub style call (i.e. S2S-only)
|
||||
*
|
||||
* - See middleware.activitypub.assertS2S
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user