187 lines
7.8 KiB
JavaScript
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;
|
|
}
|