mirror of
https://github.com/hotomoe/hotomoe
synced 2024-12-15 07:08:23 +09:00
575 lines
21 KiB
TypeScript
575 lines
21 KiB
TypeScript
|
import { Inject, Injectable } from '@nestjs/common';
|
||
|
import type { CacheableUser, ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
|
||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||
|
import { QueueService } from '@/core/QueueService.js';
|
||
|
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||
|
import { IdService } from '@/core/IdService.js';
|
||
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||
|
import type { Packed } from '@/misc/schema.js';
|
||
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||
|
import { WebhookService } from '@/core/WebhookService.js';
|
||
|
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||
|
import { DI } from '@/di-symbols.js';
|
||
|
import Logger from '../logger.js';
|
||
|
import { UserEntityService } from './entities/UserEntityService.js';
|
||
|
import { ApRendererService } from './remote/activitypub/ApRendererService.js';
|
||
|
|
||
|
const logger = new Logger('following/create');
|
||
|
|
||
|
type Local = ILocalUser | {
|
||
|
id: ILocalUser['id'];
|
||
|
host: ILocalUser['host'];
|
||
|
uri: ILocalUser['uri']
|
||
|
};
|
||
|
type Remote = IRemoteUser | {
|
||
|
id: IRemoteUser['id'];
|
||
|
host: IRemoteUser['host'];
|
||
|
uri: IRemoteUser['uri'];
|
||
|
inbox: IRemoteUser['inbox'];
|
||
|
};
|
||
|
type Both = Local | Remote;
|
||
|
|
||
|
@Injectable()
|
||
|
export class UserFollowingService {
|
||
|
constructor(
|
||
|
@Inject(DI.usersRepository)
|
||
|
private usersRepository: UsersRepository,
|
||
|
|
||
|
@Inject(DI.userProfilesRepository)
|
||
|
private userProfilesRepository: UserProfilesRepository,
|
||
|
|
||
|
@Inject(DI.followingsRepository)
|
||
|
private followingsRepository: FollowingsRepository,
|
||
|
|
||
|
@Inject(DI.followRequestsRepository)
|
||
|
private followRequestsRepository: FollowRequestsRepository,
|
||
|
|
||
|
@Inject(DI.blockingsRepository)
|
||
|
private blockingsRepository: BlockingsRepository,
|
||
|
|
||
|
@Inject(DI.instancesRepository)
|
||
|
private instancesRepository: InstancesRepository,
|
||
|
|
||
|
private userEntityService: UserEntityService,
|
||
|
private idService: IdService,
|
||
|
private queueService: QueueService,
|
||
|
private globalEventServie: GlobalEventService,
|
||
|
private createNotificationService: CreateNotificationService,
|
||
|
private federatedInstanceService: FederatedInstanceService,
|
||
|
private webhookService: WebhookService,
|
||
|
private apRendererService: ApRendererService,
|
||
|
private perUserFollowingChart: PerUserFollowingChart,
|
||
|
private instanceChart: InstanceChart,
|
||
|
) {
|
||
|
}
|
||
|
|
||
|
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||
|
const [follower, followee] = await Promise.all([
|
||
|
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||
|
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||
|
]);
|
||
|
|
||
|
// check blocking
|
||
|
const [blocking, blocked] = await Promise.all([
|
||
|
this.blockingsRepository.findOneBy({
|
||
|
blockerId: follower.id,
|
||
|
blockeeId: followee.id,
|
||
|
}),
|
||
|
this.blockingsRepository.findOneBy({
|
||
|
blockerId: followee.id,
|
||
|
blockeeId: follower.id,
|
||
|
}),
|
||
|
]);
|
||
|
|
||
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
|
||
|
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||
|
this.queueService.deliver(followee, content, follower.inbox);
|
||
|
return;
|
||
|
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
|
||
|
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
|
||
|
await this.blockingsRepository.delete(blocking.id);
|
||
|
} else {
|
||
|
// それ以外は単純に例外
|
||
|
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
|
||
|
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
|
||
|
}
|
||
|
|
||
|
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
|
||
|
|
||
|
// フォロー対象が鍵アカウントである or
|
||
|
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||
|
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
||
|
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||
|
if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) {
|
||
|
let autoAccept = false;
|
||
|
|
||
|
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||
|
const following = await this.followingsRepository.findOneBy({
|
||
|
followerId: follower.id,
|
||
|
followeeId: followee.id,
|
||
|
});
|
||
|
if (following) {
|
||
|
autoAccept = true;
|
||
|
}
|
||
|
|
||
|
// フォローしているユーザーは自動承認オプション
|
||
|
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
||
|
const followed = await this.followingsRepository.findOneBy({
|
||
|
followerId: followee.id,
|
||
|
followeeId: follower.id,
|
||
|
});
|
||
|
|
||
|
if (followed) autoAccept = true;
|
||
|
}
|
||
|
|
||
|
if (!autoAccept) {
|
||
|
await this.createFollowRequest(follower, followee, requestId);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await this.#insertFollowingDoc(followee, follower);
|
||
|
|
||
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||
|
this.queueService.deliver(followee, content, follower.inbox);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async #insertFollowingDoc(
|
||
|
followee: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']
|
||
|
},
|
||
|
follower: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']
|
||
|
},
|
||
|
): Promise<void> {
|
||
|
if (follower.id === followee.id) return;
|
||
|
|
||
|
let alreadyFollowed = false as boolean;
|
||
|
|
||
|
await this.followingsRepository.insert({
|
||
|
id: this.idService.genId(),
|
||
|
createdAt: new Date(),
|
||
|
followerId: follower.id,
|
||
|
followeeId: followee.id,
|
||
|
|
||
|
// 非正規化
|
||
|
followerHost: follower.host,
|
||
|
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null,
|
||
|
followerSharedInbox: this.userEntityService.isRemoteUser(follower) ? follower.sharedInbox : null,
|
||
|
followeeHost: followee.host,
|
||
|
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : null,
|
||
|
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null,
|
||
|
}).catch(err => {
|
||
|
if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||
|
logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
|
||
|
alreadyFollowed = true;
|
||
|
} else {
|
||
|
throw err;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const req = await this.followRequestsRepository.findOneBy({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
if (req) {
|
||
|
await this.followRequestsRepository.delete({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
// 通知を作成
|
||
|
this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||
|
notifierId: followee.id,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (alreadyFollowed) return;
|
||
|
|
||
|
//#region Increment counts
|
||
|
await Promise.all([
|
||
|
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
||
|
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
||
|
]);
|
||
|
//#endregion
|
||
|
|
||
|
//#region Update instance stats
|
||
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||
|
this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => {
|
||
|
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||
|
this.instanceChart.updateFollowing(i.host, true);
|
||
|
});
|
||
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||
|
this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => {
|
||
|
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||
|
this.instanceChart.updateFollowers(i.host, true);
|
||
|
});
|
||
|
}
|
||
|
//#endregion
|
||
|
|
||
|
this.perUserFollowingChart.update(follower, followee, true);
|
||
|
|
||
|
// Publish follow event
|
||
|
if (this.userEntityService.isLocalUser(follower)) {
|
||
|
this.userEntityService.pack(followee.id, follower, {
|
||
|
detail: true,
|
||
|
}).then(async packed => {
|
||
|
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||
|
this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||
|
|
||
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||
|
for (const webhook of webhooks) {
|
||
|
this.queueService.webhookDeliver(webhook, 'follow', {
|
||
|
user: packed,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Publish followed event
|
||
|
if (this.userEntityService.isLocalUser(followee)) {
|
||
|
this.userEntityService.pack(follower.id, followee).then(async packed => {
|
||
|
this.globalEventServie.publishMainStream(followee.id, 'followed', packed);
|
||
|
|
||
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||
|
for (const webhook of webhooks) {
|
||
|
this.queueService.webhookDeliver(webhook, 'followed', {
|
||
|
user: packed,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// 通知を作成
|
||
|
this.createNotificationService.createNotification(followee.id, 'follow', {
|
||
|
notifierId: follower.id,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public async unfollow(
|
||
|
follower: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||
|
},
|
||
|
followee: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||
|
},
|
||
|
silent = false,
|
||
|
): Promise<void> {
|
||
|
const following = await this.followingsRepository.findOneBy({
|
||
|
followerId: follower.id,
|
||
|
followeeId: followee.id,
|
||
|
});
|
||
|
|
||
|
if (following == null) {
|
||
|
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
await this.followingsRepository.delete(following.id);
|
||
|
|
||
|
this.#decrementFollowing(follower, followee);
|
||
|
|
||
|
// Publish unfollow event
|
||
|
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
||
|
this.userEntityService.pack(followee.id, follower, {
|
||
|
detail: true,
|
||
|
}).then(async packed => {
|
||
|
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
||
|
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
|
||
|
|
||
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||
|
for (const webhook of webhooks) {
|
||
|
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||
|
user: packed,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||
|
this.queueService.deliver(follower, content, followee.inbox);
|
||
|
}
|
||
|
|
||
|
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||
|
// local user has null host
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||
|
this.queueService.deliver(followee, content, follower.inbox);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async #decrementFollowing(
|
||
|
follower: {id: User['id']; host: User['host']; },
|
||
|
followee: { id: User['id']; host: User['host']; },
|
||
|
): Promise<void> {
|
||
|
//#region Decrement following / followers counts
|
||
|
await Promise.all([
|
||
|
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||
|
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
||
|
]);
|
||
|
//#endregion
|
||
|
|
||
|
//#region Update instance stats
|
||
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||
|
this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => {
|
||
|
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||
|
this.instanceChart.updateFollowing(i.host, false);
|
||
|
});
|
||
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||
|
this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => {
|
||
|
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||
|
this.instanceChart.updateFollowers(i.host, false);
|
||
|
});
|
||
|
}
|
||
|
//#endregion
|
||
|
|
||
|
this.perUserFollowingChart.update(follower, followee, false);
|
||
|
}
|
||
|
|
||
|
public async createFollowRequest(
|
||
|
follower: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||
|
},
|
||
|
followee: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||
|
},
|
||
|
requestId?: string,
|
||
|
): Promise<void> {
|
||
|
if (follower.id === followee.id) return;
|
||
|
|
||
|
// check blocking
|
||
|
const [blocking, blocked] = await Promise.all([
|
||
|
this.blockingsRepository.findOneBy({
|
||
|
blockerId: follower.id,
|
||
|
blockeeId: followee.id,
|
||
|
}),
|
||
|
this.blockingsRepository.findOneBy({
|
||
|
blockerId: followee.id,
|
||
|
blockeeId: follower.id,
|
||
|
}),
|
||
|
]);
|
||
|
|
||
|
if (blocking != null) throw new Error('blocking');
|
||
|
if (blocked != null) throw new Error('blocked');
|
||
|
|
||
|
const followRequest = await this.followRequestsRepository.insert({
|
||
|
id: this.idService.genId(),
|
||
|
createdAt: new Date(),
|
||
|
followerId: follower.id,
|
||
|
followeeId: followee.id,
|
||
|
requestId,
|
||
|
|
||
|
// 非正規化
|
||
|
followerHost: follower.host,
|
||
|
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined,
|
||
|
followerSharedInbox: this.userEntityService.isRemoteUser(follower) ? follower.sharedInbox : undefined,
|
||
|
followeeHost: followee.host,
|
||
|
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
||
|
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
||
|
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||
|
|
||
|
// Publish receiveRequest event
|
||
|
if (this.userEntityService.isLocalUser(followee)) {
|
||
|
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed));
|
||
|
|
||
|
this.userEntityService.pack(followee.id, followee, {
|
||
|
detail: true,
|
||
|
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||
|
|
||
|
// 通知を作成
|
||
|
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||
|
notifierId: follower.id,
|
||
|
followRequestId: followRequest.id,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee));
|
||
|
this.queueService.deliver(follower, content, followee.inbox);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public async cancelFollowRequest(
|
||
|
followee: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']
|
||
|
},
|
||
|
follower: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']
|
||
|
},
|
||
|
): Promise<void> {
|
||
|
if (this.userEntityService.isRemoteUser(followee)) {
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||
|
|
||
|
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
||
|
this.queueService.deliver(follower, content, followee.inbox);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const request = await this.followRequestsRepository.findOneBy({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
if (request == null) {
|
||
|
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||
|
}
|
||
|
|
||
|
await this.followRequestsRepository.delete({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
this.userEntityService.pack(followee.id, followee, {
|
||
|
detail: true,
|
||
|
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||
|
}
|
||
|
|
||
|
public async acceptFollowRequest(
|
||
|
followee: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||
|
},
|
||
|
follower: CacheableUser,
|
||
|
): Promise<void> {
|
||
|
const request = await this.followRequestsRepository.findOneBy({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
if (request == null) {
|
||
|
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
|
||
|
}
|
||
|
|
||
|
await this.#insertFollowingDoc(followee, follower);
|
||
|
|
||
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
|
||
|
this.queueService.deliver(followee, content, follower.inbox);
|
||
|
}
|
||
|
|
||
|
this.userEntityService.pack(followee.id, followee, {
|
||
|
detail: true,
|
||
|
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||
|
}
|
||
|
|
||
|
public async acceptAllFollowRequests(
|
||
|
user: {
|
||
|
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||
|
},
|
||
|
): Promise<void> {
|
||
|
const requests = await this.followRequestsRepository.findBy({
|
||
|
followeeId: user.id,
|
||
|
});
|
||
|
|
||
|
for (const request of requests) {
|
||
|
const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId });
|
||
|
this.acceptFollowRequest(user, follower);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* API following/request/reject
|
||
|
*/
|
||
|
public async rejectFollowRequest(user: Local, follower: Both): Promise<void> {
|
||
|
if (this.userEntityService.isRemoteUser(follower)) {
|
||
|
this.#deliverReject(user, follower);
|
||
|
}
|
||
|
|
||
|
await this.#removeFollowRequest(user, follower);
|
||
|
|
||
|
if (this.userEntityService.isLocalUser(follower)) {
|
||
|
this.#publishUnfollow(user, follower);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* API following/reject
|
||
|
*/
|
||
|
public async rejectFollow(user: Local, follower: Both): Promise<void> {
|
||
|
if (this.userEntityService.isRemoteUser(follower)) {
|
||
|
this.#deliverReject(user, follower);
|
||
|
}
|
||
|
|
||
|
await this.#removeFollow(user, follower);
|
||
|
|
||
|
if (this.userEntityService.isLocalUser(follower)) {
|
||
|
this.#publishUnfollow(user, follower);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* AP Reject/Follow
|
||
|
*/
|
||
|
public async remoteReject(actor: Remote, follower: Local): Promise<void> {
|
||
|
await this.#removeFollowRequest(actor, follower);
|
||
|
await this.#removeFollow(actor, follower);
|
||
|
this.#publishUnfollow(actor, follower);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove follow request record
|
||
|
*/
|
||
|
async #removeFollowRequest(followee: Both, follower: Both): Promise<void> {
|
||
|
const request = await this.followRequestsRepository.findOneBy({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
if (!request) return;
|
||
|
|
||
|
await this.followRequestsRepository.delete(request.id);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove follow record
|
||
|
*/
|
||
|
async #removeFollow(followee: Both, follower: Both): Promise<void> {
|
||
|
const following = await this.followingsRepository.findOneBy({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
if (!following) return;
|
||
|
|
||
|
await this.followingsRepository.delete(following.id);
|
||
|
this.#decrementFollowing(follower, followee);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deliver Reject to remote
|
||
|
*/
|
||
|
async #deliverReject(followee: Local, follower: Remote): Promise<void> {
|
||
|
const request = await this.followRequestsRepository.findOneBy({
|
||
|
followeeId: followee.id,
|
||
|
followerId: follower.id,
|
||
|
});
|
||
|
|
||
|
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee));
|
||
|
this.queueService.deliver(followee, content, follower.inbox);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Publish unfollow to local
|
||
|
*/
|
||
|
async #publishUnfollow(followee: Both, follower: Local): Promise<void> {
|
||
|
const packedFollowee = await this.userEntityService.pack(followee.id, follower, {
|
||
|
detail: true,
|
||
|
});
|
||
|
|
||
|
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
||
|
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
||
|
|
||
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||
|
for (const webhook of webhooks) {
|
||
|
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||
|
user: packedFollowee,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|