import { IsNull, Not } from "typeorm"; import { Followings, Users } from "../../models/index.js"; import { deliver } from "../../queue/index.js"; import { skippedInstances } from "../../misc/skipped-instances.js"; import { apLogger } from "./logger.js"; const isFollowers = (recipe)=>recipe.type === "Followers"; const isDirect = (recipe)=>recipe.type === "Direct"; //#endregion export default class DeliverManager { actor; activity; recipes = []; /** * Constructor * @param actor Actor * @param activity Activity to deliver */ constructor(actor, activity){ this.actor = actor; this.activity = activity; } /** * Add recipe for followers deliver */ addFollowersRecipe() { const deliver = { type: "Followers" }; this.addRecipe(deliver); } /** * Add recipe for direct deliver * @param to To */ addDirectRecipe(to) { const recipe = { type: "Direct", to }; this.addRecipe(recipe); } /** * Add recipe * @param recipe Recipe */ addRecipe(recipe) { this.recipes.push(recipe); } /** * Execute delivers */ async execute() { if (!Users.isLocalUser(this.actor)) return; const inboxes = new Set(); /* build inbox list Process follower recipes first to avoid duplication when processing direct recipes later. */ if (this.recipes.some((r)=>isFollowers(r))) { // followers deliver // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? const followers = await Followings.find({ where: { followeeId: this.actor.id, followerHost: Not(IsNull()) }, select: { followerSharedInbox: true, followerInbox: true } }); for (const following of followers){ const inbox = following.followerSharedInbox || following.followerInbox; inboxes.add(inbox); } } this.recipes.filter((recipe)=>// followers recipes have already been processed isDirect(recipe) && // check that shared inbox has not been added yet !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) && // check that they actually have an inbox recipe.to.inbox != null).forEach((recipe)=>inboxes.add(recipe.to.inbox)); const instancesToSkip = await skippedInstances(// get (unique) list of hosts Array.from(new Set(Array.from(inboxes).map((inbox)=>{ try { return new URL(inbox).host; } catch (e) { apLogger.error(`Invalid inbox URL: ${inbox}`); return null; } }).filter((host)=>host != null)))); // deliver for (const inbox of inboxes){ // skip instances as indicated try { const host = new URL(inbox).host; if (instancesToSkip.includes(host)) continue; } catch (e) { // skip invalid URLs apLogger.error(`Invalid inbox URL: ${inbox}`); continue; } deliver(this.actor, this.activity, inbox); } } } //#region Utilities /** * Deliver activity to followers * @param activity Activity * @param from Followee */ export async function deliverToFollowers(actor, activity) { const manager = new DeliverManager(actor, activity); manager.addFollowersRecipe(); await manager.execute(); } /** * Deliver activity to user * @param activity Activity * @param to Target user */ export async function deliverToUser(actor, activity, to) { const manager = new DeliverManager(actor, activity); manager.addDirectRecipe(to); await manager.execute(); } //#endregion