merge: Additional performance fixes (!1095)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1095

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2025-06-06 06:36:07 +00:00
commit 20a2505543
21 changed files with 396 additions and 282 deletions

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FixIDXNoteForTimeline1749097536193 {
async up(queryRunner) {
await queryRunner.query('drop index "IDX_note_for_timelines"');
await queryRunner.query(`
create index "IDX_note_for_timelines"
on "note" ("id" desc, "channelId", "visibility", "userHost")
include ("userId", "replyId", "replyUserId", "replyUserHost", "renoteId", "renoteUserId", "renoteUserHost", "threadId")
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"');
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'`);
}
}

View file

@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js';
import type { LatestNotesRepository, NotesRepository } from '@/models/_.js'; import type { LatestNotesRepository, NotesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { QueryService } from './QueryService.js';
@Injectable() @Injectable()
export class LatestNoteService { export class LatestNoteService {
@ -14,11 +15,12 @@ export class LatestNoteService {
constructor( constructor(
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private readonly notesRepository: NotesRepository,
@Inject(DI.latestNotesRepository) @Inject(DI.latestNotesRepository)
private latestNotesRepository: LatestNotesRepository, private readonly latestNotesRepository: LatestNotesRepository,
private readonly queryService: QueryService,
loggerService: LoggerService, loggerService: LoggerService,
) { ) {
this.logger = loggerService.getLogger('LatestNoteService'); this.logger = loggerService.getLogger('LatestNoteService');
@ -91,7 +93,7 @@ export class LatestNoteService {
// Find the newest remaining note for the user. // Find the newest remaining note for the user.
// We exclude DMs and pure renotes. // We exclude DMs and pure renotes.
const nextLatest = await this.notesRepository const query = this.notesRepository
.createQueryBuilder('note') .createQueryBuilder('note')
.select() .select()
.where({ .where({
@ -106,18 +108,11 @@ export class LatestNoteService {
? Not(null) ? Not(null)
: null, : null,
}) })
.andWhere(` .orderBy({ id: 'DESC' });
(
note."renoteId" IS NULL this.queryService.andIsNotRenote(query, 'note');
OR note.text IS NOT NULL
OR note.cw IS NOT NULL const nextLatest = await query.getOne();
OR note."replyId" IS NOT NULL
OR note."hasPoll"
OR note."fileIds" != '{}'
)
`)
.orderBy({ id: 'DESC' })
.getOne();
if (!nextLatest) return; if (!nextLatest) return;
// Record it as the latest // Record it as the latest

View file

@ -160,15 +160,15 @@ export class QueryService {
// Reply to me // Reply to me
.orWhere(':meId = note.replyUserId') .orWhere(':meId = note.replyUserId')
// DM to me // DM to me
.orWhere(':meId = ANY (note.visibleUserIds)') .orWhere(':meIdAsList <@ note.visibleUserIds')
// Mentions me // Mentions me
.orWhere(':meId = ANY (note.mentions)') .orWhere(':meIdAsList <@ note.mentions')
// Followers-only post // Followers-only post
.orWhere(new Brackets(qb => this .orWhere(new Brackets(qb => this
.andFollowingUser(qb, ':meId', 'note.userId') .andFollowingUser(qb, ':meId', 'note.userId')
.andWhere('note.visibility = \'followers\''))); .andWhere('note.visibility = \'followers\'')));
q.setParameters({ meId: me.id }); q.setParameters({ meId: me.id, meIdAsList: [me.id] });
} }
})); }));
} }
@ -188,15 +188,8 @@ export class QueryService {
} }
@bindThis @bindThis
public generateExcludedRenotesQueryForNotes<E extends ObjectLiteral>(q: SelectQueryBuilder<E>): SelectQueryBuilder<E> { public generateExcludedRenotesQueryForNotes<Q extends WhereExpressionBuilder>(q: Q): Q {
return q return this.andIsNotRenote(q, 'note');
.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 @bindThis
@ -256,6 +249,120 @@ export class QueryService {
return q; return q;
} }
/**
* Adds OR condition that noteProp (note ID) refers to a quote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public orIsQuote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsQuote(q, noteProp, 'orWhere');
}
/**
* Adds AND condition that noteProp (note ID) refers to a quote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public andIsQuote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsQuote(q, noteProp, 'andWhere');
}
private addIsQuote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string, join: 'andWhere' | 'orWhere'): Q {
return q[join](new Brackets(qb => qb
.andWhere(`${noteProp}.renoteId IS NOT NULL`)
.andWhere(new Brackets(qbb => qbb
.orWhere(`${noteProp}.text IS NOT NULL`)
.orWhere(`${noteProp}.cw IS NOT NULL`)
.orWhere(`${noteProp}.replyId IS NOT NULL`)
.orWhere(`${noteProp}.hasPoll = true`)
.orWhere(`${noteProp}.fileIds != '{}'`)))));
}
/**
* Adds OR condition that noteProp (note ID) does not refer to a quote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public orIsNotQuote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsNotQuote(q, noteProp, 'orWhere');
}
/**
* Adds AND condition that noteProp (note ID) does not refer to a quote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public andIsNotQuote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsNotQuote(q, noteProp, 'andWhere');
}
private addIsNotQuote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string, join: 'andWhere' | 'orWhere'): Q {
return q[join](new Brackets(qb => qb
.orWhere(`${noteProp}.renoteId IS NULL`)
.orWhere(new Brackets(qb => qb
.andWhere(`${noteProp}.text IS NULL`)
.andWhere(`${noteProp}.cw IS NULL`)
.andWhere(`${noteProp}.replyId IS NULL`)
.andWhere(`${noteProp}.hasPoll = false`)
.andWhere(`${noteProp}.fileIds = '{}'`)))));
}
/**
* Adds OR condition that noteProp (note ID) refers to a renote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public orIsRenote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsRenote(q, noteProp, 'orWhere');
}
/**
* Adds AND condition that noteProp (note ID) refers to a renote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public andIsRenote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsRenote(q, noteProp, 'andWhere');
}
private addIsRenote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string, join: 'andWhere' | 'orWhere'): Q {
return q[join](new Brackets(qb => qb
.andWhere(`${noteProp}.renoteId IS NOT NULL`)
.andWhere(`${noteProp}.text IS NULL`)
.andWhere(`${noteProp}.cw IS NULL`)
.andWhere(`${noteProp}.replyId IS NULL`)
.andWhere(`${noteProp}.hasPoll = false`)
.andWhere(`${noteProp}.fileIds = '{}'`)));
}
/**
* Adds OR condition that noteProp (note ID) does not refer to a renote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public orIsNotRenote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsNotRenote(q, noteProp, 'orWhere');
}
/**
* Adds AND condition that noteProp (note ID) does not refer to a renote.
* The prop should be an expression, not a raw value.
*/
@bindThis
public andIsNotRenote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string): Q {
return this.addIsNotRenote(q, noteProp, 'andWhere');
}
private addIsNotRenote<Q extends WhereExpressionBuilder>(q: Q, noteProp: string, join: 'andWhere' | 'orWhere'): Q {
return q[join](new Brackets(qb => qb
.orWhere(`${noteProp}.renoteId IS NULL`)
.orWhere(`${noteProp}.text IS NOT NULL`)
.orWhere(`${noteProp}.cw IS NOT NULL`)
.orWhere(`${noteProp}.replyId IS NOT NULL`)
.orWhere(`${noteProp}.hasPoll = true`)
.orWhere(`${noteProp}.fileIds != '{}'`)));
}
/** /**
* Adds OR condition that followerProp (user ID) is following followeeProp (user ID). * Adds OR condition that followerProp (user ID) is following followeeProp (user ID).
* Both props should be expressions, not raw values. * Both props should be expressions, not raw values.
@ -283,6 +390,33 @@ export class QueryService {
return q[join](`EXISTS (${followingQuery.getQuery()})`, followingQuery.getParameters()); return q[join](`EXISTS (${followingQuery.getQuery()})`, followingQuery.getParameters());
}; };
/**
* Adds OR condition that followerProp (user ID) is following followeeProp (channel ID).
* Both props should be expressions, not raw values.
*/
@bindThis
public orFollowingChannel<Q extends WhereExpressionBuilder>(q: Q, followerProp: string, followeeProp: string): Q {
return this.addFollowingChannel(q, followerProp, followeeProp, 'orWhere');
}
/**
* Adds AND condition that followerProp (user ID) is following followeeProp (channel ID).
* Both props should be expressions, not raw values.
*/
@bindThis
public andFollowingChannel<Q extends WhereExpressionBuilder>(q: Q, followerProp: string, followeeProp: string): Q {
return this.addFollowingChannel(q, followerProp, followeeProp, 'andWhere');
}
private addFollowingChannel<Q extends WhereExpressionBuilder>(q: Q, followerProp: string, followeeProp: string, join: 'andWhere' | 'orWhere'): Q {
const followingQuery = this.channelFollowingsRepository.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). * Adds OR condition that blockerProp (user ID) is not blocking blockeeProp (user ID).
* Both props should be expressions, not raw values. * Both props should be expressions, not raw values.

