Source: inbox.js

import express from 'express';
import { ActivityPub } from '../lib/ActivityPub.js';
import { fetchUser } from '../lib/users.js';
import {
  acceptDM,
  addFollower,
  removeFollower,
  follow,
  isReplyToMyPost,
  addNotification,
  isMyPost,
  isBlocked,
  addressedOnlyToMe,
  isMention,
  deleteObject
} from '../lib/account.js';
import { createActivity, recordLike, recordUndoLike, recordBoost, getActivity } from '../lib/notes.js';
import debug from 'debug';
import { isIndexed } from '../lib/storage.js';
const logger = debug('ono:inbox');

/**
 * Express.js router for handling incoming ActivityPub requests.
 *
 * @typedef {Object} ActivityPubRouter
 * @property {Function} processActivity - Route handler for processing incoming ActivityPub requests.
 *
 * @example
 * // Example usage:
 * import { router as activityPubRouter } from './activityPubRouter';
 * app.use('/activitypub', activityPubRouter);
 */
export const router = express.Router();

/**
 * Route handler for processing incoming ActivityPub requests.
 *
 * @function
 * @async
 * @param {Object} req - Express.js request object.
 * @param {Object} res - Express.js response object.
 * @returns {Promise<void>} Resolves with a success response or rejects with an error response.
 *
 * @throws {Error} Responds with a 403 Forbidden if the actor is blocked.
 * @throws {Error} Responds with a 403 Forbidden if the signature validation fails.
 *
 * @example
 * // Example route:
 * // POST /activitypub
 * activityPubRouter.post('/', activityPubHandlers.processActivity);
 */
router.post('/', async (req, res) => {
  /**
   * The incoming ActivityPub request payload.
   *
   * @type {Object}
   * @property {string} type - The type of the incoming request (e.g., 'Create', 'Follow', 'Like').
   * @property {Object} actor - The actor associated with the request.
   * @property {Object} object - The object of the request, containing the main content.
   */
  const incomingRequest = req.body;

  if (incomingRequest) {
    if (isBlocked(incomingRequest.actor)) {
      return res.status(403).send('');
    }

    logger('New message', JSON.stringify(incomingRequest, null, 2));
    logger('Looking up actor', incomingRequest.actor);

    /**
     * The user object obtained from fetching the actor of the incoming request.
     *
     * @type {Object}
     * @property {Object} actor - The actor object representing the user.
     */
    const { actor } = await fetchUser(incomingRequest.actor);

    // FIRST, validate the actor
    if (ActivityPub.validateSignature(actor, req)) {
      switch (incomingRequest.type) {
        case 'Delete':
          logger('Delete request');
          await deleteObject(actor, incomingRequest);
          break;
        case 'Follow':
          logger('Incoming follow request');
          addFollower(incomingRequest);

          // TODO: should wait to confirm follow acceptance?
          ActivityPub.sendAccept(actor, incomingRequest);
          break;
        case 'Undo':
          logger('Incoming undo');
          switch (incomingRequest.object.type) {
            case 'Follow':
              logger('Incoming unfollow request');
              removeFollower(incomingRequest.actor);
              break;
            case 'Like':
              logger('Incoming undo like request');
              recordUndoLike(incomingRequest.object);
              break;
            default:
              logger('Unknown undo type');
          }
          break;
        case 'Accept':
          switch (incomingRequest.object.type) {
            case 'Follow':
              logger('Incoming follow request');
              follow(incomingRequest);
              break;
            default:
              logger('Unknown undo type');
          }
          break;
        case 'Like':
          logger('Incoming like');
          recordLike(incomingRequest);
          break;
        case 'Announce':
          logger('Incoming boost');
          // determine if this is a boost on MY post
          // or someone boosting a post into my feed. DIFFERENT!
          if (
            isMyPost({
              id: incomingRequest.object
            })
          ) {
            recordBoost(incomingRequest);
          } else {
            // fetch the boosted post if it doesn't exist
            try {
              await getActivity(incomingRequest.object);
            } catch (err) {
              console.error('Could not fetch boosted post');
            }

            // log the boost itself to the activity stream
            try {
              createActivity(incomingRequest);
            } catch (err) {
              console.error('Could not fetch boosted post...');
            }
          }
          break;
        case 'Create':
          logger('incoming create');

          // determine what type of post this is, if it should show up, etc.
          // - a post that is a reply to your own post from someone you follow (notification AND feed)
          // - a post that is a reply to your own post from someone you do not follow (notification only)
          // - a post that that is from someone you follow, and is a reply to a post from someone you follow (in feed)
          // - a post that is from someone you follow, but is a reply to a post from someone you do not follow (should be ignored?)
          // - a mention from a following (notification and feed)
          // - a mention from a stranger (notification only)
          if (incomingRequest.object.directMessage === true || addressedOnlyToMe(incomingRequest)) {
            await acceptDM(incomingRequest.object, incomingRequest.object.attributedTo);
          } else if (isReplyToMyPost(incomingRequest.object)) {
            // TODO: What about replies to replies? should we traverse up a bit?
            if (!isIndexed(incomingRequest.object.id)) {
              createActivity(incomingRequest.object);
              addNotification({
                type: 'Reply',
                actor: incomingRequest.object.attributedTo,
                object: incomingRequest.object.id
              });
            } else {
              logger('already created reply');
            }
          } else if (isMention(incomingRequest.object)) {
            if (!isIndexed(incomingRequest.object.id)) {
              createActivity(incomingRequest.object);
              addNotification({
                type: 'Mention',
                actor: incomingRequest.object.attributedTo,
                object: incomingRequest.object.id
              });
            } else {
              logger('already created mention');
            }
          } else if (!incomingRequest.object.inReplyTo) {
            // this is a NEW post - most likely from a follower
            createActivity(incomingRequest.object);
          } else {
            // this is a reply
            // from a following
            // or from someone else who replied to a following?
            // the visibility should be determined on the feed
            // TODO: we may want to discard things NOT from followings
            // since they may never be seen
            // and we can always go fetch them...
            createActivity(incomingRequest.object);
          }

          break;
        case 'Update':
          createActivity(incomingRequest.object);
          break;
        default:
          logger('Unknown request type:', incomingRequest.type);
      }
    } else {
      logger('Signature failed:', incomingRequest);
      return res.status(403).send('Invalid signature');
    }
  } else {
    logger('Unknown request format:', incomingRequest);
  }
  return res.status(200).send();
});