egirlskey/packages/backend/src/core/activitypub/ApDeliverManagerService.ts

218 lines
5.4 KiB
TypeScript
Raw Normal View History

2022-09-17 18:27:08 +00:00
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
2022-09-17 18:27:08 +00:00
import { DI } from '@/di-symbols.js';
2022-09-20 20:33:11 +00:00
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
2023-02-13 06:50:22 +00:00
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
2022-09-17 18:27:08 +00:00
import { QueueService } from '@/core/QueueService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import type { IActivity } from '@/core/activitypub/type.js';
import { ThinUser } from '@/queue/types.js';
interface IRecipe {
type: string;
}
interface IFollowersRecipe extends IRecipe {
type: 'Followers';
}
interface IDirectRecipe extends IRecipe {
type: 'Direct';
2023-02-13 06:50:22 +00:00
to: RemoteUser;
}
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
recipe.type === 'Followers';
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
recipe.type === 'Direct';
2022-09-17 18:27:08 +00:00
@Injectable()
export class ApDeliverManagerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private queueService: QueueService,
) {
}
/**
* Deliver activity to followers
* @param actor
2022-09-17 18:27:08 +00:00
* @param activity Activity
*/
@bindThis
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
2022-09-17 18:27:08 +00:00
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
activity,
);
manager.addFollowersRecipe();
await manager.execute();
}
/**
* Deliver activity to user
* @param actor
2022-09-17 18:27:08 +00:00
* @param activity Activity
* @param to Target user
*/
@bindThis
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
2022-09-17 18:27:08 +00:00
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
activity,
);
manager.addDirectRecipe(to);
await manager.execute();
}
@bindThis
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
2022-09-17 18:27:08 +00:00
return new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
2022-09-17 18:27:08 +00:00
activity,
);
}
}
class DeliverManager {
private actor: ThinUser;
private activity: IActivity | null;
private recipes: IRecipe[] = [];
/**
* Constructor
* @param userEntityService
* @param followingsRepository
* @param queueService
* @param actor Actor
* @param activity Activity to deliver
*/
2022-09-17 18:27:08 +00:00
constructor(
private userEntityService: UserEntityService,
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
actor: { id: User['id']; host: null; },
activity: IActivity | null,
2022-09-17 18:27:08 +00:00
) {
// 型で弾いてはいるが一応ローカルユーザーかチェック
if (actor.host != null) throw new Error('actor.host must be null');
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
this.actor = {
id: actor.id,
};
this.activity = activity;
}
/**
* Add recipe for followers deliver
*/
@bindThis
public addFollowersRecipe() {
const deliver = {
2021-12-09 14:58:30 +00:00
type: 'Followers',
} as IFollowersRecipe;
this.addRecipe(deliver);
}
/**
* Add recipe for direct deliver
* @param to To
*/
@bindThis
2023-02-13 06:50:22 +00:00
public addDirectRecipe(to: RemoteUser) {
const recipe = {
type: 'Direct',
2021-12-09 14:58:30 +00:00
to,
} as IDirectRecipe;
this.addRecipe(recipe);
}
/**
* Add recipe
* @param recipe Recipe
*/
@bindThis
public addRecipe(recipe: IRecipe) {
this.recipes.push(recipe);
}
/**
* Execute delivers
*/
@bindThis
public async execute() {
// The value flags whether it is shared or not.
// key: inbox URL, value: whether it is sharedInbox
const inboxes = new Map<string, boolean>();
/*
build inbox list
Process follower recipes first to avoid duplication when processing
direct recipes later.
*/
2022-04-02 06:31:11 +00:00
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
2022-09-17 18:27:08 +00:00
const followers = await this.followingsRepository.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
for (const following of followers) {
2022-09-17 18:27:08 +00:00
const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.set(inbox, following.followerSharedInbox != null);
}
}
2022-04-03 06:33:22 +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
2022-04-03 06:33:22 +00:00
&& recipe.to.inbox != null,
)
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes);
}
}