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

View file

@ -160,15 +160,15 @@ export class QueryService {
// Reply to me
.orWhere(':meId = note.replyUserId')
// DM to me
.orWhere(':meId = ANY (note.visibleUserIds)')
.orWhere(':meIdAsList <@ note.visibleUserIds')
// Mentions me
.orWhere(':meId = ANY (note.mentions)')
.orWhere(':meIdAsList <@ note.mentions')
// Followers-only post
.orWhere(new Brackets(qb => this
.andFollowingUser(qb, ':meId', 'note.userId')
.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
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 != \'{}\'')));
public generateExcludedRenotesQueryForNotes<Q extends WhereExpressionBuilder>(q: Q): Q {
return this.andIsNotRenote(q, 'note');
}
@bindThis
@ -256,6 +249,120 @@ export class QueryService {
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).
* Both props should be expressions, not raw values.
@ -283,6 +390,33 @@ export class QueryService {
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).
* 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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// 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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId')
.leftJoinAndSelect('note.channel', 'channel');
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel')
.limit(ps.limit);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
@ -159,6 +161,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
//#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);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
query.andWhere(':file <@ note.fileIds', { file: [file.id] });
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.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, {
detail: true,

View file

@ -64,7 +64,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.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) {
query.andWhere('note.userHost IS NULL');
@ -75,7 +84,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
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) {
@ -91,7 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// query.isBot = bot;
//}
const notes = await query.limit(ps.limit).getMany();
const notes = await query.getMany();
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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
// 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.
@ -114,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
const timeline = await query.getMany();
if (me) {
process.nextTick(() => {

View file

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

View file

@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.notesRepository
.createQueryBuilder('note')
.setParameter('me', me.id)
.setParameters({ meId: me.id })
// Limit to latest notes
.innerJoin(
@ -130,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', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
// Exclude channel notes
.andWhere({ channelId: IsNull() })
@ -177,14 +177,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
* Limit to followers (they follow us)
*/
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)
*/
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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);

View file

@ -197,51 +197,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withBots: boolean,
withRenotes: boolean,
}, 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)
.andWhere(new Brackets(qb => {
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.where('note.userId = :meId', { meId: me.id });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}
}))
// 1. by a user I follow, 2. a public local post, 3. my own post
.andWhere(new Brackets(qb => this.queryService
.orFollowingUser(qb, ':meId', 'note.userId')
.orWhere(new Brackets(qbb => qbb
.andWhere('note.visibility = \'public\'')
.andWhere('note.userHost IS NULL')))
.orWhere(':meId = note.userId')))
// 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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.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);
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');
}
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (!ps.withReplies) {
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');
}));
}));
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.generateVisibilityQuery(query, me);
@ -263,6 +244,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
//#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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
.leftJoinAndSelect('reply.user', 'replyUser')
.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.generateSilencedUserQueryForNotes(query, me);
@ -190,18 +199,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
if (!ps.withReplies) {
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();
return await query.getMany();
}
}

View file

@ -6,6 +6,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
import { MiNote } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.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,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => qb
.where(':meIdAsList <@ note.mentions')
.orWhere(':meIdAsList <@ note.visibleUserIds')))
.setParameters({ meIdAsList: [me.id] })
// Avoid scanning primary key index
.orderBy('CONCAT(note.id)', 'DESC')
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoin(qb => {
qb
.select('note.id', 'id')
.from(qbb => qbb
.select('note.id', 'id')
.from(MiNote, 'note')
.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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me);
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();
const mentions = await query.getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);

View file

@ -47,7 +47,7 @@ export const paramDef = {
type: 'object',
properties: {
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 },
sinceId: { 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');
if (ps.userId) {
query.andWhere("user.id = :userId", { userId: ps.userId });
query.andWhere('user.id = :userId', { userId: ps.userId });
}
if (ps.quote) {
query.andWhere("note.text IS NOT NULL");
this.queryService.andIsQuote(query, 'note');
} else {
query.andWhere("note.text IS NULL");
this.queryService.andIsRenote(query, 'note');
}
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.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
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.renote', 'renote')
.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.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
if (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);
});

View file

@ -12,8 +12,6 @@ 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 { CacheService } from '@/core/CacheService.js';
import { UtilityService } from '@/core/UtilityService.js';
export const meta = {
tags: ['notes', 'hashtags'],
@ -82,19 +80,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private cacheService: CacheService,
private utilityService: UtilityService,
) {
super(meta, paramDef, async (ps, me) => {
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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateBlockedHostQueryForNote(query);
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.generateMutedUserRenotesQueryForNotes(query, me);
const followings = me ? await this.cacheService.userFollowingsCache.fetch(me.id) : {};
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
try {
if (ps.tag) {
@ -135,9 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.renote != null) {
if (ps.renote) {
query.andWhere('note.renoteId IS NOT NULL');
this.queryService.andIsRenote(query, 'note');
} 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
let notes = await query.limit(ps.limit).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;
});
const notes = await query.getMany();
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) {
const followees = await this.userFollowingService.getFollowees(me.id);
const followingChannels = await this.channelFollowingsRepository.find({
where: {
followerId: me.id,
},
});
//#region Construct query
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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
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');
}));
}));
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
@ -212,11 +176,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!ps.withRenotes) {
this.queryService.generateExcludedRenotesQueryForNotes(query);
} else if (me) {
} else {
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
//#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
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.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 => {
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');
}));
}));
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
@ -192,12 +185,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!ps.withRenotes) {
this.queryService.generateExcludedRenotesQueryForNotes(query);
} else if (me) {
} else {
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
//#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')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser', 'replyUser.id = note.replyUserId')
.leftJoinAndSelect('renote.user', 'renoteUser', 'renoteUser.id = note.renoteUserId');
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSilencedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(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.channel', 'channel')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
.leftJoinAndSelect('renote.user', 'renoteUser')
.limit(ps.limit);
if (ps.withChannelNotes) {
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) {
query.andWhere('note.renoteId IS NULL');
} else if (!ps.withRenotes) {
query.andWhere(new Brackets(qb => {
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)');
}));
this.queryService.andIsNotRenote(query, 'note');
} else if (!ps.withQuotes) {
query.andWhere(`
(
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" = '{}'
)
)
`);
this.queryService.andIsNotQuote(query, 'note');
}
if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) {
@ -268,6 +252,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('"user"."isBot" = false');
}
return await query.limit(ps.limit).getMany();
return await query.getMany();
}
}