diff options
| author | Mar0xy <marie@kaifa.ch> | 2023-10-13 19:01:17 +0200 |
|---|---|---|
| committer | Mar0xy <marie@kaifa.ch> | 2023-10-13 19:01:17 +0200 |
| commit | f8f128b3477e14dc224c7c454f63379ac4c828dd (patch) | |
| tree | 1ac7ae77a43beb4d2830e61762eb7482e12a6019 /packages/backend/src/server/api/endpoints | |
| parent | merge: timeline 1 (diff) | |
| parent | enhance(frontend): TLの返信表示オプションを記憶するように (diff) | |
| download | sharkey-f8f128b3477e14dc224c7c454f63379ac4c828dd.tar.gz sharkey-f8f128b3477e14dc224c7c454f63379ac4c828dd.tar.bz2 sharkey-f8f128b3477e14dc224c7c454f63379ac4c828dd.zip | |
merge: all upstream changes
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
28 files changed, 582 insertions, 389 deletions
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 24d3a8a943..faab8ee608 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -23,6 +23,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, + duplicateName: { + message: 'Duplicate name.', + code: 'DUPLICATE_NAME', + id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', + }, }, } as const; @@ -64,6 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.duplicateName); const emoji = await this.customEmojiService.add({ driveFile, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 2d69857408..04226d8953 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -74,6 +74,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } + const emoji = await this.customEmojiService.getEmojiById(ps.id); + if (emoji != null) { + if (ps.name !== emoji.name) { + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); + } + } else { + throw new ApiError(meta.errors.noSuchEmoji); + } await this.customEmojiService.update(ps.id, { driveFile, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index c9a16cd892..3ee5620573 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -105,40 +105,32 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - userStarForReactionFallback: { - type: 'boolean', - optional: true, nullable: false, - }, pinnedUsers: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, hiddenTags: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, blockedHosts: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, sensitiveWords: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, preservedUsernames: { @@ -146,129 +138,124 @@ export const meta = { optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, hcaptchaSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, recaptchaSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, turnstileSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, sensitiveMediaDetection: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, sensitiveMediaDetectionSensitivity: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, setSensitiveFlagAutomatically: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableSensitiveMediaDetectionForVideos: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, proxyAccountId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', }, - summaryProxy: { - type: 'string', - optional: true, nullable: true, - }, email: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpSecure: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, smtpHost: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpPort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpUser: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpPass: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, swPrivateKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, useObjectStorage: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageBaseUrl: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageBucket: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStoragePrefix: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageEndpoint: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageRegion: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStoragePort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageAccessKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageUseSSL: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageUseProxy: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageSetPublicRead: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableIpLogging: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableActiveEmailValidation: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableChartsForRemoteUser: { type: 'boolean', @@ -292,12 +279,32 @@ export const meta = { }, manifestJsonOverride: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, policies: { type: 'object', optional: false, nullable: false, }, + perLocalUserUserTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perRemoteUserUserTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perUserHomeTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perUserListTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -317,7 +324,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private metaService: MetaService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async () => { const instance = await this.metaService.fetch(true); return { @@ -332,6 +339,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, @@ -404,6 +413,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- enableIdenticonGeneration: instance.enableIdenticonGeneration, policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, + perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, + notesPerOneAd: instance.notesPerOneAd, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index b1772be777..ef5627bc9a 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -61,9 +61,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 3454597532..0731413d05 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -85,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- isModerator: isModerator, isSilenced: isSilenced, isSuspended: user.isSuspended, + isHibernated: user.isHibernated, lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote ?? '', signins, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 9f6ad8e10d..4a780b2f4d 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -86,6 +86,8 @@ export const paramDef = { tosUrl: { type: 'string', nullable: true }, repositoryUrl: { type: 'string' }, feedbackUrl: { type: 'string' }, + impressumUrl: { type: 'string' }, + privacyPolicyUrl: { type: 'string' }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, @@ -109,6 +111,11 @@ export const paramDef = { serverRules: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } }, manifestJsonOverride: { type: 'string' }, + perLocalUserUserTimelineCacheMax: { type: 'integer' }, + perRemoteUserUserTimelineCacheMax: { type: 'integer' }, + perUserHomeTimelineCacheMax: { type: 'integer' }, + perUserListTimelineCacheMax: { type: 'integer' }, + notesPerOneAd: { type: 'integer' }, }, required: [], } as const; @@ -342,6 +349,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.feedbackUrl = ps.feedbackUrl; } + if (ps.impressumUrl !== undefined) { + set.impressumUrl = ps.impressumUrl; + } + + if (ps.privacyPolicyUrl !== undefined) { + set.privacyPolicyUrl = ps.privacyPolicyUrl; + } + if (ps.useObjectStorage !== undefined) { set.useObjectStorage = ps.useObjectStorage; } @@ -446,6 +461,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.manifestJsonOverride = ps.manifestJsonOverride; } + if (ps.perLocalUserUserTimelineCacheMax !== undefined) { + set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; + } + + if (ps.perRemoteUserUserTimelineCacheMax !== undefined) { + set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax; + } + + if (ps.perUserHomeTimelineCacheMax !== undefined) { + set.perUserHomeTimelineCacheMax = ps.perUserHomeTimelineCacheMax; + } + + if (ps.perUserListTimelineCacheMax !== undefined) { + set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; + } + + if (ps.notesPerOneAd !== undefined) { + set.notesPerOneAd = ps.notesPerOneAd; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 63e542cb62..6d69971e30 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -12,6 +12,7 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -69,8 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private noteReadService: NoteReadService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const antenna = await this.antennasRepository.findOneBy({ id: ps.antennaId, userId: me.id, @@ -85,19 +90,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- lastUsedAt: new Date(), }); - const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - const noteIdsRes = await this.redisForTimelines.xrevrange( - `antennaTimeline:${antenna.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', - 'COUNT', limit); - - if (noteIdsRes.length === 0) { - return []; - } - - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); - + let noteIds = await this.redisTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; } @@ -115,7 +109,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateBlockedUserQuery(query, me); const notes = await query.getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + if (sinceId != null && untilId == null) { + notes.sort((a, b) => a.id < b.id ? -1 : 1); + } else { + notes.sort((a, b) => a.id > b.id ? -1 : 1); + } if (notes.length > 0) { this.noteReadService.read(me.id, notes); diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index 65df45706b..9c78a94844 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -55,9 +55,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.query !== '') { if (ps.type === 'nameAndDescription') { - query.andWhere(new Brackets(qb => { qb - .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) - .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); + query.andWhere(new Brackets(qb => { + qb + .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) + .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); })); } else { query.andWhere('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 56b8fc5c36..2dfcf659d7 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -12,6 +12,9 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -66,9 +69,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, + private redisTimelineService: RedisTimelineService, + private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const isRangeSpecified = untilId != null && sinceId != null; + const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -77,68 +86,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchChannel); } - let timeline: MiNote[] = []; + if (me) this.activeUsersChart.read(me); - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - let noteIdsRes: [string, string[]][] = []; + if (isRangeSpecified || sinceId == null) { + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set<string>()]; - if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisForTimelines.xrevrange( - `channelTimeline:${channel.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); - } + let noteIds = await this.redisTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); - // redis から取得していないとき・取得数が足りないとき - if (noteIdsRes.length < limit) { - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + if (noteIds.length > 0) { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - } - //#endregion + let timeline = await query.getMany(); - timeline = await query.limit(ps.limit).getMany(); - } else { - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + timeline = timeline.filter(note => { + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - if (noteIds.length === 0) { - return []; - } + return true; + }); - //#region Construct query - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + // TODO: フィルタで件数が減った場合の埋め合わせ処理 - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); + + if (timeline.length > 0) { + return await this.noteEntityService.packMany(timeline, me); + } } - //#endregion + } - timeline = await query.getMany(); - timeline.sort((a, b) => a.id > b.id ? -1 : 1); + //#region fallback to database + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.channelId = :channelId', { channelId: channel.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); } + //#endregion - if (me) this.activeUsersChart.read(me); + const timeline = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); + //#endregion }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 779231a856..14a13b09c9 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -41,6 +42,9 @@ export const meta = { export const paramDef = { type: 'object', properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, fileId: { type: 'string', format: 'misskey:id' }, }, required: ['fileId'], @@ -56,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { // Fetch file @@ -68,9 +73,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchFile); } - const notes = await this.notesRepository.createQueryBuilder('note') - .where(':file = ANY(note.fileIds)', { file: file.id }) - .getMany(); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); + query.andWhere(':file = ANY(note.fileIds)', { file: file.id }); + + const notes = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index 75d4fe3819..8f382eb96b 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -3,29 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository } from '@/models/_.js'; -import type { MiNote } from '@/models/Note.js'; -import { safeForSql } from '@/misc/safe-for-sql.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; - -/* -トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 -ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる - -..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する -*/ - -const rangeA = 1000 * 60 * 60; // 60分 -//const rangeB = 1000 * 60 * 120; // 2時間 -//const coefficient = 1.25; // 「n倍」の部分 -//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか - -const max = 5; +import { FeaturedService } from '@/core/FeaturedService.js'; +import { HashtagService } from '@/core/HashtagService.js'; export const meta = { tags: ['hashtags'], @@ -71,98 +53,18 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private metaService: MetaService, + private featuredService: FeaturedService, + private hashtagService: HashtagService, ) { super(meta, paramDef, async () => { - const instance = await this.metaService.fetch(true); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); - - const now = new Date(); // 5分単位で丸めた現在日時 - now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0); - - const tagNotes = await this.notesRepository.createQueryBuilder('note') - .where('note.createdAt > :date', { date: new Date(now.getTime() - rangeA) }) - .andWhere(new Brackets(qb => { qb - .where('note.visibility = \'public\'') - .orWhere('note.visibility = \'home\''); - })) - .andWhere('note.tags != \'{}\'') - .select(['note.tags', 'note.userId']) - .cache(60000) // 1 min - .getMany(); - - if (tagNotes.length === 0) { - return []; - } - - const tags: { - name: string; - users: MiNote['userId'][]; - }[] = []; - - for (const note of tagNotes) { - for (const tag of note.tags) { - if (hiddenTags.includes(tag)) continue; - - const x = tags.find(x => x.name === tag); - if (x) { - if (!x.users.includes(note.userId)) { - x.users.push(note.userId); - } - } else { - tags.push({ - name: tag, - users: [note.userId], - }); - } - } - } - - // タグを人気順に並べ替え - const hots = tags - .sort((a, b) => b.users.length - a.users.length) - .map(tag => tag.name) - .slice(0, max); - - //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する - const countPromises: Promise<number[]>[] = []; - - const range = 20; - - // 10分 - const interval = 1000 * 60 * 10; - - for (let i = 0; i < range; i++) { - countPromises.push(Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) }) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) }) - .cache(60000) // 1 min - .getRawOne() - .then(x => parseInt(x.count, 10)), - ))); - } - - const countsLog = await Promise.all(countPromises); - //#endregion + const ranking = await this.featuredService.getHashtagsRanking(10); - const totalCounts = await Promise.all(hots.map(tag => this.notesRepository.createQueryBuilder('note') - .select('count(distinct note.userId)') - .where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`) - .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) }) - .cache(60000 * 60) // 60 min - .getRawOne() - .then(x => parseInt(x.count, 10)), - )); + const charts = ranking.length === 0 ? {} : await this.hashtagService.getCharts(ranking, 20); - const stats = hots.map((tag, i) => ({ + const stats = ranking.map((tag, i) => ({ tag, - chart: countsLog.map(counts => counts[i]), - usersCount: totalCounts[i], + chart: charts[tag], + usersCount: Math.max(...charts[tag]), })); return stats; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 976380e6fd..d37919b474 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -181,6 +181,11 @@ export const meta = { }, }, }, + notesPerOneAd: { + type: 'number', + optional: false, nullable: false, + default: 0, + }, requireSetup: { type: 'boolean', optional: false, nullable: false, @@ -214,11 +219,11 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - localTimeLine: { + localTimeline: { type: 'boolean', optional: false, nullable: false, }, - globalTimeLine: { + globalTimeline: { type: 'boolean', optional: false, nullable: false, }, @@ -299,6 +304,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, @@ -330,6 +337,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- imageUrl: ad.imageUrl, dayOfWeek: ad.dayOfWeek, })), + notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 1a82a4b5d7..1e569d9806 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -49,16 +49,19 @@ 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 }) - .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'); + .andWhere(new Brackets(qb => { + qb + .where('note.replyId = :noteId', { noteId: ps.noteId }) + .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'); + })); })); - })); })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 37a0525e25..3ae4ac044a 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -57,6 +57,12 @@ export const meta = { id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', }, + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + noSuchReplyTarget: { message: 'No such reply target.', code: 'NO_SUCH_REPLY_TARGET', @@ -231,6 +237,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.youHaveBeenBlocked); } } + + if (renote.visibility === 'followers' && renote.userId !== me.id) { + // 他人のfollowers noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } } let reply: MiNote | null = null; diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 5283b0e0bc..c456874309 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -6,9 +6,9 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['notes'], @@ -32,7 +32,7 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, + untilId: { type: 'string', format: 'misskey:id' }, channelId: { type: 'string', nullable: true, format: 'misskey:id' }, }, required: [], @@ -40,41 +40,53 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + private globalNotesRankingCache: string[] = []; + private globalNotesRankingCacheLastFetchedAt = 0; + constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, + private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { - const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで + let noteIds: string[]; + if (ps.channelId) { + noteIds = await this.featuredService.getInChannelNotesRanking(ps.channelId, 50); + } else { + if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + noteIds = this.globalNotesRankingCache; + } else { + noteIds = await this.featuredService.getGlobalNotesRanking(100); + this.globalNotesRankingCache = noteIds; + this.globalNotesRankingCacheLastFetchedAt = Date.now(); + } + } + + if (noteIds.length === 0) { + return []; + } + + noteIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + noteIds = noteIds.filter(id => id < ps.untilId!); + } + noteIds = noteIds.slice(0, ps.limit); const query = this.notesRepository.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere('note.score > 0') - .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) - .andWhere('note.visibility = \'public\'') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); - - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); - - let notes = await query - .orderBy('note.score', 'DESC') - .limit(100) - .getMany(); + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); - notes = notes.slice(ps.offset, ps.offset + ps.limit); + // TODO: ミュート等考慮 return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index e5a86905d6..be7557c213 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -67,8 +67,37 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.gtlDisabled); } - // TODO? - return []; + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.limit(ps.limit).getMany(); + + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return await this.noteEntityService.packMany(timeline, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index d6ed3db6e3..8ac5f1b038 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -15,6 +15,7 @@ import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -54,6 +55,7 @@ export const paramDef = { includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -72,8 +74,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const policies = await this.roleService.getUserPolicies(me.id); if (!policies.ltlAvailable) { throw new ApiError(meta.errors.stlDisabled); @@ -89,28 +95,29 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]); - let timeline: MiNote[] = []; - - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - let htlNoteIdsRes: [string, string[]][] = []; - let ltlNoteIdsRes: [string, string[]][] = []; + let noteIds: string[]; - if (!ps.sinceId && !ps.sinceDate) { - htlNoteIdsRes = await this.redisForTimelines.xrevrange( - ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); - ltlNoteIdsRes = await this.redisForTimelines.xrevrange( - ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); + if (ps.withFiles) { + const [htlNoteIds, ltlNoteIds] = await this.redisTimelineService.getMulti([ + `homeTimelineWithFiles:${me.id}`, + 'localTimelineWithFiles', + ], untilId, sinceId); + noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); + } else if (ps.withReplies) { + const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.redisTimelineService.getMulti([ + `homeTimeline:${me.id}`, + 'localTimeline', + 'localTimelineWithReplies', + ], untilId, sinceId); + noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); + } else { + const [htlNoteIds, ltlNoteIds] = await this.redisTimelineService.getMulti([ + `homeTimeline:${me.id}`, + 'localTimeline', + ], untilId, sinceId); + noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); } - const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); - const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); - let noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); noteIds.sort((a, b) => a > b ? -1 : 1); noteIds = noteIds.slice(0, ps.limit); @@ -127,7 +134,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - timeline = await query.getMany(); + let timeline = await query.getMany(); timeline = timeline.filter(note => { if (note.userId === me.id) { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index ed57ca1a30..d10c3bedbf 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -15,6 +15,7 @@ import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,6 +45,7 @@ export const paramDef = { properties: { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -68,8 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const policies = await this.roleService.getUserPolicies(me ? me.id : null); if (!policies.ltlAvailable) { throw new ApiError(meta.errors.ltlDisabled); @@ -85,20 +91,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]) : [new Set<string>(), new Set<string>(), new Set<string>()]; - let timeline: MiNote[] = []; - - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - let noteIdsRes: [string, string[]][] = []; + let noteIds: string[]; - if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisForTimelines.xrevrange( - ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); + if (ps.withFiles) { + noteIds = await this.redisTimelineService.get('localTimelineWithFiles', untilId, sinceId); + } else { + const [nonReplyNoteIds, replyNoteIds] = await this.redisTimelineService.getMulti([ + 'localTimeline', + 'localTimelineWithReplies', + ], untilId, sinceId); + noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds])); + noteIds.sort((a, b) => a > b ? -1 : 1); } - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; @@ -113,12 +119,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - timeline = await query.getMany(); + let timeline = await query.getMany(); timeline = timeline.filter(note => { if (me && (note.userId === me.id)) { return true; } + if (!ps.withReplies && note.replyId && (me == null || note.replyUserId !== me.id)) return false; if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; if (note.renoteId) { @@ -131,6 +138,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return true; }); + // TODO: フィルタした結果件数が足りなかった場合の対応 + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 65e7bd8cd5..6fab024d17 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -59,9 +59,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .where('following.followerId = :followerId', { followerId: me.id }); const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(`'{"${me.id}"}' <@ note.mentions`) - .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); + .andWhere(new Brackets(qb => { + qb + .where(`'{"${me.id}"}' <@ note.mentions`) + .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); })) // Avoid scanning primary key index .orderBy('CONCAT(note.id)', 'DESC') diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 29190af62a..986201e950 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -57,9 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .where('poll.userHost IS NULL') .andWhere('poll.userId != :meId', { meId: me.id }) .andWhere('poll.noteVisibility = \'public\'') - .andWhere(new Brackets(qb => { qb - .where('poll.expiresAt IS NULL') - .orWhere('poll.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('poll.expiresAt IS NULL') + .orWhere('poll.expiresAt > :now', { now: new Date() }); })); //#region exclude arleady voted polls diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 2f25d2d7ce..760d52c9db 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; export const meta = { tags: ['notes'], @@ -62,8 +63,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const [ followings, userIdsWhoMeMuting, @@ -76,20 +81,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]); - let timeline: MiNote[] = []; - - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - let noteIdsRes: [string, string[]][] = []; - - if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisForTimelines.xrevrange( - ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); - } - - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + let noteIds = await this.redisTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; @@ -104,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - timeline = await query.getMany(); + let timeline = await query.getMany(); timeline = timeline.filter(note => { if (note.userId === me.id) { diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index ae2401c4c0..f7ee58264e 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -79,8 +80,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, private idService: IdService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const list = await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, @@ -100,20 +105,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]); - let timeline: MiNote[] = []; - - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - let noteIdsRes: [string, string[]][] = []; - - if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisForTimelines.xrevrange( - ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); - } - - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + let noteIds = await this.redisTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; @@ -128,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - timeline = await query.getMany(); + let timeline = await query.getMany(); timeline = timeline.filter(note => { if (note.userId === me.id) { diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index f2533efa36..0db51abc55 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -65,8 +66,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, @@ -78,18 +83,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!role.isExplorable) { return []; } - const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - const noteIdsRes = await this.redisForTimelines.xrevrange( - `roleTimeline:${role.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', - 'COUNT', limit); - - if (noteIdsRes.length === 0) { - return []; - } - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + let noteIds = await this.redisTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 37aac908b5..caaa3735e9 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -62,9 +62,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) .andWhere('assign.roleId = :roleId', { roleId: role.id }) - .andWhere(new Brackets(qb => { qb - .where('assign.expiresAt IS NULL') - .orWhere('assign.expiresAt > :now', { now: new Date() }); + .andWhere(new Brackets(qb => { + qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); })) .innerJoinAndSelect('assign.user', 'user'); diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts new file mode 100644 index 0000000000..dec0b7a122 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private featuredService: FeaturedService, + ) { + super(meta, paramDef, async (ps, me) => { + let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); + + noteIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + noteIds = noteIds.filter(id => id < ps.untilId!); + } + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); + + // TODO: ミュート等考慮 + + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index abcc02eace..df0951ce74 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -10,16 +10,16 @@ import type { MiNote, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { QueryService } from '@/core/QueryService.js'; +import { RedisTimelineService } from '@/core/RedisTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['users', 'notes'], - description: 'Show all notes that this user created.', - res: { type: 'array', optional: false, nullable: false, @@ -45,6 +45,7 @@ export const paramDef = { userId: { type: 'string', format: 'misskey:id' }, withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -67,58 +68,118 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private getterService: GetterService, + private queryService: QueryService, private cacheService: CacheService, private idService: IdService, + private redisTimelineService: RedisTimelineService, ) { super(meta, paramDef, async (ps, me) => { - let timeline: MiNote[] = []; + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.genId(new Date(ps.untilDate!)) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.genId(new Date(ps.sinceDate!)) : null); + const isRangeSpecified = untilId != null && sinceId != null; + const isSelf = me && (me.id === ps.userId); - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - let noteIdsRes: [string, string[]][] = []; + if (isRangeSpecified || sinceId == null) { + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set<string>()]; - if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisForTimelines.xrevrange( - ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : ps.withReplies ? `userTimelineWithReplies:${ps.userId}` : `userTimeline:${ps.userId}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', - 'COUNT', limit); - } + const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ + this.redisTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), + ps.withReplies ? this.redisTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), + ps.withChannelNotes ? this.redisTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), + ]); - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + let noteIds = Array.from(new Set([ + ...noteIdsRes, + ...repliesNoteIdsRes, + ...channelNoteIdsRes, + ])); + noteIds.sort((a, b) => a > b ? -1 : 1); + noteIds = noteIds.slice(0, ps.limit); - if (noteIds.length === 0) { - return []; - } + if (noteIds.length > 0) { + const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + let timeline = await query.getMany(); + + timeline = timeline.filter(note => { + if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; + + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (ps.withRenotes === false) return false; + } + } + + if (note.channel?.isSensitive && !isSelf) return false; + if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; + if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; + + return true; + }); + + // TODO: フィルタで件数が減った場合の埋め合わせ処理 - const isFollowing = me ? Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId) : false; + timeline.sort((a, b) => a.id > b.id ? -1 : 1); - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + if (timeline.length > 0) { + return await this.noteEntityService.packMany(timeline, me); + } + } + } + + //#region fallback to database + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.userId = :userId', { userId: ps.userId }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('note.channel', 'channel') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + .leftJoinAndSelect('renote.user', 'renoteUser'); - timeline = await query.getMany(); + if (ps.withChannelNotes) { + if (!isSelf) query.andWhere('channel.isSensitive = false'); + } else { + query.andWhere('note.channelId IS NULL'); + } - timeline = timeline.filter(note => { - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (ps.withRenotes === false) return false; - } - } + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); + this.queryService.generateBlockedUserQuery(query, me); + } - if (note.visibility === 'followers' && !isFollowing) return false; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } - return true; - }); + if (ps.includeMyRenotes === false) { + 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)'); + })); + } - timeline.sort((a, b) => a.id > b.id ? -1 : 1); + const timeline = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); + //#endregion }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 74408cc64a..4bf25d9fbb 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -92,9 +92,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .andWhere(`user.id IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })); query.setParameters(followingQuery.getParameters()); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index aff5b98779..32b5c12372 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -64,9 +64,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (isUsername) { const usernameQuery = this.usersRepository.createQueryBuilder('user') .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) .andWhere('user.isSuspended = FALSE'); @@ -91,9 +92,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); } })) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) .andWhere('user.isSuspended = FALSE'); @@ -122,9 +124,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const query = this.usersRepository.createQueryBuilder('user') .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) .andWhere('user.isSuspended = FALSE') .setParameters(profQuery.getParameters()); |