Files
FrozenFriendsYume/packages/backend/built/remote/activitypub/deliver-manager.js
T
2026-07-26 18:25:37 +09:00

121 lines
4.1 KiB
JavaScript

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