UNPKG

social-butterfly

Version:

Incorporate federated social network protocols easily. Used with Hello, world federated blog.

193 lines (162 loc) 8.11 kB
"use strict"; 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; }