Add mutingType field to Muting model and update related code
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
parent
1810d6e837
commit
685847e1b6
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AddMutingType1762516776421 {
|
||||||
|
name = 'AddMutingType1762516776421'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "muting" ADD "mutingType" varchar(128) NOT NULL DEFAULT 'all'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "mutingType"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing, MiMuting } from '@/models/_.js';
|
||||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
@ -21,7 +21,7 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
public localUserByIdCache: MemoryKVCache<MiLocalUser>;
|
public localUserByIdCache: MemoryKVCache<MiLocalUser>;
|
||||||
public uriPersonCache: MemoryKVCache<MiUser | null>;
|
public uriPersonCache: MemoryKVCache<MiUser | null>;
|
||||||
public userProfileCache: RedisKVCache<MiUserProfile>;
|
public userProfileCache: RedisKVCache<MiUserProfile>;
|
||||||
public userMutingsCache: RedisKVCache<Set<string>>;
|
public userMutingsCache: RedisKVCache<Map<string, Pick<MiMuting, 'mutingType'>>>;
|
||||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||||
|
|
@ -69,12 +69,12 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
|
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
|
this.userMutingsCache = new RedisKVCache<Map<string, Pick<MiMuting, 'mutingType'>>>(this.redisClient, 'userMutings', {
|
||||||
lifetime: 1000 * 60 * 30, // 30m
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
memoryCacheLifetime: 1000 * 60, // 1m
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId', 'mutingType'] }).then(xs => new Map(xs.map(x => [x.muteeId, { mutingType: x.mutingType }]))),
|
||||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
toRedisConverter: (value) => JSON.stringify(Array.from(value.entries())),
|
||||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
fromRedisConverter: (value) => new Map(JSON.parse(value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export class FanoutTimelineEndpointService {
|
||||||
if (ps.me) {
|
if (ps.me) {
|
||||||
const me = ps.me;
|
const me = ps.me;
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMutingMap,
|
||||||
userIdsWhoMeMutingRenotes,
|
userIdsWhoMeMutingRenotes,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
userMutedInstances,
|
userMutedInstances,
|
||||||
|
|
@ -124,6 +124,8 @@ export class FanoutTimelineEndpointService {
|
||||||
this.channelMutingService.mutingChannelsCache.fetch(me.id),
|
this.channelMutingService.mutingChannelsCache.fetch(me.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
|
||||||
|
|
||||||
const parentFilter = filter;
|
const parentFilter = filter;
|
||||||
filter = (note) => {
|
filter = (note) => {
|
||||||
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||||
if (mutings.has(notifierId)) {
|
const muting = mutings.get(notifierId);
|
||||||
|
if (muting && muting.mutingType === 'all') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,14 +286,16 @@ export class SearchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMutingMap,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
] = me
|
] = me
|
||||||
? await Promise.all([
|
? await Promise.all([
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
])
|
])
|
||||||
: [new Set<string>(), new Set<string>()];
|
: [new Map<string, { mutingType: 'all' | 'timelineOnly' }>(), new Set<string>()];
|
||||||
|
|
||||||
|
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,13 @@ export class UserMutingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
|
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null, mutingType: 'all' | 'timelineOnly' = 'all'): Promise<void> {
|
||||||
await this.mutingsRepository.insert({
|
await this.mutingsRepository.insert({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
expiresAt: expiresAt ?? null,
|
expiresAt: expiresAt ?? null,
|
||||||
muterId: user.id,
|
muterId: user.id,
|
||||||
muteeId: target.id,
|
muteeId: target.id,
|
||||||
|
mutingType: mutingType,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.cacheService.userMutingsCache.refresh(user.id);
|
this.cacheService.userMutingsCache.refresh(user.id);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export class MutingEntityService {
|
||||||
id: muting.id,
|
id: muting.id,
|
||||||
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
createdAt: this.idService.parse(muting.id).date.toISOString(),
|
||||||
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
|
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
|
||||||
|
mutingType: muting.mutingType,
|
||||||
muteeId: muting.muteeId,
|
muteeId: muting.muteeId,
|
||||||
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
|
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
|
||||||
schema: 'UserDetailedNotMe',
|
schema: 'UserDetailedNotMe',
|
||||||
|
|
|
||||||
|
|
@ -289,12 +289,13 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
#validateNotifier <T extends MiNotification | MiGroupedNotification> (
|
#validateNotifier <T extends MiNotification | MiGroupedNotification> (
|
||||||
notification: T,
|
notification: T,
|
||||||
userIdsWhoMeMuting: Set<MiUser['id']>,
|
userIdsWhoMeMutingMap: Map<MiUser['id'], { mutingType: 'all' | 'timelineOnly' }>,
|
||||||
userMutedInstances: Set<string>,
|
userMutedInstances: Set<string>,
|
||||||
notifiers: MiUser[],
|
notifiers: MiUser[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!('notifierId' in notification)) return true;
|
if (!('notifierId' in notification)) return true;
|
||||||
if (userIdsWhoMeMuting.has(notification.notifierId)) return false;
|
const muting = userIdsWhoMeMutingMap.get(notification.notifierId);
|
||||||
|
if (muting && muting.mutingType === 'all') return false;
|
||||||
|
|
||||||
const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;
|
const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;
|
||||||
|
|
||||||
|
|
@ -324,7 +325,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMutingMap,
|
||||||
userMutedInstances,
|
userMutedInstances,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.cacheService.userMutingsCache.fetch(meId),
|
this.cacheService.userMutingsCache.fetch(meId),
|
||||||
|
|
@ -337,7 +338,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
|
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
|
||||||
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
|
const isValid = this.#validateNotifier(notification, userIdsWhoMeMutingMap, userMutedInstances, notifiers);
|
||||||
return isValid ? notification : null;
|
return isValid ? notification : null;
|
||||||
}))) as [T | null] ).filter(x => x != null);
|
}))) as [T | null] ).filter(x => x != null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,12 @@ export class MiMuting {
|
||||||
})
|
})
|
||||||
public expiresAt: Date | null;
|
public expiresAt: Date | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
default: 'all',
|
||||||
|
})
|
||||||
|
public mutingType: 'all' | 'timelineOnly';
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ export const packedMutingSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
mutingType: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['all', 'timelineOnly'],
|
||||||
|
},
|
||||||
muteeId: {
|
muteeId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,12 @@ export const paramDef = {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
|
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
|
||||||
},
|
},
|
||||||
|
mutingType: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['all', 'timelineOnly'],
|
||||||
|
default: 'all',
|
||||||
|
description: 'Type of muting. `all` mutes everything including notifications. `timelineOnly` mutes only timeline and search, but allows notifications.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['userId'],
|
required: ['userId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
@ -98,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
|
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null, ps.mutingType ?? 'all');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMutingMap,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
] = me ? await Promise.all([
|
] = me ? await Promise.all([
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
]) : [new Set<string>(), new Set<string>()];
|
]) : [new Map<string, { mutingType: 'all' | 'timelineOnly' }>(), new Set<string>()];
|
||||||
|
|
||||||
|
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMutingMap,
|
||||||
] = me ? await Promise.all([
|
] = me ? await Promise.all([
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
]) : [new Set<string>()];
|
]) : [new Map<string, { mutingType: 'all' | 'timelineOnly' }>()];
|
||||||
|
|
||||||
|
// Convert to Set for backward compatibility with isUserRelated
|
||||||
|
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
|
||||||
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
|
const userIdsWhoMeMutingMap = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Map<string, { mutingType: 'all' | 'timelineOnly' }>();
|
||||||
|
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
|
||||||
|
|
||||||
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export default class Connection {
|
||||||
public followingChannels: Set<string> = new Set();
|
public followingChannels: Set<string> = new Set();
|
||||||
public mutingChannels: Set<string> = new Set();
|
public mutingChannels: Set<string> = new Set();
|
||||||
public userIdsWhoMeMuting: Set<string> = new Set();
|
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||||
|
public userIdsWhoMeMutingMap: Map<string, { mutingType: 'all' | 'timelineOnly' }> = new Map();
|
||||||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||||
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
||||||
public userMutedInstances: Set<string> = new Set();
|
public userMutedInstances: Set<string> = new Set();
|
||||||
|
|
@ -64,7 +65,7 @@ export default class Connection {
|
||||||
following,
|
following,
|
||||||
followingChannels,
|
followingChannels,
|
||||||
mutingChannels,
|
mutingChannels,
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMutingMap,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
userIdsWhoMeMutingRenotes,
|
userIdsWhoMeMutingRenotes,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
|
@ -80,7 +81,8 @@ export default class Connection {
|
||||||
this.following = following;
|
this.following = following;
|
||||||
this.followingChannels = followingChannels;
|
this.followingChannels = followingChannels;
|
||||||
this.mutingChannels = mutingChannels;
|
this.mutingChannels = mutingChannels;
|
||||||
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
|
this.userIdsWhoMeMutingMap = userIdsWhoMeMutingMap;
|
||||||
|
this.userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
|
||||||
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
||||||
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
||||||
this.userMutedInstances = new Set(userProfile.mutedInstances);
|
this.userMutedInstances = new Set(userProfile.mutedInstances);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue