social-butterfly
Version:
Incorporate federated social network protocols easily. Used with Hello, world federated blog.
338 lines (292 loc) • 9.97 kB
JavaScript
;
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 */
);
}