summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/GlobalModule.ts14
-rw-r--r--packages/backend/src/config.ts3
-rw-r--r--packages/backend/src/core/AccountMoveService.ts30
-rw-r--r--packages/backend/src/core/AntennaService.ts14
-rw-r--r--packages/backend/src/core/CacheService.ts19
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/NoteCreateService.ts247
-rw-r--r--packages/backend/src/core/NoteEditService.ts203
-rw-r--r--packages/backend/src/core/NotificationService.ts8
-rw-r--r--packages/backend/src/core/QueryService.ts64
-rw-r--r--packages/backend/src/core/UserBlockingService.ts8
-rw-r--r--packages/backend/src/core/UserFollowingService.ts6
-rw-r--r--packages/backend/src/core/UserListService.ts36
-rw-r--r--packages/backend/src/core/UserService.ts53
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts6
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/UserListEntityService.ts24
-rw-r--r--packages/backend/src/di-symbols.ts4
-rw-r--r--packages/backend/src/models/Following.ts12
-rw-r--r--packages/backend/src/models/MutedNote.ts53
-rw-r--r--packages/backend/src/models/RepositoryModule.ts20
-rw-r--r--packages/backend/src/models/User.ts5
-rw-r--r--packages/backend/src/models/UserListJoining.ts (renamed from packages/backend/src/models/UserListMembership.ts)12
-rw-r--r--packages/backend/src/models/_.ts9
-rw-r--r--packages/backend/src/models/json-schema/user.ts4
-rw-r--r--packages/backend/src/postgres.ts6
-rw-r--r--packages/backend/src/queue/processors/CleanProcessorService.ts15
-rw-r--r--packages/backend/src/queue/processors/ExportAntennasProcessorService.ts10
-rw-r--r--packages/backend/src/queue/processors/ExportUserListsProcessorService.ts10
-rw-r--r--packages/backend/src/queue/processors/ImportUserListsProcessorService.ts8
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts20
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts10
-rw-r--r--packages/backend/src/server/api/endpoints.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/following/update.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts51
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts46
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts135
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts102
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts132
-rw-r--r--packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts120
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts79
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/push.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/update-membership.ts79
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts101
-rw-r--r--packages/backend/src/server/api/stream/Connection.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts13
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts13
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts31
54 files changed, 779 insertions, 1143 deletions
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 3e9d19f825..9f1ee9fcaa 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -70,19 +70,11 @@ const $redisForSub: Provider = {
inject: [DI.config],
};
-const $redisForTimelines: Provider = {
- provide: DI.redisForTimelines,
- useFactory: (config: Config) => {
- return new Redis.Redis(config.redisForTimelines);
- },
- inject: [DI.config],
-};
-
@Global()
@Module({
imports: [RepositoryModule],
- providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
- exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
+ providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
+ exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
@@ -90,7 +82,6 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redis) private redisClient: Redis.Redis,
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
- @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
) {}
public async dispose(): Promise<void> {
@@ -107,7 +98,6 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisClient.disconnect(),
this.redisForPub.disconnect(),
this.redisForSub.disconnect(),
- this.redisForTimelines.disconnect(),
]);
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index ef59a80950..f89879d535 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -47,7 +47,6 @@ type Source = {
redis: RedisOptionsSource;
redisForPubsub?: RedisOptionsSource;
redisForJobQueue?: RedisOptionsSource;
- redisForTimelines?: RedisOptionsSource;
meilisearch?: {
host: string;
port: string;
@@ -162,7 +161,6 @@ export type Config = {
redis: RedisOptions & RedisOptionsSource;
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
- redisForTimelines: RedisOptions & RedisOptionsSource;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
@@ -229,7 +227,6 @@ export function loadConfig(): Config {
redis,
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
- redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index ba3413007d..ec1d013922 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
-import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
+import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js';
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
import { IdService } from '@/core/IdService.js';
@@ -42,8 +42,8 @@ export class AccountMoveService {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
@@ -215,40 +215,40 @@ export class AccountMoveService {
@bindThis
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
// Return if there is no list to be updated.
- const oldMemberships = await this.userListMembershipsRepository.find({
+ const oldJoinings = await this.userListJoiningsRepository.find({
where: {
userId: src.id,
},
});
- if (oldMemberships.length === 0) return;
+ if (oldJoinings.length === 0) return;
- const existingUserListIds = await this.userListMembershipsRepository.find({
+ const existingUserListIds = await this.userListJoiningsRepository.find({
where: {
userId: dst.id,
},
- }).then(memberships => memberships.map(membership => membership.userListId));
+ }).then(joinings => joinings.map(joining => joining.userListId));
- const newMemberships: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
+ const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
// 重複しないようにIDを生成
const genId = (): string => {
let id: string;
do {
id = this.idService.genId();
- } while (newMemberships.has(id));
+ } while (newJoinings.has(id));
return id;
};
- for (const membership of oldMemberships) {
- if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list
- newMemberships.set(genId(), {
+ for (const joining of oldJoinings) {
+ if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
+ newJoinings.set(genId(), {
createdAt: new Date(),
userId: dst.id,
- userListId: membership.userListId,
+ userListId: joining.userListId,
});
}
- const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
- await this.userListMembershipsRepository.insert(arrayToInsert);
+ const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
+ await this.userListJoiningsRepository.insert(arrayToInsert);
// Have the proxy account follow the new account in the same way as UserListService.push
if (this.userEntityService.isRemoteUser(dst)) {
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 95712b35b7..d9f27b8c63 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -12,7 +12,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
-import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
+import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
@@ -24,8 +24,8 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: MiAntenna[];
constructor(
- @Inject(DI.redisForTimelines)
- private redisForTimelines: Redis.Redis,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@@ -33,8 +33,8 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private utilityService: UtilityService,
private globalEventService: GlobalEventService,
@@ -81,7 +81,7 @@ export class AntennaService implements OnApplicationShutdown {
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
- const redisPipeline = this.redisForTimelines.pipeline();
+ const redisPipeline = this.redisClient.pipeline();
for (const antenna of matchedAntennas) {
redisPipeline.xadd(
@@ -108,7 +108,7 @@ export class AntennaService implements OnApplicationShutdown {
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
- const listUsers = (await this.userListMembershipsRepository.findBy({
+ const listUsers = (await this.userListJoiningsRepository.findBy({
userListId: antenna.userListId!,
})).map(x => x.userId);
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index 22c510cc37..561979c4bf 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
+import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
@@ -25,7 +25,7 @@ export class CacheService implements OnApplicationShutdown {
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
- public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
+ public userFollowingsCache: RedisKVCache<Set<string>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor(
@@ -136,18 +136,12 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
- this.userFollowingsCache = new RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.redisClient, 'userFollowings', {
+ this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
- fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => {
- const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
- for (const x of xs) {
- obj[x.followeeId] = { withReplies: x.withReplies };
- }
- return obj;
- }),
- toRedisConverter: (value) => JSON.stringify(value),
- fromRedisConverter: (value) => JSON.parse(value),
+ fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
+ toRedisConverter: (value) => JSON.stringify(Array.from(value)),
+ fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
@@ -194,7 +188,6 @@ export class CacheService implements OnApplicationShutdown {
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
- this.userFollowingsCache.delete(body.followerId);
break;
}
default:
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 955b9fdcf6..7d6b76e9c2 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -47,7 +47,6 @@ import { SignupService } from './SignupService.js';
import { WebAuthnService } from './WebAuthnService.js';
import { UserBlockingService } from './UserBlockingService.js';
import { CacheService } from './CacheService.js';
-import { UserService } from './UserService.js';
import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js';
@@ -176,7 +175,6 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
-const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
@@ -308,7 +306,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebAuthnService,
UserBlockingService,
CacheService,
- UserService,
UserFollowingService,
UserKeypairService,
UserListService,
@@ -433,7 +430,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebAuthnService,
$UserBlockingService,
$CacheService,
- $UserService,
$UserFollowingService,
$UserKeypairService,
$UserListService,
@@ -559,7 +555,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebAuthnService,
UserBlockingService,
CacheService,
- UserService,
UserFollowingService,
UserKeypairService,
UserListService,
@@ -683,7 +678,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebAuthnService,
$UserBlockingService,
$CacheService,
- $UserService,
$UserFollowingService,
$UserKeypairService,
$UserListService,
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 8fb34fd637..f20727ce41 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -5,7 +5,7 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
-import { In, DataSource, IsNull, LessThan } from 'typeorm';
+import { In, DataSource } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2';
@@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
-import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@@ -54,6 +54,8 @@ import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
+const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5);
+
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
class NotificationManager {
@@ -155,8 +157,8 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redisForTimelines)
- private redisForTimelines: Redis.Redis,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -173,8 +175,8 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.mutedNotesRepository)
+ private mutedNotesRepository: MutedNotesRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@@ -185,9 +187,6 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- @Inject(DI.channelFollowingsRepository)
- private channelFollowingsRepository: ChannelFollowingsRepository,
-
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
@@ -335,7 +334,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
if (data.channel) {
- this.redisForTimelines.xadd(
+ this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
'*',
@@ -481,13 +480,26 @@ export class NoteCreateService implements OnApplicationShutdown {
// Increment notes count (user)
this.incNotesCountOfUser(user);
- if (data.visibility === 'public' || data.visibility === 'home') {
- this.pushToTl(note, user);
- } else if (data.visibility === 'followers') {
- this.pushToTl(note, user);
- } else if (data.visibility === 'specified') {
- // TODO
- }
+ // Word mute
+ mutedWordsCache.fetch(() => this.userProfilesRepository.find({
+ where: {
+ enableWordMute: true,
+ },
+ select: ['userId', 'mutedWords'],
+ })).then(us => {
+ for (const u of us) {
+ checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
+ if (shouldMute) {
+ this.mutedNotesRepository.insert({
+ id: this.idService.genId(),
+ userId: u.userId,
+ noteId: note.id,
+ reason: 'word',
+ });
+ }
+ });
+ }
+ });
this.antennaService.addNoteToAntennas(note, user);
@@ -496,13 +508,11 @@ export class NoteCreateService implements OnApplicationShutdown {
}
if (data.reply == null) {
- // TODO: キャッシュ
this.followingsRepository.findBy({
followeeId: user.id,
notify: 'normal',
}).then(followings => {
for (const following of followings) {
- // TODO: ワードミュート考慮
this.notificationService.createNotification(following.followerId, 'note', {
noteId: note.id,
}, user.id);
@@ -802,205 +812,6 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
- const redisPipeline = this.redisForTimelines.pipeline();
-
- if (note.channelId) {
- const channelFollowings = await this.channelFollowingsRepository.find({
- where: {
- followeeId: note.channelId,
- },
- select: ['followerId'],
- });
-
- for (const channelFollowing of channelFollowings) {
- redisPipeline.xadd(
- `homeTimeline:${channelFollowing.followerId}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `homeTimelineWithFiles:${channelFollowing.followerId}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
- } else {
- // TODO: キャッシュ?
- const followings = await this.followingsRepository.find({
- where: {
- followeeId: user.id,
- followerHost: IsNull(),
- isFollowerHibernated: false,
- },
- select: ['followerId', 'withReplies'],
- });
-
- const userListMemberships = await this.userListMembershipsRepository.find({
- where: {
- userId: user.id,
- },
- select: ['userListId', 'withReplies'],
- });
-
- // TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
- for (const following of followings) {
- // 自分自身以外への返信
- if (note.replyId && note.replyUserId !== note.userId) {
- if (!following.withReplies) continue;
- }
-
- redisPipeline.xadd(
- `homeTimeline:${following.followerId}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `homeTimelineWithFiles:${following.followerId}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
-
- // TODO
- //if (note.visibility === 'followers') {
- // // TODO: 重そうだから何とかしたい Set 使う?
- // userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId));
- //}
-
- for (const userListMembership of userListMemberships) {
- // 自分自身以外への返信
- if (note.replyId && note.replyUserId !== note.userId) {
- if (!userListMembership.withReplies) continue;
- }
-
- redisPipeline.xadd(
- `userListTimeline:${userListMembership.userListId}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `userListTimelineWithFiles:${userListMembership.userListId}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
-
- { // 自分自身のHTL
- redisPipeline.xadd(
- `homeTimeline:${user.id}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `homeTimelineWithFiles:${user.id}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
-
- if (note.visibility === 'public' || note.visibility === 'home') {
- // 自分自身以外への返信
- if (note.replyId && note.replyUserId !== note.userId) {
- redisPipeline.xadd(
- `userTimelineWithReplies:${user.id}`,
- 'MAXLEN', '~', '1000',
- '*',
- 'note', note.id);
- } else {
- redisPipeline.xadd(
- `userTimeline:${user.id}`,
- 'MAXLEN', '~', '1000',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `userTimelineWithFiles:${user.id}`,
- 'MAXLEN', '~', '500',
- '*',
- 'note', note.id);
- }
-
- if (note.visibility === 'public' && note.userHost == null) {
- redisPipeline.xadd(
- 'localTimeline',
- 'MAXLEN', '~', '1000',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- 'localTimelineWithFiles',
- 'MAXLEN', '~', '500',
- '*',
- 'note', note.id);
- }
- }
- }
- }
-
- if (Math.random() < 0.1) {
- process.nextTick(() => {
- this.checkHibernation(followings);
- });
- }
- }
-
- redisPipeline.exec();
- }
-
- @bindThis
- public async checkHibernation(followings: MiFollowing[]) {
- if (followings.length === 0) return;
-
- const shuffle = (array: MiFollowing[]) => {
- for (let i = array.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [array[i], array[j]] = [array[j], array[i]];
- }
- return array;
- };
-
- // ランダムに最大1000件サンプリング
- const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
-
- const hibernatedUsers = await this.usersRepository.find({
- where: {
- id: In(samples.map(x => x.followerId)),
- lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
- },
- select: ['id'],
- });
-
- if (hibernatedUsers.length > 0) {
- this.usersRepository.update({
- id: In(hibernatedUsers.map(x => x.id)),
- }, {
- isHibernated: true,
- });
-
- this.followingsRepository.update({
- followerId: In(hibernatedUsers.map(x => x.id)),
- }, {
- isFollowerHibernated: true,
- });
- }
- }
-
- @bindThis
public dispose(): void {
this.#shutdownController.abort();
}
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index f331c8a9a8..d38962fe34 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
-import type { NoteEditRepository, ChannelsRepository, InstancesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository, UserListMembershipsRepository, ChannelFollowingsRepository, MiFollowing } from '@/models/_.js';
+import type { NoteEditRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@@ -49,6 +49,8 @@ import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
+const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5);
+
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
class NotificationManager {
@@ -149,8 +151,8 @@ export class NoteEditService implements OnApplicationShutdown {
@Inject(DI.config)
private config: Config,
- @Inject(DI.redisForTimelines)
- private redisForTimelines: Redis.Redis,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -170,11 +172,8 @@ export class NoteEditService implements OnApplicationShutdown {
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
-
- @Inject(DI.channelFollowingsRepository)
- private channelFollowingsRepository: ChannelFollowingsRepository,
+ @Inject(DI.mutedNotesRepository)
+ private mutedNotesRepository: MutedNotesRepository,
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@@ -423,7 +422,7 @@ export class NoteEditService implements OnApplicationShutdown {
await this.notesRepository.update(oldnote.id, note);
if (data.channel) {
- this.redisForTimelines.xadd(
+ this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
'*',
@@ -461,6 +460,27 @@ export class NoteEditService implements OnApplicationShutdown {
this.hashtagService.updateHashtags(user, tags);
}
+ // Word mute
+ mutedWordsCache.fetch(() => this.userProfilesRepository.find({
+ where: {
+ enableWordMute: true,
+ },
+ select: ['userId', 'mutedWords'],
+ })).then(us => {
+ for (const u of us) {
+ checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
+ if (shouldMute) {
+ this.mutedNotesRepository.insert({
+ id: this.idService.genId(),
+ userId: u.userId,
+ noteId: note.id,
+ reason: 'word',
+ });
+ }
+ });
+ }
+ });
+
if (data.poll && data.poll.expiresAt) {
const delay = data.poll.expiresAt.getTime() - Date.now();
this.queueService.endedPollNotificationQueue.add(note.id, {
@@ -471,14 +491,6 @@ export class NoteEditService implements OnApplicationShutdown {
});
}
- if (data.visibility === 'public' || data.visibility === 'home') {
- this.pushToTl(note, user);
- } else if (data.visibility === 'followers') {
- this.pushToTl(note, user);
- } else if (data.visibility === 'specified') {
- // TODO
- }
-
if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
@@ -640,163 +652,6 @@ export class NoteEditService implements OnApplicationShutdown {
}
@bindThis
- private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
- const redisPipeline = this.redisForTimelines.pipeline();
-
- if (note.channelId) {
- const channelFollowings = await this.channelFollowingsRepository.find({
- where: {
- followeeId: note.channelId,
- },
- select: ['followerId'],
- });
-
- for (const channelFollowing of channelFollowings) {
- redisPipeline.xadd(
- `homeTimeline:${channelFollowing.followerId}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `homeTimelineWithFiles:${channelFollowing.followerId}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
- } else {
- // TODO: キャッシュ?
-
- const userListMemberships = await this.userListMembershipsRepository.find({
- where: {
- userId: user.id,
- },
- select: ['userListId', 'withReplies'],
- });
-
- // TODO
- //if (note.visibility === 'followers') {
- // // TODO: 重そうだから何とかしたい Set 使う?
- // userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId));
- //}
-
- for (const userListMembership of userListMemberships) {
- // 自分自身以外への返信
- if (note.replyId && note.replyUserId !== note.userId) {
- if (!userListMembership.withReplies) continue;
- }
-
- redisPipeline.xadd(
- `userListTimeline:${userListMembership.userListId}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `userListTimelineWithFiles:${userListMembership.userListId}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
-
- { // 自分自身のHTL
- redisPipeline.xadd(
- `homeTimeline:${user.id}`,
- 'MAXLEN', '~', '200',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `homeTimelineWithFiles:${user.id}`,
- 'MAXLEN', '~', '100',
- '*',
- 'note', note.id);
- }
- }
-
- if (note.visibility === 'public' || note.visibility === 'home') {
- // 自分自身以外への返信
- if (note.replyId && note.replyUserId !== note.userId) {
- redisPipeline.xadd(
- `userTimelineWithReplies:${user.id}`,
- 'MAXLEN', '~', '1000',
- '*',
- 'note', note.id);
- } else {
- redisPipeline.xadd(
- `userTimeline:${user.id}`,
- 'MAXLEN', '~', '1000',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- `userTimelineWithFiles:${user.id}`,
- 'MAXLEN', '~', '500',
- '*',
- 'note', note.id);
- }
-
- if (note.visibility === 'public' && note.userHost == null) {
- redisPipeline.xadd(
- 'localTimeline',
- 'MAXLEN', '~', '1000',
- '*',
- 'note', note.id);
-
- if (note.fileIds.length > 0) {
- redisPipeline.xadd(
- 'localTimelineWithFiles',
- 'MAXLEN', '~', '500',
- '*',
- 'note', note.id);
- }
- }
- }
- }
- }
-
- redisPipeline.exec();
- }
-
- @bindThis
- public async checkHibernation(followings: MiFollowing[]) {
- if (followings.length === 0) return;
-
- const shuffle = (array: MiFollowing[]) => {
- for (let i = array.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [array[i], array[j]] = [array[j], array[i]];
- }
- return array;
- };
-
- // ランダムに最大1000件サンプリング
- const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
-
- const hibernatedUsers = await this.usersRepository.find({
- where: {
- id: In(samples.map(x => x.followerId)),
- lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
- },
- select: ['id'],
- });
-
- if (hibernatedUsers.length > 0) {
- this.usersRepository.update({
- id: In(hibernatedUsers.map(x => x.id)),
- }, {
- isHibernated: true,
- });
- }
- }
-
- @bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) {
const text = note.cw ?? note.text ?? '';
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index 32d54d2576..ca05989a4a 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -99,19 +99,19 @@ export class NotificationService implements OnApplicationShutdown {
}
if (recieveConfig?.type === 'following') {
- const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId));
+ const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId));
if (!isFollowing) {
return null;
}
} else if (recieveConfig?.type === 'follower') {
- const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId));
+ const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId));
if (!isFollower) {
return null;
}
} else if (recieveConfig?.type === 'mutualFollow') {
const [isFollowing, isFollower] = await Promise.all([
- this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
- this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
+ this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
+ this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
]);
if (!isFollowing && !isFollower) {
return null;
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index 18bd49286e..9145726f86 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
-import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
+import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { SelectQueryBuilder } from 'typeorm';
@@ -23,6 +23,9 @@ export class QueryService {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
+ @Inject(DI.mutedNotesRepository)
+ private mutedNotesRepository: MutedNotesRepository,
+
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@@ -106,6 +109,39 @@ export class QueryService {
}
@bindThis
+ public generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
+ if (me == null) {
+ q.andWhere('note.channelId IS NULL');
+ } else {
+ q.leftJoinAndSelect('note.channel', 'channel');
+
+ const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
+ .select('channelFollowing.followeeId')
+ .where('channelFollowing.followerId = :followerId', { followerId: me.id });
+
+ q.andWhere(new Brackets(qb => { qb
+ // チャンネルのノートではない
+ .where('note.channelId IS NULL')
+ // または自分がフォローしているチャンネルのノート
+ .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
+ }));
+
+ q.setParameters(channelFollowingQuery.getParameters());
+ }
+ }
+
+ @bindThis
+ public generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
+ const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
+ .select('muted.noteId')
+ .where('muted.userId = :userId', { userId: me.id });
+
+ q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
+
+ q.setParameters(mutedQuery.getParameters());
+ }
+
+ @bindThis
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
.select('threadMuted.threadId')
@@ -177,6 +213,32 @@ export class QueryService {
}
@bindThis
+ public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<MiUser, 'id'> | null): void {
+ if (me == null) {
+ q.andWhere(new Brackets(qb => { qb
+ .where('note.replyId IS NULL') // 返信ではない
+ .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
+ .where('note.replyId IS NOT NULL')
+ .andWhere('note.replyUserId = note.userId');
+ }));
+ }));
+ } else if (!withReplies) {
+ q.andWhere(new Brackets(qb => { qb
+ .where('note.replyId IS NULL') // 返信ではない
+ .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
+ .orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
+ .where('note.replyId IS NOT NULL')
+ .andWhere('note.userId = :meId', { meId: me.id });
+ }))
+ .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
+ .where('note.replyId IS NOT NULL')
+ .andWhere('note.replyUserId = note.userId');
+ }));
+ }));
+ }
+ }
+
+ @bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
if (me == null) {
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 087dfd9214..37031e341e 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/Blocking.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js';
+import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit {
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService,
@@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit {
});
for (const userList of userLists) {
- await this.userListMembershipsRepository.delete({
+ await this.userListJoiningsRepository.delete({
userListId: userList.id,
userId: user.id,
});
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index beffcc2e9c..230f6ef261 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -123,11 +123,7 @@ export class UserFollowingService implements OnModuleInit {
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
- if (
- followee.isLocked ||
- (followeeProfile.carefulBot && follower.isBot) ||
- (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true')
- ) {
+ if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) {
let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー
diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index bece1e442e..93dc5edbba 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -5,10 +5,10 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { UserListMembershipsRepository } from '@/models/_.js';
+import type { UserListJoiningsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js';
-import type { MiUserListMembership } from '@/models/UserListMembership.js';
+import type { MiUserListJoining } from '@/models/UserListJoining.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
@@ -33,8 +33,8 @@ export class UserListService implements OnApplicationShutdown {
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private userEntityService: UserEntityService,
private idService: IdService,
@@ -46,7 +46,7 @@ export class UserListService implements OnApplicationShutdown {
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
- fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
+ fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
@@ -85,19 +85,19 @@ export class UserListService implements OnApplicationShutdown {
@bindThis
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
- const currentCount = await this.userListMembershipsRepository.countBy({
+ const currentCount = await this.userListJoiningsRepository.countBy({
userListId: list.id,
});
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError();
}
- await this.userListMembershipsRepository.insert({
+ await this.userListJoiningsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: target.id,
userListId: list.id,
- } as MiUserListMembership);
+ } as MiUserListJoining);
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
@@ -113,7 +113,7 @@ export class UserListService implements OnApplicationShutdown {
@bindThis
public async removeMember(target: MiUser, list: MiUserList) {
- await this.userListMembershipsRepository.delete({
+ await this.userListJoiningsRepository.delete({
userId: target.id,
userListId: list.id,
});
@@ -123,24 +123,6 @@ export class UserListService implements OnApplicationShutdown {
}
@bindThis
- public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) {
- const membership = await this.userListMembershipsRepository.findOneBy({
- userId: target.id,
- userListId: list.id,
- });
-
- if (membership == null) {
- throw new Error('User is not a member of the list');
- }
-
- await this.userListMembershipsRepository.update({
- id: membership.id,
- }, {
- withReplies: options.withReplies,
- });
- }
-
- @bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
this.membersCache.dispose();
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
deleted file mode 100644
index d16e1be615..0000000000
--- a/packages/backend/src/core/UserService.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
-import type { MiUser } from '@/models/User.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-
-@Injectable()
-export class UserService {
- constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
- ) {
- }
-
- @bindThis
- public async updateLastActiveDate(user: MiUser): Promise<void> {
- if (user.isHibernated) {
- const result = await this.usersRepository.createQueryBuilder().update()
- .set({
- lastActiveDate: new Date(),
- })
- .where('id = :id', { id: user.id })
- .returning('*')
- .execute()
- .then((response) => {
- return response.raw[0];
- });
- const wokeUp = result.isHibernated;
- if (wokeUp) {
- this.usersRepository.update(user.id, {
- isHibernated: false,
- });
- this.followingsRepository.update({
- followerId: user.id,
- }, {
- isFollowerHibernated: false,
- });
- }
- } else {
- this.usersRepository.update(user.id, {
- lastActiveDate: new Date(),
- });
- }
- }
-}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 4444e4118d..026f84bb39 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -102,13 +102,13 @@ export class NoteEntityService implements OnModuleInit {
} else if (meId === packedNote.userId) {
hide = false;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
- // 自分の投稿に対するリプライ
+ // 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
- // 自分へのメンション
+ // 自分へのメンション
hide = false;
} else {
- // フォロワーかどうか
+ // フォロワーかどうか
const isFollowing = await this.followingsRepository.exist({
where: {
followeeId: packedNote.userId,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b8fb9f8db7..cdd1182f6d 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -493,7 +493,6 @@ export class UserEntityService implements OnModuleInit {
isMuted: relation.isMuted,
isRenoteMuted: relation.isRenoteMuted,
notify: relation.following?.notify ?? 'none',
- withReplies: relation.following?.withReplies ?? false,
} : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts
index 06b6e852b1..a7f2885194 100644
--- a/packages/backend/src/core/entities/UserListEntityService.ts
+++ b/packages/backend/src/core/entities/UserListEntityService.ts
@@ -5,12 +5,11 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
+import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiUserList } from '@/models/UserList.js';
import { bindThis } from '@/decorators.js';
-import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class UserListEntityService {
@@ -18,10 +17,8 @@ export class UserListEntityService {
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
-
- private userEntityService: UserEntityService,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
) {
}
@@ -31,7 +28,7 @@ export class UserListEntityService {
): Promise<Packed<'UserList'>> {
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
- const users = await this.userListMembershipsRepository.findBy({
+ const users = await this.userListJoiningsRepository.findBy({
userListId: userList.id,
});
@@ -43,18 +40,5 @@ export class UserListEntityService {
isPublic: userList.isPublic,
};
}
-
- @bindThis
- public async packMembershipsMany(
- memberships: MiUserListMembership[],
- ) {
- return Promise.all(memberships.map(async x => ({
- id: x.id,
- createdAt: x.createdAt.toISOString(),
- userId: x.userId,
- user: await this.userEntityService.pack(x.userId),
- withReplies: x.withReplies,
- })));
- }
}
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index ccaa810f5c..7034fff058 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -10,7 +10,6 @@ export const DI = {
redis: Symbol('redis'),
redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'),
- redisForTimelines: Symbol('redisForTimelines'),
//#region Repositories
usersRepository: Symbol('usersRepository'),
@@ -31,7 +30,7 @@ export const DI = {
userPublickeysRepository: Symbol('userPublickeysRepository'),
userListsRepository: Symbol('userListsRepository'),
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
- userListMembershipsRepository: Symbol('userListMembershipsRepository'),
+ userListJoiningsRepository: Symbol('userListJoiningsRepository'),
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
userIpsRepository: Symbol('userIpsRepository'),
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
@@ -64,6 +63,7 @@ export const DI = {
promoNotesRepository: Symbol('promoNotesRepository'),
promoReadsRepository: Symbol('promoReadsRepository'),
relaysRepository: Symbol('relaysRepository'),
+ mutedNotesRepository: Symbol('mutedNotesRepository'),
channelsRepository: Symbol('channelsRepository'),
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts
index 607538b1e7..8c9f965fad 100644
--- a/packages/backend/src/models/Following.ts
+++ b/packages/backend/src/models/Following.ts
@@ -9,7 +9,6 @@ import { MiUser } from './User.js';
@Entity('following')
@Index(['followerId', 'followeeId'], { unique: true })
-@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
export class MiFollowing {
@PrimaryColumn(id())
public id: string;
@@ -46,17 +45,6 @@ export class MiFollowing {
@JoinColumn()
public follower: MiUser | null;
- @Column('boolean', {
- default: false,
- })
- public isFollowerHibernated: boolean;
-
- // タイムラインにその人のリプライまで含めるかどうか
- @Column('boolean', {
- default: false,
- })
- public withReplies: boolean;
-
@Index()
@Column('varchar', {
length: 32,
diff --git a/packages/backend/src/models/MutedNote.ts b/packages/backend/src/models/MutedNote.ts
new file mode 100644
index 0000000000..89a678a2a7
--- /dev/null
+++ b/packages/backend/src/models/MutedNote.ts
@@ -0,0 +1,53 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { mutedNoteReasons } from '@/types.js';
+import { id } from './util/id.js';
+import { MiNote } from './Note.js';
+import { MiUser } from './User.js';
+
+@Entity('muted_note')
+@Index(['noteId', 'userId'], { unique: true })
+export class MiMutedNote {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The note ID.',
+ })
+ public noteId: MiNote['id'];
+
+ @ManyToOne(type => MiNote, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public note: MiNote | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The user ID.',
+ })
+ public userId: MiUser['id'];
+
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: MiUser | null;
+
+ /**
+ * ミュートされた理由。
+ */
+ @Index()
+ @Column('enum', {
+ enum: mutedNoteReasons,
+ comment: 'The reason of the MutedNote.',
+ })
+ public reason: typeof mutedNoteReasons[number];
+}
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index bef053610b..7e2bee8c44 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -5,7 +5,7 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js';
+import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit } from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -117,9 +117,9 @@ const $userListFavoritesRepository: Provider = {
inject: [DI.db],
};
-const $userListMembershipsRepository: Provider = {
- provide: DI.userListMembershipsRepository,
- useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
+const $userListJoiningsRepository: Provider = {
+ provide: DI.userListJoiningsRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiUserListJoining),
inject: [DI.db],
};
@@ -315,6 +315,12 @@ const $relaysRepository: Provider = {
inject: [DI.db],
};
+const $mutedNotesRepository: Provider = {
+ provide: DI.mutedNotesRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiMutedNote),
+ inject: [DI.db],
+};
+
const $channelsRepository: Provider = {
provide: DI.channelsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChannel),
@@ -421,7 +427,7 @@ const $noteEditRepository: Provider = {
$userPublickeysRepository,
$userListsRepository,
$userListFavoritesRepository,
- $userListMembershipsRepository,
+ $userListJoiningsRepository,
$userNotePiningsRepository,
$userIpsRepository,
$usedUsernamesRepository,
@@ -454,6 +460,7 @@ const $noteEditRepository: Provider = {
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
+ $mutedNotesRepository,
$channelsRepository,
$channelFollowingsRepository,
$channelFavoritesRepository,
@@ -488,7 +495,7 @@ const $noteEditRepository: Provider = {
$userPublickeysRepository,
$userListsRepository,
$userListFavoritesRepository,
- $userListMembershipsRepository,
+ $userListJoiningsRepository,
$userNotePiningsRepository,
$userIpsRepository,
$usedUsernamesRepository,
@@ -521,6 +528,7 @@ const $noteEditRepository: Provider = {
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
+ $mutedNotesRepository,
$channelsRepository,
$channelFollowingsRepository,
$channelFavoritesRepository,
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 9650622dd5..8f0122a90c 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -187,11 +187,6 @@ export class MiUser {
})
public isExplorable: boolean;
- @Column('boolean', {
- default: false,
- })
- public isHibernated: boolean;
-
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', {
default: false,
diff --git a/packages/backend/src/models/UserListMembership.ts b/packages/backend/src/models/UserListJoining.ts
index f337f19a47..4918f2f700 100644
--- a/packages/backend/src/models/UserListMembership.ts
+++ b/packages/backend/src/models/UserListJoining.ts
@@ -8,14 +8,14 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiUserList } from './UserList.js';
-@Entity('user_list_membership')
+@Entity('user_list_joining')
@Index(['userId', 'userListId'], { unique: true })
-export class MiUserListMembership {
+export class MiUserListJoining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
- comment: 'The created date of the UserListMembership.',
+ comment: 'The created date of the UserListJoining.',
})
public createdAt: Date;
@@ -44,10 +44,4 @@ export class MiUserListMembership {
})
@JoinColumn()
public userList: MiUserList | null;
-
- // タイムラインにその人のリプライまで含めるかどうか
- @Column('boolean', {
- default: false,
- })
- public withReplies: boolean;
}
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index b76f6d5420..ca047569cb 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -28,6 +28,7 @@ import { MiHashtag } from '@/models/Hashtag.js';
import { MiInstance } from '@/models/Instance.js';
import { MiMeta } from '@/models/Meta.js';
import { MiModerationLog } from '@/models/ModerationLog.js';
+import { MiMutedNote } from '@/models/MutedNote.js';
import { MiMuting } from '@/models/Muting.js';
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js';
@@ -52,7 +53,7 @@ import { MiUser } from '@/models/User.js';
import { MiUserIp } from '@/models/UserIp.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUserList } from '@/models/UserList.js';
-import { MiUserListMembership } from '@/models/UserListMembership.js';
+import { MiUserListJoining } from '@/models/UserListJoining.js';
import { MiUserNotePining } from '@/models/UserNotePining.js';
import { MiUserPending } from '@/models/UserPending.js';
import { MiUserProfile } from '@/models/UserProfile.js';
@@ -96,6 +97,7 @@ export {
MiInstance,
MiMeta,
MiModerationLog,
+ MiMutedNote,
MiMuting,
MiRenoteMuting,
MiNote,
@@ -121,7 +123,7 @@ export {
MiUserKeypair,
MiUserList,
MiUserListFavorite,
- MiUserListMembership,
+ MiUserListJoining,
MiUserNotePining,
MiUserPending,
MiUserProfile,
@@ -163,6 +165,7 @@ export type HashtagsRepository = Repository<MiHashtag>;
export type InstancesRepository = Repository<MiInstance>;
export type MetasRepository = Repository<MiMeta>;
export type ModerationLogsRepository = Repository<MiModerationLog>;
+export type MutedNotesRepository = Repository<MiMutedNote>;
export type MutingsRepository = Repository<MiMuting>;
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
export type NotesRepository = Repository<MiNote>;
@@ -188,7 +191,7 @@ export type UserIpsRepository = Repository<MiUserIp>;
export type UserKeypairsRepository = Repository<MiUserKeypair>;
export type UserListsRepository = Repository<MiUserList>;
export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
-export type UserListMembershipsRepository = Repository<MiUserListMembership>;
+export type UserListJoiningsRepository = Repository<MiUserListJoining>;
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
export type UserPendingsRepository = Repository<MiUserPending>;
export type UserProfilesRepository = Repository<MiUserProfile>;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 25f0547281..79b14bb65f 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -283,10 +283,6 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
- withReplies: {
- type: 'boolean',
- nullable: false, optional: true,
- },
//#endregion
},
} as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 5cf9d7d1aa..b12a84ac96 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -36,6 +36,7 @@ import { MiHashtag } from '@/models/Hashtag.js';
import { MiInstance } from '@/models/Instance.js';
import { MiMeta } from '@/models/Meta.js';
import { MiModerationLog } from '@/models/ModerationLog.js';
+import { MiMutedNote } from '@/models/MutedNote.js';
import { MiMuting } from '@/models/Muting.js';
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { MiNote } from '@/models/Note.js';
@@ -61,7 +62,7 @@ import { MiUserIp } from '@/models/UserIp.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUserList } from '@/models/UserList.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
-import { MiUserListMembership } from '@/models/UserListMembership.js';
+import { MiUserListJoining } from '@/models/UserListJoining.js';
import { MiUserNotePining } from '@/models/UserNotePining.js';
import { MiUserPending } from '@/models/UserPending.js';
import { MiUserProfile } from '@/models/UserProfile.js';
@@ -138,7 +139,7 @@ export const entities = [
MiUserPublickey,
MiUserList,
MiUserListFavorite,
- MiUserListMembership,
+ MiUserListJoining,
MiUserNotePining,
MiUserSecurityKey,
MiUsedUsername,
@@ -174,6 +175,7 @@ export const entities = [
MiPromoNote,
MiPromoRead,
MiRelay,
+ MiMutedNote,
MiChannel,
MiChannelFollowing,
MiChannelFavorite,
diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts
index e252c5d8a1..f0453f7054 100644
--- a/packages/backend/src/queue/processors/CleanProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanProcessorService.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { AntennasRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js';
+import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@@ -25,6 +25,9 @@ export class CleanProcessorService {
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
+ @Inject(DI.mutedNotesRepository)
+ private mutedNotesRepository: MutedNotesRepository,
+
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@@ -45,6 +48,16 @@ export class CleanProcessorService {
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
});
+ this.mutedNotesRepository.delete({
+ id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
+ reason: 'word',
+ });
+
+ this.mutedNotesRepository.delete({
+ id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
+ reason: 'word',
+ });
+
// 使われてないアンテナを停止
if (this.config.deactivateAntennaThreshold > 0) {
this.antennasRepository.update({
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
index a0afbee3ba..f941fb6e85 100644
--- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { format as DateFormat } from 'date-fns';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js';
+import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js';
import Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { bindThis } from '@/decorators.js';
@@ -29,8 +29,8 @@ export class ExportAntennasProcessorService {
@Inject(DI.antennasRepository)
private antennsRepository: AntennasRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private driveService: DriveService,
private utilityService: UtilityService,
@@ -65,9 +65,9 @@ export class ExportAntennasProcessorService {
for (const [index, antenna] of antennas.entries()) {
let users: MiUser[] | undefined;
if (antenna.userListId !== null) {
- const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId });
+ const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId });
users = await this.usersRepository.findBy({
- id: In(memberships.map(j => j.userId)),
+ id: In(joinings.map(j => j.userId)),
});
}
write(JSON.stringify({
diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
index a3f9441dc2..7baaa7081a 100644
--- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
@@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
-import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
+import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
@@ -29,8 +29,8 @@ export class ExportUserListsProcessorService {
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private utilityService: UtilityService,
private driveService: DriveService,
@@ -61,9 +61,9 @@ export class ExportUserListsProcessorService {
const stream = fs.createWriteStream(path, { flags: 'a' });
for (const list of lists) {
- const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id });
+ const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id });
const users = await this.usersRepository.findBy({
- id: In(memberships.map(j => j.userId)),
+ id: In(joinings.map(j => j.userId)),
});
for (const u of users) {
diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
index 9be36a9d0d..60a0d1605f 100644
--- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
+import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
@@ -33,8 +33,8 @@ export class ImportUserListsProcessorService {
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private utilityService: UtilityService,
private idService: IdService,
@@ -99,7 +99,7 @@ export class ImportUserListsProcessorService {
target = await this.remoteUserResolveService.resolveUser(username, host);
}
- if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
+ if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
this.userListService.addMember(target, list!, user);
} catch (e) {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 74b26af27a..15a2621da2 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -205,6 +205,7 @@ 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,9 +337,7 @@ 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_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_lists_create_from_public from './endpoints/users/lists/create-from-public.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';
@@ -556,6 +555,7 @@ 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 };
@@ -687,9 +687,7 @@ 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_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_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.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 };
@@ -911,6 +909,7 @@ 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,
@@ -1042,9 +1041,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_lists_update,
$users_lists_favorite,
$users_lists_unfavorite,
- $users_lists_createFromPublic,
- $users_lists_updateMembership,
- $users_lists_getMemberships,
+ $users_lists_create_from_public,
$users_notes,
$users_pages,
$users_flashs,
@@ -1260,6 +1257,7 @@ 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,
@@ -1388,9 +1386,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_lists_update,
$users_lists_favorite,
$users_lists_unfavorite,
- $users_lists_createFromPublic,
- $users_lists_updateMembership,
- $users_lists_getMemberships,
+ $users_lists_create_from_public,
$users_notes,
$users_pages,
$users_flashs,
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index badcec1b33..9acaa688c5 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -14,7 +14,6 @@ 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';
@@ -38,7 +37,6 @@ export class StreamingApiServerService {
private authenticateService: AuthenticateService,
private channelsService: ChannelsService,
private notificationService: NotificationService,
- private usersService: UserService,
) {
}
@@ -132,10 +130,14 @@ export class StreamingApiServerService {
this.#connections.set(connection, Date.now());
const userUpdateIntervalId = user ? setInterval(() => {
- this.usersService.updateLastActiveDate(user);
+ this.usersRepository.update(user.id, {
+ lastActiveDate: new Date(),
+ });
}, 1000 * 60 * 5) : null;
if (user) {
- this.usersService.updateLastActiveDate(user);
+ this.usersRepository.update(user.id, {
+ lastActiveDate: new Date(),
+ });
}
connection.once('close', () => {
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 279d56d1e5..5ea4121918 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -205,6 +205,7 @@ 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,10 +336,8 @@ 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_createFromPublic from './endpoints/users/lists/create-from-public.js';
+import * as ep___users_lists_create_from_public 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';
@@ -554,6 +553,7 @@ 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],
@@ -685,9 +685,7 @@ 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_createFromPublic],
- ['users/lists/update-membership', ep___users_lists_updateMembership],
- ['users/lists/get-memberships', ep___users_lists_getMemberships],
+ ['users/lists/create-from-public', ep___users_lists_create_from_public],
['users/notes', ep___users_notes],
['users/pages', ep___users_pages],
['users/flashs', ep___users_flashs],
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 63e542cb62..eaae7bff62 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -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.redisForTimelines)
- private redisForTimelines: Redis.Redis,
+ @Inject(DI.redis)
+ private redisClient: 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.redisForTimelines.xrevrange(
+ const noteIdsRes = await this.redisClient.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 ?? '-',
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 56b8fc5c36..026b649537 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -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.redisForTimelines)
- private redisForTimelines: Redis.Redis,
+ @Inject(DI.redis)
+ private redisClient: 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.redisForTimelines.xrevrange(
+ noteIdsRes = await this.redisClient.xrevrange(
`channelTimeline:${channel.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
@@ -104,6 +104,7 @@ 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
@@ -128,6 +129,7 @@ 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
diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts
index db17d151df..25f393e517 100644
--- a/packages/backend/src/server/api/endpoints/following/update.ts
+++ b/packages/backend/src/server/api/endpoints/following/update.ts
@@ -57,9 +57,8 @@ export const paramDef = {
properties: {
userId: { type: 'string', format: 'misskey:id' },
notify: { type: 'string', enum: ['normal', 'none'] },
- withReplies: { type: 'boolean' },
},
- required: ['userId'],
+ required: ['userId', 'notify'],
} as const;
@Injectable()
@@ -99,8 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.followingsRepository.update({
id: exist.id,
}, {
- notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined,
- withReplies: ps.withReplies != null ? ps.withReplies : undefined,
+ notify: ps.notify === 'none' ? null : ps.notify,
});
return await this.userEntityService.pack(follower.id, me);
diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
new file mode 100644
index 0000000000..d62bfbb3ed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
@@ -0,0 +1,51 @@
+/*
+ * 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',
+ }),
+ };
+ });
+ }
+}
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..8784e86153 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -40,6 +40,7 @@ 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' },
@@ -67,8 +68,49 @@ 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');
+
+ 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);
});
}
}
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..9bde5dee21 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -5,16 +5,14 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
+import type { NotesRepository, FollowingsRepository } 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 = {
@@ -53,6 +51,7 @@ 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: [],
@@ -61,17 +60,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);
@@ -79,75 +78,79 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.stlDisabled);
}
- 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 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 [];
- }
+ //#region Construct query
+ const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- const query = this.notesRepository.createQueryBuilder('note')
- .where('note.id IN (:...noteIds)', { noteIds: noteIds })
+ 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)');
+ }))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('note.channel', 'channel');
+ .setParameters(followingQuery.getParameters());
- timeline = await query.getMany();
+ 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 = 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.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)');
+ }));
+ }
- return true;
- });
+ 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 != \'{}\'');
+ }
- // TODO: フィルタした結果件数が足りなかった場合の対応
+ 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
- timeline.sort((a, b) => a.id > b.id ? -1 : 1);
+ const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
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..0fefddc51b 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -5,16 +5,14 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { MiNote, NotesRepository } from '@/models/_.js';
+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 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 = {
@@ -43,7 +41,11 @@ 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' },
@@ -57,17 +59,14 @@ 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);
@@ -75,63 +74,56 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.ltlDisabled);
}
- 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 })
+ //#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)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('note.channel', 'channel');
+ .leftJoinAndSelect('renote.user', 'renoteUser');
- timeline = await query.getMany();
+ 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 = 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.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)');
}
+ }
- return true;
- });
+ 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
- timeline.sort((a, b) => a.id > b.id ? -1 : 1);
+ const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
if (me) {
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 2f25d2d7ce..0d47cc1702 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -5,16 +5,13 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
+import type { NotesRepository, FollowingsRepository } 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'],
@@ -44,6 +41,7 @@ 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: [],
@@ -52,82 +50,96 @@ 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 [
- 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),
- ]);
+ const followees = await this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id })
+ .getMany();
- 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.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
- const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- let noteIdsRes: [string, string[]][] = [];
+ if (followees.length > 0) {
+ const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
- 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);
+ query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
+ } else {
+ query.andWhere('note.userId = :meId', { meId: me.id });
}
- const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
+ 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);
- if (noteIds.length === 0) {
- return [];
+ 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)');
+ }));
}
- 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');
-
- timeline = await query.getMany();
+ 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)');
+ }));
+ }
- 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;
- }
+ 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)');
+ }));
+ }
- return true;
- });
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
- // TODO: フィルタした結果件数が足りなかった場合の対応
+ 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
- timeline.sort((a, b) => a.id > b.id ? -1 : 1);
+ const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
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 8e943826d5..c20274b2ba 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
@@ -5,16 +5,12 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js';
+import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } 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 = {
@@ -67,19 +63,18 @@ 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({
@@ -91,65 +86,72 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchList);
}
- 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 })
+ //#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')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('note.channel', 'channel');
+ .andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
- timeline = await query.getMany();
+ 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 = 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.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)');
+ }));
+ }
- return true;
- });
+ 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)');
+ }));
+ }
- // 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
- timeline.sort((a, b) => a.id > b.id ? -1 : 1);
+ const timeline = await query.limit(ps.limit).getMany();
this.activeUsersChart.read(me);
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index f2533efa36..6dc35907e1 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -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.redisForTimelines)
- private redisForTimelines: Redis.Redis,
+ @Inject(DI.redis)
+ private redisClient: 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.redisForTimelines.xrevrange(
+ const noteIdsRes = await this.redisClient.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 ?? '-',
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
index f2f6c4303a..eae55905d3 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
+import type { UserListsRepository, UserListJoiningsRepository, 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.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
@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.userListMembershipsRepository.findBy({
+ const users = (await this.userListJoiningsRepository.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.userListMembershipsRepository.exist({
+ const exist = await this.userListJoiningsRepository.exist({
where: {
userListId: userList.id,
userId: currentUser.id,
diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
deleted file mode 100644
index ae8b4e9b81..0000000000
--- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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);
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts
index c4ceec575b..72a6a7380d 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/push.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
-import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js';
+import type { UserListsRepository, UserListJoiningsRepository, 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.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
- const exist = await this.userListMembershipsRepository.exist({
+ const exist = await this.userListJoiningsRepository.exist({
where: {
userListId: userList.id,
userId: user.id,
diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts
deleted file mode 100644
index b69465b940..0000000000
--- a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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,
- });
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 9321a41d58..e660a0bb25 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -5,14 +5,12 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { MiNote, NotesRepository } from '@/models/_.js';
+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 { 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 = {
@@ -52,6 +50,9 @@ 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'],
@@ -60,52 +61,64 @@ 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) => {
- let timeline: MiNote[] = [];
+ // 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;
+ });
- const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- let noteIdsRes: [string, string[]][] = [];
+ //#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 })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('note.channel', 'channel')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
- 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);
- }
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.channelId IS NULL');
+ qb.orWhere('channel.isSensitive = false');
+ }));
- const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) {
+ this.queryService.generateMutedUserQuery(query, me, user);
+ this.queryService.generateBlockedUserQuery(query, me);
+ }
- if (noteIds.length === 0) {
- return [];
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
- const isFollowing = me ? Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId) : false;
+ 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 });
+ }
+ }));
- 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 (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 (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
@@ -116,21 +129,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
}
- timeline = await query.getMany();
-
- timeline = timeline.filter(note => {
- if (note.renoteId) {
- if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
- if (ps.withRenotes === false) return false;
- }
- }
-
- if (note.visibility === 'followers' && !isFollowing) return false;
+ 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)');
+ }));
+ }
- return true;
- });
+ //#endregion
- 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);
});
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index f981e63871..a73071ea99 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -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 { MiFollowing, MiUserProfile } from '@/models/_.js';
+import { 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: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
+ public following: Set<string> = new Set();
public followingChannels: Set<string> = new Set();
public userIdsWhoMeMuting: Set<string> = new Set();
public userIdsWhoBlockingMe: Set<string> = new Set();
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index f0ac50349c..fef52b6856 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -18,6 +18,7 @@ class GlobalTimelineChannel extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
+ private withReplies: boolean;
private withRenotes: boolean;
constructor(
@@ -37,6 +38,7 @@ 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
@@ -62,7 +64,7 @@ class GlobalTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && !this.following[note.userId]?.withReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
@@ -80,6 +82,13 @@ 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);
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 1c1b1c2ae4..198c68e1c2 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -16,6 +16,7 @@ class HomeTimelineChannel extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
+ private withReplies: boolean;
private withRenotes: boolean;
constructor(
@@ -30,6 +31,7 @@ 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);
@@ -41,7 +43,7 @@ class HomeTimelineChannel extends Channel {
if (!this.followingChannels.has(note.channelId)) return;
} else {
// その投稿のユーザーをフォローしていなかったら弾く
- if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return;
+ if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return;
}
// Ignore notes from instances the user has muted
@@ -71,7 +73,7 @@ class HomeTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && !this.following[note.userId]?.withReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
@@ -86,6 +88,13 @@ 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);
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index e2f4817bfa..cde4297478 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -18,6 +18,7 @@ class HybridTimelineChannel extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = true;
public static requireCredential = true;
+ private withReplies: boolean;
private withRenotes: boolean;
constructor(
@@ -37,6 +38,7 @@ 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
@@ -51,7 +53,7 @@ class HybridTimelineChannel extends Channel {
// フォローしているチャンネルの投稿 の場合だけ
if (!(
(note.channelId == null && this.user!.id === note.userId) ||
- (note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
+ (note.channelId == null && this.following.has(note.userId)) ||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
@@ -83,7 +85,7 @@ class HybridTimelineChannel extends Channel {
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
// 関係ない返信は除外
- if (note.reply && !this.following[note.userId]?.withReplies) {
+ if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
@@ -98,6 +100,13 @@ 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);
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index ca563b5d19..ef708c4fee 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -17,6 +17,7 @@ class LocalTimelineChannel extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
+ private withReplies: boolean;
private withRenotes: boolean;
constructor(
@@ -36,6 +37,7 @@ 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
@@ -62,7 +64,7 @@ class LocalTimelineChannel extends Channel {
}
// 関係ない返信は除外
- if (note.reply && this.user && !this.following[note.userId]?.withReplies) {
+ if (note.reply && this.user && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
@@ -77,6 +79,13 @@ 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);
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 03f7760d8e..8bbba0b6db 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
+import type { UserListJoiningsRepository, 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 membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
+ public listUsers: MiUser['id'][] = [];
private listUsersClock: NodeJS.Timeout;
constructor(
private userListsRepository: UserListsRepository,
- private userListMembershipsRepository: UserListMembershipsRepository,
+ private userListJoiningsRepository: UserListJoiningsRepository,
private noteEntityService: NoteEntityService,
id: string,
@@ -58,25 +58,19 @@ class UserListChannel extends Channel {
@bindThis
private async updateListUsers() {
- const memberships = await this.userListMembershipsRepository.find({
+ const users = await this.userListJoiningsRepository.find({
where: {
userListId: this.listId,
},
select: ['userId'],
});
- const membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
- for (const membership of memberships) {
- membershipsMap[membership.userId] = {
- withReplies: membership.withReplies,
- };
- }
- this.membershipsMap = membershipsMap;
+ this.listUsers = users.map(x => x.userId);
}
@bindThis
private async onNote(note: Packed<'Note'>) {
- if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
+ if (!this.listUsers.includes(note.userId)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await this.noteEntityService.pack(note.id, this.user, {
@@ -101,13 +95,6 @@ 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がブロックされているユーザーが関わるものだったら無視する
@@ -137,8 +124,8 @@ export class UserListChannelService {
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
- @Inject(DI.userListMembershipsRepository)
- private userListMembershipsRepository: UserListMembershipsRepository,
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
private noteEntityService: NoteEntityService,
) {
@@ -148,7 +135,7 @@ export class UserListChannelService {
public create(id: string, connection: Channel['connection']): UserListChannel {
return new UserListChannel(
this.userListsRepository,
- this.userListMembershipsRepository,
+ this.userListJoiningsRepository,
this.noteEntityService,
id,
connection,