UNPKG

social-butterfly

Version:

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

338 lines (292 loc) 9.97 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.accept = accept; exports.like = like; exports.follow = follow; exports.reply = reply; exports.createGenericMessage = createGenericMessage; exports.createArticle = createArticle; exports.handle = handle; exports.findUserRemote = findUserRemote; var _discover_user = require("./discover_user"); var _activitypub = require("./activitypub"); var _url_factory = require("./util/url_factory"); var _email = require("./email"); var _crawler = require("./util/crawler"); var _nanoid = require("nanoid"); var _salmon = require("./salmon"); var _syndicate = _interopRequireDefault(require("./syndicate")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } async function accept(req, contentOwner, userRemote) { const id = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/accept', searchParams: { id: (0, _nanoid.nanoid)(10), resource: req.body } }); const message = createGenericMessage('Accept', req, id, contentOwner, req.body); send(req, userRemote, contentOwner, message); } async function like(req, contentOwner, contentRemote, userRemote, isFavorite) { // TODO(mime): add back unfavorite const id = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/like', searchParams: { id: (0, _nanoid.nanoid)(10), resource: contentRemote.link } }); const message = createGenericMessage('Like', req, id, contentOwner, { type: 'Post', id: contentRemote.link, displayName: contentRemote.title, url: contentRemote.link }); send(req, userRemote, contentOwner, message); } async function follow(req, contentOwner, userRemote, isFollow) { // TODO(mime): add back unfollow const id = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/follow', searchParams: { id: (0, _nanoid.nanoid)(10), resource: userRemote.profile_url } }); const message = createGenericMessage('Follow', req, id, contentOwner, userRemote.profile_url); send(req, userRemote, contentOwner, message); } async function reply(req, contentOwner, content, userRemote, mentionedRemoteUsers) { const message = await createArticle(req, content, contentOwner, mentionedRemoteUsers); send(req, userRemote, contentOwner, message); } async function send(req, userRemote, contentOwner, message) { try { if (userRemote?.activitypub_inbox_url) { (0, _activitypub.send)(req, userRemote, contentOwner, message); } else if (userRemote?.salmon_url) { (0, _salmon.send)(req, userRemote, contentOwner, message); } } catch (ex) {// Not a big deal if this fails. // TODO(mime): add logging later. } } function createGenericMessage(type, req, id, localUser, object, opt_follower) { const actor = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/actor', searchParams: { resource: localUser.url } }); const json = { '@context': 'https://www.w3.org/ns/activitystreams', type, id, actor, to: ['https://www.w3.org/ns/activitystreams#Public'], cc: opt_follower ? [opt_follower.profile_url] : undefined, object }; return json; } async function createArticle(req, localContent, localUser, opt_follower) { const messageUrl = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/message', searchParams: { resource: localContent.url } }); const actorUrl = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/actor', searchParams: { resource: localUser.url } }); const statsImgSrc = (0, _url_factory.buildUrl)({ req, pathname: '/api/stats', searchParams: { resource: localContent.url } }); const statsImg = `<img src="${statsImgSrc}" />`; const absoluteUrlReplacement = (0, _url_factory.buildUrl)({ req, pathname: '/resource' }); // TODO(mime): this replacement is nite-lite specific... const view = localContent.view.replace(/(['"])\/resource/gm, `$1${absoluteUrlReplacement}`) + statsImg; let inReplyTo = localContent.thread; if (localContent.thread) { try { const activityObject = await (0, _crawler.fetchJSON)(localContent.thread, { Accept: 'application/activity+json' }); if (activityObject) { inReplyTo = activityObject.id; } } catch (ex) {} } return createGenericMessage('Create', req, messageUrl, localUser, { id: messageUrl, url: localContent.url, type: 'Article', published: new Date(localContent.createdAt).toISOString(), updated: new Date(localContent.updatedAt).toISOString(), attributedTo: actorUrl, inReplyTo, title: localContent.title, content: view, to: 'https://www.w3.org/ns/activitystreams#Public' }, opt_follower); } async function handle(type, options, req, res, activity, user, userRemote) { switch (type) { case 'Accept': // Do nothing. break; case 'Create': await handleCreate(options, req, res, activity, user, userRemote); break; case 'Follow': await handleFollow(options, req, user, userRemote, true); break; case 'Like': await handleLike(options, req, res, activity, userRemote, true); break; default: break; } } async function findUserRemote(options, json, res, user) { const actorUrl = json.actor; let userRemote = await options.getRemoteUserByActor(user.username, actorUrl); if (!userRemote) { const actorJSON = await (0, _discover_user.getActivityPubActor)(actorUrl); if (actorJSON.url) { const userRemoteInfo = await (0, _discover_user.getUserRemoteInfo)(actorJSON.url, user.username); await options.saveRemoteUser(userRemoteInfo); userRemote = await options.getRemoteUser(user.username, actorJSON.url); } else { return res.sendStatus(400); } } return userRemote; } async function handleFollow(options, req, user, userRemote, isFollow) { if (isFollow) { await options.saveRemoteUser(Object.assign({}, userRemote.dataValues, { follower: true })); (0, _email.follow)(req, user.username, user.email, userRemote.profile_url); } else { if (userRemote.following) { await options.saveRemoteUser(Object.assign({}, userRemote.dataValues, { follower: false })); } else { await options.removeRemoteUser(userRemote); } } } async function handleLike(options, req, res, activity, userRemote, isLike) { const localContentUrl = activity.object; const { username, name } = await options.getLocalContent(localContentUrl, req); if (!name) { return res.sendStatus(400); } const postId = `${userRemote.profile_url},${localContentUrl},favorite`; const remoteContent = { from_user: userRemote.profile_url, local_content_name: name, post_id: postId, to_username: username, type: 'favorite', username: userRemote.username }; if (!isLike) { await options.removeRemoteContent(remoteContent); return; } const existingFavorite = await options.getRemoteContent(username, postId); if (existingFavorite) { return; } await options.saveRemoteContent(Object.assign({}, remoteContent, { content: '', createdAt: new Date(), updatedAt: new Date(), link: '', title: '', view: '' })); } async function handleCreate(options, req, res, activity, user, userRemote) { const activityObject = activity.object; const atomContent = (0, _crawler.sanitizeHTML)(activityObject.content); const existingContentRemote = await options.getRemoteContent(user.username, activityObject.id); const contentRemote = { id: existingContentRemote?.id || undefined, avatar: userRemote.avatar, comments_count: parseInt(activityObject.repliesCount), comments_updated: new Date(activityObject.repliesUpdated || new Date()), content: '', createdAt: new Date(activityObject.published || new Date()), from_user: userRemote.profile_url, from_user_remote_id: userRemote.id, creator: userRemote.name, link: activityObject.id, post_id: activityObject.id, title: activityObject.title, to_username: user.username, updatedAt: new Date(activityObject.updated || new Date()), username: userRemote.username, view: atomContent }; if (activityObject.inReplyTo) { handleComment(options, req, res, contentRemote, activityObject.inReplyTo); } else { handlePost(options, req, res, contentRemote, user, activityObject); } //const repliesCount = activityObject.repliesCount; // if (repliesCount) { // // TODO(mime): these used to be known as 'remote-comment' types. // // need to refactor this. // const comments = await retrieveFeed(activityObject.repliesLink); // await parseFeedAndInsertIntoDb(salmon, userRemote, comments); // } } async function handlePost(options, req, res, contentRemote, user, activityObject) { // TODO(mime): need to unify this with activitypub.js, currently salmon specific... const wasUserMentioned = activityObject.mentioned || activityObject.attention; contentRemote.type = 'post'; await options.saveRemoteContent(contentRemote); if (wasUserMentioned) { (0, _email.mention)(req, 'Remote User', undefined /* fromEmail */ , user.email, contentRemote.link); } } async function handleComment(options, req, res, contentRemote, inReplyTo) { const content = await options.getLocalContent(inReplyTo, req); if (!content) { return res.sendStatus(404); } contentRemote.type = 'comment'; contentRemote.local_content_name = content.name; await options.saveRemoteContent(contentRemote); const contentOwner = await options.getLocalUser(inReplyTo, req); await (0, _syndicate.default)(req, contentOwner, content, contentRemote, true /* isComment */ ); }