note edit endpoint
This commit is contained in:
parent
ef97630460
commit
697361ca03
5 changed files with 455 additions and 154 deletions
|
@ -162,6 +162,7 @@ export const scyllaQueries = {
|
|||
poll: {
|
||||
select: `SELECT * FROM poll_vote WHERE "noteId" = ?`,
|
||||
insert: `INSERT INTO poll_vote ("noteId", "userId", "userHost", "choice", "createdAt") VALUES (?, ?, ?, ?, ?)`,
|
||||
delete: `DELETE FROM poll_vote WHERE "noteId" = ?`
|
||||
},
|
||||
notification: {
|
||||
insert: `INSERT INTO notification
|
||||
|
|
|
@ -109,7 +109,7 @@ export interface ScyllaDriveFile {
|
|||
comment: string | null;
|
||||
blurhash: string | null;
|
||||
url: string;
|
||||
thumbnailUrl: string;
|
||||
thumbnailUrl: string | null;
|
||||
isSensitive: boolean;
|
||||
isLink: boolean;
|
||||
md5: string;
|
||||
|
@ -140,8 +140,8 @@ export function getScyllaDrivePublicUrl(
|
|||
}
|
||||
|
||||
export interface ScyllaNoteEditHistory {
|
||||
content: string;
|
||||
cw: string;
|
||||
content: string | null;
|
||||
cw: string | null;
|
||||
files: ScyllaDriveFile[];
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export default async (
|
|||
});
|
||||
|
||||
if (isActor(object)) {
|
||||
await updatePerson(actor.uri!, resolver, object);
|
||||
await updatePerson(actor.uri as string, resolver, object);
|
||||
return "ok: Person updated";
|
||||
}
|
||||
|
||||
|
@ -37,12 +37,13 @@ export default async (
|
|||
case "Note":
|
||||
case "Article":
|
||||
case "Document":
|
||||
case "Page":
|
||||
case "Page": {
|
||||
let failed = false;
|
||||
await updateNote(object, resolver).catch((e: Error) => {
|
||||
failed = true;
|
||||
});
|
||||
return failed ? "skip: Note update failed" : "ok: Note updated";
|
||||
}
|
||||
|
||||
default:
|
||||
return `skip: Unknown type: ${objectType}`;
|
||||
|
|
|
@ -53,7 +53,17 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
|||
import { truncate } from "@/misc/truncate.js";
|
||||
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js";
|
||||
import {
|
||||
type ScyllaNote,
|
||||
type ScyllaPoll,
|
||||
type ScyllaDriveFile,
|
||||
type ScyllaNoteEditHistory,
|
||||
parseScyllaNote,
|
||||
prepared,
|
||||
scyllaClient,
|
||||
parseHomeTimeline,
|
||||
} from "@/db/scylla.js";
|
||||
import type { Client } from "cassandra-driver";
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
|
@ -573,20 +583,33 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
|||
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
|
||||
|
||||
// A new resolver is created if not specified
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
let _resolver = resolver;
|
||||
if (!_resolver) _resolver = new Resolver();
|
||||
|
||||
// Resolve the updated Note object
|
||||
const post = (await resolver.resolve(value)) as IPost;
|
||||
const post = (await _resolver.resolve(value)) as IPost;
|
||||
|
||||
const actor = (await resolvePerson(
|
||||
getOneApId(post.attributedTo),
|
||||
resolver,
|
||||
_resolver,
|
||||
)) as CacheableRemoteUser;
|
||||
|
||||
// Already registered with this server?
|
||||
const note = await Notes.findOneBy({ uri });
|
||||
if (note == null) {
|
||||
return await createNote(post, resolver);
|
||||
let note: Note | ScyllaNote | null = null;
|
||||
if (scyllaClient) {
|
||||
const result = await scyllaClient.execute(
|
||||
prepared.note.select.byUri,
|
||||
[uri],
|
||||
{ prepare: true },
|
||||
);
|
||||
if (result.rowLength > 0) {
|
||||
note = parseScyllaNote(result.first());
|
||||
}
|
||||
} else {
|
||||
note = await Notes.findOneBy({ uri });
|
||||
}
|
||||
if (!note) {
|
||||
return await createNote(post, _resolver);
|
||||
}
|
||||
|
||||
// Whether to tell clients the note has been updated and requires refresh.
|
||||
|
@ -613,7 +636,9 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
|||
? post.attachment
|
||||
: [post.attachment]
|
||||
: [];
|
||||
const files = fileList.map((f) => (f.sensitive = post.sensitive));
|
||||
const files = fileList.map((f) => {
|
||||
f.sensitive = post.sensitive;
|
||||
});
|
||||
|
||||
// Fetch files
|
||||
const limit = promiseLimit(2);
|
||||
|
@ -654,9 +679,9 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
|||
await extractEmojis(post.tag || [], actor.host).catch((e) => [])
|
||||
).map((emoji) => emoji.name);
|
||||
const apMentions = await extractApMentions(post.tag);
|
||||
const apHashtags = await extractApHashtags(post.tag);
|
||||
const apHashtags = extractApHashtags(post.tag);
|
||||
|
||||
const poll = await extractPollFromQuestion(post, resolver).catch(
|
||||
const poll = await extractPollFromQuestion(post, _resolver).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
|
@ -719,65 +744,179 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
|||
update.hasPoll = !!poll;
|
||||
}
|
||||
|
||||
let scyllaPoll: ScyllaPoll | null = null;
|
||||
let scyllaUpdating = false;
|
||||
|
||||
if (poll) {
|
||||
const dbPoll = await Polls.findOneBy({ noteId: note.id });
|
||||
if (dbPoll == null) {
|
||||
await Polls.insert({
|
||||
noteId: note.id,
|
||||
choices: poll?.choices,
|
||||
multiple: poll?.multiple,
|
||||
votes: poll?.votes,
|
||||
expiresAt: poll?.expiresAt,
|
||||
noteVisibility: note.visibility === "hidden" ? "home" : note.visibility,
|
||||
userId: actor.id,
|
||||
userHost: actor.host,
|
||||
if (scyllaClient) {
|
||||
let expiresAt: Date | null;
|
||||
if (!poll.expiresAt || isNaN(poll.expiresAt.getTime())) {
|
||||
expiresAt = null;
|
||||
} else {
|
||||
expiresAt = poll.expiresAt;
|
||||
}
|
||||
|
||||
scyllaPoll = {
|
||||
expiresAt,
|
||||
choices: Object.fromEntries(
|
||||
poll.choices.map((v, i) => [i, v] as [number, string]),
|
||||
),
|
||||
multiple: poll.multiple,
|
||||
};
|
||||
|
||||
scyllaUpdating = true;
|
||||
publishing = true;
|
||||
|
||||
// Delete all votes cast to the target note (i.e., delete whole rows in the partition)
|
||||
await scyllaClient.execute(prepared.poll.delete, [note.id], {
|
||||
prepare: true,
|
||||
});
|
||||
updating = true;
|
||||
} else if (
|
||||
dbPoll.multiple !== poll.multiple ||
|
||||
dbPoll.expiresAt !== poll.expiresAt ||
|
||||
dbPoll.noteVisibility !== note.visibility ||
|
||||
JSON.stringify(dbPoll.choices) !== JSON.stringify(poll.choices)
|
||||
) {
|
||||
await Polls.update(
|
||||
{ noteId: note.id },
|
||||
{
|
||||
} else {
|
||||
const dbPoll = await Polls.findOneBy({ noteId: note.id });
|
||||
if (dbPoll == null) {
|
||||
await Polls.insert({
|
||||
noteId: note.id,
|
||||
choices: poll?.choices,
|
||||
multiple: poll?.multiple,
|
||||
votes: poll?.votes,
|
||||
expiresAt: poll?.expiresAt,
|
||||
noteVisibility:
|
||||
note.visibility === "hidden" ? "home" : note.visibility,
|
||||
},
|
||||
);
|
||||
updating = true;
|
||||
} else {
|
||||
for (let i = 0; i < poll.choices.length; i++) {
|
||||
if (dbPoll.votes[i] !== poll.votes?.[i]) {
|
||||
await Polls.update({ noteId: note.id }, { votes: poll?.votes });
|
||||
publishing = true;
|
||||
break;
|
||||
userId: actor.id,
|
||||
userHost: actor.host,
|
||||
});
|
||||
} else if (
|
||||
dbPoll.multiple !== poll.multiple ||
|
||||
dbPoll.expiresAt !== poll.expiresAt ||
|
||||
dbPoll.noteVisibility !== note.visibility ||
|
||||
JSON.stringify(dbPoll.choices) !== JSON.stringify(poll.choices)
|
||||
) {
|
||||
await Polls.update(
|
||||
{ noteId: note.id },
|
||||
{
|
||||
choices: poll?.choices,
|
||||
multiple: poll?.multiple,
|
||||
votes: poll?.votes,
|
||||
expiresAt: poll?.expiresAt,
|
||||
noteVisibility:
|
||||
note.visibility === "hidden" ? "home" : note.visibility,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
for (let i = 0; i < poll.choices.length; i++) {
|
||||
if (dbPoll.votes[i] !== poll.votes?.[i]) {
|
||||
await Polls.update({ noteId: note.id }, { votes: poll?.votes });
|
||||
publishing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Note
|
||||
if (notEmpty(update)) {
|
||||
if (notEmpty(update) || scyllaUpdating) {
|
||||
update.updatedAt = new Date();
|
||||
|
||||
// Save updated note to the database
|
||||
await Notes.update({ uri }, update);
|
||||
if (scyllaClient) {
|
||||
const client = scyllaClient as Client;
|
||||
const fileMapper = (file: DriveFile) => ({
|
||||
...file,
|
||||
width: file.properties.width ?? null,
|
||||
height: file.properties.height ?? null,
|
||||
});
|
||||
const scyllaFiles: ScyllaDriveFile[] = driveFiles.map(fileMapper);
|
||||
const scyllaNote = note as ScyllaNote;
|
||||
const editHistory: ScyllaNoteEditHistory = {
|
||||
content: scyllaNote.text,
|
||||
cw: scyllaNote.cw,
|
||||
files: scyllaNote.files,
|
||||
updatedAt: update.updatedAt,
|
||||
};
|
||||
|
||||
// Save an edit history for the previous note
|
||||
await NoteEdits.insert({
|
||||
id: genId(),
|
||||
noteId: note.id,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
fileIds: note.fileIds,
|
||||
updatedAt: update.updatedAt,
|
||||
});
|
||||
const newScyllaNote: ScyllaNote = {
|
||||
...scyllaNote,
|
||||
...update,
|
||||
hasPoll: !!scyllaPoll,
|
||||
poll: scyllaPoll,
|
||||
files: scyllaFiles,
|
||||
noteEdit: [...scyllaNote.noteEdit, editHistory],
|
||||
};
|
||||
|
||||
const params = [
|
||||
newScyllaNote.createdAt,
|
||||
newScyllaNote.createdAt,
|
||||
newScyllaNote.id,
|
||||
scyllaNote.visibility,
|
||||
newScyllaNote.text,
|
||||
newScyllaNote.name,
|
||||
newScyllaNote.cw,
|
||||
newScyllaNote.localOnly,
|
||||
newScyllaNote.renoteCount ?? 0,
|
||||
newScyllaNote.repliesCount ?? 0,
|
||||
newScyllaNote.uri,
|
||||
newScyllaNote.url,
|
||||
newScyllaNote.score ?? 0,
|
||||
newScyllaNote.files,
|
||||
newScyllaNote.visibleUserIds,
|
||||
newScyllaNote.mentions,
|
||||
newScyllaNote.mentionedRemoteUsers,
|
||||
newScyllaNote.emojis,
|
||||
newScyllaNote.tags,
|
||||
newScyllaNote.hasPoll,
|
||||
newScyllaNote.poll,
|
||||
newScyllaNote.threadId,
|
||||
newScyllaNote.channelId,
|
||||
newScyllaNote.userId,
|
||||
newScyllaNote.userHost ?? "local",
|
||||
newScyllaNote.replyId,
|
||||
newScyllaNote.replyUserId,
|
||||
newScyllaNote.replyUserHost,
|
||||
newScyllaNote.replyText,
|
||||
newScyllaNote.replyCw,
|
||||
newScyllaNote.replyFiles,
|
||||
newScyllaNote.renoteId,
|
||||
newScyllaNote.renoteUserId,
|
||||
newScyllaNote.renoteUserHost,
|
||||
newScyllaNote.renoteText,
|
||||
newScyllaNote.renoteCw,
|
||||
newScyllaNote.renoteFiles,
|
||||
newScyllaNote.reactions,
|
||||
newScyllaNote.noteEdit,
|
||||
newScyllaNote.updatedAt,
|
||||
];
|
||||
|
||||
// To let ScyllaDB do upsert, do NOT change the visibility.
|
||||
await client.execute(prepared.note.insert, params, { prepare: true });
|
||||
|
||||
// Update home timelines
|
||||
client.eachRow(
|
||||
prepared.homeTimeline.select.byId,
|
||||
[scyllaNote.id],
|
||||
{ prepare: true },
|
||||
(_, row) => {
|
||||
const timeline = parseHomeTimeline(row);
|
||||
client.execute(
|
||||
prepared.homeTimeline.insert,
|
||||
[timeline.feedUserId, ...params],
|
||||
{ prepare: true },
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Save updated note to the database
|
||||
await Notes.update({ uri }, update);
|
||||
|
||||
// Save an edit history for the previous note
|
||||
await NoteEdits.insert({
|
||||
id: genId(),
|
||||
noteId: note.id,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
fileIds: note.fileIds,
|
||||
updatedAt: update.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
publishing = true;
|
||||
}
|
||||
|
@ -788,4 +927,6 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
|||
updatedAt: update.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return { ...note, ...update };
|
||||
}
|
||||
|
|
|
@ -35,6 +35,17 @@ import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
|||
import { deliverToRelays } from "@/services/relay.js";
|
||||
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import {
|
||||
type ScyllaNote,
|
||||
type ScyllaPoll,
|
||||
type ScyllaDriveFile,
|
||||
type ScyllaNoteEditHistory,
|
||||
scyllaClient,
|
||||
prepared,
|
||||
parseScyllaNote,
|
||||
parseHomeTimeline,
|
||||
} from "@/db/scylla.js";
|
||||
import type { Client } from "cassandra-driver";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -248,7 +259,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
|
||||
if (user.movedToUri) throw new ApiError(meta.errors.accountLocked);
|
||||
|
||||
if (!Users.isLocalUser(user)) {
|
||||
throw new ApiError(meta.errors.notLocalUser);
|
||||
|
@ -259,11 +270,23 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
let publishing = false;
|
||||
let note = await Notes.findOneBy({
|
||||
id: ps.editId,
|
||||
});
|
||||
let note: Note | ScyllaNote | null = null;
|
||||
if (scyllaClient) {
|
||||
const result = await scyllaClient.execute(
|
||||
prepared.note.select.byId,
|
||||
[ps.editId],
|
||||
{ prepare: true },
|
||||
);
|
||||
if (result.rowLength > 0) {
|
||||
note = parseScyllaNote(result.first());
|
||||
}
|
||||
} else {
|
||||
note = await Notes.findOneBy({
|
||||
id: ps.editId,
|
||||
});
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
if (!note) {
|
||||
throw new ApiError(meta.errors.noSuchNote);
|
||||
}
|
||||
|
||||
|
@ -272,7 +295,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
let renote: Note | null = null;
|
||||
if (ps.renoteId != null) {
|
||||
if (ps.renoteId) {
|
||||
// Fetch renote to note
|
||||
renote = await getNote(ps.renoteId, user).catch((e) => {
|
||||
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
|
||||
|
@ -301,7 +324,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
let reply: Note | null = null;
|
||||
if (ps.replyId != null) {
|
||||
if (ps.replyId) {
|
||||
// Fetch reply
|
||||
reply = await getNote(ps.replyId, user).catch((e) => {
|
||||
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
|
||||
|
@ -326,10 +349,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
let channel: Channel | null = null;
|
||||
if (ps.channelId != null) {
|
||||
if (ps.channelId) {
|
||||
channel = await Channels.findOneBy({ id: ps.channelId });
|
||||
|
||||
if (channel == null) {
|
||||
if (!channel) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
}
|
||||
|
@ -443,6 +466,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.getMany();
|
||||
}
|
||||
|
||||
let scyllaPoll: ScyllaPoll | null = null;
|
||||
let scyllaUpdating = false;
|
||||
|
||||
if (ps.poll) {
|
||||
let expires = ps.poll.expiresAt;
|
||||
if (typeof expires === "number") {
|
||||
|
@ -453,51 +479,78 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
expires = Date.now() + ps.poll.expiredAfter;
|
||||
}
|
||||
|
||||
let poll = await Polls.findOneBy({ noteId: note.id });
|
||||
const pp = ps.poll;
|
||||
if (!poll && pp) {
|
||||
poll = new Poll({
|
||||
noteId: note.id,
|
||||
choices: pp.choices,
|
||||
expiresAt: expires ? new Date(expires) : null,
|
||||
multiple: pp.multiple,
|
||||
votes: new Array(pp.choices.length).fill(0),
|
||||
noteVisibility: ps.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
if (scyllaClient) {
|
||||
let expiresAt: Date | null;
|
||||
if (!expires || isNaN(expires)) {
|
||||
expiresAt = null;
|
||||
} else {
|
||||
expiresAt = new Date(expires);
|
||||
}
|
||||
|
||||
scyllaPoll = {
|
||||
expiresAt,
|
||||
choices: Object.fromEntries(
|
||||
ps.poll.choices.map((v, i) => [i, v] as [number, string]),
|
||||
),
|
||||
multiple: ps.poll.multiple,
|
||||
};
|
||||
|
||||
publishing = true;
|
||||
scyllaUpdating = true;
|
||||
|
||||
// FIXME: Keep votes for unmodified choices, reset votes if choice is modified or new
|
||||
|
||||
// Delete all votes cast to the target note (i.e., delete whole rows in the partition)
|
||||
await scyllaClient.execute(prepared.poll.delete, [note.id], {
|
||||
prepare: true,
|
||||
});
|
||||
await Polls.insert(poll);
|
||||
publishing = true;
|
||||
} else if (poll && !pp) {
|
||||
await Polls.remove(poll);
|
||||
publishing = true;
|
||||
} else if (poll && pp) {
|
||||
const pollUpdate: Partial<Poll> = {};
|
||||
if (poll.expiresAt !== expires) {
|
||||
pollUpdate.expiresAt = expires ? new Date(expires) : null;
|
||||
} else {
|
||||
let poll = await Polls.findOneBy({ noteId: note.id });
|
||||
const pp = ps.poll;
|
||||
if (!poll && pp) {
|
||||
poll = new Poll({
|
||||
noteId: note.id,
|
||||
choices: pp.choices,
|
||||
expiresAt: expires ? new Date(expires) : null,
|
||||
multiple: pp.multiple,
|
||||
votes: new Array(pp.choices.length).fill(0),
|
||||
noteVisibility: ps.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
await Polls.insert(poll);
|
||||
publishing = true;
|
||||
} else if (poll && !pp) {
|
||||
await Polls.remove(poll);
|
||||
publishing = true;
|
||||
} else if (poll && pp) {
|
||||
const pollUpdate: Partial<Poll> = {};
|
||||
if (poll.expiresAt !== expires) {
|
||||
pollUpdate.expiresAt = expires ? new Date(expires) : null;
|
||||
}
|
||||
if (poll.multiple !== pp.multiple) {
|
||||
pollUpdate.multiple = pp.multiple;
|
||||
}
|
||||
if (poll.noteVisibility !== ps.visibility) {
|
||||
pollUpdate.noteVisibility = ps.visibility;
|
||||
}
|
||||
// Keep votes for unmodified choices, reset votes if choice is modified or new
|
||||
const oldVoteCounts = new Map<string, number>();
|
||||
for (let i = 0; i < poll.choices.length; i++) {
|
||||
oldVoteCounts.set(poll.choices[i], poll.votes[i]);
|
||||
}
|
||||
const newVotes = pp.choices.map(
|
||||
(choice) => oldVoteCounts.get(choice) || 0,
|
||||
);
|
||||
pollUpdate.choices = pp.choices;
|
||||
pollUpdate.votes = newVotes;
|
||||
if (notEmpty(pollUpdate)) {
|
||||
await Polls.update(note.id, pollUpdate);
|
||||
// Seemingly already handled by the rendered update activity
|
||||
// await deliverQuestionUpdate(note.id);
|
||||
}
|
||||
publishing = true;
|
||||
}
|
||||
if (poll.multiple !== pp.multiple) {
|
||||
pollUpdate.multiple = pp.multiple;
|
||||
}
|
||||
if (poll.noteVisibility !== ps.visibility) {
|
||||
pollUpdate.noteVisibility = ps.visibility;
|
||||
}
|
||||
// Keep votes for unmodified choices, reset votes if choice is modified or new
|
||||
const oldVoteCounts = new Map<string, number>();
|
||||
for (let i = 0; i < poll.choices.length; i++) {
|
||||
oldVoteCounts.set(poll.choices[i], poll.votes[i]);
|
||||
}
|
||||
const newVotes = pp.choices.map(
|
||||
(choice) => oldVoteCounts.get(choice) || 0,
|
||||
);
|
||||
pollUpdate.choices = pp.choices;
|
||||
pollUpdate.votes = newVotes;
|
||||
if (notEmpty(pollUpdate)) {
|
||||
await Polls.update(note.id, pollUpdate);
|
||||
// Seemingly already handled by by the rendered update activity
|
||||
// await deliverQuestionUpdate(note.id);
|
||||
}
|
||||
publishing = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -542,6 +595,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
// update.visibility = ps.visibility;
|
||||
throw new ApiError(meta.errors.cannotChangeVisibility);
|
||||
}
|
||||
update.localOnly = note.localOnly;
|
||||
if (ps.localOnly !== note.localOnly) {
|
||||
update.localOnly = ps.localOnly;
|
||||
}
|
||||
|
@ -586,24 +640,126 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (notEmpty(update)) {
|
||||
if (notEmpty(update) || scyllaUpdating) {
|
||||
update.updatedAt = new Date();
|
||||
await Notes.update(note.id, update);
|
||||
|
||||
// Add NoteEdit history
|
||||
await NoteEdits.insert({
|
||||
id: genId(),
|
||||
noteId: note.id,
|
||||
text: ps.text || undefined,
|
||||
cw: ps.cw,
|
||||
fileIds: ps.fileIds,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
if (scyllaClient) {
|
||||
const client = scyllaClient as Client;
|
||||
const fileMapper = (file: DriveFile) => ({
|
||||
...file,
|
||||
width: file.properties.width ?? null,
|
||||
height: file.properties.height ?? null,
|
||||
});
|
||||
const scyllaFiles: ScyllaDriveFile[] = files.map(fileMapper);
|
||||
const scyllaNote = note as ScyllaNote;
|
||||
const editHistory: ScyllaNoteEditHistory = {
|
||||
content: scyllaNote.text,
|
||||
cw: scyllaNote.cw,
|
||||
files: scyllaNote.files,
|
||||
updatedAt: update.updatedAt,
|
||||
};
|
||||
|
||||
const newScyllaNote: ScyllaNote = {
|
||||
...scyllaNote,
|
||||
...update,
|
||||
hasPoll: !!scyllaPoll,
|
||||
poll: scyllaPoll,
|
||||
files: scyllaFiles,
|
||||
noteEdit: [...scyllaNote.noteEdit, editHistory],
|
||||
};
|
||||
|
||||
const params = [
|
||||
newScyllaNote.createdAt,
|
||||
newScyllaNote.createdAt,
|
||||
newScyllaNote.id,
|
||||
scyllaNote.visibility,
|
||||
newScyllaNote.text,
|
||||
newScyllaNote.name,
|
||||
newScyllaNote.cw,
|
||||
newScyllaNote.localOnly,
|
||||
newScyllaNote.renoteCount ?? 0,
|
||||
newScyllaNote.repliesCount ?? 0,
|
||||
newScyllaNote.uri,
|
||||
newScyllaNote.url,
|
||||
newScyllaNote.score ?? 0,
|
||||
newScyllaNote.files,
|
||||
newScyllaNote.visibleUserIds,
|
||||
newScyllaNote.mentions,
|
||||
newScyllaNote.mentionedRemoteUsers,
|
||||
newScyllaNote.emojis,
|
||||
newScyllaNote.tags,
|
||||
newScyllaNote.hasPoll,
|
||||
newScyllaNote.poll,
|
||||
newScyllaNote.threadId,
|
||||
newScyllaNote.channelId,
|
||||
newScyllaNote.userId,
|
||||
newScyllaNote.userHost ?? "local",
|
||||
newScyllaNote.replyId,
|
||||
newScyllaNote.replyUserId,
|
||||
newScyllaNote.replyUserHost,
|
||||
newScyllaNote.replyText,
|
||||
newScyllaNote.replyCw,
|
||||
newScyllaNote.replyFiles,
|
||||
newScyllaNote.renoteId,
|
||||
newScyllaNote.renoteUserId,
|
||||
newScyllaNote.renoteUserHost,
|
||||
newScyllaNote.renoteText,
|
||||
newScyllaNote.renoteCw,
|
||||
newScyllaNote.renoteFiles,
|
||||
newScyllaNote.reactions,
|
||||
newScyllaNote.noteEdit,
|
||||
newScyllaNote.updatedAt,
|
||||
];
|
||||
|
||||
// To let ScyllaDB do upsert, do NOT change the visibility.
|
||||
await client.execute(prepared.note.insert, params, { prepare: true });
|
||||
|
||||
// Update home timelines
|
||||
client.eachRow(
|
||||
prepared.homeTimeline.select.byId,
|
||||
[scyllaNote.id],
|
||||
{ prepare: true },
|
||||
(_, row) => {
|
||||
const timeline = parseHomeTimeline(row);
|
||||
client.execute(
|
||||
prepared.homeTimeline.insert,
|
||||
[timeline.feedUserId, ...params],
|
||||
{ prepare: true },
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await Notes.update(note.id, update);
|
||||
|
||||
// Add NoteEdit history
|
||||
await NoteEdits.insert({
|
||||
id: genId(),
|
||||
noteId: note.id,
|
||||
text: ps.text || undefined,
|
||||
cw: ps.cw,
|
||||
fileIds: ps.fileIds,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
publishing = true;
|
||||
}
|
||||
|
||||
note = await Notes.findOneBy({ id: note.id });
|
||||
if (scyllaClient) {
|
||||
const result = await scyllaClient.execute(
|
||||
prepared.note.select.byId,
|
||||
[note.id],
|
||||
{ prepare: true },
|
||||
);
|
||||
if (result.rowLength > 0) {
|
||||
note = parseScyllaNote(result.first());
|
||||
}
|
||||
} else {
|
||||
note = await Notes.findOneBy({
|
||||
id: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (!note) {
|
||||
throw new ApiError(meta.errors.noSuchNote);
|
||||
}
|
||||
|
@ -616,45 +772,47 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
updatedAt: update.updatedAt,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const noteActivity = await renderNote(note, false);
|
||||
noteActivity.updated = note.updatedAt.toISOString();
|
||||
const updateActivity = renderUpdate(noteActivity, user);
|
||||
updateActivity.to = noteActivity.to;
|
||||
updateActivity.cc = noteActivity.cc;
|
||||
const activity = renderActivity(updateActivity);
|
||||
const dm = new DeliverManager(user, activity);
|
||||
if (!update.localOnly) {
|
||||
(async () => {
|
||||
const noteActivity = await renderNote(note, false);
|
||||
noteActivity.updated = note.updatedAt.toISOString();
|
||||
const updateActivity = renderUpdate(noteActivity, user);
|
||||
updateActivity.to = noteActivity.to;
|
||||
updateActivity.cc = noteActivity.cc;
|
||||
const activity = renderActivity(updateActivity);
|
||||
const dm = new DeliverManager(user, activity);
|
||||
|
||||
// Delivery to remote mentioned users
|
||||
for (const u of mentionedUsers.filter((u) => Users.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
}
|
||||
// Delivery to remote mentioned users
|
||||
for (const u of mentionedUsers.filter((u) => Users.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
}
|
||||
|
||||
// Post is a reply and remote user is the contributor of the original post
|
||||
if (note.reply && note.reply.userHost !== null) {
|
||||
const u = await Users.findOneBy({ id: note.reply.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
// Post is a reply and remote user is the contributor of the original post
|
||||
if (note.reply && note.reply.userHost !== null) {
|
||||
const u = await Users.findOneBy({ id: note.reply.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// Post is a renote and remote user is the contributor of the original post
|
||||
if (note.renote && note.renote.userHost !== null) {
|
||||
const u = await Users.findOneBy({ id: note.renote.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
// Post is a renote and remote user is the contributor of the original post
|
||||
if (note.renote && note.renote.userHost !== null) {
|
||||
const u = await Users.findOneBy({ id: note.renote.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// Deliver to followers for non-direct posts.
|
||||
if (["public", "home", "followers"].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
}
|
||||
// Deliver to followers for non-direct posts.
|
||||
if (["public", "home", "followers"].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
}
|
||||
|
||||
// Deliver to relays for public posts.
|
||||
if (["public"].includes(note.visibility)) {
|
||||
deliverToRelays(user, activity);
|
||||
}
|
||||
// Deliver to relays for public posts.
|
||||
if (["public"].includes(note.visibility)) {
|
||||
deliverToRelays(user, activity);
|
||||
}
|
||||
|
||||
// GO!
|
||||
dm.execute();
|
||||
})();
|
||||
// GO!
|
||||
dm.execute();
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
Loading…
Reference in a new issue