additional fixes and cleanup to all note endpoints

This commit is contained in:
Hazelnoot 2025-06-05 02:34:57 -04:00
parent 65983d0030
commit 05d7aa0b91
19 changed files with 282 additions and 202 deletions

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

@ -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.

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 });
qb.orWhere('note.replyId = :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(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

@ -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

@ -142,11 +142,6 @@ 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) {
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
// 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')
@ -160,10 +155,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.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')
.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);

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();
}
}