From fb74a5eeda96b070f94345d779da000834a139f4 Mon Sep 17 00:00:00 2001 From: naskya Date: Wed, 21 Feb 2024 08:42:12 +0900 Subject: [PATCH] feat: ability to make existing public posts private Co-authored-by: sup39 --- docs/api-change.md | 1 + docs/changelog.md | 1 + locales/en-US.yml | 2 + locales/ja-JP.yml | 2 + locales/zh-TW.yml | 2 + packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/notes/make-private.ts | 67 +++++++++++++++++++ packages/backend/src/services/note/delete.ts | 25 ++++--- packages/client/src/scripts/get-note-menu.ts | 25 +++++++ .../client/src/scripts/use-note-capture.ts | 25 ++++--- 10 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notes/make-private.ts diff --git a/docs/api-change.md b/docs/api-change.md index 86ae3859a5..dc4d24e25a 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -10,6 +10,7 @@ Breaking changes are indicated by the :warning: icon. - `full`: `mod` permission + delete existing custom emojis - Emoji moderators are able to access to the endpoints under `admin/emoji/` - Removed `lang` from the response of `i` and the request parameter of `i/update`. +- Added `notes/make-private` endpoint. ## v20240217 diff --git a/docs/changelog.md b/docs/changelog.md index 2e02b5eb2d..16e26994cf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,7 @@ Critical security updates are indicated by the :warning: icon. - Fix a bug that made impossible to update user profiles under some conditions - Add "private" (only me) post visibility - It's just a paraphrase of DMs without recipients + - You can also convert your existing public posts to private posts ## :warning: v20240217-1 diff --git a/locales/en-US.yml b/locales/en-US.yml index 6f2b3c3eca..c3250a2aa5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1174,6 +1174,8 @@ emojiModPerm: "Custom emoji management permission" emojiModPermDescription: "Add: Allow this user to add new custom emojis and to set tag/category/license to newly added custom emojis.\nAdd and Edit: \"Add\" Permission + Allow this user to edit the name/category/tag/license of the existing custom emojis.\nAllow All: \"Add and Edit\" Permission + Allow this user to delete existing custom emojis." private: "Private" privateDescription: "Make visible for you only" +makePrivate: "Make private" +makePrivateConfirm: "This operation will send a deletion request to remote servers and change the visibility to private. Proceed?" _emojiModPerm: unauthorized: "None" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8daa360001..ad94b89477 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2028,3 +2028,5 @@ _emojiModPerm: full: "全て許可" private: "秘密" privateDescription: "あなた以外には非公開" +makePrivate: "秘密にする" +makePrivateConfirm: "リモートサーバーに削除リクエストを送信し、投稿の公開範囲を「秘密」にして他の人から見られないようにします。実行しますか?" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 0eccaf8060..2ad41c2a68 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -2014,3 +2014,5 @@ preventMisclick: "預防誤觸" hideFollowButtons: "隱藏會誤觸的追隨按鈕" private: "祕密" privateDescription: "僅你可見" +makePrivate: "設為祕密" +makePrivateConfirm: "此操作將向遠端伺服器發送刪除請求,並將貼文的公開範圍設為「祕密」。是否繼續?" diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 24da7eef8d..b6cb1ba0ac 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -256,6 +256,7 @@ import * as ep___notes_globalTimeline from "./endpoints/notes/global-timeline.js import * as ep___notes_hybridTimeline from "./endpoints/notes/hybrid-timeline.js"; import * as ep___notes_localTimeline from "./endpoints/notes/local-timeline.js"; import * as ep___notes_recommendedTimeline from "./endpoints/notes/recommended-timeline.js"; +import * as ep___notes_makePrivate from "./endpoints/notes/make-private.js"; import * as ep___notes_mentions from "./endpoints/notes/mentions.js"; import * as ep___notes_polls_recommendation from "./endpoints/notes/polls/recommendation.js"; import * as ep___notes_polls_vote from "./endpoints/notes/polls/vote.js"; @@ -611,6 +612,7 @@ const eps = [ ["notes/hybrid-timeline", ep___notes_hybridTimeline], ["notes/local-timeline", ep___notes_localTimeline], ["notes/recommended-timeline", ep___notes_recommendedTimeline], + ["notes/make-private", ep___notes_makePrivate], ["notes/mentions", ep___notes_mentions], ["notes/polls/recommendation", ep___notes_polls_recommendation], ["notes/polls/vote", ep___notes_polls_vote], diff --git a/packages/backend/src/server/api/endpoints/notes/make-private.ts b/packages/backend/src/server/api/endpoints/notes/make-private.ts new file mode 100644 index 0000000000..7b9ebc4d1a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/make-private.ts @@ -0,0 +1,67 @@ +import deleteNote from "@/services/note/delete.js"; +import { Notes } from "@/models/index.js"; +import define from "@/server/api/define.js"; +import { getNote } from "@/server/api/common/getters.js"; +import { ApiError } from "@/server/api/error.js"; +import { SECOND, HOUR } from "@/const.js"; +import { publishNoteStream } from "@/services/stream.js"; + +export const meta = { + tags: ["notes"], + + requireCredential: true, + + kind: "write:notes", + + limit: { + duration: HOUR, + max: 300, + minInterval: SECOND, + }, + + errors: { + noSuchNote: { + message: "No such note.", + code: "NO_SUCH_NOTE", + id: "490be23f-8c1f-4796-819f-94cb4f9d1630", + }, + + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, +} as const; + +export const paramDef = { + type: "object", + properties: { + noteId: { type: "string", format: "misskey:id" }, + }, + required: ["noteId"], +} as const; + +export default define(meta, paramDef, async (ps, user) => { + const note = await getNote(ps.noteId, user).catch((err) => { + if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") + throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + if (note.userId !== user.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await deleteNote(user, note, false, false); + await Notes.update(note.id, { + visibility: "specified", + visibleUserIds: [], + }); + + // Publish update event for the updated note details + // TODO: Send "deleted" to other users? + publishNoteStream(note.id, "updated", { + updatedAt: new Date(), + }); +}); diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 8a75825809..28bdfc5b03 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -32,26 +32,31 @@ export default async function ( user: { id: User["id"]; uri: User["uri"]; host: User["host"] }, note: Note, quiet = false, + deleteFromDb = true, ) { const deletedAt = new Date(); // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if ( note.renoteId && - (await countSameRenotes(user.id, note.renoteId, note.id)) === 0 + (await countSameRenotes(user.id, note.renoteId, note.id)) === 0 && + deleteFromDb ) { Notes.decrement({ id: note.renoteId }, "renoteCount", 1); Notes.decrement({ id: note.renoteId }, "score", 1); } - if (note.replyId) { + if (note.replyId && deleteFromDb) { await Notes.decrement({ id: note.replyId }, "repliesCount", 1); } if (!quiet) { - publishNoteStream(note.id, "deleted", { - deletedAt: deletedAt, - }); + // Only broadcast "deleted" to local if the note is deleted from db + if (deleteFromDb) { + publishNoteStream(note.id, "deleted", { + deletedAt: deletedAt, + }); + } //#region ローカルの投稿なら削除アクティビティを配送 if (Users.isLocalUser(user) && !note.localOnly) { @@ -116,10 +121,12 @@ export default async function ( } } - await Notes.delete({ - id: note.id, - userId: user.id, - }); + if (deleteFromDb) { + await Notes.delete({ + id: note.id, + userId: user.id, + }); + } if (meilisearch) { await meilisearch.deleteNotes(note.id); diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index aa29fcf346..43c90d75c5 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -73,6 +73,19 @@ export function getNoteMenu(props: { }); } + function makePrivate(): void { + os.confirm({ + type: "warning", + text: i18n.ts.makePrivateConfirm, + }).then(async ({ canceled }) => { + if (canceled) return; + + await os.api("notes/make-private", { + noteId: appearNote.id, + }); + }); + } + function toggleFavorite(favorite: boolean): void { os.apiWithDialog( favorite ? "notes/favorites/create" : "notes/favorites/delete", @@ -437,6 +450,18 @@ export function getNoteMenu(props: { action: edit, } : undefined, + isAppearAuthor && + !( + appearNote.visibility === "specified" && + appearNote.visibleUserIds.length === 0 + ) + ? { + icon: `${icon("ph-eye-slash")}`, + text: i18n.ts.makePrivate, + danger: true, + action: makePrivate, + } + : undefined, isAppearAuthor ? { icon: `${icon("ph-eraser")}`, diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts index eb72c58f9d..b1dd847054 100644 --- a/packages/client/src/scripts/use-note-capture.ts +++ b/packages/client/src/scripts/use-note-capture.ts @@ -80,17 +80,22 @@ export function useNoteCapture(props: { } case "updated": { - const editedNote = await os.api("notes/show", { - noteId: id, - }); + try { + const editedNote = await os.api("notes/show", { + noteId: id, + }); - const keys = new Set(); - Object.keys(editedNote) - .concat(Object.keys(note.value)) - .forEach((key) => keys.add(key)); - keys.forEach((key) => { - note.value[key] = editedNote[key]; - }); + const keys = new Set(); + Object.keys(editedNote) + .concat(Object.keys(note.value)) + .forEach((key) => keys.add(key)); + keys.forEach((key) => { + note.value[key] = editedNote[key]; + }); + } catch { + // delete the note if failing to get the edited note + props.isDeletedRef.value = true; + } break; } }