Merge 062b6391f6
into 66187174d4
This commit is contained in:
commit
98e32898bc
|
@ -11581,6 +11581,32 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"serverHostPlaceholder": string;
|
||||
};
|
||||
"_noteMuting": {
|
||||
/**
|
||||
* ミュートしたノート
|
||||
*/
|
||||
"noteMuting": string;
|
||||
/**
|
||||
* ノートをミュート
|
||||
*/
|
||||
"muteNote": string;
|
||||
/**
|
||||
* ノートのミュートを解除
|
||||
*/
|
||||
"unmuteNote": string;
|
||||
/**
|
||||
* このノートはミュートされていません
|
||||
*/
|
||||
"notMutedNote": string;
|
||||
/**
|
||||
* のノート
|
||||
*/
|
||||
"labelSuffix": string;
|
||||
/**
|
||||
* ミュートを解除したノートを再表示するにはタイムラインの再読み込みが必要です。
|
||||
*/
|
||||
"unmuteCaption": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -3098,3 +3098,11 @@ _search:
|
|||
pleaseEnterServerHost: "サーバーのホストを入力してください"
|
||||
pleaseSelectUser: "ユーザーを選択してください"
|
||||
serverHostPlaceholder: "例: misskey.example.com"
|
||||
|
||||
_noteMuting:
|
||||
noteMuting: "ミュートしたノート"
|
||||
muteNote: "ノートをミュート"
|
||||
unmuteNote: "ノートのミュートを解除"
|
||||
notMutedNote: "このノートはミュートされていません"
|
||||
labelSuffix: "のノート"
|
||||
unmuteCaption: "ミュートを解除したノートを再表示するにはタイムラインの再読み込みが必要です。"
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class NoteMuting1739882320354 {
|
||||
name = 'NoteMuting1739882320354'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "note_muting" (
|
||||
"id" varchar(32) NOT NULL,
|
||||
"userId" varchar(32) NOT NULL,
|
||||
"noteId" varchar(32) NOT NULL,
|
||||
"expiresAt" TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT "PK_note_muting_id" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "FK_note_muting_userId" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_note_muting_noteId" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
);
|
||||
CREATE INDEX "IDX_note_muting_userId" ON "note_muting" ("userId");
|
||||
CREATE INDEX "IDX_note_muting_noteId" ON "note_muting" ("noteId");
|
||||
CREATE INDEX "IDX_note_muting_expiresAt" ON "note_muting" ("expiresAt");
|
||||
CREATE UNIQUE INDEX "IDX_note_muting_userId_noteId_unique" ON note_muting ("userId", "noteId");
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX "IDX_note_muting_userId_noteId_unique";
|
||||
DROP INDEX "IDX_note_muting_expiresAt";
|
||||
DROP INDEX "IDX_note_muting_noteId";
|
||||
DROP INDEX "IDX_note_muting_userId";
|
||||
DROP TABLE "note_muting";
|
||||
`);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||
|
@ -185,6 +186,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
|
|||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
||||
const $NoteMutingService: Provider = { provide: 'NoteMutingService', useExisting: NoteMutingService };
|
||||
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
|
||||
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
|
||||
const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
|
||||
|
@ -335,6 +337,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
NoteMutingService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
SystemAccountService,
|
||||
|
@ -481,6 +484,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$NoteMutingService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
$SystemAccountService,
|
||||
|
@ -628,6 +632,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
NoteMutingService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
SystemAccountService,
|
||||
|
@ -773,6 +778,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$NoteMutingService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
$SystemAccountService,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { NoteMutingService } from './note/NoteMutingService.js';
|
||||
|
||||
type TimelineOptions = {
|
||||
untilId: string | null,
|
||||
|
@ -45,6 +46,7 @@ export class FanoutTimelineEndpointService {
|
|||
private noteEntityService: NoteEntityService,
|
||||
private cacheService: CacheService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private noteMutingService: NoteMutingService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -101,11 +103,13 @@ export class FanoutTimelineEndpointService {
|
|||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
userMutedInstances,
|
||||
noteMutings,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(ps.me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(ps.me.id),
|
||||
this.cacheService.userBlockedCache.fetch(ps.me.id),
|
||||
this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)),
|
||||
this.noteMutingService.getMutingNoteIdsSet(me.id),
|
||||
]);
|
||||
|
||||
const parentFilter = filter;
|
||||
|
@ -114,6 +118,7 @@ export class FanoutTimelineEndpointService {
|
|||
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
|
||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||
if (noteMutings.has(note.id)) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { MiPage } from '@/models/Page.js';
|
|||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiAvatarDecoration, MiChatMessage, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import { MiAvatarDecoration, MiNoteMuting, MiChatMessage, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -260,6 +260,8 @@ export interface InternalEventTypes {
|
|||
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||
noteMuteCreated: MiNoteMuting;
|
||||
noteMuteDeleted: MiNoteMuting;
|
||||
}
|
||||
|
||||
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
|
||||
|
|
|
@ -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, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository, NoteMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
|
@ -21,12 +21,12 @@ export class QueryService {
|
|||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.noteMutingsRepository)
|
||||
private noteMutingsRepository: NoteMutingsRepository,
|
||||
|
||||
@Inject(DI.noteThreadMutingsRepository)
|
||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||
|
||||
|
@ -110,6 +110,16 @@ export class QueryService {
|
|||
q.setParameters(blockedQuery.getParameters());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const query = this.noteMutingsRepository.createQueryBuilder('noteMuting')
|
||||
.select('noteMuting.noteId')
|
||||
.where('noteMuting.userId = :userId', { userId: me.id });
|
||||
|
||||
q.andWhere(`note.id NOT IN (${ query.getQuery() })`);
|
||||
q.setParameters(query.getParameters());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||
|
|
|
@ -234,8 +234,11 @@ export class SearchService {
|
|||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
return query.limit(pagination.limit).getMany();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiNoteMuting, NoteMutingsRepository, NotesRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteMutingService implements OnApplicationShutdown {
|
||||
public static NoSuchNoteError = class extends Error {
|
||||
};
|
||||
public static NotMutedError = class extends Error {
|
||||
};
|
||||
|
||||
private cache: RedisKVCache<Set<string>>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
@Inject(DI.noteMutingsRepository)
|
||||
private noteMutingsRepository: NoteMutingsRepository,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
this.cache = new RedisKVCache<Set<MiNoteMuting['noteId']>>(this.redisClient, 'noteMutings', {
|
||||
// 使用頻度が高く使用される期間も長いためキャッシュの有効期限切れ→再取得が頻発すると思われる。
|
||||
// よって、有効期限を長めに設定して再取得の頻度を抑える(キャッシュの鮮度はRedisイベント経由で保たれているので問題ないはず)
|
||||
lifetime: 1000 * 60 * 60 * 24, // 1d
|
||||
memoryCacheLifetime: 1000 * 60 * 60 * 24, // 1d
|
||||
fetcher: async (userId) => {
|
||||
return this.noteMutingsRepository.createQueryBuilder('noteMuting')
|
||||
.select('noteMuting.noteId')
|
||||
.where('noteMuting.userId = :userId', { userId })
|
||||
.getRawMany<{ noteMuting_noteId: string }>()
|
||||
.then((results) => new Set(results.map(x => x.noteMuting_noteId)));
|
||||
},
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'noteMuteCreated': {
|
||||
const noteIds = await this.cache.get(body.userId);
|
||||
if (noteIds) {
|
||||
noteIds.add(body.noteId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'noteMuteDeleted': {
|
||||
const noteIds = await this.cache.get(body.userId);
|
||||
if (noteIds) {
|
||||
noteIds.delete(body.noteId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async listByUserId(
|
||||
params: {
|
||||
userId: MiNoteMuting['userId'],
|
||||
sinceId?: MiNoteMuting['id'] | null,
|
||||
untilId?: MiNoteMuting['id'] | null,
|
||||
},
|
||||
opts?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
joinUser?: boolean;
|
||||
joinNote?: boolean;
|
||||
},
|
||||
): Promise<MiNoteMuting[]> {
|
||||
const q = this.queryService.makePaginationQuery(this.noteMutingsRepository.createQueryBuilder('noteMuting'), params.sinceId, params.untilId);
|
||||
|
||||
q.where('noteMuting.userId = :userId', { userId: params.userId });
|
||||
if (opts?.joinUser) {
|
||||
q.leftJoinAndSelect('noteMuting.user', 'user');
|
||||
}
|
||||
if (opts?.joinNote) {
|
||||
q.leftJoinAndSelect('noteMuting.note', 'note');
|
||||
}
|
||||
|
||||
q.orderBy('noteMuting.id', 'DESC');
|
||||
|
||||
const limit = opts?.limit ?? 10;
|
||||
q.limit(limit);
|
||||
|
||||
if (opts?.offset) {
|
||||
q.offset(opts.offset);
|
||||
}
|
||||
|
||||
return q.getMany();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMutingNoteIdsSet(userId: MiNoteMuting['userId']): Promise<Set<string>> {
|
||||
return this.cache.fetch(userId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isMuting(userId: MiNoteMuting['userId'], noteId: MiNoteMuting['noteId']): Promise<boolean> {
|
||||
return this.cache.fetch(userId).then(noteIds => noteIds.has(noteId));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
params: Pick<MiNoteMuting, 'userId' | 'noteId' | 'expiresAt'>,
|
||||
): Promise<void> {
|
||||
if (!await this.notesRepository.existsBy({ id: params.noteId })) {
|
||||
throw new NoteMutingService.NoSuchNoteError();
|
||||
}
|
||||
|
||||
const id = this.idService.gen();
|
||||
const result = await this.noteMutingsRepository.insertOne({
|
||||
id,
|
||||
...params,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('noteMuteCreated', result);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(userId: MiNoteMuting['userId'], noteId: MiNoteMuting['noteId']): Promise<void> {
|
||||
const value = await this.noteMutingsRepository.findOne({ where: { userId, noteId } });
|
||||
if (!value) {
|
||||
throw new NoteMutingService.NotMutedError();
|
||||
}
|
||||
|
||||
await this.noteMutingsRepository.delete(value.id);
|
||||
this.globalEventService.publishInternalEvent('noteMuteDeleted', value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async cleanupExpiredMutes(): Promise<void> {
|
||||
const now = new Date();
|
||||
const noteMutings = await this.noteMutingsRepository.createQueryBuilder('noteMuting')
|
||||
.select(['noteMuting.id', 'noteMuting.userId'])
|
||||
.where('noteMuting.expiresAt < :now', { now })
|
||||
.andWhere('noteMuting.expiresAt IS NOT NULL')
|
||||
.getRawMany<{ noteMuting_id: MiNoteMuting['id'], noteMuting_userId: MiNoteMuting['id'] }>();
|
||||
|
||||
await this.noteMutingsRepository.delete(noteMutings.map(x => x.noteMuting_id));
|
||||
|
||||
for (const id of [...new Set(noteMutings.map(x => x.noteMuting_userId))]) {
|
||||
// 同時多発的なDBアクセスが発生することを避けるため1回ごとにawaitする
|
||||
await this.cache.refresh(id);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ export const DI = {
|
|||
appsRepository: Symbol('appsRepository'),
|
||||
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
|
||||
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
||||
noteMutingsRepository: Symbol('noteMutingsRepository'),
|
||||
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
||||
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
||||
pollsRepository: Symbol('pollsRepository'),
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
type NoteCompat = {
|
||||
id: string;
|
||||
reply?: NoteCompat | null;
|
||||
renote?: NoteCompat | null;
|
||||
};
|
||||
|
||||
export function isMutingNoteRelated(note: NoteCompat, noteIds: Set<string>) {
|
||||
if (noteIds.has(note.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (note.reply != null && noteIds.has(note.reply.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (note.renote != null && noteIds.has(note.renote.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiNote } from './Note.js';
|
||||
|
||||
@Entity('note_muting')
|
||||
export class MiNoteMuting {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
...id(),
|
||||
})
|
||||
public noteId: string;
|
||||
|
||||
@ManyToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: MiNote | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public expiresAt: Date | null;
|
||||
}
|
|
@ -40,6 +40,7 @@ import {
|
|||
MiMuting,
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteMuting,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiPage,
|
||||
|
@ -128,6 +129,12 @@ const $noteFavoritesRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteMutingsRepository: Provider = {
|
||||
provide: DI.noteMutingsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteMuting).extend(miRepository as MiRepository<MiNoteMuting>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteThreadMutingsRepository: Provider = {
|
||||
provide: DI.noteThreadMutingsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository<MiNoteThreadMuting>),
|
||||
|
@ -540,6 +547,7 @@ const $reversiGamesRepository: Provider = {
|
|||
$appsRepository,
|
||||
$avatarDecorationsRepository,
|
||||
$noteFavoritesRepository,
|
||||
$noteMutingsRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$pollsRepository,
|
||||
|
@ -616,6 +624,7 @@ const $reversiGamesRepository: Provider = {
|
|||
$appsRepository,
|
||||
$avatarDecorationsRepository,
|
||||
$noteFavoritesRepository,
|
||||
$noteMutingsRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$pollsRepository,
|
||||
|
|
|
@ -56,6 +56,7 @@ import { MiModerationLog } from '@/models/ModerationLog.js';
|
|||
import { MiMuting } from '@/models/Muting.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteMuting } from '@/models/NoteMuting.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
|
@ -190,6 +191,7 @@ export {
|
|||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteMuting,
|
||||
MiNoteThreadMuting,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
|
@ -268,6 +270,7 @@ export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<
|
|||
export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
|
||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
|
||||
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
|
||||
export type NoteMutingsRepository = Repository<MiNoteMuting> & MiRepository<MiNoteMuting>;
|
||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
|
||||
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
|
||||
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import pg from 'pg';
|
||||
import { DataSource, Logger, type QueryRunner } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import { MiNoteMuting } from '@/models/NoteMuting.js';
|
||||
import { entities as charts } from '@/core/chart/entities.js';
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
|
@ -210,6 +211,7 @@ export const entities = [
|
|||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteMuting,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
MiGalleryPost,
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { MutingsRepository } from '@/models/_.js';
|
|||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
|
@ -22,6 +23,7 @@ export class CheckExpiredMutingsProcessorService {
|
|||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private userMutingService: UserMutingService,
|
||||
private noteMutingService: NoteMutingService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
|
||||
|
@ -41,6 +43,8 @@ export class CheckExpiredMutingsProcessorService {
|
|||
await this.userMutingService.unmute(expired);
|
||||
}
|
||||
|
||||
await this.noteMutingService.cleanupExpiredMutes();
|
||||
|
||||
this.logger.succ('All expired mutings checked.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { EventEmitter } from 'events';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as WebSocket from 'ws';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
@ -39,6 +40,7 @@ export class StreamingApiServerService {
|
|||
private notificationService: NotificationService,
|
||||
private usersService: UserService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
private noteMutingService: NoteMutingService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -97,7 +99,9 @@ export class StreamingApiServerService {
|
|||
this.notificationService,
|
||||
this.cacheService,
|
||||
this.channelFollowingService,
|
||||
user, app,
|
||||
this.noteMutingService,
|
||||
user,
|
||||
app,
|
||||
);
|
||||
|
||||
await stream.init();
|
||||
|
|
|
@ -330,6 +330,9 @@ export * as 'notes/timeline' from './endpoints/notes/timeline.js';
|
|||
export * as 'notes/translate' from './endpoints/notes/translate.js';
|
||||
export * as 'notes/unrenote' from './endpoints/notes/unrenote.js';
|
||||
export * as 'notes/user-list-timeline' from './endpoints/notes/user-list-timeline.js';
|
||||
export * as 'notes/muting/create' from './endpoints/notes/muting/create.js';
|
||||
export * as 'notes/muting/delete' from './endpoints/notes/muting/delete.js';
|
||||
export * as 'notes/muting/list' from './endpoints/notes/muting/list.js';
|
||||
export * as 'notifications/create' from './endpoints/notifications/create.js';
|
||||
export * as 'notifications/flush' from './endpoints/notifications/flush.js';
|
||||
export * as 'notifications/mark-all-as-read' from './endpoints/notifications/mark-all-as-read.js';
|
||||
|
|
|
@ -114,6 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
||||
const notes = await query.getMany();
|
||||
if (sinceId != null && untilId == null) {
|
||||
|
|
|
@ -124,6 +124,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
const notes = await query
|
||||
|
|
|
@ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
const notes = await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -82,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
@ -46,7 +46,7 @@ export const meta = {
|
|||
bothWithRepliesAndWithFiles: {
|
||||
message: 'Specifying both withReplies and withFiles is not supported',
|
||||
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
|
||||
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f'
|
||||
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -246,6 +246,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -156,9 +156,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
|
|
|
@ -75,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
||||
if (ps.visibility) {
|
||||
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'a58e7999-f6d3-1780-a688-f43661719662',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
expiresAt: { type: 'integer', nullable: true },
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private readonly noteMutingService: NoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
await this.noteMutingService.create({
|
||||
userId: me.id,
|
||||
noteId: ps.noteId,
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof NoteMutingService.NoSuchNoteError) {
|
||||
throw new ApiError(meta.errors.noSuchNote);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
notMuted: {
|
||||
message: 'Not muted.',
|
||||
code: 'NOT_MUTED',
|
||||
id: '6ad3b6c9-f173-60f7-b558-5eea13896254',
|
||||
httpStatusCode: 400,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['noteId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private readonly noteMutingService: NoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
await this.noteMutingService.delete(me.id, ps.noteId);
|
||||
} catch (e) {
|
||||
if (e instanceof NoteMutingService.NotMutedError) {
|
||||
throw new ApiError(meta.errors.notMuted);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
expiresAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
note: { type: 'object', ref: 'Note' },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sinceId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
untilId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private readonly noteMutingService: NoteMutingService,
|
||||
private readonly noteEntityService: NoteEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const mutings = await this.noteMutingService.listByUserId(
|
||||
{ userId: me.id },
|
||||
{
|
||||
joinNote: true,
|
||||
limit: ps.limit,
|
||||
offset: ps.offset,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const packedNotes = await this.noteEntityService.packMany(mutings.map(m => m.note!))
|
||||
.then(res => new Map(res.map(it => [it.id, it])));
|
||||
|
||||
return mutings.map(m => ({
|
||||
id: m.id,
|
||||
expiresAt: m.expiresAt?.toISOString(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
note: packedNotes.get(m.noteId)!,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -72,8 +72,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
const renotes = await query.limit(ps.limit).getMany();
|
||||
|
||||
|
|
|
@ -56,8 +56,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
|
|
|
@ -81,8 +81,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -26,6 +27,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isMutedNote: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -49,11 +54,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
@Inject(DI.noteFavoritesRepository)
|
||||
private noteFavoritesRepository: NoteFavoritesRepository,
|
||||
|
||||
private noteMutingService: NoteMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const note = await this.notesRepository.findOneByOrFail({ id: ps.noteId });
|
||||
|
||||
const [favorite, threadMuting] = await Promise.all([
|
||||
const [favorite, threadMuting, isMutedNote] = await Promise.all([
|
||||
this.noteFavoritesRepository.count({
|
||||
where: {
|
||||
userId: me.id,
|
||||
|
@ -68,11 +75,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
},
|
||||
take: 1,
|
||||
}),
|
||||
this.noteMutingService.isMuting(me.id, note.id),
|
||||
]);
|
||||
|
||||
return {
|
||||
isFavorited: favorite !== 0,
|
||||
isMutedThread: threadMuting !== 0,
|
||||
isMutedNote,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -202,6 +202,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -187,6 +187,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -104,6 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
||||
const notes = await query.getMany();
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
|
|
@ -187,6 +187,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService
|
|||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { isJsonObject } from '@/misc/json-value.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
|
@ -40,6 +41,7 @@ export default class Connection {
|
|||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
||||
public userMutedInstances: Set<string> = new Set();
|
||||
public noteMuting: Set<string> = new Set();
|
||||
private fetchIntervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
|
@ -47,6 +49,7 @@ export default class Connection {
|
|||
private notificationService: NotificationService,
|
||||
private cacheService: CacheService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
private noteMutingService: NoteMutingService,
|
||||
|
||||
user: MiUser | null | undefined,
|
||||
token: MiAccessToken | null | undefined,
|
||||
|
@ -58,13 +61,14 @@ export default class Connection {
|
|||
@bindThis
|
||||
public async fetch() {
|
||||
if (this.user == null) return;
|
||||
const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([
|
||||
const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes, noteMuting] = await Promise.all([
|
||||
this.cacheService.userProfileCache.fetch(this.user.id),
|
||||
this.cacheService.userFollowingsCache.fetch(this.user.id),
|
||||
this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id),
|
||||
this.cacheService.userMutingsCache.fetch(this.user.id),
|
||||
this.cacheService.userBlockedCache.fetch(this.user.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(this.user.id),
|
||||
this.noteMutingService.getMutingNoteIdsSet(this.user.id),
|
||||
]);
|
||||
this.userProfile = userProfile;
|
||||
this.following = following;
|
||||
|
@ -73,6 +77,7 @@ export default class Connection {
|
|||
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
||||
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
||||
this.userMutedInstances = new Set(userProfile.mutedInstances);
|
||||
this.noteMuting = noteMuting;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { isMutingNoteRelated } from '@/misc/is-muting-note-related.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
@ -51,6 +52,10 @@ export default abstract class Channel {
|
|||
return this.connection.userMutedInstances;
|
||||
}
|
||||
|
||||
protected get noteMuting() {
|
||||
return this.connection.noteMuting;
|
||||
}
|
||||
|
||||
protected get followingChannels() {
|
||||
return this.connection.followingChannels;
|
||||
}
|
||||
|
@ -74,6 +79,9 @@ export default abstract class Channel {
|
|||
// 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
|
||||
|
||||
// 流れてきたNoteがミュートしているNoteに関わる(ミュートしたノートがリノートされた or リプライがついた時)
|
||||
if (isMutingNoteRelated(note, this.noteMuting)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -512,6 +512,28 @@ describe('Timelines', () => {
|
|||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('ノートミュートが機能する', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
await api('following/create', { userId: bob.id }, alice);
|
||||
await setTimeout(1000);
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// ミュート前はノートが表示される
|
||||
const res1 = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res1.body.some(note => note.id === bobNote.id), true);
|
||||
|
||||
// ノートをミュート
|
||||
await api('notes/muting/create', { noteId: bobNote.id }, alice);
|
||||
await setTimeout(1000);
|
||||
|
||||
// ミュート後はノートが表示されない
|
||||
const res2 = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res2.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
|
||||
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
|
||||
|
||||
|
@ -744,7 +766,27 @@ describe('Timelines', () => {
|
|||
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
}, 1000 * 30);
|
||||
|
||||
test.concurrent('ノートミュートが機能する', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// ミュート前はノートが表示される
|
||||
const res1 = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res1.body.some(note => note.id === bobNote.id), true);
|
||||
|
||||
// ノートをミュート
|
||||
await api('notes/muting/create', { noteId: bobNote.id }, alice);
|
||||
await setTimeout(1000);
|
||||
|
||||
// ミュート後はノートが表示されない
|
||||
const res2 = await api('notes/local-timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res2.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Social TL', () => {
|
||||
|
@ -955,7 +997,27 @@ describe('Timelines', () => {
|
|||
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
}, 1000 * 30);
|
||||
|
||||
test.concurrent('ノートミュートが機能する', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// ミュート前はノートが表示される
|
||||
const res1 = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res1.body.some(note => note.id === bobNote.id), true);
|
||||
|
||||
// ノートをミュート
|
||||
await api('notes/muting/create', { noteId: bobNote.id }, alice);
|
||||
await setTimeout(1000);
|
||||
|
||||
// ミュート後はノートが表示されない
|
||||
const res2 = await api('notes/hybrid-timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res2.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User List TL', () => {
|
||||
|
@ -1168,7 +1230,7 @@ describe('Timelines', () => {
|
|||
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
}, 1000 * 30);
|
||||
|
||||
test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
@ -1201,6 +1263,29 @@ describe('Timelines', () => {
|
|||
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('ノートミュートが機能する', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
await setTimeout(1000);
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// ミュート前はノートが表示される
|
||||
const res1 = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
assert.strictEqual(res1.body.some(note => note.id === bobNote.id), true);
|
||||
|
||||
// ノートをミュート
|
||||
await api('notes/muting/create', { noteId: bobNote.id }, alice);
|
||||
await setTimeout(1000);
|
||||
|
||||
// ミュート後はノートが表示されない
|
||||
const res2 = await api('notes/user-list-timeline', { listId: list.id }, alice);
|
||||
assert.strictEqual(res2.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User TL', () => {
|
||||
|
@ -1327,7 +1412,7 @@ describe('Timelines', () => {
|
|||
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
|
||||
}, 1000 * 10);
|
||||
}, 1000 * 30);
|
||||
|
||||
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
@ -1451,6 +1536,26 @@ describe('Timelines', () => {
|
|||
const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id });
|
||||
assert.deepStrictEqual(res.body, [note3, note2, note1]);
|
||||
});
|
||||
|
||||
test.concurrent('ノートミュートが機能する', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
// ミュート前はノートが表示される
|
||||
const res1 = await api('users/notes', { userId: bob.id }, alice);
|
||||
assert.strictEqual(res1.body.some(note => note.id === bobNote.id), true);
|
||||
|
||||
// ノートをミュート
|
||||
await api('notes/muting/create', { noteId: bobNote.id }, alice);
|
||||
await setTimeout(1000);
|
||||
|
||||
// ミュート後はノートが表示されない
|
||||
const res2 = await api('users/notes', { userId: bob.id }, alice);
|
||||
assert.strictEqual(res2.body.some(note => note.id === bobNote.id), false);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: リノートミュート済みユーザーのテスト
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, jest, expect, beforeAll, beforeEach, afterEach, afterAll, test } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { randomString } from '../utils.js';
|
||||
import { NoteMutingService } from '@/core/note/NoteMutingService.js';
|
||||
import {
|
||||
MiNoteMuting,
|
||||
MiNote,
|
||||
MiUser,
|
||||
NoteMutingsRepository,
|
||||
NotesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
describe('NoteMutingService', () => {
|
||||
let app: TestingModule;
|
||||
let service: NoteMutingService;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let notesRepository: NotesRepository;
|
||||
let noteMutingsRepository: NoteMutingsRepository;
|
||||
let usersRepository: UsersRepository;
|
||||
let idService: IdService;
|
||||
let globalEventService: GlobalEventService;
|
||||
let queryService: QueryService;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
// Helper function to create a user
|
||||
async function createUser(data: Partial<MiUser> = {}): Promise<MiUser> {
|
||||
const user = {
|
||||
id: idService.gen(),
|
||||
username: randomString(),
|
||||
usernameLower: randomString().toLowerCase(),
|
||||
host: null,
|
||||
...data,
|
||||
};
|
||||
|
||||
return await usersRepository.insert(user)
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
// Helper function to create a note
|
||||
async function createNote(data: Partial<MiNote> = {}): Promise<MiNote> {
|
||||
return await notesRepository.insert({
|
||||
id: idService.gen(),
|
||||
userId: data.userId ?? (await createUser()).id,
|
||||
text: randomString(),
|
||||
visibility: 'public',
|
||||
...data,
|
||||
})
|
||||
.then(x => notesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
// Helper function to create a note muting
|
||||
async function createNoteMuting(data: Partial<MiNoteMuting> = {}): Promise<MiNoteMuting> {
|
||||
const id = idService.gen();
|
||||
const noteMuting = {
|
||||
id,
|
||||
userId: data.userId || (await createUser()).id,
|
||||
noteId: data.noteId || (await createNote()).id,
|
||||
expiresAt: null,
|
||||
...data,
|
||||
};
|
||||
|
||||
return await noteMutingsRepository.insert(noteMuting)
|
||||
.then(x => noteMutingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test
|
||||
.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
CoreModule,
|
||||
],
|
||||
})
|
||||
.compile();
|
||||
|
||||
service = app.get(NoteMutingService);
|
||||
idService = app.get(IdService);
|
||||
queryService = app.get(QueryService);
|
||||
globalEventService = app.get(GlobalEventService);
|
||||
notesRepository = app.get(DI.notesRepository);
|
||||
noteMutingsRepository = app.get(DI.noteMutingsRepository);
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
|
||||
app.enableShutdownHooks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean database before each test
|
||||
await noteMutingsRepository.delete({});
|
||||
await notesRepository.delete({});
|
||||
await usersRepository.delete({});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean database after each test
|
||||
await noteMutingsRepository.delete({});
|
||||
await notesRepository.delete({});
|
||||
await usersRepository.delete({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
describe('create', () => {
|
||||
test('should create a note muting', async () => {
|
||||
// Create a user and a note
|
||||
const user = await createUser();
|
||||
const note = await createNote();
|
||||
|
||||
// Create a note muting
|
||||
await service.create({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
expiresAt: null,
|
||||
});
|
||||
|
||||
// Verify the note muting was created
|
||||
const noteMuting = await noteMutingsRepository.findOneBy({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
expect(noteMuting).not.toBeNull();
|
||||
expect(noteMuting?.userId).toBe(user.id);
|
||||
expect(noteMuting?.noteId).toBe(note.id);
|
||||
});
|
||||
|
||||
test('should throw NoSuchNoteError if note does not exist', async () => {
|
||||
// Create a user
|
||||
const user = await createUser();
|
||||
const nonexistentNoteId = idService.gen();
|
||||
|
||||
// Attempt to create a note muting with a non-existent note
|
||||
await expect(service.create({
|
||||
userId: user.id,
|
||||
noteId: nonexistentNoteId,
|
||||
expiresAt: null,
|
||||
})).rejects.toThrow(NoteMutingService.NoSuchNoteError);
|
||||
|
||||
// Verify no note muting was created
|
||||
const noteMuting = await noteMutingsRepository.findOneBy({
|
||||
userId: user.id,
|
||||
noteId: nonexistentNoteId,
|
||||
});
|
||||
|
||||
expect(noteMuting).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
test('should delete a note muting', async () => {
|
||||
// Create a user, note, and note muting
|
||||
const user = await createUser();
|
||||
const note = await createNote();
|
||||
const noteMuting = await createNoteMuting({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
// Verify the note muting exists
|
||||
const beforeDelete = await noteMutingsRepository.findOneBy({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
expect(beforeDelete).not.toBeNull();
|
||||
|
||||
// Delete the note muting
|
||||
await service.delete(user.id, note.id);
|
||||
|
||||
// Verify the note muting was deleted
|
||||
const afterDelete = await noteMutingsRepository.findOneBy({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
expect(afterDelete).toBeNull();
|
||||
});
|
||||
|
||||
test('should throw NotMutedError if muting does not exist', async () => {
|
||||
// Create a user and note
|
||||
const user = await createUser();
|
||||
const note = await createNote();
|
||||
|
||||
// Attempt to delete a non-existent note muting
|
||||
await expect(service.delete(user.id, note.id)).rejects.toThrow(NoteMutingService.NotMutedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMuting', () => {
|
||||
test('should return true if user is muting the note', async () => {
|
||||
// Create a user, note, and note muting
|
||||
const user = await createUser();
|
||||
const note = await createNote();
|
||||
await createNoteMuting({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
// Check if the user is muting the note
|
||||
const result = await service.isMuting(user.id, note.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if user is not muting the note', async () => {
|
||||
// Create a user and note, but no muting
|
||||
const user = await createUser();
|
||||
const note = await createNote();
|
||||
|
||||
// Check if the user is muting the note
|
||||
const result = await service.isMuting(user.id, note.id);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMutingNoteIdsSet', () => {
|
||||
test('should return a set of muted note IDs', async () => {
|
||||
// Create a user and multiple notes
|
||||
const user = await createUser();
|
||||
const note1 = await createNote();
|
||||
const note2 = await createNote();
|
||||
const note3 = await createNote();
|
||||
|
||||
// Create note mutings for two of the notes
|
||||
await createNoteMuting({
|
||||
userId: user.id,
|
||||
noteId: note1.id,
|
||||
});
|
||||
await createNoteMuting({
|
||||
userId: user.id,
|
||||
noteId: note2.id,
|
||||
});
|
||||
|
||||
// Get the set of muted note IDs
|
||||
const result = await service.getMutingNoteIdsSet(user.id);
|
||||
|
||||
// Verify the result is a Set containing the muted note IDs
|
||||
expect(result).toBeInstanceOf(Set);
|
||||
expect(result.has(note1.id)).toBe(true);
|
||||
expect(result.has(note2.id)).toBe(true);
|
||||
expect(result.has(note3.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listByUserId', () => {
|
||||
test('should return a list of note mutings for a user', async () => {
|
||||
// Create a user and multiple notes
|
||||
const user = await createUser();
|
||||
const note1 = await createNote();
|
||||
const note2 = await createNote();
|
||||
|
||||
// Create note mutings
|
||||
const muting1 = await createNoteMuting({
|
||||
userId: user.id,
|
||||
noteId: note1.id,
|
||||
});
|
||||
const muting2 = await createNoteMuting({
|
||||
userId: user.id,
|
||||
noteId: note2.id,
|
||||
});
|
||||
|
||||
// Get the list of note mutings
|
||||
const result = await service.listByUserId({ userId: user.id });
|
||||
|
||||
// Verify the result contains the expected mutings
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map(m => m.id).sort()).toEqual([muting1.id, muting2.id].sort());
|
||||
expect(result.map(m => m.noteId).sort()).toEqual([note1.id, note2.id].sort());
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupExpiredMutes', () => {
|
||||
test('should delete expired mutes', async () => {
|
||||
// Create users and notes
|
||||
const user1 = await createUser();
|
||||
const user2 = await createUser();
|
||||
const note1 = await createNote();
|
||||
const note2 = await createNote();
|
||||
const note3 = await createNote();
|
||||
|
||||
// Set the expiration date to 1 hour ago
|
||||
const expiredDate = new Date();
|
||||
expiredDate.setHours(expiredDate.getHours() - 1);
|
||||
|
||||
// Set the expiration date to 1 hour in the future
|
||||
const futureDate = new Date();
|
||||
futureDate.setHours(futureDate.getHours() + 1);
|
||||
|
||||
// Create expired note mutings
|
||||
const expiredMuting1 = await createNoteMuting({
|
||||
userId: user1.id,
|
||||
noteId: note1.id,
|
||||
expiresAt: expiredDate,
|
||||
});
|
||||
|
||||
const expiredMuting2 = await createNoteMuting({
|
||||
userId: user1.id,
|
||||
noteId: note2.id,
|
||||
expiresAt: expiredDate,
|
||||
});
|
||||
|
||||
const expiredMuting3 = await createNoteMuting({
|
||||
userId: user2.id,
|
||||
noteId: note3.id,
|
||||
expiresAt: expiredDate,
|
||||
});
|
||||
|
||||
// Create non-expired note muting
|
||||
const activeMuting = await createNoteMuting({
|
||||
userId: user2.id,
|
||||
noteId: note1.id,
|
||||
expiresAt: futureDate,
|
||||
});
|
||||
|
||||
// Create permanent note muting (no expiration)
|
||||
const permanentMuting = await createNoteMuting({
|
||||
userId: user2.id,
|
||||
noteId: note2.id,
|
||||
expiresAt: null,
|
||||
});
|
||||
|
||||
// Verify all mutings exist before cleanup
|
||||
expect(await noteMutingsRepository.findOneBy({ id: expiredMuting1.id })).not.toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: expiredMuting2.id })).not.toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: expiredMuting3.id })).not.toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: activeMuting.id })).not.toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: permanentMuting.id })).not.toBeNull();
|
||||
|
||||
// Run cleanup
|
||||
await service.cleanupExpiredMutes();
|
||||
|
||||
// Verify expired mutings are deleted and others remain
|
||||
expect(await noteMutingsRepository.findOneBy({ id: expiredMuting1.id })).toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: expiredMuting2.id })).toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: expiredMuting3.id })).toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: activeMuting.id })).not.toBeNull();
|
||||
expect(await noteMutingsRepository.findOneBy({ id: permanentMuting.id })).not.toBeNull();
|
||||
|
||||
// Verify cache is updated by checking isMuting
|
||||
expect(await service.isMuting(user1.id, note1.id)).toBe(false);
|
||||
expect(await service.isMuting(user1.id, note2.id)).toBe(false);
|
||||
expect(await service.isMuting(user2.id, note3.id)).toBe(false);
|
||||
expect(await service.isMuting(user2.id, note1.id)).toBe(true);
|
||||
expect(await service.isMuting(user2.id, note2.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination ref="pagingComponent" :pagination="noteMutingPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkFolder v-for="item in (items as entities.NotesMutingListResponse)" :key="item.id" style="margin-bottom: 1rem;">
|
||||
<template #label>
|
||||
<div>
|
||||
<span>[{{ i18n.ts.expiration }}: </span>
|
||||
<MkTime v-if="item.expiresAt" :time="item.expiresAt" mode="absolute"/>
|
||||
<span v-else>{{ i18n.ts.none }}</span>
|
||||
<span>] </span>
|
||||
<span>
|
||||
{{ ((item.note.user.name) ? item.note.user.name + ` (@${item.note.user.username})` : `@${item.note.user.username}`) }}
|
||||
</span>
|
||||
<span>
|
||||
{{ i18n.ts._noteMuting.labelSuffix }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<MkNoteSub :note="item.note"/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div style="display: flex; flex-direction: column" class="_gaps">
|
||||
<MkButton :danger="true" @click="onClickUnmuteNote(item.note.id)">{{ i18n.ts._noteMuting.unmuteNote }}</MkButton>
|
||||
<span :class="$style.caption">{{ i18n.ts._noteMuting.unmuteCaption }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { entities } from 'misskey-js';
|
||||
import { shallowRef } from 'vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
import * as os from '@/os';
|
||||
|
||||
const noteMutingPagination: Paging = {
|
||||
endpoint: 'notes/muting/list',
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
async function onClickUnmuteNote(noteId: string) {
|
||||
await os.apiWithDialog(
|
||||
'notes/muting/delete',
|
||||
{
|
||||
noteId,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'6ad3b6c9-f173-60f7-b558-5eea13896254': {
|
||||
title: i18n.ts.error,
|
||||
text: i18n.ts._noteMuting.notMutedNote,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.caption {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: var(--MI_THEME-fgTransparentWeak);
|
||||
}
|
||||
</style>
|
|
@ -171,6 +171,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkPagination>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker
|
||||
:label="i18n.ts._noteMuting.noteMuting"
|
||||
:keywords="['mute', 'note']"
|
||||
>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ban"></i></template>
|
||||
<template #label>{{ 'ミュートしたノート' }}</template>
|
||||
|
||||
<XNoteMute/>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
|
@ -180,6 +192,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, computed, watch } from 'vue';
|
||||
import XInstanceMute from './mute-block.instance-mute.vue';
|
||||
import XWordMute from './mute-block.word-mute.vue';
|
||||
import XNoteMute from './mute-block.note-mute.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
@ -234,6 +234,70 @@ export function getNoteMenu(props: {
|
|||
});
|
||||
}
|
||||
|
||||
async function toggleNoteMute(mute: boolean) {
|
||||
if (!mute) {
|
||||
await os.apiWithDialog(
|
||||
'notes/muting/delete',
|
||||
{
|
||||
noteId: appearNote.id,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'6ad3b6c9-f173-60f7-b558-5eea13896254': {
|
||||
title: i18n.ts.error,
|
||||
text: i18n.ts._noteMuting.notMutedNote,
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const { canceled, result: period } = await os.select({
|
||||
title: i18n.ts.mutePeriod,
|
||||
items: [{
|
||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||
}, {
|
||||
value: 'tenMinutes', text: i18n.ts.tenMinutes,
|
||||
}, {
|
||||
value: 'oneHour', text: i18n.ts.oneHour,
|
||||
}, {
|
||||
value: 'oneDay', text: i18n.ts.oneDay,
|
||||
}, {
|
||||
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||
}],
|
||||
default: 'indefinitely',
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const expiresAt = period === 'indefinitely'
|
||||
? null
|
||||
: period === 'tenMinutes'
|
||||
? Date.now() + (1000 * 60 * 10)
|
||||
: period === 'oneHour'
|
||||
? Date.now() + (1000 * 60 * 60)
|
||||
: period === 'oneDay'
|
||||
? Date.now() + (1000 * 60 * 60 * 24)
|
||||
: period === 'oneWeek'
|
||||
? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||
: null;
|
||||
|
||||
await os.apiWithDialog(
|
||||
'notes/muting/create',
|
||||
{
|
||||
noteId: appearNote.id,
|
||||
expiresAt,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'a58e7999-f6d3-1780-a688-f43661719662': {
|
||||
title: i18n.ts.error,
|
||||
text: i18n.ts._noteMuting.noNotes,
|
||||
},
|
||||
},
|
||||
).then(() => {
|
||||
props.isDeleted.value = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyContent(): void {
|
||||
copyToClipboard(appearNote.text);
|
||||
}
|
||||
|
@ -379,6 +443,16 @@ export function getNoteMenu(props: {
|
|||
action: () => toggleThreadMute(true),
|
||||
}));
|
||||
|
||||
menuItems.push(statePromise.then(state => state.isMutedNote ? {
|
||||
icon: 'ti ti-message',
|
||||
text: i18n.ts._noteMuting.unmuteNote,
|
||||
action: () => toggleNoteMute(false),
|
||||
} : {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts._noteMuting.muteNote,
|
||||
action: () => toggleNoteMute(true),
|
||||
}));
|
||||
|
||||
if (appearNote.userId === $i.id) {
|
||||
if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {
|
||||
menuItems.push({
|
||||
|
|
|
@ -1971,6 +1971,10 @@ declare namespace entities {
|
|||
NotesLocalTimelineResponse,
|
||||
NotesMentionsRequest,
|
||||
NotesMentionsResponse,
|
||||
NotesMutingCreateRequest,
|
||||
NotesMutingDeleteRequest,
|
||||
NotesMutingListRequest,
|
||||
NotesMutingListResponse,
|
||||
NotesPollsRecommendationRequest,
|
||||
NotesPollsRecommendationResponse,
|
||||
NotesPollsVoteRequest,
|
||||
|
@ -3031,6 +3035,18 @@ type NotesMentionsRequest = operations['notes___mentions']['requestBody']['conte
|
|||
// @public (undocumented)
|
||||
type NotesMentionsResponse = operations['notes___mentions']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesMutingCreateRequest = operations['notes___muting___create']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesMutingDeleteRequest = operations['notes___muting___delete']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesMutingListRequest = operations['notes___muting___list']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesMutingListResponse = operations['notes___muting___list']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type NotesPollsRecommendationRequest = operations['notes___polls___recommendation']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
"openapi-typescript": "6.7.6",
|
||||
"ts-case-convert": "2.1.0",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.2",
|
||||
"eslint": "9.22.0"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
|
|
|
@ -3648,6 +3648,39 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:account*
|
||||
*/
|
||||
request<E extends 'notes/muting/create', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:account*
|
||||
*/
|
||||
request<E extends 'notes/muting/delete', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
request<E extends 'notes/muting/list', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -503,6 +503,10 @@ import type {
|
|||
NotesLocalTimelineResponse,
|
||||
NotesMentionsRequest,
|
||||
NotesMentionsResponse,
|
||||
NotesMutingCreateRequest,
|
||||
NotesMutingDeleteRequest,
|
||||
NotesMutingListRequest,
|
||||
NotesMutingListResponse,
|
||||
NotesPollsRecommendationRequest,
|
||||
NotesPollsRecommendationResponse,
|
||||
NotesPollsVoteRequest,
|
||||
|
@ -969,6 +973,9 @@ export type Endpoints = {
|
|||
'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse };
|
||||
'notes/local-timeline': { req: NotesLocalTimelineRequest; res: NotesLocalTimelineResponse };
|
||||
'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse };
|
||||
'notes/muting/create': { req: NotesMutingCreateRequest; res: EmptyResponse };
|
||||
'notes/muting/delete': { req: NotesMutingDeleteRequest; res: EmptyResponse };
|
||||
'notes/muting/list': { req: NotesMutingListRequest; res: NotesMutingListResponse };
|
||||
'notes/polls/recommendation': { req: NotesPollsRecommendationRequest; res: NotesPollsRecommendationResponse };
|
||||
'notes/polls/vote': { req: NotesPollsVoteRequest; res: EmptyResponse };
|
||||
'notes/reactions': { req: NotesReactionsRequest; res: NotesReactionsResponse };
|
||||
|
|
|
@ -506,6 +506,10 @@ export type NotesLocalTimelineRequest = operations['notes___local-timeline']['re
|
|||
export type NotesLocalTimelineResponse = operations['notes___local-timeline']['responses']['200']['content']['application/json'];
|
||||
export type NotesMentionsRequest = operations['notes___mentions']['requestBody']['content']['application/json'];
|
||||
export type NotesMentionsResponse = operations['notes___mentions']['responses']['200']['content']['application/json'];
|
||||
export type NotesMutingCreateRequest = operations['notes___muting___create']['requestBody']['content']['application/json'];
|
||||
export type NotesMutingDeleteRequest = operations['notes___muting___delete']['requestBody']['content']['application/json'];
|
||||
export type NotesMutingListRequest = operations['notes___muting___list']['requestBody']['content']['application/json'];
|
||||
export type NotesMutingListResponse = operations['notes___muting___list']['responses']['200']['content']['application/json'];
|
||||
export type NotesPollsRecommendationRequest = operations['notes___polls___recommendation']['requestBody']['content']['application/json'];
|
||||
export type NotesPollsRecommendationResponse = operations['notes___polls___recommendation']['responses']['200']['content']['application/json'];
|
||||
export type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -3150,6 +3150,33 @@ export type paths = {
|
|||
*/
|
||||
post: operations['notes___mentions'];
|
||||
};
|
||||
'/notes/muting/create': {
|
||||
/**
|
||||
* notes/muting/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:account*
|
||||
*/
|
||||
post: operations['notes___muting___create'];
|
||||
};
|
||||
'/notes/muting/delete': {
|
||||
/**
|
||||
* notes/muting/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:account*
|
||||
*/
|
||||
post: operations['notes___muting___delete'];
|
||||
};
|
||||
'/notes/muting/list': {
|
||||
/**
|
||||
* notes/muting/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
post: operations['notes___muting___list'];
|
||||
};
|
||||
'/notes/polls/recommendation': {
|
||||
/**
|
||||
* notes/polls/recommendation
|
||||
|
@ -25142,6 +25169,181 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* notes/muting/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:account*
|
||||
*/
|
||||
notes___muting___create: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
noteId: string;
|
||||
expiresAt?: number | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Too many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* notes/muting/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:account*
|
||||
*/
|
||||
notes___muting___delete: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
noteId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* notes/muting/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
notes___muting___list: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string | null;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string | null;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': ({
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
expiresAt: string | null;
|
||||
note: components['schemas']['Note'];
|
||||
})[];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* notes/polls/recommendation
|
||||
* @description No description provided.
|
||||
|
@ -25766,6 +25968,7 @@ export type operations = {
|
|||
'application/json': {
|
||||
isFavorited: boolean;
|
||||
isMutedThread: boolean;
|
||||
isMutedNote: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1389,6 +1389,9 @@ importers:
|
|||
'@typescript-eslint/parser':
|
||||
specifier: 8.29.0
|
||||
version: 8.29.0(eslint@9.22.0)(typescript@5.8.2)
|
||||
eslint:
|
||||
specifier: 9.22.0
|
||||
version: 9.22.0
|
||||
openapi-types:
|
||||
specifier: 12.1.3
|
||||
version: 12.1.3
|
||||
|
@ -4303,9 +4306,6 @@ packages:
|
|||
'@types/eslint@7.29.0':
|
||||
resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
|
||||
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
'@types/estree@1.0.7':
|
||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||
|
||||
|
@ -10850,6 +10850,9 @@ packages:
|
|||
vue-component-type-helpers@2.0.16:
|
||||
resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==}
|
||||
|
||||
vue-component-type-helpers@2.2.10:
|
||||
resolution: {integrity: sha512-iDUO7uQK+Sab2tYuiP9D1oLujCWlhHELHMgV/cB13cuGbG4qwkLHvtfWb6FzvxrIOPDnU0oHsz2MlQjhYDeaHA==}
|
||||
|
||||
vue-component-type-helpers@2.2.8:
|
||||
resolution: {integrity: sha512-4bjIsC284coDO9om4HPA62M7wfsTvcmZyzdfR0aUlFXqq4tXxM1APyXpNVxPC8QazKw9OhmZNHBVDA6ODaZsrA==}
|
||||
|
||||
|
@ -14445,7 +14448,7 @@ snapshots:
|
|||
ts-dedent: 2.2.0
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
vue-component-type-helpers: 2.2.8
|
||||
vue-component-type-helpers: 2.2.10
|
||||
|
||||
'@stylistic/eslint-plugin@2.13.0(eslint@9.22.0)(typescript@5.8.2)':
|
||||
dependencies:
|
||||
|
@ -14775,8 +14778,6 @@ snapshots:
|
|||
'@types/estree': 1.0.7
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@types/estree@1.0.7': {}
|
||||
|
||||
'@types/express-serve-static-core@4.17.33':
|
||||
|
@ -17483,7 +17484,7 @@ snapshots:
|
|||
|
||||
eslint@9.22.0:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@9.22.0)
|
||||
'@eslint-community/eslint-utils': 4.6.1(eslint@9.22.0)
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
'@eslint/config-array': 0.19.2
|
||||
'@eslint/config-helpers': 0.1.0
|
||||
|
@ -17494,7 +17495,7 @@ snapshots:
|
|||
'@humanfs/node': 0.16.6
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.2
|
||||
'@types/estree': 1.0.6
|
||||
'@types/estree': 1.0.7
|
||||
'@types/json-schema': 7.0.15
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
|
@ -22699,6 +22700,8 @@ snapshots:
|
|||
|
||||
vue-component-type-helpers@2.0.16: {}
|
||||
|
||||
vue-component-type-helpers@2.2.10: {}
|
||||
|
||||
vue-component-type-helpers@2.2.8: {}
|
||||
|
||||
vue-demi@0.14.7(vue@3.5.13(typescript@5.8.3)):
|
||||
|
|
Loading…
Reference in New Issue