calckey/packages/backend/src/remote/activitypub/deliver-manager.ts

172 lines
4.0 KiB
TypeScript
Raw Normal View History

2023-01-13 04:40:33 +00:00
import { IsNull, Not } from "typeorm";
import { Users, Followings } from "@/models/index.js";
import type { ILocalUser, IRemoteUser, User } from "@/models/entities/user.js";
import { deliver } from "@/queue/index.js";
import { skippedInstances } from "@/misc/skipped-instances.js";
//#region types
interface IRecipe {
type: string;
}
interface IFollowersRecipe extends IRecipe {
2023-01-13 04:40:33 +00:00
type: "Followers";
}
interface IDirectRecipe extends IRecipe {
2023-01-13 04:40:33 +00:00
type: "Direct";
to: IRemoteUser;
}
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
2023-01-13 04:40:33 +00:00
recipe.type === "Followers";
const isDirect = (recipe: any): recipe is IDirectRecipe =>
2023-01-13 04:40:33 +00:00
recipe.type === "Direct";
//#endregion
export default class DeliverManager {
2023-01-13 04:40:33 +00:00
private actor: { id: User["id"]; host: null };
private activity: any;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param actor Actor
* @param activity Activity to deliver
*/
2023-01-13 04:40:33 +00:00
constructor(actor: { id: User["id"]; host: null }, activity: any) {
this.actor = actor;
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
public addFollowersRecipe() {
const deliver = {
2023-01-13 04:40:33 +00:00
type: "Followers",
} as IFollowersRecipe;
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
public addDirectRecipe(to: IRemoteUser) {
const recipe = {
2023-01-13 04:40:33 +00:00
type: "Direct",
2021-12-09 14:58:30 +00:00
to,
} as IDirectRecipe;
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
public addRecipe(recipe: IRecipe) {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
public async execute() {
if (!Users.isLocalUser(this.actor)) return;
2021-03-21 12:00:59 +00:00
const inboxes = new Set<string>();
/*
build inbox list
Process follower recipes first to avoid duplication when processing
direct recipes later.
*/
2023-01-13 04:40:33 +00:00
if (this.recipes.some((r) => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
2023-01-13 04:40:33 +00:00
const followers = (await Followings.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
2023-01-13 04:40:33 +00:00
})) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
for (const following of followers) {
const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox);
}
}
2023-01-13 04:40:33 +00:00
this.recipes
.filter(
(recipe): recipe is IDirectRecipe =>
// 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
2023-01-13 04:40:33 +00:00
Array.from(
new Set(Array.from(inboxes).map((inbox) => new URL(inbox).host)),
),
);
// deliver
for (const inbox of inboxes) {
// skip instances as indicated
if (instancesToSkip.includes(new URL(inbox).host)) continue;
deliver(this.actor, this.activity, inbox);
}
}
}
//#region Utilities
/**
* Deliver activity to followers
* @param activity Activity
* @param from Followee
*/
2023-01-13 04:40:33 +00:00
export async function deliverToFollowers(
actor: { id: ILocalUser["id"]; host: null },
activity: any,
) {
const manager = new DeliverManager(actor, activity);
manager.addFollowersRecipe();
await manager.execute();
}
/**
* Deliver activity to user
* @param activity Activity
* @param to Target user
*/
2023-01-13 04:40:33 +00:00
export async function deliverToUser(
actor: { id: ILocalUser["id"]; host: null },
activity: any,
to: IRemoteUser,
) {
const manager = new DeliverManager(actor, activity);
manager.addDirectRecipe(to);
await manager.execute();
}
//#endregion