social-butterfly
Version:
Incorporate federated social network protocols easily. Used with Hello, world federated blog.
193 lines (162 loc) • 8.11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getLRDD = getLRDD;
exports.getWebfinger = getWebfinger;
exports.getActivityPubActor = getActivityPubActor;
exports.getHTML = getHTML;
exports.discoverUserRemoteInfoSaveAndSubscribe = discoverUserRemoteInfoSaveAndSubscribe;
exports.getUserRemoteInfo = getUserRemoteInfo;
var _url_factory = require("./util/url_factory");
var _crawler = require("./util/crawler");
var _cheerio = _interopRequireDefault(require("cheerio"));
var _feeds = require("./util/feeds");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Get the /.well-known/host-meta resource.
// In other words, get the link to the WebFinger resource.
// We prefer JSON, or XML if JSON is not found.
async function getLRDD(url) {
const parsedUrl = new URL(url);
const hostMetaUrl = `${parsedUrl.protocol}//${parsedUrl.host}/.well-known/host-meta`;
let lrddUrl, $;
try {
const hostMetaXML = await (0, _crawler.fetchText)(hostMetaUrl);
$ = _cheerio.default.load(hostMetaXML);
lrddUrl = $('link[rel="lrdd"][type="application/json"]').attr('template');
if (!lrddUrl) {
lrddUrl = $('link[rel="lrdd"]').attr('template');
}
} catch (ex) {}
return lrddUrl;
} // Has all the links to different feeds/data about the user. See webfinger.js for example.
async function getWebfinger(lrddUrl, uri) {
const webfingerUrl = lrddUrl.replace('{uri}', encodeURIComponent(uri));
let webfingerDoc, webfingerInfo;
try {
webfingerDoc = await (0, _crawler.fetchText)(webfingerUrl);
} catch (ex) {
try {
// Fallback to probing for username@hostname if possible. Some sites like socialhome require this.
const parsedUrl = new URL(uri);
const acct = `${parsedUrl.pathname.split('/').filter(p => !!p).slice(-1)}@${parsedUrl.hostname}`;
const acctWebfingerUrl = lrddUrl.replace('{uri}', encodeURIComponent(acct));
webfingerDoc = await (0, _crawler.fetchText)(acctWebfingerUrl);
} catch (ex) {
return null;
}
}
let success = false;
try {
const json = JSON.parse(webfingerDoc);
const linkMap = {};
json.links.map(link => linkMap[link.rel] = link);
const activityPubActorUrl = json.links.find(link => link.rel === 'self' && link.type === 'application/activity+json');
webfingerInfo = {
feed_url: linkMap['http://schemas.google.com/g/2010#updates-from']?.href,
salmon_url: linkMap['salmon']?.href,
activitypub_actor_url: activityPubActorUrl?.href,
webmention_url: linkMap['webmention']?.href,
magic_key: (linkMap['magic-public-key']?.href || '').replace('data:application/magic-public-key,', ''),
profile_url: json.aliases.find(alias => alias.startsWith('https:') || alias.startsWith('http:'))
};
success = true;
} catch (ex) {// Fall-through, and try XML parsing.
}
if (!success) {
try {
const $ = _cheerio.default.load(webfingerDoc);
webfingerInfo = {
feed_url: $('link[rel="http://schemas.google.com/g/2010#updates-from"]').attr('href'),
salmon_url: $('link[rel="salmon"]').attr('href'),
activitypub_actor_url: $('link[rel="self"][type="application/activity+json"]').attr('href'),
webmention_url: $('link[rel="webmention"]').attr('href'),
magic_key: $('link[rel="magic-public-key"]').attr('href').replace('data:application/magic-public-key,', ''),
profile_url: $('alias').first().text().startsWith('https:') || $('alias').first().text().startsWith('http:') || $('alias').last().text()
};
} catch (ex) {
return null;
}
}
if (webfingerInfo.activitypub_actor_url) {
try {
const actorJSON = await getActivityPubActor(webfingerInfo.activitypub_actor_url); // TODO(mime): not the cleanest naming, we're overwriting the magic_key and preferring the PEM from
// the actor JSON. should rename this field to reflect that it is has magic or PEM format for public key.
webfingerInfo.magic_key = actorJSON['publicKey']['publicKeyPem'];
webfingerInfo.activitypub_inbox_url = actorJSON['inbox'];
} catch (ex) {// Ignore, if we can't get the actor info.
}
}
return webfingerInfo;
}
async function getActivityPubActor(url) {
return await (0, _crawler.fetchJSON)(url, {
Accept: 'application/activity+json'
});
}
async function getHTML(url) {
let $;
try {
const html = await (0, _crawler.fetchText)(url);
$ = _cheerio.default.load(html);
} catch (ex) {
return null;
}
return $;
}
async function discoverUserRemoteInfoSaveAndSubscribe(req, options, url, local_username) {
const userRemote = await getUserRemoteInfo(url, local_username);
const existingUserRemote = await options.getRemoteUser(userRemote.local_username, userRemote.profile_url);
userRemote.id = existingUserRemote?.id || undefined;
userRemote.following = true;
await options.saveRemoteUser(userRemote);
if (req && userRemote.hub_url) {
const userRemoteParams = {
localUsername: userRemote.local_username,
remoteProfileUrl: userRemote.profile_url
};
const callbackUrl = (0, _url_factory.buildUrl)({
req,
pathname: '/websub',
searchParams: userRemoteParams
});
await options.webSubSubscriberServer.subscribe(userRemote.hub_url, options.constants.webSubHub, callbackUrl);
}
return await options.getRemoteUser(userRemote.local_username, userRemote.profile_url);
}
async function getUserRemoteInfo(websiteUrl, local_username) {
let userRemote = {
local_username
};
const lrddUrl = await getLRDD(websiteUrl);
if (lrddUrl) {
const webfingerInfo = await getWebfinger(lrddUrl, websiteUrl);
userRemote = Object.assign({}, userRemote, webfingerInfo);
}
userRemote.feed_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.feed_url);
const {
feedMeta,
feedUrl
} = await (0, _feeds.discoverAndParseFeedFromUrl)(userRemote.feed_url || websiteUrl);
userRemote.feed_url = feedUrl;
const htmlDoc = await getHTML(websiteUrl);
const atomLinks = feedMeta['atom:link'] ? [feedMeta['atom:link']].flat(1) : [];
userRemote.profile_url = userRemote.profile_url || feedMeta['atom:author']?.['uri']?.['#'] || websiteUrl;
userRemote.hub_url = atomLinks.find(el => el['@'].rel === 'hub')?.['@'].href;
userRemote.salmon_url = userRemote.salmon_url || atomLinks.find(el => el['@'].rel === 'salmon')?.['@'].href;
userRemote.webmention_url = userRemote.webmention_url || htmlDoc('link[rel="webmention"]').attr('href');
userRemote.username = userRemote.username || feedMeta['atom:author']?.['poco:preferredusername']?.['#'] || feedMeta.title;
userRemote.name = feedMeta['atom:author']?.['poco:displayname']?.['#'] || '';
userRemote.favicon = feedMeta.favicon || (0, _crawler.createAbsoluteUrl)(websiteUrl, htmlDoc('link[rel="shortcut icon"]')['href']) || (0, _crawler.createAbsoluteUrl)(websiteUrl, htmlDoc('link[rel="icon"]')['href']) || (0, _crawler.createAbsoluteUrl)(websiteUrl, '/favicon.ico');
userRemote.avatar = feedMeta.image?.url || userRemote.favicon;
userRemote.order = Math.pow(2, 31) - 1; // If activitypub_actor_url not found, fallback to profile_url. Used in Salmon lookups.
userRemote.activitypub_actor_url = userRemote.activitypub_actor_url || userRemote.profile_url;
userRemote.salmon_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.salmon_url);
userRemote.activitypub_actor_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.activitypub_actor_url);
userRemote.activitypub_inbox_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.activitypub_inbox_url);
userRemote.webmention_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.webmention_url);
userRemote.profile_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.profile_url);
userRemote.feed_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.feed_url);
userRemote.hub_url = (0, _url_factory.ensureAbsoluteUrl)(websiteUrl, userRemote.hub_url);
return userRemote;
}