feat: improve tl performance (#11946)
* wip * wip * wip * wip * wip * wip * Update NoteCreateService.ts * wip * wip * wip * wip * Update NoteCreateService.ts * wip * Update NoteCreateService.ts * wip * Update user-notes.ts * wip * wip * wip * Update NoteCreateService.ts * wip * Update timeline.ts * Update timeline.ts * Update timeline.ts * Update timeline.ts * Update timeline.ts * wip * Update timelines.ts * Update timelines.ts * Update timelines.ts * wip * wip * wip * Update timelines.ts * Update misskey-js.api.md * Update timelines.ts * Update timelines.ts * wip * wip * wip * Update timelines.ts * wip * Update timelines.ts * wip * test * Update activitypub.ts * refactor: UserListJoining -> UserListMembership * Update NoteCreateService.ts * wip
This commit is contained in:
@@ -205,7 +205,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
||||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||
import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js';
|
||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
||||
@@ -336,7 +335,9 @@ import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
|
||||
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
||||
@@ -554,7 +555,6 @@ const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass:
|
||||
const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default };
|
||||
const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default };
|
||||
const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default };
|
||||
const $i_getWordMutedNotesCount: Provider = { provide: 'ep:i/get-word-muted-notes-count', useClass: ep___i_getWordMutedNotesCount.default };
|
||||
const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default };
|
||||
const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default };
|
||||
const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default };
|
||||
@@ -685,7 +685,9 @@ const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass:
|
||||
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
|
||||
const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
|
||||
const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
|
||||
const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
|
||||
const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default };
|
||||
const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default };
|
||||
const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default };
|
||||
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
|
||||
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
|
||||
const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default };
|
||||
@@ -907,7 +909,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_favorites,
|
||||
$i_gallery_likes,
|
||||
$i_gallery_posts,
|
||||
$i_getWordMutedNotesCount,
|
||||
$i_importBlocking,
|
||||
$i_importFollowing,
|
||||
$i_importMuting,
|
||||
@@ -1038,7 +1039,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_lists_createFromPublic,
|
||||
$users_lists_updateMembership,
|
||||
$users_lists_getMemberships,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_flashs,
|
||||
@@ -1254,7 +1257,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_favorites,
|
||||
$i_gallery_likes,
|
||||
$i_gallery_posts,
|
||||
$i_getWordMutedNotesCount,
|
||||
$i_importBlocking,
|
||||
$i_importFollowing,
|
||||
$i_importMuting,
|
||||
@@ -1382,7 +1384,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_lists_createFromPublic,
|
||||
$users_lists_updateMembership,
|
||||
$users_lists_getMemberships,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_flashs,
|
||||
|
@@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/Connection.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
@@ -37,6 +38,7 @@ export class StreamingApiServerService {
|
||||
private authenticateService: AuthenticateService,
|
||||
private channelsService: ChannelsService,
|
||||
private notificationService: NotificationService,
|
||||
private usersService: UserService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -130,14 +132,10 @@ export class StreamingApiServerService {
|
||||
this.#connections.set(connection, Date.now());
|
||||
|
||||
const userUpdateIntervalId = user ? setInterval(() => {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
this.usersService.updateLastActiveDate(user);
|
||||
}, 1000 * 60 * 5) : null;
|
||||
if (user) {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
this.usersService.updateLastActiveDate(user);
|
||||
}
|
||||
|
||||
connection.once('close', () => {
|
||||
|
@@ -205,7 +205,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
||||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||
import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js';
|
||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
||||
@@ -335,8 +334,10 @@ import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
||||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
|
||||
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
import * as ep___users_flashs from './endpoints/users/flashs.js';
|
||||
@@ -552,7 +553,6 @@ const eps = [
|
||||
['i/favorites', ep___i_favorites],
|
||||
['i/gallery/likes', ep___i_gallery_likes],
|
||||
['i/gallery/posts', ep___i_gallery_posts],
|
||||
['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount],
|
||||
['i/import-blocking', ep___i_importBlocking],
|
||||
['i/import-following', ep___i_importFollowing],
|
||||
['i/import-muting', ep___i_importMuting],
|
||||
@@ -683,7 +683,9 @@ const eps = [
|
||||
['users/lists/favorite', ep___users_lists_favorite],
|
||||
['users/lists/unfavorite', ep___users_lists_unfavorite],
|
||||
['users/lists/update', ep___users_lists_update],
|
||||
['users/lists/create-from-public', ep___users_lists_create_from_public],
|
||||
['users/lists/create-from-public', ep___users_lists_createFromPublic],
|
||||
['users/lists/update-membership', ep___users_lists_updateMembership],
|
||||
['users/lists/get-memberships', ep___users_lists_getMemberships],
|
||||
['users/notes', ep___users_notes],
|
||||
['users/pages', ep___users_pages],
|
||||
['users/flashs', ep___users_flashs],
|
||||
|
@@ -56,8 +56,8 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
@@ -86,7 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
});
|
||||
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const noteIdsRes = await this.redisClient.xrevrange(
|
||||
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 ?? '-',
|
||||
|
@@ -54,8 +54,8 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
@@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
let noteIdsRes: [string, string[]][] = [];
|
||||
|
||||
if (!ps.sinceId && !ps.sinceDate) {
|
||||
noteIdsRes = await this.redisClient.xrevrange(
|
||||
noteIdsRes = await this.redisForTimelines.xrevrange(
|
||||
`channelTimeline:${channel.id}`,
|
||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
|
||||
'-',
|
||||
@@ -104,7 +104,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
}
|
||||
//#endregion
|
||||
@@ -129,7 +128,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
@@ -57,8 +57,9 @@ export const paramDef = {
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
notify: { type: 'string', enum: ['normal', 'none'] },
|
||||
withReplies: { type: 'boolean' },
|
||||
},
|
||||
required: ['userId', 'notify'],
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
@@ -98,7 +99,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
await this.followingsRepository.update({
|
||||
id: exist.id,
|
||||
}, {
|
||||
notify: ps.notify === 'none' ? null : ps.notify,
|
||||
notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined,
|
||||
withReplies: ps.withReplies != null ? ps.withReplies : undefined,
|
||||
});
|
||||
|
||||
return await this.userEntityService.pack(follower.id, me);
|
||||
|
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { MutedNotesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
count: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.mutedNotesRepository)
|
||||
private mutedNotesRepository: MutedNotesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return {
|
||||
count: await this.mutedNotesRepository.countBy({
|
||||
userId: me.id,
|
||||
reason: 'word',
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@@ -40,7 +40,6 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
@@ -68,49 +67,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.gtlDisabled);
|
||||
}
|
||||
|
||||
//#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');
|
||||
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
// TODO?
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -5,14 +5,16 @@
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
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 { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -51,7 +53,6 @@ export const paramDef = {
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
},
|
||||
required: [],
|
||||
@@ -60,17 +61,17 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
@@ -78,79 +79,75 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.stlDisabled);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
|
||||
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||
}))
|
||||
let timeline: MiNote[] = [];
|
||||
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
let htlNoteIdsRes: [string, string[]][] = [];
|
||||
let ltlNoteIdsRes: [string, 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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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')
|
||||
.setParameters(followingQuery.getParameters());
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
timeline = await query.getMany();
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
|
@@ -5,14 +5,16 @@
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -41,11 +43,7 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
fileType: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
excludeNsfw: { type: 'boolean', default: false },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
@@ -59,14 +57,17 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
@@ -74,56 +75,63 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.ltlDisabled);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
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[]][] = [];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||
|
||||
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('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedNoteQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
timeline = await query.getMany();
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.fileType != null) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const type of ps.fileType!) {
|
||||
const i = ps.fileType!.indexOf(type);
|
||||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
|
||||
}
|
||||
}));
|
||||
|
||||
if (ps.excludeNsfw) {
|
||||
query.andWhere('note.cw IS NULL');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
||||
timeline = timeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
return true;
|
||||
});
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
|
@@ -5,13 +5,16 @@
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
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';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -41,7 +44,6 @@ export const paramDef = {
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
},
|
||||
required: [],
|
||||
@@ -50,96 +52,82 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const followees = await this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id })
|
||||
.getMany();
|
||||
const [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
// パフォーマンス上の利点が無さそう?
|
||||
//.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
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);
|
||||
|
||||
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('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
if (followees.length > 0) {
|
||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||
timeline = await query.getMany();
|
||||
|
||||
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||
} else {
|
||||
query.andWhere('note.userId = :meId', { meId: me.id });
|
||||
}
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
if (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
|
@@ -5,12 +5,16 @@
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
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 { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -49,7 +53,6 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
@@ -63,18 +66,19 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private cacheService: CacheService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const list = await this.userListsRepository.findOneBy({
|
||||
@@ -86,72 +90,65 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
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);
|
||||
|
||||
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')
|
||||
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
timeline = await query.getMany();
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
|
@@ -53,8 +53,8 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
return [];
|
||||
}
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const noteIdsRes = await this.redisClient.xrevrange(
|
||||
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 ?? '-',
|
||||
|
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUserList } from '@/models/UserList.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
@@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
name: ps.name,
|
||||
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const users = (await this.userListJoiningsRepository.findBy({
|
||||
const users = (await this.userListMembershipsRepository.findBy({
|
||||
userListId: ps.listId,
|
||||
})).map(x => x.userId);
|
||||
|
||||
@@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.exist({
|
||||
const exist = await this.userListMembershipsRepository.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: currentUser.id,
|
||||
|
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository, UserListFavoritesRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'account'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
forPublic: { type: 'boolean', default: false },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
|
||||
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private userListEntityService: UserListEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? {
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
} : {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId)
|
||||
.andWhere('membership.userListId = :userListId', { userListId: userList.id })
|
||||
.innerJoinAndSelect('membership.user', 'user');
|
||||
|
||||
const memberships = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.userListEntityService.packMembershipsMany(memberships);
|
||||
});
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
@@ -76,8 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
@@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.exist({
|
||||
const exist = await this.userListMembershipsRepository.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
|
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'users'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02',
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '588e7f72-c744-4a61-b180-d354e912bda2',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
withReplies: { type: 'boolean' },
|
||||
},
|
||||
required: ['listId', 'userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
private userListService: UserListService,
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
await this.userListService.updateMembership(user, userList, {
|
||||
withReplies: ps.withReplies,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -5,12 +5,14 @@
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiNote, 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 { GetterService } from '@/server/api/GetterService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -50,9 +52,6 @@ export const paramDef = {
|
||||
untilDate: { type: 'integer' },
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
fileType: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
excludeNsfw: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['userId'],
|
||||
@@ -61,87 +60,63 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private getterService: GetterService,
|
||||
private cacheService: CacheService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Lookup user
|
||||
const user = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
let timeline: MiNote[] = [];
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
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 ? `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 noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isFollowing = me ? Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId) : false;
|
||||
|
||||
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('note.channel', 'channel')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.channelId IS NULL');
|
||||
qb.orWhere('channel.isSensitive = false');
|
||||
}));
|
||||
timeline = await query.getMany();
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me, user);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.fileType != null) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const type of ps.fileType!) {
|
||||
const i = ps.fileType!.indexOf(type);
|
||||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}));
|
||||
|
||||
if (ps.excludeNsfw) {
|
||||
query.andWhere('note.cw IS NULL');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
||||
}
|
||||
}
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
if (note.visibility === 'followers' && !isFollowing) return false;
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :userId', { userId: user.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
});
|
||||
|
@@ -11,7 +11,7 @@ import type { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import type { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiUserProfile } from '@/models/_.js';
|
||||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
@@ -30,7 +30,7 @@ export default class Connection {
|
||||
private subscribingNotes: any = {};
|
||||
private cachedNotes: Packed<'Note'>[] = [];
|
||||
public userProfile: MiUserProfile | null = null;
|
||||
public following: Set<string> = new Set();
|
||||
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||
public followingChannels: Set<string> = new Set();
|
||||
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||
|
@@ -18,7 +18,6 @@ class GlobalTimelineChannel extends Channel {
|
||||
public readonly chName = 'globalTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -38,7 +37,6 @@ class GlobalTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.gtlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Subscribe events
|
||||
@@ -64,7 +62,7 @@ class GlobalTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.withReplies) {
|
||||
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
@@ -82,13 +80,6 @@ class GlobalTimelineChannel extends Channel {
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
|
@@ -16,7 +16,6 @@ class HomeTimelineChannel extends Channel {
|
||||
public readonly chName = 'homeTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -31,7 +30,6 @@ class HomeTimelineChannel extends Channel {
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@@ -43,7 +41,7 @@ class HomeTimelineChannel extends Channel {
|
||||
if (!this.followingChannels.has(note.channelId)) return;
|
||||
} else {
|
||||
// その投稿のユーザーをフォローしていなかったら弾く
|
||||
if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return;
|
||||
if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return;
|
||||
}
|
||||
|
||||
// Ignore notes from instances the user has muted
|
||||
@@ -73,7 +71,7 @@ class HomeTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.withReplies) {
|
||||
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
@@ -88,13 +86,6 @@ class HomeTimelineChannel extends Channel {
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||
if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
|
@@ -18,7 +18,6 @@ class HybridTimelineChannel extends Channel {
|
||||
public readonly chName = 'hybridTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -38,7 +37,6 @@ class HybridTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Subscribe events
|
||||
@@ -53,7 +51,7 @@ class HybridTimelineChannel extends Channel {
|
||||
// フォローしているチャンネルの投稿 の場合だけ
|
||||
if (!(
|
||||
(note.channelId == null && this.user!.id === note.userId) ||
|
||||
(note.channelId == null && this.following.has(note.userId)) ||
|
||||
(note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
|
||||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
|
||||
(note.channelId != null && this.followingChannels.has(note.channelId))
|
||||
)) return;
|
||||
@@ -85,7 +83,7 @@ class HybridTimelineChannel extends Channel {
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.withReplies) {
|
||||
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
@@ -100,13 +98,6 @@ class HybridTimelineChannel extends Channel {
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
|
@@ -17,7 +17,6 @@ class LocalTimelineChannel extends Channel {
|
||||
public readonly chName = 'localTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -37,7 +36,6 @@ class LocalTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Subscribe events
|
||||
@@ -64,7 +62,7 @@ class LocalTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && this.user && !this.withReplies) {
|
||||
if (note.reply && this.user && !this.following[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||
@@ -79,13 +77,6 @@ class LocalTimelineChannel extends Channel {
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
|
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
@@ -18,12 +18,12 @@ class UserListChannel extends Channel {
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = false;
|
||||
private listId: string;
|
||||
public listUsers: MiUser['id'][] = [];
|
||||
public membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
||||
id: string,
|
||||
@@ -58,19 +58,25 @@ class UserListChannel extends Channel {
|
||||
|
||||
@bindThis
|
||||
private async updateListUsers() {
|
||||
const users = await this.userListJoiningsRepository.find({
|
||||
const memberships = await this.userListMembershipsRepository.find({
|
||||
where: {
|
||||
userListId: this.listId,
|
||||
},
|
||||
select: ['userId'],
|
||||
});
|
||||
|
||||
this.listUsers = users.map(x => x.userId);
|
||||
const membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
for (const membership of memberships) {
|
||||
membershipsMap[membership.userId] = {
|
||||
withReplies: membership.withReplies,
|
||||
};
|
||||
}
|
||||
this.membershipsMap = membershipsMap;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
if (!this.listUsers.includes(note.userId)) return;
|
||||
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||
|
||||
if (['followers', 'specified'].includes(note.visibility)) {
|
||||
note = await this.noteEntityService.pack(note.id, this.user, {
|
||||
@@ -95,6 +101,13 @@ class UserListChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.membershipsMap[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
@@ -124,8 +137,8 @@ export class UserListChannelService {
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {
|
||||
@@ -135,7 +148,7 @@ export class UserListChannelService {
|
||||
public create(id: string, connection: Channel['connection']): UserListChannel {
|
||||
return new UserListChannel(
|
||||
this.userListsRepository,
|
||||
this.userListJoiningsRepository,
|
||||
this.userListMembershipsRepository,
|
||||
this.noteEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
Reference in New Issue
Block a user