mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2025-03-04 07:18:50 -07:00
222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
import * as mfm from "mfm-js";
|
|
import {
|
|
publishNoteStream,
|
|
} from "@/services/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 type { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
|
import { Note } from "@/models/entities/note.js";
|
|
import {
|
|
Users,
|
|
Notes,
|
|
UserProfiles,
|
|
Polls, NoteEdits,
|
|
} from "@/models/index.js";
|
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
|
import { In } from "typeorm";
|
|
import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
|
import { genId } from "@/misc/gen-id.js";
|
|
import type { IPoll } from "@/models/entities/poll.js";
|
|
import { deliverToRelays } from "../relay.js";
|
|
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
|
import { extractMentionedUsers, index } from "@/services/note/create.js";
|
|
|
|
type Option = {
|
|
text?: string | null;
|
|
files?: DriveFile[] | null;
|
|
poll?: IPoll | null;
|
|
cw?: string | null;
|
|
};
|
|
|
|
export default async function (
|
|
user: ILocalUser,
|
|
note: Note,
|
|
data: Option,
|
|
): Promise<Note> {
|
|
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: string[] = extractHashtags(tokens);
|
|
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,
|
|
} as IMentionedRemoteUsers[0];
|
|
});
|
|
|
|
let publishing = false;
|
|
const update = {} as Partial<Note>;
|
|
if (data.text !== null && data.text !== note.text) {
|
|
update.text = data.text;
|
|
}
|
|
if (data.cw !== note.cw) {
|
|
update.cw = data.cw ?? null;
|
|
}
|
|
if (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 (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: data.poll?.votes,
|
|
expiresAt: data.poll?.expiresAt,
|
|
noteVisibility: note.visibility === "hidden" ? "home" : note.visibility,
|
|
userId: user.id,
|
|
userHost: user.host,
|
|
});
|
|
publishing = true;
|
|
} else if (
|
|
dbPoll.multiple !== data.poll.multiple ||
|
|
dbPoll.expiresAt !== data.poll.expiresAt ||
|
|
dbPoll.noteVisibility !== note.visibility ||
|
|
JSON.stringify(dbPoll.choices) !== JSON.stringify(data.poll.choices)
|
|
) {
|
|
await Polls.update(
|
|
{ noteId: note.id },
|
|
{
|
|
choices: data.poll?.choices,
|
|
multiple: data.poll?.multiple,
|
|
votes: data.poll?.votes,
|
|
expiresAt: data.poll?.expiresAt,
|
|
noteVisibility:
|
|
note.visibility === "hidden" ? "home" : note.visibility,
|
|
},
|
|
);
|
|
publishing = true;
|
|
} else {
|
|
for (let i = 0; i < data.poll.choices.length; i++) {
|
|
if (dbPoll.votes[i] !== data.poll.votes?.[i]) {
|
|
await Polls.update({ noteId: note.id }, { votes: data.poll?.votes });
|
|
publishing = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notEmpty(update)) {
|
|
update.updatedAt = new Date();
|
|
await Notes.update(note.id, update);
|
|
|
|
// 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,
|
|
});
|
|
|
|
publishing = true;
|
|
}
|
|
|
|
note = await Notes.findOneByOrFail({ id: note.id });
|
|
|
|
if (publishing) {
|
|
index(note, true);
|
|
|
|
// Publish update event for the updated note details
|
|
publishNoteStream(note.id, "updated", {
|
|
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);
|
|
|
|
// Delivery to remote mentioned users
|
|
for (const u of mentionUsers.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 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: Partial<any>) {
|
|
return Object.keys(partial).length > 0;
|
|
}
|
|
|