Files
FrozenFriendsYume/packages/backend/built/services/note/edit.js
T
2026-07-26 18:25:37 +09:00

187 lines
7.8 KiB
JavaScript

import * as mfm from "mfm-js";
import { publishNoteStream, publishNoteUpdatesStream } from "../stream.js";
import DeliverManager from "../../remote/activitypub/deliver-manager.js";
import renderNote from "../../remote/activitypub/renderer/note.js";
import { renderActivity } from "../../remote/activitypub/renderer/index.js";
import { extractCustomEmojisFromMfm } from "../../misc/extract-custom-emojis-from-mfm.js";
import { extractHashtags } from "../../misc/extract-hashtags.js";
import { Users, Notes, UserProfiles, Polls, NoteEdits, PollVotes } from "../../models/index.js";
import { In } from "typeorm";
import { genId } from "../../misc/gen-id.js";
import { deliverToRelays } from "../relay.js";
import renderUpdate from "../../remote/activitypub/renderer/update.js";
import { extractMentionedUsers } from "./create.js";
import { normalizeForSearch } from "../../misc/normalize-for-search.js";
import { NoteConverter } from "../../server/api/mastodon/converters/note.js";
export default async function(user, note, data, suppressEvidence = false) {
if (data.text !== undefined && data.text !== null) {
data.text = data.text.trim();
} else {
data.text = null;
}
const fileIds = data.files?.map((file)=>file.id);
const fileTypes = data.files?.map((file)=>file.type);
const tokens = mfm.parse(data.text || "").concat(mfm.parse(data.cw || ""));
const tags = extractHashtags(tokens).filter((tag)=>Array.from(tag || "").length <= 128).splice(0, 32).map(normalizeForSearch);
const emojis = extractCustomEmojisFromMfm(tokens);
const mentionUsers = await extractMentionedUsers(user, tokens);
const mentionUserIds = mentionUsers.map((user)=>user.id);
const remoteUsers = mentionUsers.filter((user)=>user.host != null);
const remoteUserIds = remoteUsers.map((user)=>user.id);
const remoteProfiles = await UserProfiles.findBy({
userId: In(remoteUserIds)
});
const mentionedRemoteUsers = remoteUsers.map((user)=>{
const profile = remoteProfiles.find((profile)=>profile.userId === user.id);
return {
username: user.username,
host: user.host ?? null,
uri: user.uri,
url: profile ? profile.url : undefined
};
});
let publishing = false;
const update = {};
if (data.text !== null && data.text !== note.text) {
update.text = data.text;
}
if (data.cw !== note.cw) {
update.cw = data.cw ?? null;
}
if (data.files !== undefined && fileIds.sort().join(",") !== note.fileIds.sort().join(",")) {
update.fileIds = fileIds;
update.attachedFileTypes = fileTypes;
}
if (tags.sort().join(",") !== note.tags.sort().join(",")) {
update.tags = tags;
}
if (mentionUserIds.sort().join(",") !== note.mentions.sort().join(",")) {
update.mentions = mentionUserIds;
update.mentionedRemoteUsers = JSON.stringify(mentionedRemoteUsers);
}
if (emojis.sort().join(",") !== note.emojis.sort().join(",")) {
update.emojis = emojis;
}
if (data.poll !== undefined && note.hasPoll !== !!data.poll) {
update.hasPoll = !!data.poll;
}
if (data.poll) {
const dbPoll = await Polls.findOneBy({
noteId: note.id
});
if (dbPoll == null) {
await Polls.insert({
noteId: note.id,
choices: data.poll?.choices,
multiple: data.poll?.multiple,
votes: new Array(data.poll?.choices.length).fill(0),
expiresAt: data.poll?.expiresAt,
noteVisibility: note.visibility === "hidden" ? "home" : note.visibility,
userId: user.id,
userHost: user.host
});
publishing = true;
} else {
const choicesChanged = JSON.stringify(dbPoll.choices) !== JSON.stringify(data.poll.choices);
if (dbPoll.multiple !== data.poll.multiple || dbPoll.expiresAt !== data.poll.expiresAt || dbPoll.noteVisibility !== note.visibility || choicesChanged) {
await Polls.update({
noteId: note.id
}, {
choices: data.poll?.choices,
multiple: data.poll?.multiple,
votes: choicesChanged ? new Array(data.poll.choices.length).fill(0) : undefined,
expiresAt: data.poll?.expiresAt,
noteVisibility: note.visibility === "hidden" ? "home" : note.visibility
});
// Reset votes
if (JSON.stringify(dbPoll.choices) !== JSON.stringify(data.poll.choices)) {
await PollVotes.delete({
noteId: dbPoll.noteId
});
}
publishing = true;
}
}
}
if (data.quoteAuthorization !== undefined) {
update.quoteAuthorization = data.quoteAuthorization;
}
if (notEmpty(update)) {
if (!suppressEvidence) update.updatedAt = new Date();
await Notes.update(note.id, update);
if (!suppressEvidence) {
// Add previous note contents to NoteEdit history
await NoteEdits.insert({
id: genId(),
noteId: note.id,
text: note.text || undefined,
cw: note.cw,
fileIds: note.fileIds,
updatedAt: update.updatedAt ?? undefined
});
}
publishing = true;
}
note = await Notes.findOneByOrFail({
id: note.id
});
if (publishing) {
NoteConverter.prewarmCache(note);
// Publish update event for the updated note details
if (!suppressEvidence) {
publishNoteStream(note.id, "updated", {
updatedAt: update.updatedAt
});
publishNoteUpdatesStream("updated", note);
}
(async ()=>{
if (note.localOnly) return;
const noteActivity = await renderNote(note, false);
if (!suppressEvidence) 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 mentionUsers.filter((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);
}
// 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);
}
// GO!
dm.execute();
})();
}
return note;
}
function notEmpty(partial) {
return Object.keys(partial).length > 0;
}