mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 20:44:34 +00:00
merge: Rework queries and add indexes to improve timeline performance (!1091)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1091 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
dae544b353
33 changed files with 743 additions and 548 deletions
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ReplaceNoteUserHostIndex1748990452958 {
|
||||
name = 'ReplaceNoteUserHostIndex1748990452958'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7125a826ab192eb27e11d358a5"`);
|
||||
await queryRunner.query(`
|
||||
create index "IDX_note_userHost_id"
|
||||
on "note" ("userHost", "id" desc)
|
||||
nulls not distinct`);
|
||||
await queryRunner.query(`comment on index "IDX_note_userHost_id" is 'User host with ID included'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`drop index if exists "IDX_note_userHost_id"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7125a826ab192eb27e11d358a5" ON "note" ("userHost") `);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FixIDXInstanceHostKey1748990662839 {
|
||||
async up(queryRunner) {
|
||||
// must include host for index-only scans: https://www.postgresql.org/docs/current/indexes-index-only-scans.html
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_instance_host_key"`);
|
||||
await queryRunner.query(`
|
||||
create index "IDX_instance_host_key"
|
||||
on "instance" ((lower(reverse("host"::text)) || '.'::text) text_pattern_ops)
|
||||
include ("host")
|
||||
`);
|
||||
await queryRunner.query(`comment on index "IDX_instance_host_key" is 'Expression index for finding instances by base domain'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_instance_host_key"`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_instance_host_key" ON "instance" (((lower(reverse("host")) || '.')::text) text_pattern_ops)`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class CreateIDXNoteForTimelines1748991828473 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
create index "IDX_note_for_timelines"
|
||||
on "note" ("id" desc, "channelId", "visibility", "userHost")
|
||||
include ("userId", "userHost", "replyId", "replyUserId", "replyUserHost", "renoteId", "renoteUserId", "renoteUserHost")
|
||||
NULLS NOT DISTINCT`);
|
||||
await queryRunner.query(`comment on index "IDX_note_for_timelines" is 'Covering index for timeline queries'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "IDX_note_for_timelines"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class CreateIDXInstanceHostFilters1748992017688 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
create index "IDX_instance_host_filters"
|
||||
on "instance" ("host", "isBlocked", "isSilenced", "isMediaSilenced", "isAllowListed", "isBubbled", "suspensionState")`);
|
||||
await queryRunner.query(`comment on index "IDX_instance_host_filters" is 'Covering index for host filter queries'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "IDX_instance_host_filters"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class CreateStatistics1748992128683 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_instance_isBlocked_isBubbled" (mcv) ON "isBlocked", "isBubbled" FROM "instance"`);
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_instance_isBlocked_isSilenced" (mcv) ON "isBlocked", "isSilenced" FROM "instance"`);
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_note_replyId_replyUserId_replyUserHost" (dependencies) ON "replyId", "replyUserId", "replyUserHost" FROM "note"`)
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_note_renoteId_renoteUserId_renoteUserHost" (dependencies) ON "renoteId", "renoteUserId", "renoteUserHost" FROM "note"`);
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_note_userId_userHost" (mcv) ON "userId", "userHost" FROM "note"`);
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_note_replyUserId_replyUserHost" (mcv) ON "replyUserId", "replyUserHost" FROM "note"`);
|
||||
await queryRunner.query(`CREATE STATISTICS "STTS_note_renoteUserId_renoteUserHost" (mcv) ON "renoteUserId", "renoteUserHost" FROM "note"`);
|
||||
await queryRunner.query(`ANALYZE "note", "instance"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_instance_isBlocked_isBubbled"`);
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_instance_isBlocked_isSilenced"`);
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_note_replyId_replyUserId_replyUserHost"`);
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_note_renoteId_renoteUserId_renoteUserHost"`);
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_note_userId_userHost"`);
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_note_replyUserId_replyUserHost"`);
|
||||
await queryRunner.query(`DROP STATISTICS "STTS_note_renoteUserId_renoteUserHost"`);
|
||||
await queryRunner.query(`ANALYZE "note", "instance"`);
|
||||
}
|
||||
}
|
|
@ -4,13 +4,14 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||
import { Brackets, Not, WhereExpressionBuilder } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository, MiMeta } from '@/models/_.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository, MiMeta, InstancesRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
import type { SelectQueryBuilder, ObjectLiteral } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class QueryService {
|
||||
|
@ -36,6 +37,9 @@ export class QueryService {
|
|||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private readonly instancesRepository: InstancesRepository,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
|
@ -72,215 +76,349 @@ export class QueryService {
|
|||
|
||||
// ここでいうBlockedは被Blockedの意
|
||||
@bindThis
|
||||
public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||
|
||||
public generateBlockedUserQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
// 投稿の作者にブロックされていない かつ
|
||||
// 投稿の返信先の作者にブロックされていない かつ
|
||||
// 投稿の引用元の作者にブロックされていない
|
||||
q
|
||||
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyUserId IS NULL')
|
||||
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
|
||||
}))
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.renoteUserId IS NULL')
|
||||
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(blockingQuery.getParameters());
|
||||
return this
|
||||
.andNotBlockingUser(q, 'note.userId', ':meId')
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotBlockingUser(qb, 'note.replyUserId', ':meId')
|
||||
.orWhere('note.replyUserId IS NULL')))
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotBlockingUser(qb, 'note.renoteUserId', ':meId')
|
||||
.orWhere('note.renoteUserId IS NULL')))
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
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());
|
||||
public generateBlockQueryForUsers<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
this.andNotBlockingUser(q, ':meId', 'user.id');
|
||||
this.andNotBlockingUser(q, 'user.id', ':me.id');
|
||||
return q.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||
.select('threadMuted.threadId')
|
||||
.where('threadMuted.userId = :userId', { userId: me.id });
|
||||
|
||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.threadId IS NULL')
|
||||
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(mutedQuery.getParameters());
|
||||
public generateMutedNoteThreadQuery<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
return this
|
||||
.andNotMutingThread(q, ':meId', 'note.id')
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingThread(qb, ':meId', 'note.threadId')
|
||||
.orWhere('note.threadId IS NULL')))
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
|
||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
if (exclude) {
|
||||
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
||||
}
|
||||
|
||||
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||
.select('user_profile.mutedInstances')
|
||||
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||
|
||||
public generateMutedUserQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
// 投稿の作者をミュートしていない かつ
|
||||
// 投稿の返信先の作者をミュートしていない かつ
|
||||
// 投稿の引用元の作者をミュートしていない
|
||||
q
|
||||
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyUserId IS NULL')
|
||||
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}))
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.renoteUserId IS NULL')
|
||||
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}))
|
||||
return this
|
||||
.andNotMutingUser(q, ':meId', 'note.userId', exclude)
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingUser(qb, ':meId', 'note.replyUserId', exclude)
|
||||
.orWhere('note.replyUserId IS NULL')))
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingUser(qb, ':meId', 'note.renoteUserId', exclude)
|
||||
.orWhere('note.renoteUserId IS NULL')))
|
||||
// TODO exclude should also pass a host to skip these instances
|
||||
// mute instances
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.andWhere('note.userHost IS NULL')
|
||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
|
||||
}))
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyUserHost IS NULL')
|
||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
|
||||
}))
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.renoteUserHost IS NULL')
|
||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
||||
}));
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
q.setParameters(mutingInstanceQuery.getParameters());
|
||||
.andWhere(new Brackets(qb => this
|
||||
.andNotMutingInstance(qb, ':meId', 'note.userHost')
|
||||
.orWhere('note.userHost IS NULL')))
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingInstance(qb, ':meId', 'note.replyUserHost')
|
||||
.orWhere('note.replyUserHost IS NULL')))
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingInstance(qb, ':meId', 'note.renoteUserHost')
|
||||
.orWhere('note.renoteUserHost IS NULL')))
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
public generateMutedUserQueryForUsers<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
return this
|
||||
.andNotMutingUser(q, ':meId', 'user.id')
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
// This intentionally skips isSuspended, isDeleted, makeNotesFollowersOnlyBefore, makeNotesHiddenBefore, and requireSigninToViewContents.
|
||||
// NoteEntityService checks these automatically and calls hideNote() to hide them without breaking threads.
|
||||
// For moderation purposes, you can set isSilenced to forcibly hide existing posts by a user.
|
||||
@bindThis
|
||||
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
||||
public generateVisibilityQuery<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me?: { id: MiUser['id'] } | null): SelectQueryBuilder<E> {
|
||||
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.visibility = \'public\'')
|
||||
return q.andWhere(new Brackets(qb => {
|
||||
// Public post
|
||||
qb.orWhere('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}));
|
||||
} else {
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :meId');
|
||||
|
||||
q.andWhere(new Brackets(qb => {
|
||||
if (me != null) {
|
||||
qb
|
||||
// 公開投稿である
|
||||
.where(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
// または 自分自身
|
||||
.orWhere('note.userId = :meId')
|
||||
// または 自分宛て
|
||||
.orWhere(':meIdAsList <@ note.visibleUserIds')
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb
|
||||
// または フォロワー宛ての投稿であり、
|
||||
.where('note.visibility = \'followers\'')
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
// 自分がフォロワーである
|
||||
.where(`note.userId IN (${ followingQuery.getQuery() })`)
|
||||
// または 自分の投稿へのリプライ
|
||||
.orWhere('note.replyUserId = :meId')
|
||||
.orWhere(':meIdAsList <@ note.mentions');
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
// My post
|
||||
.orWhere(':meId = note.userId')
|
||||
// Reply to me
|
||||
.orWhere(':meId = note.replyUserId')
|
||||
// DM to me
|
||||
.orWhere(':meId = ANY (note.visibleUserIds)')
|
||||
// Mentions me
|
||||
.orWhere(':meId = ANY (note.mentions)')
|
||||
// Followers-only post
|
||||
.orWhere(new Brackets(qb => this
|
||||
.andFollowingUser(qb, ':meId', 'note.userId')
|
||||
.andWhere('note.visibility = \'followers\'')));
|
||||
|
||||
q.setParameters({ meId: me.id, meIdAsList: [me.id] });
|
||||
q.setParameters({ meId: me.id });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@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
|
||||
public generateMutedUserRenotesQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me: { id: MiUser['id'] }): SelectQueryBuilder<E> {
|
||||
return q
|
||||
.andWhere(new Brackets(qb => this
|
||||
.orNotMutingRenote(qb, ':meId', 'note.userId')
|
||||
.orWhere('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL')
|
||||
.orWhere('note.cw IS NOT NULL')
|
||||
.orWhere('note.replyId IS NOT NULL')
|
||||
.orWhere('note.hasPoll = false')
|
||||
.orWhere('note.fileIds != \'{}\'')
|
||||
.orWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
.orWhere('note.hasPoll = true')
|
||||
.orWhere('note.fileIds != \'{}\'')))
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateBlockedHostQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean, allowSilenced = true): void {
|
||||
function checkFor(key: 'user' | 'replyUser' | 'renoteUser') {
|
||||
q.leftJoin(`note.${key}Instance`, `${key}Instance`);
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb.orWhere(`note.${key}Id IS NULL`) // no corresponding user
|
||||
.orWhere(`note.${key}Host IS NULL`); // local
|
||||
|
||||
if (allowSilenced) {
|
||||
qb.orWhere(`${key}Instance.isBlocked = false`); // not blocked
|
||||
} else {
|
||||
qb.orWhere(new Brackets(qbb => qbb
|
||||
.andWhere(`${key}Instance.isBlocked = false`) // not blocked
|
||||
.andWhere(`${key}Instance.isSilenced = false`))); // not silenced
|
||||
public generateExcludedRenotesQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>): SelectQueryBuilder<E> {
|
||||
return q
|
||||
.andWhere(new Brackets(qb => qb
|
||||
.orWhere('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL')
|
||||
.orWhere('note.cw IS NOT NULL')
|
||||
.orWhere('note.replyId IS NOT NULL')
|
||||
.orWhere('note.hasPoll = true')
|
||||
.orWhere('note.fileIds != \'{}\'')));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateBlockedHostQueryForNote<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, excludeAuthor?: boolean): SelectQueryBuilder<E> {
|
||||
const checkFor = (key: 'user' | 'replyUser' | 'renoteUser') => this
|
||||
.leftJoinInstance(q, `note.${key}Instance`, `${key}Instance`)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.orWhere(`"${key}Instance" IS NULL`) // local
|
||||
.orWhere(`"${key}Instance"."isBlocked" = false`); // not blocked
|
||||
|
||||
if (excludeAuthor) {
|
||||
qb.orWhere(`note.userId = note.${key}Id`); // author
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (!excludeAuthor) {
|
||||
checkFor('user');
|
||||
}
|
||||
checkFor('replyUser');
|
||||
checkFor('renoteUser');
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateSilencedUserQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, me?: { id: MiUser['id'] } | null): SelectQueryBuilder<E> {
|
||||
if (!me) {
|
||||
return q.andWhere('user.isSilenced = false');
|
||||
}
|
||||
|
||||
return this
|
||||
.leftJoinInstance(q, 'note.userInstance', 'userInstance')
|
||||
.andWhere(new Brackets(qb => this
|
||||
// case 1: we are following the user
|
||||
.orFollowingUser(qb, ':meId', 'note.userId')
|
||||
// case 2: user not silenced AND instance not silenced
|
||||
.orWhere(new Brackets(qbb => qbb
|
||||
.andWhere(new Brackets(qbbb => qbbb
|
||||
.orWhere('"userInstance"."isSilenced" = false')
|
||||
.orWhere('"userInstance" IS NULL')))
|
||||
.andWhere('user.isSilenced = false')))))
|
||||
.setParameters({ meId: me.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Left-joins an instance in to the query with a given alias and optional condition.
|
||||
* These calls are de-duplicated - multiple uses of the same alias are skipped.
|
||||
*/
|
||||
@bindThis
|
||||
public leftJoinInstance<E extends ObjectLiteral>(q: SelectQueryBuilder<E>, relation: string | typeof MiInstance, alias: string, condition?: string): SelectQueryBuilder<E> {
|
||||
// Skip if it's already joined, otherwise we'll get an error
|
||||
if (!q.expressionMap.joinAttributes.some(j => j.alias.name === alias)) {
|
||||
q.leftJoin(relation, alias, condition);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds OR condition that followerProp (user ID) is following followeeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public orFollowingUser<Q extends WhereExpressionBuilder>(q: Q, followerProp: string, followeeProp: string): Q {
|
||||
return this.addFollowingUser(q, followerProp, followeeProp, 'orWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AND condition that followerProp (user ID) is following followeeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public andFollowingUser<Q extends WhereExpressionBuilder>(q: Q, followerProp: string, followeeProp: string): Q {
|
||||
return this.addFollowingUser(q, followerProp, followeeProp, 'andWhere');
|
||||
}
|
||||
|
||||
private addFollowingUser<Q extends WhereExpressionBuilder>(q: Q, followerProp: string, followeeProp: string, join: 'andWhere' | 'orWhere'): Q {
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('1')
|
||||
.andWhere(`following.followerId = ${followerProp}`)
|
||||
.andWhere(`following.followeeId = ${followeeProp}`);
|
||||
|
||||
return q[join](`EXISTS (${followingQuery.getQuery()})`, followingQuery.getParameters());
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds OR condition that blockerProp (user ID) is not blocking blockeeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public orNotBlockingUser<Q extends WhereExpressionBuilder>(q: Q, blockerProp: string, blockeeProp: string): Q {
|
||||
return this.excludeBlockingUser(q, blockerProp, blockeeProp, 'orWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AND condition that blockerProp (user ID) is not blocking blockeeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public andNotBlockingUser<Q extends WhereExpressionBuilder>(q: Q, blockerProp: string, blockeeProp: string): Q {
|
||||
return this.excludeBlockingUser(q, blockerProp, blockeeProp, 'andWhere');
|
||||
}
|
||||
|
||||
private excludeBlockingUser<Q extends WhereExpressionBuilder>(q: Q, blockerProp: string, blockeeProp: string, join: 'andWhere' | 'orWhere'): Q {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('1')
|
||||
.andWhere(`blocking.blockerId = ${blockerProp}`)
|
||||
.andWhere(`blocking.blockeeId = ${blockeeProp}`);
|
||||
|
||||
return q[join](`NOT EXISTS (${blockingQuery.getQuery()})`, blockingQuery.getParameters());
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds OR condition that muterProp (user ID) is not muting muteeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public orNotMutingUser<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string, exclude?: { id: MiUser['id'] }): Q {
|
||||
return this.excludeMutingUser(q, muterProp, muteeProp, 'orWhere', exclude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AND condition that muterProp (user ID) is not muting muteeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public andNotMutingUser<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string, exclude?: { id: MiUser['id'] }): Q {
|
||||
return this.excludeMutingUser(q, muterProp, muteeProp, 'andWhere', exclude);
|
||||
}
|
||||
|
||||
private excludeMutingUser<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string, join: 'andWhere' | 'orWhere', exclude?: { id: MiUser['id'] }): Q {
|
||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('1')
|
||||
.andWhere(`muting.muterId = ${muterProp}`)
|
||||
.andWhere(`muting.muteeId = ${muteeProp}`);
|
||||
|
||||
if (exclude) {
|
||||
mutingQuery.andWhere({ muteeId: Not(exclude.id) });
|
||||
}
|
||||
|
||||
return q[join](`NOT EXISTS (${mutingQuery.getQuery()})`, mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds OR condition that muterProp (user ID) is not muting renotes by muteeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public orNotMutingRenote<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string): Q {
|
||||
return this.excludeMutingRenote(q, muterProp, muteeProp, 'orWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AND condition that muterProp (user ID) is not muting renotes by muteeProp (user ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public andNotMutingRenote<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string): Q {
|
||||
return this.excludeMutingRenote(q, muterProp, muteeProp, 'andWhere');
|
||||
}
|
||||
|
||||
private excludeMutingRenote<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string, join: 'andWhere' | 'orWhere'): Q {
|
||||
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
|
||||
.select('1')
|
||||
.andWhere(`renote_muting.muterId = ${muterProp}`)
|
||||
.andWhere(`renote_muting.muteeId = ${muteeProp}`);
|
||||
|
||||
return q[join](`NOT EXISTS (${mutingQuery.getQuery()})`, mutingQuery.getParameters());
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds OR condition that muterProp (user ID) is not muting muteeProp (instance host).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public orNotMutingInstance<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string): Q {
|
||||
return this.excludeMutingInstance(q, muterProp, muteeProp, 'orWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AND condition that muterProp (user ID) is not muting muteeProp (instance host).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public andNotMutingInstance<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string): Q {
|
||||
return this.excludeMutingInstance(q, muterProp, muteeProp, 'andWhere');
|
||||
}
|
||||
|
||||
private excludeMutingInstance<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string, join: 'andWhere' | 'orWhere'): Q {
|
||||
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||
.select('1')
|
||||
.andWhere(`user_profile.userId = ${muterProp}`)
|
||||
.andWhere(`"user_profile"."mutedInstances"::jsonb ? ${muteeProp}`);
|
||||
|
||||
return q[join](`NOT EXISTS (${mutingInstanceQuery.getQuery()})`, mutingInstanceQuery.getParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds OR condition that muterProp (user ID) is not muting muteeProp (note ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public orNotMutingThread<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string): Q {
|
||||
return this.excludeMutingThread(q, muterProp, muteeProp, 'orWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds AND condition that muterProp (user ID) is not muting muteeProp (note ID).
|
||||
* Both props should be expressions, not raw values.
|
||||
*/
|
||||
@bindThis
|
||||
public andNotMutingThread<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string): Q {
|
||||
return this.excludeMutingThread(q, muterProp, muteeProp, 'andWhere');
|
||||
}
|
||||
|
||||
private excludeMutingThread<Q extends WhereExpressionBuilder>(q: Q, muterProp: string, muteeProp: string, join: 'andWhere' | 'orWhere'): Q {
|
||||
const threadMutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||
.select('1')
|
||||
.andWhere(`threadMuted.userId = ${muterProp}`)
|
||||
.andWhere(`threadMuted.threadId = ${muteeProp}`);
|
||||
|
||||
return q[join](`NOT EXISTS (${threadMutedQuery.getQuery()})`, threadMutedQuery.getParameters());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -617,6 +617,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
iconUrl: instance.iconUrl,
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
isSilenced: instance.isSilenced,
|
||||
} : undefined) : undefined,
|
||||
followersCount: followersCount ?? 0,
|
||||
followingCount: followingCount ?? 0,
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
|
||||
@Index('IDX_instance_host_key', { synchronize: false })
|
||||
@Index('IDX_instance_host_key', { synchronize: false }) // ((lower(reverse("host"::text)) || '.'::text)
|
||||
@Index('IDX_instance_host_filters', { synchronize: false }) // ("host", "isBlocked", "isSilenced", "isMediaSilenced", "isAllowListed", "isBubbled", "suspensionState")
|
||||
@Entity('instance')
|
||||
export class MiInstance {
|
||||
@PrimaryColumn(id())
|
||||
|
|
|
@ -12,6 +12,8 @@ import { MiChannel } from './Channel.js';
|
|||
import type { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
@Index('IDX_724b311e6f883751f261ebe378', ['userId', 'id'])
|
||||
@Index('IDX_note_userHost_id', { synchronize: false }) // (userHost, id desc)
|
||||
@Index('IDX_note_for_timelines', { synchronize: false }) // (id desc, channelId, visibility, userHost)
|
||||
@Entity('note')
|
||||
export class MiNote {
|
||||
@PrimaryColumn(id())
|
||||
|
@ -216,7 +218,6 @@ export class MiNote {
|
|||
public processErrors: string[] | null;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: '[Denormalized]',
|
||||
|
|
|
@ -200,6 +200,10 @@ export const packedUserLiteSchema = {
|
|||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
isSilenced: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
emojis: {
|
||||
|
|
|
@ -145,7 +145,10 @@ class MyCustomLogger implements Logger {
|
|||
@bindThis
|
||||
private transformParameters(parameters?: any[]) {
|
||||
if (this.props.enableQueryParamLogging && parameters && parameters.length > 0) {
|
||||
return parameters.map(stringifyParameter);
|
||||
return parameters.reduce((params, p, i) => {
|
||||
params[`$${i + 1}`] = stringifyParameter(p);
|
||||
return params;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -158,7 +161,8 @@ class MyCustomLogger implements Logger {
|
|||
const prefix = (this.props.printReplicationMode && queryRunner)
|
||||
? `[${queryRunner.getReplicationMode()}] `
|
||||
: undefined;
|
||||
sqlLogger.info(this.transformQueryLog(query, { prefix }), this.transformParameters(parameters));
|
||||
const transformed = this.transformQueryLog(query, { prefix });
|
||||
sqlLogger.debug(`Query run: ${transformed}`, this.transformParameters(parameters));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -75,6 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private queryService: QueryService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private readonly activeUsersChart: ActiveUsersChart,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
@ -106,13 +108,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return [];
|
||||
}
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId)
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
|
||||
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
|
||||
|
@ -124,11 +127,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
const notes = await query.getMany();
|
||||
if (sinceId != null && untilId == null) {
|
||||
notes.sort((a, b) => a.id < b.id ? -1 : 1);
|
||||
} else {
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
|
|
|
@ -96,7 +96,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
if (me) this.activeUsersChart.read(me);
|
||||
if (me) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.serverSettings.enableFanoutTimeline) {
|
||||
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
|
||||
|
@ -133,8 +137,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
@ -142,22 +146,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (!ps.withRenotes) {
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
return await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: Marie and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import type { NotesRepository, MiMeta } from '@/models/_.js';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -56,9 +59,6 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private serverSettings: MiMeta,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
|
@ -66,7 +66,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
|
@ -74,27 +73,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.btlDisabled);
|
||||
}
|
||||
|
||||
const followings = me ? await this.cacheService.userFollowingsCache.fetch(me.id) : undefined;
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.channelId IS NULL')
|
||||
.andWhere('note.userHost IS NOT NULL')
|
||||
.andWhere('userInstance.isBubbled = true') // This comes from generateBlockedHostQueryForNote below
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
// This subquery mess teaches postgres how to use the right indexes.
|
||||
// Using WHERE or ON conditions causes a fallback to full sequence scan, which times out.
|
||||
// Important: don't use a query builder here or TypeORM will get confused and stop quoting column names! (known, unfixed bug apparently)
|
||||
query
|
||||
.leftJoin('(select "host" from "instance" where "isBubbled" = true)', 'bubbleInstance', '"bubbleInstance"."host" = "note"."userHost"')
|
||||
.andWhere('"bubbleInstance" IS NOT NULL');
|
||||
this.queryService
|
||||
.leftJoinInstance(query, 'note.userInstance', 'userInstance', '"userInstance"."host" = "bubbleInstance"."host"');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
if (!me) query.andWhere('user.requireSigninToViewContents = false');
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
|
@ -103,34 +108,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
if (!ps.withRenotes) {
|
||||
query.andWhere(new Brackets(qb => qb
|
||||
.orWhere('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL')
|
||||
.orWhere('note.cw IS NOT NULL')
|
||||
.orWhere('note.replyId IS NOT NULL')
|
||||
.orWhere('note.hasPoll = false')
|
||||
.orWhere('note.fileIds != \'{}\'')));
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
let timeline = await query.limit(ps.limit).getMany();
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.user?.isSilenced) {
|
||||
if (!me) return false;
|
||||
if (!followings) return false;
|
||||
if (note.userId !== me.id) {
|
||||
return followings[note.userId];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -76,8 +77,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private readonly noteEntityService: NoteEntityService,
|
||||
private readonly queryService: QueryService,
|
||||
private readonly activeUsersChart: ActiveUsersChart,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.includeReplies && ps.filesOnly) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
|
||||
|
@ -128,8 +130,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId')
|
||||
|
||||
// Exclude channel notes
|
||||
.andWhere({ channelId: IsNull() })
|
||||
|
@ -157,11 +159,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
// Support pagination
|
||||
this.queryService
|
||||
.makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.orderBy('note.id', 'DESC')
|
||||
.take(ps.limit);
|
||||
|
||||
// Query and return the next page
|
||||
const notes = await query.getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me, { skipHide: true });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -68,7 +67,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
|
@ -76,8 +74,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.gtlDisabled);
|
||||
}
|
||||
|
||||
const followings = me ? await this.cacheService.userFollowingsCache.fetch(me.id) : {};
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
|
@ -86,15 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
@ -103,29 +98,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.where('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
if (!ps.withRenotes) {
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
let timeline = await query.limit(ps.limit).getMany();
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
});
|
||||
|
|
|
@ -66,9 +66,6 @@ export const paramDef = {
|
|||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
|
@ -114,12 +111,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
|
||||
process.nextTick(() => {
|
||||
|
@ -178,12 +173,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId,
|
||||
sinceId,
|
||||
limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me),
|
||||
});
|
||||
|
||||
|
@ -199,12 +192,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
limit: number,
|
||||
includeMyRenotes: boolean,
|
||||
includeRenotedMyNotes: boolean,
|
||||
includeLocalRenotes: boolean,
|
||||
withFiles: boolean,
|
||||
withReplies: boolean,
|
||||
withBots: boolean,
|
||||
withRenotes: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
const followingChannels = await this.channelFollowingsRepository.find({
|
||||
|
@ -227,8 +218,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
if (followingChannels.length > 0) {
|
||||
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||
|
@ -255,45 +246,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
if (!ps.withRenotes) {
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
return await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -103,13 +103,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
|
@ -136,14 +137,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me),
|
||||
});
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return timeline;
|
||||
});
|
||||
|
@ -156,26 +158,38 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withFiles: boolean,
|
||||
withReplies: boolean,
|
||||
withBots: boolean,
|
||||
withRenotes: boolean,
|
||||
}, me: MiLocalUser | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId)
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL) AND (note.channelId IS NULL)')
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.channelId IS NULL')
|
||||
.andWhere('note.userHost IS NULL')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
if (!ps.withRenotes) {
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
|
@ -188,8 +202,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}));
|
||||
}
|
||||
|
||||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
return await query.limit(ps.limit).getMany();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -57,43 +58,44 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private readonly activeUsersChart: ActiveUsersChart,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb // このmeIdAsListパラメータはqueryServiceのgenerateVisibilityQueryでセットされる
|
||||
.where(':meIdAsList <@ note.mentions')
|
||||
.orWhere(':meIdAsList <@ note.visibleUserIds');
|
||||
}))
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => qb
|
||||
.orWhere(':meId = ANY (note.mentions)')
|
||||
.orWhere(':meId = ANY (note.visibleUserIds)')))
|
||||
.setParameters({ meId: me.id })
|
||||
// Avoid scanning primary key index
|
||||
.orderBy('CONCAT(note.id)', 'DESC')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.visibility) {
|
||||
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
|
||||
}
|
||||
|
||||
if (ps.following) {
|
||||
query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id });
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
this.queryService.andFollowingUser(query, ':meId', 'note.userId');
|
||||
}
|
||||
|
||||
const mentions = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(mentions, me);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -96,7 +96,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query, undefined, false);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
|
|
@ -49,9 +49,6 @@ export const paramDef = {
|
|||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
withBots: { type: 'boolean', default: true },
|
||||
|
@ -88,9 +85,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
withBots: ps.withBots,
|
||||
|
@ -131,9 +125,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId,
|
||||
sinceId,
|
||||
limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
withBots: ps.withBots,
|
||||
|
@ -148,7 +139,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
}
|
||||
|
||||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; withBots: boolean; }, me: MiLocalUser) {
|
||||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; withFiles: boolean; withRenotes: boolean; withBots: boolean; }, me: MiLocalUser) {
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
const followingChannels = await this.channelFollowingsRepository.find({
|
||||
where: {
|
||||
|
@ -161,8 +152,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
if (followees.length > 0 && followingChannels.length > 0) {
|
||||
// ユーザー・チャンネルともにフォローあり
|
||||
|
@ -212,47 +203,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere('note.renoteId IS NULL');
|
||||
}
|
||||
|
||||
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
if (!ps.withRenotes) {
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
return await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -57,9 +57,6 @@ export const paramDef = {
|
|||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
|
@ -109,14 +106,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
|
@ -135,15 +131,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId,
|
||||
sinceId,
|
||||
limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me),
|
||||
});
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return timeline;
|
||||
});
|
||||
|
@ -153,9 +148,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
limit: number,
|
||||
includeMyRenotes: boolean,
|
||||
includeRenotedMyNotes: boolean,
|
||||
includeLocalRenotes: boolean,
|
||||
withFiles: boolean,
|
||||
withRenotes: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
|
@ -165,8 +157,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => {
|
||||
|
@ -193,51 +185,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (!ps.withRenotes) {
|
||||
this.queryService.generateExcludedRenotesQueryForNotes(query);
|
||||
} else if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
return await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private readonly activeUsersChart: ActiveUsersChart,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
@ -101,11 +103,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.andWhere('(note.visibility = \'public\')')
|
||||
.orderBy('note.id', 'DESC')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
|
@ -113,7 +116,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
const notes = await query.getMany();
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
|
|
|
@ -61,6 +61,21 @@ export default abstract class Channel {
|
|||
return this.connection.subscriber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a note is visible to the current user *excluding* blocks and mutes.
|
||||
*/
|
||||
protected isNoteVisibleToMe(note: Packed<'Note'>): boolean {
|
||||
if (note.visibility === 'public') return true;
|
||||
if (note.visibility === 'home') return true;
|
||||
if (!this.user) return false;
|
||||
if (this.user.id === note.userId) return true;
|
||||
if (note.visibility === 'followers') {
|
||||
return this.following[note.userId] != null;
|
||||
}
|
||||
if (!note.visibleUserIds) return false;
|
||||
return note.visibleUserIds.includes(this.user.id);
|
||||
}
|
||||
|
||||
/*
|
||||
* ミュートとブロックされてるを処理する
|
||||
*/
|
||||
|
@ -69,7 +84,7 @@ export default abstract class Channel {
|
|||
if (note.user.requireSigninToViewContents && !this.user) return true;
|
||||
|
||||
// 流れてきたNoteがインスタンスミュートしたインスタンスが関わる
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return true;
|
||||
if (isInstanceMuted(note, this.userMutedInstances) && !this.following[note.userId]) return true;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わる
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return true;
|
||||
|
@ -82,6 +97,15 @@ export default abstract class Channel {
|
|||
// If it's a boost (pure renote) then we need to check the target as well
|
||||
if (isPackedPureRenote(note) && note.renote && this.isNoteMutedOrBlocked(note.renote)) return true;
|
||||
|
||||
// Hide silenced notes
|
||||
if (note.user.isSilenced || note.user.instance?.isSilenced) {
|
||||
if (this.user == null) return true;
|
||||
if (this.user.id === note.userId) return false;
|
||||
if (this.following[note.userId] == null) return true;
|
||||
}
|
||||
|
||||
// TODO muted threads
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
@ -22,10 +20,8 @@ class BubbleTimelineChannel extends Channel {
|
|||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
private withBots: boolean;
|
||||
private instance: MiMeta;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
private readonly utilityService: UtilityService,
|
||||
noteEntityService: NoteEntityService,
|
||||
|
@ -44,7 +40,6 @@ class BubbleTimelineChannel extends Channel {
|
|||
this.withRenotes = !!(params.withRenotes ?? true);
|
||||
this.withFiles = !!(params.withFiles ?? false);
|
||||
this.withBots = !!(params.withBots ?? true);
|
||||
this.instance = await this.metaService.fetch();
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
@ -52,24 +47,37 @@ class BubbleTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
if (!this.withBots && note.user.isBot) return;
|
||||
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
if (note.user.host == null) return;
|
||||
if (!this.utilityService.isBubbledHost(note.user.host)) return;
|
||||
if (note.user.requireSigninToViewContents && this.user == null) return;
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
|
||||
|
||||
if (note.user.isSilenced) {
|
||||
if (!this.user) return;
|
||||
if (note.userId !== this.user.id && !this.following[note.userId]) return;
|
||||
}
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
const clonedNote = await this.assignMyReaction(note);
|
||||
await this.hideNote(clonedNote);
|
||||
|
||||
|
@ -90,7 +98,6 @@ export class BubbleTimelineChannelService implements MiChannelService<false> {
|
|||
public readonly kind = BubbleTimelineChannel.kind;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private readonly utilityService: UtilityService,
|
||||
|
@ -100,7 +107,6 @@ export class BubbleTimelineChannelService implements MiChannelService<false> {
|
|||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): BubbleTimelineChannel {
|
||||
return new BubbleTimelineChannel(
|
||||
this.metaService,
|
||||
this.roleService,
|
||||
this.utilityService,
|
||||
this.noteEntityService,
|
||||
|
|
|
@ -48,20 +48,36 @@ class GlobalTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
if (!this.withBots && note.user.isBot) return;
|
||||
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
if (note.user.requireSigninToViewContents && this.user == null) return;
|
||||
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
|
||||
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
|
||||
|
||||
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
const clonedNote = await this.assignMyReaction(note);
|
||||
await this.hideNote(clonedNote);
|
||||
|
|
|
@ -50,37 +50,29 @@ class HomeTimelineChannel extends Channel {
|
|||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
}
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} else if (note.visibility === 'specified') {
|
||||
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
if (this.following[note.userId]?.withReplies) {
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
|
||||
} else {
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const clonedNote = await this.assignMyReaction(note);
|
||||
await this.hideNote(clonedNote);
|
||||
|
||||
|
|
|
@ -67,34 +67,26 @@ class HybridTimelineChannel extends Channel {
|
|||
(note.channelId != null && this.followingChannels.has(note.channelId))
|
||||
)) return;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} else if (note.visibility === 'specified') {
|
||||
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
|
||||
} else {
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,28 +50,37 @@ class LocalTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
if (!this.withBots && note.user.isBot) return;
|
||||
|
||||
if (note.user.host !== null) return;
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
if (note.user.requireSigninToViewContents && this.user == null) return;
|
||||
if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return;
|
||||
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
const clonedNote = await this.assignMyReaction(note);
|
||||
await this.hideNote(clonedNote);
|
||||
|
|
|
@ -32,10 +32,12 @@ class MainChannel extends Channel {
|
|||
switch (data.type) {
|
||||
case 'notification': {
|
||||
// Ignore notifications from instances the user has muted
|
||||
if (isUserFromMutedInstance(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
if (isUserFromMutedInstance(data.body, this.userMutedInstances)) return;
|
||||
if (data.body.userId && this.userIdsWhoMeMuting.has(data.body.userId)) return;
|
||||
|
||||
if (data.body.note && data.body.note.isHidden) {
|
||||
if (this.isNoteMutedOrBlocked(data.body.note)) return;
|
||||
if (!this.isNoteVisibleToMe(data.body.id)) return;
|
||||
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
|
@ -44,9 +46,7 @@ class MainChannel extends Channel {
|
|||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
if (this.userIdsWhoMeMuting.has(data.body.userId)) return;
|
||||
if (this.isNoteMutedOrBlocked(data.body)) return;
|
||||
if (data.body.isHidden) {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
||||
detail: true,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
|
@ -40,7 +41,9 @@ class RoleTimelineChannel extends Channel {
|
|||
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
|
||||
if (data.type === 'note') {
|
||||
const note = data.body;
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
// TODO this should be cached
|
||||
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
|
||||
return;
|
||||
}
|
||||
|
@ -48,6 +51,25 @@ class RoleTimelineChannel extends Channel {
|
|||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
const clonedNote = await this.assignMyReaction(note);
|
||||
await this.hideNote(clonedNote);
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ import Channel, { type MiChannelService } from '../channel.js';
|
|||
class UserListChannel extends Channel {
|
||||
public readonly chName = 'userList';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = false as const;
|
||||
public static requireCredential = true as const;
|
||||
public static kind = 'read:account';
|
||||
private listId: string;
|
||||
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
|
@ -81,7 +82,7 @@ class UserListChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user!.id === note.userId;
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
// チャンネル投稿は無視する
|
||||
if (note.channelId) return;
|
||||
|
@ -90,26 +91,28 @@ class UserListChannel extends Channel {
|
|||
|
||||
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} else if (note.visibility === 'specified') {
|
||||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
if (this.membershipsMap[note.userId]?.withReplies) {
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
|
||||
} else {
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following[note.userId]?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
|
||||
const clonedNote = await this.assignMyReaction(note);
|
||||
await this.hideNote(clonedNote);
|
||||
|
@ -128,7 +131,7 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserListChannelService implements MiChannelService<false> {
|
||||
export class UserListChannelService implements MiChannelService<true> {
|
||||
public readonly shouldShare = UserListChannel.shouldShare;
|
||||
public readonly requireCredential = UserListChannel.requireCredential;
|
||||
public readonly kind = UserListChannel.kind;
|
||||
|
|
|
@ -4281,6 +4281,7 @@ export type components = {
|
|||
iconUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
themeColor: string | null;
|
||||
isSilenced: boolean;
|
||||
};
|
||||
emojis: {
|
||||
[key: string]: string;
|
||||
|
@ -27232,12 +27233,6 @@ export type operations = {
|
|||
untilDate?: number;
|
||||
/** @default false */
|
||||
allowPartial?: boolean;
|
||||
/** @default true */
|
||||
includeMyRenotes?: boolean;
|
||||
/** @default true */
|
||||
includeRenotedMyNotes?: boolean;
|
||||
/** @default true */
|
||||
includeLocalRenotes?: boolean;
|
||||
/** @default false */
|
||||
withFiles?: boolean;
|
||||
/** @default true */
|
||||
|
@ -28653,12 +28648,6 @@ export type operations = {
|
|||
untilDate?: number;
|
||||
/** @default false */
|
||||
allowPartial?: boolean;
|
||||
/** @default true */
|
||||
includeMyRenotes?: boolean;
|
||||
/** @default true */
|
||||
includeRenotedMyNotes?: boolean;
|
||||
/** @default true */
|
||||
includeLocalRenotes?: boolean;
|
||||
/** @default false */
|
||||
withFiles?: boolean;
|
||||
/** @default true */
|
||||
|
@ -28860,12 +28849,6 @@ export type operations = {
|
|||
/** @default false */
|
||||
allowPartial?: boolean;
|
||||
/** @default true */
|
||||
includeMyRenotes?: boolean;
|
||||
/** @default true */
|
||||
includeRenotedMyNotes?: boolean;
|
||||
/** @default true */
|
||||
includeLocalRenotes?: boolean;
|
||||
/** @default true */
|
||||
withRenotes?: boolean;
|
||||
/**
|
||||
* @description Only show notes that have attached files.
|
||||
|
|
Loading…
Add table
Reference in a new issue