import { In, IsNull } from "typeorm"; import config from "../../../config/index.js"; import { DriveFiles, Notes, Users, Emojis, Polls } from "../../../models/index.js"; import toHtml from "../misc/get-note-html.js"; import renderEmoji from "./emoji.js"; import renderMention from "./mention.js"; import renderHashtag from "./hashtag.js"; import renderDocument from "./document.js"; import sanitizeHtml from "sanitize-html"; export default async function renderNote(note, dive = true, isTalk = false) { const getPromisedFiles = async (ids)=>{ if (!ids || ids.length === 0) return []; const items = await DriveFiles.findBy({ id: In(ids) }); return ids.map((id)=>items.find((item)=>item.id === id)).filter((item)=>item != null); }; let inReplyTo; let inReplyToNote; if (note.replyId) { inReplyToNote = await Notes.findOneBy({ id: note.replyId }); if (inReplyToNote != null) { const inReplyToUser = await Users.findOneBy({ id: inReplyToNote.userId }); if (inReplyToUser != null) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { if (dive) { inReplyTo = await renderNote(inReplyToNote, false); } else { inReplyTo = `${config.url}/notes/${inReplyToNote.id}`; } } } } } else { inReplyTo = null; } let quoteId; let quoteUrl; if (note.renoteId) { const renote = await Notes.findOneBy({ id: note.renoteId }); if (renote) { if (renote.userHost) { quoteId = renote.uri; quoteUrl = renote.url ?? renote.uri; } else { quoteId = quoteUrl = `${config.url}/notes/${renote.id}`; } } } const attributedTo = `${config.url}/users/${note.userId}`; const mentions = JSON.parse(note.mentionedRemoteUsers).map((x)=>x.uri); let to = []; let cc = []; if (note.visibility === "public") { to = [ "https://www.w3.org/ns/activitystreams#Public" ]; cc = [ `${attributedTo}/followers` ].concat(mentions); } else if (note.visibility === "home") { to = [ `${attributedTo}/followers` ]; cc = [ "https://www.w3.org/ns/activitystreams#Public" ].concat(mentions); } else if (note.visibility === "followers") { to = [ `${attributedTo}/followers` ]; cc = mentions; } else { to = mentions; } const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ id: In(note.mentions) }) : []; const hashtagTags = (note.tags || []).map((tag)=>renderHashtag(tag)); const mentionTags = mentionedUsers.map((u)=>renderMention(u)); const files = await getPromisedFiles(note.fileIds); const text = note.text ?? ""; let poll = null; if (note.hasPoll) { poll = await Polls.findOneBy({ noteId: note.id }); } const summary = note.cw === "" ? String.fromCharCode(0x200b) : note.cw; let content = await toHtml(Object.assign({}, note, { text })); if (quoteId) { // wrapping in p.quote-inline lets mastodon automatically strip the link const quoteHREFSan = (quoteUrl || quoteId).replaceAll("&", "&").replaceAll('"', """); const quoteTextSan = sanitizeHtml(quoteUrl || quoteId); content += `

RE: ${quoteTextSan}

`; } const emojis = await getEmojis(note.emojis); const apemojis = emojis.map((emoji)=>renderEmoji(emoji)); const tag = [ ...hashtagTags, ...mentionTags, ...apemojis ]; const asPoll = poll ? { type: "Question", content: await toHtml(Object.assign({}, note, { text: text })), [poll.expiresAt && poll.expiresAt < new Date() ? "closed" : "endTime"]: poll.expiresAt, [poll.multiple ? "anyOf" : "oneOf"]: poll.choices.map((text, i)=>({ type: "Note", name: text, replies: { type: "Collection", totalItems: poll.votes[i] } })) } : {}; const asTalk = isTalk ? { _misskey_talk: true } : {}; return { id: `${config.url}/notes/${note.id}`, type: "Note", attributedTo, summary, content, _misskey_content: text, source: { content: text, mediaType: "text/x.misskeymarkdown" }, _misskey_quote: quoteId, quoteUri: quoteId, quoteUrl: quoteId, quote: note.canQuote ? quoteId : undefined, published: note.createdAt.toISOString(), to, cc, inReplyTo, attachment: files.map(renderDocument), sensitive: note.cw != null || files.some((file)=>file.isSensitive), tag, interactionPolicy: note.visibility === "public" || note.visibility === "home" ? { canQuote: { automaticApproval: "https://www.w3.org/ns/activitystreams#Public" } } : undefined, quoteAuthorization: note.quoteAuthorization || undefined, ...asPoll, ...asTalk }; } export async function getEmojis(names) { if (names == null || names.length === 0) return []; const emojis = await Promise.all(names.map((name)=>{ const parts = name.split("@"); return Emojis.findOneBy({ name: parts[0], host: parts.length === 2 ? parts[1] : IsNull() }); })); return emojis.filter((emoji)=>emoji != null); }