Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2026.6.0

### General
<<<<<<< Updated upstream
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
- Feat: アンテナのタイムラインから個別のノートを削除できるように
- Fix: コンパネからrootユーザーのパスワードをリセットしようとした際にエラーが通知されない問題を修正
Expand All @@ -18,8 +19,15 @@
- Fix: 「D」キーでダークモードを切り替える際にsyncDeviceDarkModeのチェックがバイパスされる問題を修正
- Fix: パスキー登録完了時の認証ダイアログの入力値が使われていない問題を修正
- Fix: メンションのサジェスト時に表示されるアイコン表示が画像サイズ次第で崩れる問題を修正
=======
- Feat: ノート単位でリノートをロックできる機能を追加 (自分のノートを他人にリノートされないようにする / モデレーターが全員のリノートを禁止する)

### Client
- Feat: 投稿フォームとノートメニューからリノートロックを設定できるように
>>>>>>> Stashed changes

### Server
- Feat: リノートロック用 API (`notes/renote-lock/create`・`delete`、`admin/notes/renote-lock/create`・`delete`) を追加
- Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善
- Fix: PerUserDriveChart がシステム所有ファイル (userId が null) の更新で `"group"` の非NULL制約違反によりクラッシュする問題を修正 (#17498)
- Enhance: リモートノートクリーニングジョブの削除対象検索処理のパフォーマンス改善
Expand Down
10 changes: 10 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2522,6 +2522,7 @@ _permissions:
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
"write:admin:meta": "インスタンスのメタデータを操作する"
"write:admin:user-note": "モデレーションノートを操作する"
"write:admin:note-renote-lock": "ノートのリノートロックを操作する"
"write:admin:roles": "ロールを操作する"
"read:admin:roles": "ロールを見る"
"write:admin:relays": "リレーを操作する"
Expand Down Expand Up @@ -3076,6 +3077,8 @@ _moderationLogTypes:
deleteGalleryPost: "ギャラリーの投稿を削除"
deleteChatRoom: "ダイレクトメッセージのグループを削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
lockNoteRenote: "ノートのリノートをロック"
unlockNoteRenote: "ノートのリノートのロックを解除"

_fileViewer:
title: "ファイルの詳細"
Expand Down Expand Up @@ -3534,6 +3537,13 @@ _imageEffector:
circle: "円形"

drafts: "下書き"
_renoteLock:
lockRenoteByOthersInPost: "他の人によるリノートを禁止"
prohibitRenote: "リノートを禁止する"
allowRenote: "リノートを許可する"
lockByModeration: "リノートをロックする (モデレーション)"
unlockByModeration: "リノートのロックを解除する (モデレーション)"

_drafts:
select: "下書きを選択"
cannotCreateDraftAnymore: "下書きの作成可能数を超えています。"
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/migration/1780190369217-noteRenoteLock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class NoteRenoteLock1780190369217 {
name = 'NoteRenoteLock1780190369217'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "userRenoteLock" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "note" ADD "moderationRenoteLock" boolean NOT NULL DEFAULT false`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "moderationRenoteLock"`);
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "userRenoteLock"`);
}
}
16 changes: 16 additions & 0 deletions packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ type Option = {
poll?: IPoll | null;
localOnly?: boolean | null;
reactionAcceptance?: MiNote['reactionAcceptance'];
userRenoteLock?: boolean | null;
cw?: string | null;
visibility?: string;
visibleUsers?: MinimumUser[] | null;
Expand Down Expand Up @@ -291,6 +292,7 @@ export class NoteCreateService implements OnApplicationShutdown {
channelId: MiChannel['id'] | null;
localOnly: boolean;
reactionAcceptance: MiNote['reactionAcceptance'];
userRenoteLock?: boolean;
poll: IPoll | null;
apMentions?: MinimumUser[] | null;
apHashtags?: string[] | null;
Expand Down Expand Up @@ -367,6 +369,18 @@ export class NoteCreateService implements OnApplicationShutdown {
throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external');
}
}

// リノートロックのチェック (純粋リノートのみ対象。引用リノートはフロント側で制御する)
const isPureRenoteRequest = data.text == null && data.cw == null && data.replyId == null && data.poll == null && data.fileIds.length === 0;
if (isPureRenoteRequest) {
if (renote.moderationRenoteLock) {
// モデレーションによるロック: 所有者を含む全員のリノートを禁止
throw new IdentifiableError('5f8d7a3c-2b1e-4c9a-9e7d-1a2b3c4d5e6f', 'Renote of this note has been locked by moderation');
} else if (renote.userRenoteLock && renote.userId !== user.id) {
// ユーザーによるロック: 所有者本人以外のリノートを禁止
throw new IdentifiableError('a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', 'Renote of this note has been locked by the author');
}
}
}

let reply: MiNote | null = null;
Expand Down Expand Up @@ -428,6 +442,7 @@ export class NoteCreateService implements OnApplicationShutdown {
cw: data.cw,
localOnly: data.localOnly,
reactionAcceptance: data.reactionAcceptance,
userRenoteLock: data.userRenoteLock,
visibility: data.visibility,
visibleUsers,
channel,
Expand Down Expand Up @@ -640,6 +655,7 @@ export class NoteCreateService implements OnApplicationShutdown {
userId: user.id,
localOnly: data.localOnly!,
reactionAcceptance: data.reactionAcceptance ?? null,
userRenoteLock: data.userRenoteLock ?? false,
visibility: data.visibility as any,
visibleUserIds: data.visibility === 'specified'
? data.visibleUsers
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/WebhookTestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
user: null,
localOnly: true,
reactionAcceptance: 'likeOnly',
userRenoteLock: false,
moderationRenoteLock: false,
renoteCount: 10,
repliesCount: 5,
clippedCount: 0,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/entities/NoteEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@ export class NoteEntityService implements OnModuleInit {
visibility: note.visibility,
localOnly: note.localOnly,
reactionAcceptance: note.reactionAcceptance,
userRenoteLock: note.userRenoteLock,
moderationRenoteLock: note.moderationRenoteLock,
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
Expand Down
12 changes: 12 additions & 0 deletions packages/backend/src/models/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ export class MiNote {
})
public reactionAcceptance: typeof noteReactionAcceptances[number];

@Column('boolean', {
default: false,
comment: 'Whether the note owner has locked renotes by others (the owner can still renote).',
})
public userRenoteLock: boolean;

@Column('boolean', {
default: false,
comment: 'Whether a moderator has locked renotes of this note (nobody, including the owner, can renote).',
})
public moderationRenoteLock: boolean;

@Column('smallint', {
default: 0,
})
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/models/json-schema/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ export const packedNoteSchema = {
optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
userRenoteLock: {
type: 'boolean',
optional: true, nullable: false,
},
moderationRenoteLock: {
type: 'boolean',
optional: true, nullable: false,
},
reactionEmojis: {
type: 'object',
optional: false, nullable: false,
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/endpoint-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export * as 'admin/get-user-ips' from './endpoints/admin/get-user-ips.js';
export * as 'admin/invite/create' from './endpoints/admin/invite/create.js';
export * as 'admin/invite/list' from './endpoints/admin/invite/list.js';
export * as 'admin/meta' from './endpoints/admin/meta.js';
export * as 'admin/notes/renote-lock/create' from './endpoints/admin/notes/renote-lock/create.js';
export * as 'admin/notes/renote-lock/delete' from './endpoints/admin/notes/renote-lock/delete.js';
export * as 'admin/promo/create' from './endpoints/admin/promo/create.js';
export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js';
export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js';
Expand Down Expand Up @@ -334,6 +336,8 @@ export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js';
export * as 'notes/reactions' from './endpoints/notes/reactions.js';
export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js';
export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js';
export * as 'notes/renote-lock/create' from './endpoints/notes/renote-lock/create.js';
export * as 'notes/renote-lock/delete' from './endpoints/notes/renote-lock/delete.js';
export * as 'notes/renotes' from './endpoints/notes/renotes.js';
export * as 'notes/replies' from './endpoints/notes/replies.js';
export * as 'notes/search' from './endpoints/notes/search.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '@/server/api/error.js';

export const meta = {
tags: ['admin', 'notes'],

requireCredential: true,
requireModerator: true,
kind: 'write:admin:note-renote-lock',

description: 'Lock renotes of the note for everyone, including the author.',

errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'e3f4a5b6-7c8d-9e0f-1a2b-3c4d5e6f7081',
},
},
} 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(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private getterService: GetterService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});

await this.notesRepository.update({ id: note.id }, {
moderationRenoteLock: true,
});

const user = await this.usersRepository.findOneByOrFail({ id: note.userId });

this.moderationLogService.log(me, 'lockNoteRenote', {
noteId: note.id,
noteUserId: note.userId,
noteUserUsername: user.username,
noteUserHost: user.host,
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '@/server/api/error.js';

export const meta = {
tags: ['admin', 'notes'],

requireCredential: true,
requireModerator: true,
kind: 'write:admin:note-renote-lock',

description: 'Unlock renotes of the note that were locked by moderation.',

errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'f4a5b6c7-8d9e-0f1a-2b3c-4d5e6f708192',
},
},
} 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(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private getterService: GetterService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});

await this.notesRepository.update({ id: note.id }, {
moderationRenoteLock: false,
});

const user = await this.usersRepository.findOneByOrFail({ id: note.userId });

this.moderationLogService.log(me, 'unlockNoteRenote', {
noteId: note.id,
noteUserId: note.userId,
noteUserUsername: user.username,
noteUserHost: user.host,
});
});
}
}
18 changes: 18 additions & 0 deletions packages/backend/src/server/api/endpoints/notes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ export const meta = {
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},

renoteLockedByModeration: {
message: 'Renote of this note has been locked by moderation.',
code: 'RENOTE_LOCKED_BY_MODERATION',
id: '5f8d7a3c-2b1e-4c9a-9e7d-1a2b3c4d5e6f',
},

renoteLockedByAuthor: {
message: 'Renote of this note has been locked by the author.',
code: 'RENOTE_LOCKED_BY_AUTHOR',
id: 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d',
},
},
} as const;

Expand All @@ -136,6 +148,7 @@ export const paramDef = {
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
userRenoteLock: { type: 'boolean', default: false },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
Expand Down Expand Up @@ -235,6 +248,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
cw: ps.cw ?? null,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
userRenoteLock: ps.userRenoteLock,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? null,
Expand All @@ -259,6 +273,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') {
throw new ApiError(meta.errors.cannotReRenote);
} else if (err.id === '5f8d7a3c-2b1e-4c9a-9e7d-1a2b3c4d5e6f') {
throw new ApiError(meta.errors.renoteLockedByModeration);
} else if (err.id === 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d') {
throw new ApiError(meta.errors.renoteLockedByAuthor);
} else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') {
throw new ApiError(meta.errors.youHaveBeenBlocked);
} else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') {
Expand Down
Loading
Loading