monkeeShark/packages/backend/src/core/UserBlockingService.ts

285 lines
10 KiB
TypeScript
Raw Normal View History

2022-09-17 18:27:08 +00:00
2023-02-04 03:40:40 +00:00
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import Redis from 'ioredis';
2022-09-17 18:27:08 +00:00
import { IdService } from '@/core/IdService.js';
2023-02-13 06:28:07 +00:00
import type { User } from '@/models/entities/User.js';
2022-09-17 18:27:08 +00:00
import type { Blocking } from '@/models/entities/Blocking.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.js';
2022-09-22 21:21:31 +00:00
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
2022-09-23 22:12:11 +00:00
import Logger from '@/logger.js';
2022-12-04 01:16:03 +00:00
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
2023-03-24 07:43:42 +00:00
import { KVCache } from '@/misc/cache.js';
2023-02-04 03:40:40 +00:00
import { StreamMessages } from '@/server/api/stream/types.js';
2022-09-17 18:27:08 +00:00
@Injectable()
2023-02-04 03:40:40 +00:00
export class UserBlockingService implements OnApplicationShutdown {
2022-09-23 22:12:11 +00:00
private logger: Logger;
2023-02-04 03:40:40 +00:00
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
2023-03-24 07:43:42 +00:00
private blockingsByUserIdCache: KVCache<User['id'][]>;
2023-02-04 03:40:40 +00:00
2022-09-17 18:27:08 +00:00
constructor(
2023-02-04 03:40:40 +00:00
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
2022-09-17 18:27:08 +00:00
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private queueService: QueueService,
2023-02-04 01:02:03 +00:00
private globalEventService: GlobalEventService,
2022-09-17 18:27:08 +00:00
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart,
2022-09-23 22:12:11 +00:00
private loggerService: LoggerService,
2022-09-17 18:27:08 +00:00
) {
2022-09-23 22:12:11 +00:00
this.logger = this.loggerService.getLogger('user-block');
2023-02-04 03:40:40 +00:00
2023-03-24 07:43:42 +00:00
this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity);
2023-02-04 03:40:40 +00:00
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'blockingCreated': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
}
break;
}
case 'blockingDeleted': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
}
break;
}
default:
break;
}
}
2022-09-17 18:27:08 +00:00
}
@bindThis
2022-09-17 18:27:08 +00:00
public async block(blocker: User, blockee: User) {
await Promise.all([
2022-09-18 18:11:50 +00:00
this.cancelRequest(blocker, blockee),
this.cancelRequest(blockee, blocker),
this.unFollow(blocker, blockee),
this.unFollow(blockee, blocker),
this.removeFromList(blockee, blocker),
2022-09-17 18:27:08 +00:00
]);
const blocking = {
id: this.idService.genId(),
createdAt: new Date(),
blocker,
blockerId: blocker.id,
blockee,
blockeeId: blockee.id,
} as Blocking;
await this.blockingsRepository.insert(blocking);
2023-02-04 03:40:40 +00:00
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
2022-09-17 18:27:08 +00:00
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
2023-02-12 09:47:30 +00:00
const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox, false);
2022-09-17 18:27:08 +00:00
}
}
@bindThis
2022-09-18 18:11:50 +00:00
private async cancelRequest(follower: User, followee: User) {
2022-09-17 18:27:08 +00:00
const request = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
});
if (request == null) {
return;
}
await this.followRequestsRepository.delete({
followeeId: followee.id,
followerId: follower.id,
});
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, {
detail: true,
2023-02-04 01:02:03 +00:00
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
2022-09-17 18:27:08 +00:00
}
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
2022-09-17 18:27:08 +00:00
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,
});
}
});
}
// リモートにフォローリクエストをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
2023-02-12 09:47:30 +00:00
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
2022-09-17 18:27:08 +00:00
}
// リモートからフォローリクエストを受けていたらReject送信
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
2023-02-12 09:47:30 +00:00
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
2022-09-17 18:27:08 +00:00
}
}
@bindThis
2022-09-18 18:11:50 +00:00
private async unFollow(follower: User, followee: User) {
2022-09-17 18:27:08 +00:00
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
});
if (following == null) {
return;
}
await Promise.all([
this.followingsRepository.delete(following.id),
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
this.perUserFollowingChart.update(follower, followee, false),
]);
// Publish unfollow event
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
2022-09-17 18:27:08 +00:00
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,
});
}
});
}
// リモートにフォローをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
2023-02-12 09:47:30 +00:00
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
2022-09-17 18:27:08 +00:00
}
// リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
2023-02-12 09:47:30 +00:00
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
2022-09-17 18:27:08 +00:00
}
@bindThis
2022-09-18 18:11:50 +00:00
private async removeFromList(listOwner: User, user: User) {
2022-09-17 18:27:08 +00:00
const userLists = await this.userListsRepository.findBy({
userId: listOwner.id,
});
for (const userList of userLists) {
await this.userListJoiningsRepository.delete({
userListId: userList.id,
userId: user.id,
});
}
}
@bindThis
2023-02-13 06:28:07 +00:00
public async unblock(blocker: User, blockee: User) {
2022-09-17 18:27:08 +00:00
const blocking = await this.blockingsRepository.findOneBy({
blockerId: blocker.id,
blockeeId: blockee.id,
});
2022-09-17 18:27:08 +00:00
if (blocking == null) {
2022-09-23 22:12:11 +00:00
this.logger.warn('ブロック解除がリクエストされましたがブロックしていませんでした');
2022-09-17 18:27:08 +00:00
return;
}
2022-09-17 18:27:08 +00:00
// Since we already have the blocker and blockee, we do not need to fetch
// them in the query above and can just manually insert them here.
blocking.blocker = blocker;
blocking.blockee = blockee;
2022-09-17 18:27:08 +00:00
await this.blockingsRepository.delete(blocking.id);
2023-02-04 03:40:40 +00:00
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
2022-09-17 18:27:08 +00:00
// deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
2023-02-12 09:47:30 +00:00
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox, false);
2022-09-17 18:27:08 +00:00
}
}
2023-02-04 03:40:40 +00:00
@bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
where: {
blockerId,
},
select: ['blockeeId'],
}).then(records => records.map(record => record.blockeeId)));
return blockedUserIds.includes(blockeeId);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
2022-09-17 18:27:08 +00:00
}