View file

@ -114,8 +114,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser');
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。 // NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255 // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255

View file

@ -137,12 +137,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId') .leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel'); .leftJoinAndSelect('note.channel', 'channel')
.limit(ps.limit);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
if (me) { if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me);
@ -159,6 +161,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
//#endregion //#endregion
return await query.limit(ps.limit).getMany(); return await query.getMany();
} }
} }

View file

@ -81,10 +81,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchFile); throw new ApiError(meta.errors.noSuchFile);
} }
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
query.andWhere(':file <@ note.fileIds', { file: [file.id] }); .andWhere(':file <@ note.fileIds', { file: [file.id] })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
const notes = await query.limit(ps.limit).getMany(); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
return await this.noteEntityService.packMany(notes, me, { return await this.noteEntityService.packMany(notes, me, {
detail: true, detail: true,

View file

@ -64,7 +64,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
if (me) {
this.queryService.generateSilencedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
if (ps.local) { if (ps.local) {
query.andWhere('note.userHost IS NULL'); query.andWhere('note.userHost IS NULL');
@ -75,7 +84,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.renote !== undefined) { if (ps.renote !== undefined) {
query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); if (ps.renote) {
this.queryService.andIsRenote(query, 'note');
if (me) {
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
} else {
this.queryService.andIsNotRenote(query, 'note');
}
} }
if (ps.withFiles !== undefined) { if (ps.withFiles !== undefined) {
@ -91,7 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// query.isBot = bot; // query.isBot = bot;
//} //}
const notes = await query.limit(ps.limit).getMany(); const notes = await query.getMany();
return await this.noteEntityService.packMany(notes); return await this.noteEntityService.packMany(notes);
}); });

View file

@ -82,8 +82,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
// This subquery mess teaches postgres how to use the right indexes. // 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. // Using WHERE or ON conditions causes a fallback to full sequence scan, which times out.
@ -114,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
//#endregion //#endregion
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.getMany();
if (me) { if (me) {
process.nextTick(() => { process.nextTick(() => {

View file

@ -57,26 +57,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { .andWhere(new Brackets(qb => {
qb qb.orWhere('note.replyId = :noteId');
.where('note.replyId = :noteId', { noteId: ps.noteId });
if (ps.showQuotes) { if (ps.showQuotes) {
qb.orWhere(new Brackets(qb => { qb.orWhere(new Brackets(qbb => this.queryService
qb .andIsQuote(qbb, 'note')
.where('note.renoteId = :noteId', { noteId: ps.noteId }) .andWhere('note.renoteId = :noteId'),
.andWhere(new Brackets(qb => { ));
qb }
.where('note.text IS NOT NULL')
.orWhere('note.fileIds != \'{}\'')
.orWhere('note.hasPoll = TRUE');
}));
}));
}
})) }))
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters({ noteId: ps.noteId })
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
@ -85,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me);
} }
const notes = await query.limit(ps.limit).getMany(); const notes = await query.getMany();
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });

View file

@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.notesRepository const query = this.notesRepository
.createQueryBuilder('note') .createQueryBuilder('note')
.setParameter('me', me.id) .setParameters({ meId: me.id })
// Limit to latest notes // Limit to latest notes
.innerJoin( .innerJoin(
@ -130,8 +130,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId') .leftJoinAndSelect('renote.user', 'renoteUser')
// Exclude channel notes // Exclude channel notes
.andWhere({ channelId: IsNull() }) .andWhere({ channelId: IsNull() })
@ -177,14 +177,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
* Limit to followers (they follow us) * Limit to followers (they follow us)
*/ */
function addFollower<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T { function addFollower<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T {
return query.innerJoin(MiFollowing, 'follower', 'follower."followerId" = latest.user_id AND follower."followeeId" = :me'); return query.innerJoin(MiFollowing, 'follower', 'follower."followerId" = latest.user_id AND follower."followeeId" = :meId');
} }
/** /**
* Limit to followees (we follow them) * Limit to followees (we follow them)
*/ */
function addFollowee<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T { function addFollowee<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T {
return query.innerJoin(MiFollowing, 'followee', 'followee."followerId" = :me AND followee."followeeId" = latest.user_id'); return query.innerJoin(MiFollowing, 'followee', 'followee."followerId" = :meId AND followee."followeeId" = latest.user_id');
} }
/** /**

View file

@ -82,8 +82,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me); this.queryService.generateSilencedUserQueryForNotes(query, me);

View file

@ -197,51 +197,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withBots: boolean, withBots: boolean,
withRenotes: boolean, withRenotes: boolean,
}, me: MiLocalUser) { }, me: MiLocalUser) {
const followees = await this.userFollowingService.getFollowees(me.id);
const followingChannels = await this.channelFollowingsRepository.find({
where: {
followerId: me.id,
},
});
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { // 1. by a user I follow, 2. a public local post, 3. my own post
if (followees.length > 0) { .andWhere(new Brackets(qb => this.queryService
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; .orFollowingUser(qb, ':meId', 'note.userId')
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); .orWhere(new Brackets(qbb => qbb
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); .andWhere('note.visibility = \'public\'')
} else { .andWhere('note.userHost IS NULL')))
qb.where('note.userId = :meId', { meId: me.id }); .orWhere(':meId = note.userId')))
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); // 1. in a channel I follow, 2. not in a channel
} .andWhere(new Brackets(qb => this.queryService
})) .orFollowingChannel(qb, ':meId', 'note.channelId')
.orWhere('note.channelId IS NULL')))
.setParameters({ meId: me.id })
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (followingChannels.length > 0) {
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere(new Brackets(qb => {
qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
qb.orWhere('note.channelId IS NULL');
}));
} else {
query.andWhere('note.channelId IS NULL');
}
if (!ps.withReplies) { if (!ps.withReplies) {
query.andWhere(new Brackets(qb => { query
qb // 1. Not a reply, 2. a self-reply
.where('note.replyId IS NULL') // 返信ではない .andWhere(new Brackets(qb => qb
.orWhere(new Brackets(qb => { .orWhere('note.replyId IS NULL') // 返信ではない
qb // 返信だけど投稿者自身への返信 .orWhere('note.replyUserId = note.userId')));
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
} }
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
@ -263,6 +244,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
//#endregion //#endregion
return await query.limit(ps.limit).getMany(); return await query.getMany();
} }
} }

View file

@ -168,8 +168,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (!ps.withReplies) {
query
// 1. Not a reply, 2. a self-reply
.andWhere(new Brackets(qb => qb
.orWhere('note.replyId IS NULL') // 返信ではない
.orWhere('note.replyUserId = note.userId')));
}
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me); this.queryService.generateSilencedUserQueryForNotes(query, me);
@ -190,18 +199,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserRenotesQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
} }
if (!ps.withReplies) { return await query.getMany();
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
return await query.limit(ps.limit).getMany();
} }
} }

View file

@ -6,6 +6,7 @@
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
import { MiNote } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@ -61,37 +62,52 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private readonly activeUsersChart: ActiveUsersChart, private readonly activeUsersChart: ActiveUsersChart,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
ps.sinceId, ps.untilId) .innerJoin(qb => {
.andWhere(new Brackets(qb => qb qb
.where(':meIdAsList <@ note.mentions') .select('note.id', 'id')
.orWhere(':meIdAsList <@ note.visibleUserIds'))) .from(qbb => qbb
.setParameters({ meIdAsList: [me.id] }) .select('note.id', 'id')
// Avoid scanning primary key index .from(MiNote, 'note')
.orderBy('CONCAT(note.id)', 'DESC') .where(new Brackets(qbbb => qbbb
// DM to me
.orWhere(':meIdAsList <@ note.visibleUserIds')
// Mentions me
.orWhere(':meIdAsList <@ note.mentions'),
))
.setParameters({ meIdAsList: [me.id] })
, 'source')
.innerJoin(MiNote, 'note', 'note.id = source.id');
// Mentioned or visible users can always access
//this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(qb);
this.queryService.generateMutedUserQueryForNotes(qb, me);
this.queryService.generateMutedNoteThreadQuery(qb, me);
this.queryService.generateBlockedUserQueryForNotes(qb, me);
// A renote can't mention a user, so it will never appear here anyway.
//this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.visibility) {
qb.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
}
if (ps.following) {
this.queryService
.andFollowingUser(qb, ':meId', 'note.userId')
.setParameters({ meId: me.id });
}
return qb;
}, 'source', 'source.id = note.id')
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me); const mentions = await query.getMany();
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
// A renote can't mention a user, so it will never appear here anyway.
//this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
}
if (ps.following) {
this.queryService.andFollowingUser(query, ':meId', 'note.userId');
}
const mentions = await query.limit(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {
this.activeUsersChart.read(me); this.activeUsersChart.read(me);

View file

@ -47,7 +47,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
noteId: { type: 'string', format: 'misskey:id' }, noteId: { type: 'string', format: 'misskey:id' },
userId: { type: "string", format: "misskey:id" }, userId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
@ -81,19 +81,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser');
if (ps.userId) { if (ps.userId) {
query.andWhere("user.id = :userId", { userId: ps.userId }); query.andWhere('user.id = :userId', { userId: ps.userId });
} }
if (ps.quote) { if (ps.quote) {
query.andWhere("note.text IS NOT NULL"); this.queryService.andIsQuote(query, 'note');
} else { } else {
query.andWhere("note.text IS NULL"); this.queryService.andIsRenote(query, 'note');
} }
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) {
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const renotes = await query.limit(ps.limit).getMany(); const renotes = await query.limit(ps.limit).getMany();

View file

@ -59,14 +59,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me); if (me) {
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const timeline = await query.limit(ps.limit).getMany(); const timeline = await query.getMany();
return await this.noteEntityService.packMany(timeline, me); return await this.noteEntityService.packMany(timeline, me);
}); });

View file

@ -12,8 +12,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { UtilityService } from '@/core/UtilityService.js';
export const meta = { export const meta = {
tags: ['notes', 'hashtags'], tags: ['notes', 'hashtags'],
@ -82,19 +80,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private cacheService: CacheService,
private utilityService: UtilityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere("note.visibility IN ('public', 'home')") // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()` .andWhere(new Brackets(qb => qb
.orWhere('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\''))) // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()`
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me); this.queryService.generateSilencedUserQueryForNotes(query, me);
@ -102,7 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me); if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
const followings = me ? await this.cacheService.userFollowingsCache.fetch(me.id) : {}; if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
try { try {
if (ps.tag) { if (ps.tag) {
@ -135,9 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.renote != null) { if (ps.renote != null) {
if (ps.renote) { if (ps.renote) {
query.andWhere('note.renoteId IS NOT NULL'); this.queryService.andIsRenote(query, 'note');
} else { } else {
query.andWhere('note.renoteId IS NULL'); this.queryService.andIsNotRenote(query, 'note');
} }
} }
@ -154,16 +151,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
// Search notes // Search notes
let notes = await query.limit(ps.limit).getMany(); const notes = await query.getMany();
notes = notes.filter(note => {
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
if (note.user?.isSuspended) return false;
if (note.userHost) {
if (!this.utilityService.isFederationAllowedHost(note.userHost)) return false;
}
return true;
});
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });

View file

@ -140,67 +140,31 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; 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: {
followerId: me.id,
},
});
//#region Construct query //#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
// 1. in a channel I follow, 2. my own post, 3. by a user I follow
.andWhere(new Brackets(qb => this.queryService
.orFollowingChannel(qb, ':meId', 'note.channelId')
.orWhere(':meId = note.userId')
.orWhere(new Brackets(qb2 => this.queryService
.andFollowingUser(qb2, ':meId', 'note.userId')
.andWhere('note.channelId IS NULL'))),
))
// 1. Not a reply, 2. a self-reply
.andWhere(new Brackets(qb => qb
.orWhere('note.replyId IS NULL') // 返信ではない
.orWhere('note.replyUserId = note.userId')))
.setParameters({ meId: me.id })
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (followees.length > 0 && followingChannels.length > 0) {
// ユーザー・チャンネルともにフォローあり
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb2 => {
qb2
.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds })
.andWhere('note.channelId IS NULL');
}))
.orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
}));
} else if (followees.length > 0) {
// ユーザーフォローのみ(チャンネルフォローなし)
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
query
.andWhere('note.channelId IS NULL')
.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
} else if (followingChannels.length > 0) {
// チャンネルフォローのみ(ユーザーフォローなし)
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere(new Brackets(qb => {
qb
.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds })
.orWhere('note.userId = :meId', { meId: me.id });
}));
} else {
// フォローなし
query
.andWhere('note.channelId IS NULL')
.andWhere('note.userId = :meId', { meId: me.id });
}
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me);
@ -212,11 +176,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!ps.withRenotes) { if (!ps.withRenotes) {
this.queryService.generateExcludedRenotesQueryForNotes(query); this.queryService.generateExcludedRenotesQueryForNotes(query);
} else if (me) { } else {
this.queryService.generateMutedUserRenotesQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
} }
//#endregion //#endregion
return await query.limit(ps.limit).getMany(); return await query.getMany();
} }
} }

View file

@ -154,32 +154,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
//#region Construct query //#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId') .innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
.andWhere('note.channelId IS NULL') // チャンネルノートではない
.andWhere(new Brackets(qb => qb
// 返信ではない
.orWhere('note.replyId IS NULL')
// 返信だけど投稿者自身への返信
.orWhere('note.replyUserId = note.userId')
// 返信だけど自分宛ての返信
.orWhere('note.replyUserId = :meId')
// 返信だけどwithRepliesがtrueの場合
.orWhere('userListMemberships.withReplies = true'),
))
.setParameters({ meId: me.id })
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId') .leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id }) .limit(ps.limit);
.andWhere('note.channelId IS NULL') // チャンネルノートではない
.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}))
.orWhere(new Brackets(qb => {
qb // 返信だけど自分宛ての返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = :meId', { meId: me.id });
}))
.orWhere(new Brackets(qb => {
qb // 返信だけどwithRepliesがtrueの場合
.where('note.replyId IS NOT NULL')
.andWhere('userListMemberships.withReplies = true');
}));
}));
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
@ -192,12 +185,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!ps.withRenotes) { if (!ps.withRenotes) {
this.queryService.generateExcludedRenotesQueryForNotes(query); this.queryService.generateExcludedRenotesQueryForNotes(query);
} else if (me) { } else {
this.queryService.generateMutedUserRenotesQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
} }
//#endregion //#endregion
return await query.limit(ps.limit).getMany(); return await query.getMany();
} }
} }

View file

@ -107,10 +107,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.innerJoinAndSelect('note.user', 'user') .innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId'); .leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me); this.queryService.generateMutedUserRenotesQueryForNotes(query, me);

View file

@ -205,7 +205,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('note.channel', 'channel') .leftJoinAndSelect('note.channel', 'channel')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (ps.withChannelNotes) { if (ps.withChannelNotes) {
if (!isSelf) query.andWhere(new Brackets(qb => { if (!isSelf) query.andWhere(new Brackets(qb => {
@ -230,26 +231,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!ps.withRenotes && !ps.withQuotes) { if (!ps.withRenotes && !ps.withQuotes) {
query.andWhere('note.renoteId IS NULL'); query.andWhere('note.renoteId IS NULL');
} else if (!ps.withRenotes) { } else if (!ps.withRenotes) {
query.andWhere(new Brackets(qb => { this.queryService.andIsNotRenote(query, 'note');
qb.orWhere('note.userId != :userId', { userId: ps.userId });
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)');
}));
} else if (!ps.withQuotes) { } else if (!ps.withQuotes) {
query.andWhere(` this.queryService.andIsNotQuote(query, 'note');
(
note."renoteId" IS NULL
OR (
note.text IS NULL
AND note.cw IS NULL
AND note."replyId" IS NULL
AND note."hasPoll" IS FALSE
AND note."fileIds" = '{}'
)
)
`);
} }
if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) { if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) {
@ -268,6 +252,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('"user"."isBot" = false'); query.andWhere('"user"."isBot" = false');
} }
return await query.limit(ps.limit).getMany(); return await query.getMany();
} }
} }