misskey/packages/backend/src/core/QueryService.ts

255 lines
9.1 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
2022-09-17 20:27:08 +02:00
import { Inject, Injectable } from '@nestjs/common';
2023-02-09 03:02:37 +01:00
import { Brackets, ObjectLiteral } from 'typeorm';
2022-09-17 20:27:08 +02:00
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
2023-02-09 03:02:37 +01:00
import type { SelectQueryBuilder } from 'typeorm';
2022-09-17 20:27:08 +02:00
@Injectable()
export class QueryService {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private idService: IdService,
2022-09-17 20:27:08 +02:00
) {
}
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
2022-09-17 20:27:08 +02:00
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.orderBy(`${q.alias}.id`, 'ASC');
} else if (untilId) {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceDate && untilDate) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.id`, 'DESC');
2022-09-17 20:27:08 +02:00
} else if (sinceDate) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.orderBy(`${q.alias}.id`, 'ASC');
2022-09-17 20:27:08 +02:00
} else if (untilDate) {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.id`, 'DESC');
2022-09-17 20:27:08 +02:00
} else {
q.orderBy(`${q.alias}.id`, 'DESC');
}
return q;
}
2022-09-17 20:27:08 +02:00
// ここでいうBlockedは被Blockedの意
@bindThis
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
2022-09-17 20:27:08 +02:00
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
.select('blocking.blockerId')
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
// 投稿の作者にブロックされていない かつ
// 投稿の返信先の作者にブロックされていない かつ
// 投稿の引用元の作者にブロックされていない
q
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
2022-09-17 20:27:08 +02:00
}))
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
2022-09-17 20:27:08 +02:00
}));
q.setParameters(blockingQuery.getParameters());
}
@bindThis
public generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
2022-09-17 20:27:08 +02:00
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
.select('blocking.blockeeId')
.where('blocking.blockerId = :blockerId', { blockerId: me.id });
const blockedQuery = this.blockingsRepository.createQueryBuilder('blocking')
.select('blocking.blockerId')
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`);
q.setParameters(blockingQuery.getParameters());
q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`);
q.setParameters(blockedQuery.getParameters());
}
@bindThis
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
2022-09-17 20:27:08 +02:00
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
.select('threadMuted.threadId')
.where('threadMuted.userId = :userId', { userId: me.id });
2022-09-17 20:27:08 +02:00
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
2023-10-09 06:32:41 +02:00
q.andWhere(new Brackets(qb => {
qb
.where('note.threadId IS NULL')
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
2022-09-17 20:27:08 +02:00
}));
2022-09-17 20:27:08 +02:00
q.setParameters(mutedQuery.getParameters());
}
@bindThis
2023-10-09 10:41:54 +02:00
public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
2022-09-17 20:27:08 +02:00
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
2022-09-17 20:27:08 +02:00
if (exclude) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
}
2022-09-17 20:27:08 +02:00
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
2022-09-17 20:27:08 +02:00
// 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない
q
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
2022-09-17 20:27:08 +02:00
}))
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
2022-09-17 20:27:08 +02:00
}))
// mute instances
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
2022-09-17 20:27:08 +02:00
}))
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
2022-09-17 20:27:08 +02:00
}))
2023-10-09 06:32:41 +02:00
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
2022-09-17 20:27:08 +02:00
}));
2022-09-17 20:27:08 +02:00
q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters());
}
@bindThis
public generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
2022-09-17 20:27:08 +02:00
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
2022-09-17 20:27:08 +02:00
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
2022-09-17 20:27:08 +02:00
q.setParameters(mutingQuery.getParameters());
}
@bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
2022-09-17 20:27:08 +02:00
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
if (me == null) {
2023-10-09 06:32:41 +02:00
q.andWhere(new Brackets(qb => {
qb
.where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\'');
2022-09-17 20:27:08 +02:00
}));
} else {
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :meId');
2023-10-09 06:32:41 +02:00
q.andWhere(new Brackets(qb => {
qb
2022-09-17 20:27:08 +02:00
// 公開投稿である
2023-10-09 06:32:41 +02:00
.where(new Brackets(qb => {
qb
.where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\'');
}))
2022-09-17 20:27:08 +02:00
// または 自分自身
2023-10-09 06:32:41 +02:00
.orWhere('note.userId = :meId')
2022-09-17 20:27:08 +02:00
// または 自分宛て
.orWhere(':meIdAsList <@ note.visibleUserIds')
.orWhere(':meIdAsList <@ note.mentions')
2023-10-09 06:32:41 +02:00
.orWhere(new Brackets(qb => {
qb
// または フォロワー宛ての投稿であり、
.where('note.visibility = \'followers\'')
.andWhere(new Brackets(qb => {
qb
// 自分がフォロワーである
.where(`note.userId IN (${ followingQuery.getQuery() })`)
// または 自分の投稿へのリプライ
.orWhere('note.replyUserId = :meId');
}));
2022-09-17 20:27:08 +02:00
}));
}));
q.setParameters({ meId: me.id, meIdAsList: [me.id] });
2022-09-17 20:27:08 +02:00
}
}
@bindThis
public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
.select('renote_muting.muteeId')
.where('renote_muting.muterId = :muterId', { muterId: me.id });
q.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb => {
qb.where('note.renoteId IS NOT NULL');
qb.andWhere('note.text IS NULL');
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
}))
.orWhere('note.renoteId IS NULL')
.orWhere('note.text IS NOT NULL');
}));
q.setParameters(mutingQuery.getParameters());
}
}