feat: ability to make existing public posts private

Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
naskya 2024-02-21 08:42:12 +09:00
parent fa0e65cc1b
commit fb74a5eeda
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
10 changed files with 133 additions and 19 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -2028,3 +2028,5 @@ _emojiModPerm:
full: "全て許可"
private: "秘密"
privateDescription: "あなた以外には非公開"
makePrivate: "秘密にする"
makePrivateConfirm: "リモートサーバーに削除リクエストを送信し、投稿の公開範囲を「秘密」にして他の人から見られないようにします。実行しますか?"

View file

@ -2014,3 +2014,5 @@ preventMisclick: "預防誤觸"
hideFollowButtons: "隱藏會誤觸的追隨按鈕"
private: "祕密"
privateDescription: "僅你可見"
makePrivate: "設為祕密"
makePrivateConfirm: "此操作將向遠端伺服器發送刪除請求,並將貼文的公開範圍設為「祕密」。是否繼續?"

View file

@ -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],

View file

@ -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(),
});
});

View file

@ -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);

View file

@ -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")}`,

View file

@ -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<string>();
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<string>();
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;
}
}