Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -0,0 +1,9 @@
import config from "../../../config/index.js";
import renderFollow from "./follow.js";
export default ((follower, followee, requestId)=>{
return {
type: "Accept",
actor: `${config.url}/users/${followee.id}`,
object: renderFollow(follower, followee, requestId)
};
});
@@ -0,0 +1,15 @@
import config from "../../../config/index.js";
// assumes stamp.targetNote is populated
export default (async (request, stamp)=>({
type: "Accept",
to: request.actor,
actor: `${config.url}/users/${stamp.targetNote.userId}`,
object: {
type: "QuoteRequest",
id: request.id,
actor: request.actor,
object: request.object,
instrument: request.instrument
},
result: `${config.url}/stamp/${stamp.id}`
}));
@@ -0,0 +1,7 @@
import config from "../../../config/index.js";
export default ((user, target, object)=>({
type: "Add",
actor: `${config.url}/users/${user.id}`,
target,
object
}));
@@ -0,0 +1,39 @@
import config from "../../../config/index.js";
export default ((object, note)=>{
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`
];
} else if (note.visibility === "home") {
to = [
`${attributedTo}/followers`
];
cc = [
"https://www.w3.org/ns/activitystreams#Public"
];
} else if (note.visibility === "followers") {
to = [
`${attributedTo}/followers`
];
} else if (note.visibility === "specified") {
to = mentions;
} else {
return null;
}
return {
id: `${config.url}/notes/${note.id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: "Announce",
published: note.createdAt.toISOString(),
to,
cc,
object
};
});
@@ -0,0 +1,10 @@
import config from "../../../config/index.js";
import { Bites } from "../../../models/index.js";
export default (async (bite)=>({
id: `${config.url}/bites/${bite.id}`,
type: "Bite",
actor: `${config.url}/users/${bite.userId}`,
target: await Bites.targetUri(bite),
published: bite.createdAt.toISOString(),
to: await Bites.targetUserUri(bite)
}));
@@ -0,0 +1,16 @@
import config from "../../../config/index.js";
/**
* Renders a block into its ActivityPub representation.
*
* @param block The block to be rendered. The blockee relation must be loaded.
*/ export function renderBlock(block) {
if (block.blockee?.uri == null) {
throw new Error("renderBlock: missing blockee uri");
}
return {
type: "Block",
id: `${config.url}/blocks/${block.id}`,
actor: `${config.url}/users/${block.blockerId}`,
object: block.blockee.uri
};
}
@@ -0,0 +1,13 @@
import config from "../../../config/index.js";
export default ((object, note)=>{
const activity = {
id: `${config.url}/notes/${note.id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: "Create",
published: note.createdAt.toISOString(),
object
};
if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc;
return activity;
});
@@ -0,0 +1,7 @@
import config from "../../../config/index.js";
export default ((object, user)=>({
type: "Delete",
actor: `${config.url}/users/${user.id}`,
object,
published: new Date().toISOString()
}));
@@ -0,0 +1,7 @@
import { DriveFiles } from "../../../models/index.js";
export default ((file)=>({
type: "Document",
mediaType: file.type,
url: DriveFiles.getPublicUrl(file),
name: file.comment
}));
@@ -0,0 +1,12 @@
import config from "../../../config/index.js";
export default ((emoji)=>({
id: `${config.url}/emojis/${emoji.name}`,
type: "Emoji",
name: `:${emoji.name}:`,
updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
icon: {
type: "Image",
mediaType: emoji.glyph ? "image/svg+xml" : emoji.type || "image/png",
url: emoji.glyph ? emoji.originalUrl : emoji.publicUrl || emoji.originalUrl
}
}));
@@ -0,0 +1,11 @@
import config from "../../../config/index.js";
// to anonymise reporters, the reporting actor must be a system user
// object has to be a uri or array of uris
export const renderFlag = (user, object, content)=>{
return {
type: "Flag",
actor: `${config.url}/users/${user.id}`,
content,
object
};
};
@@ -0,0 +1,10 @@
import config from "../../../config/index.js";
export function renderFollowRelay(relay, relayActor) {
const follow = {
id: `${config.url}/activities/follow-relay/${relay.id}`,
type: "Follow",
actor: `${config.url}/users/${relayActor.id}`,
object: "https://www.w3.org/ns/activitystreams#Public"
};
return follow;
}
@@ -0,0 +1,11 @@
import config from "../../../config/index.js";
import { Users } from "../../../models/index.js";
/**
* Convert (local|remote)(Follower|Followee)ID to URL
* @param id Follower|Followee ID
*/ export default async function renderFollowUser(id) {
const user = await Users.findOneByOrFail({
id: id
});
return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
}
@@ -0,0 +1,11 @@
import config from "../../../config/index.js";
import { Users } from "../../../models/index.js";
export default ((follower, followee, requestId)=>{
const follow = {
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
type: "Follow",
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri
};
return follow;
});
@@ -0,0 +1,6 @@
import config from "../../../config/index.js";
export default ((tag)=>({
type: "Hashtag",
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
name: `#${tag}`
}));
@@ -0,0 +1,7 @@
import { DriveFiles } from "../../../models/index.js";
export default ((file)=>({
type: "Image",
url: DriveFiles.getPublicUrl(file),
sensitive: file.isSensitive,
name: file.comment
}));
@@ -0,0 +1,20 @@
import { v4 as uuid } from "uuid";
import config from "../../../config/index.js";
import { getUserKeypair } from "../../../misc/keypair-store.js";
import { LdSignature } from "../misc/ld-signature.js";
import { WellKnownContext } from "../misc/contexts.js";
export const renderActivity = (x)=>{
if (x == null) return null;
if (typeof x === "object" && x.id == null) {
x.id = `${config.url}/${uuid()}`;
}
return Object.assign({}, WellKnownContext, x);
};
export const attachLdSignature = async (activity, user)=>{
if (activity == null) return null;
const keypair = await getUserKeypair(user.id);
const ldSignature = new LdSignature();
ldSignature.debug = false;
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);
return activity;
};
@@ -0,0 +1,11 @@
import config from "../../../config/index.js";
import { createPublicKey } from "node:crypto";
export default ((user, key, postfix)=>({
id: `${config.url}/users/${user.id}${postfix || "/publickey"}`,
type: "Key",
owner: `${config.url}/users/${user.id}`,
publicKeyPem: createPublicKey(key.publicKey).export({
type: "spki",
format: "pem"
})
}));
@@ -0,0 +1,30 @@
import { IsNull } from "typeorm";
import config from "../../../config/index.js";
import { Emojis } from "../../../models/index.js";
import renderEmoji from "./emoji.js";
import { fetchMeta } from "../../../misc/fetch-meta.js";
export const renderLike = async (noteReaction, note)=>{
const reaction = noteReaction.reaction;
const meta = await fetchMeta();
const object = {
type: "Like",
id: `${config.url}/likes/${noteReaction.id}`,
actor: `${config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
...!meta.defaultReaction.includes(reaction) ? {
content: reaction,
_misskey_reaction: reaction
} : {}
};
if (reaction.startsWith(":")) {
const name = reaction.replace(/:/g, "");
const emoji = await Emojis.findOneBy({
name,
host: IsNull()
});
if (emoji) object.tag = [
renderEmoji(emoji)
];
}
return object;
};
@@ -0,0 +1,7 @@
import config from "../../../config/index.js";
import { Users } from "../../../models/index.js";
export default ((mention)=>({
type: "Mention",
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${mention.id}`,
name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`
}));
@@ -0,0 +1,174 @@
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 += `<p class="quote-inline">RE: <a href="${quoteHREFSan}">${quoteTextSan}</p>`;
}
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);
}
@@ -0,0 +1,20 @@
/**
* Render OrderedCollectionPage
* @param id URL of self
* @param totalItems Number of total items
* @param orderedItems Items
* @param partOf URL of base
* @param prev URL of prev page (optional)
* @param next URL of next page (optional)
*/ export default function(id, totalItems, orderedItems, partOf, prev, next) {
const page = {
id,
partOf,
type: "OrderedCollectionPage",
totalItems,
orderedItems
};
if (prev) page.prev = prev;
if (next) page.next = next;
return page;
}
@@ -0,0 +1,18 @@
/**
* Render OrderedCollection
* @param id URL of self
* @param totalItems Total number of items
* @param first URL of first page (optional)
* @param last URL of last page (optional)
* @param orderedItems attached objects (optional)
*/ export default function(id, totalItems, first, last, orderedItems) {
const page = {
id,
type: "OrderedCollection",
totalItems
};
if (first) page.first = first;
if (last) page.last = last;
if (orderedItems) page.orderedItems = orderedItems;
return page;
}
@@ -0,0 +1,91 @@
import * as mfm from "mfm-js";
import config from "../../../config/index.js";
import { DriveFiles, UserProfiles } from "../../../models/index.js";
import { getUserKeypair } from "../../../misc/keypair-store.js";
import { toHtml } from "../../../mfm/to-html.js";
import renderImage from "./image.js";
import renderKey from "./key.js";
import { getEmojis } from "./note.js";
import renderEmoji from "./emoji.js";
import renderHashtag from "./hashtag.js";
export async function renderPerson(user) {
const id = `${config.url}/users/${user.id}`;
const isSystem = !!user.username.match(/\./);
const [avatar, banner, profile] = await Promise.all([
user.avatarId ? DriveFiles.findOneBy({
id: user.avatarId
}) : Promise.resolve(undefined),
user.bannerId ? DriveFiles.findOneBy({
id: user.bannerId
}) : Promise.resolve(undefined),
UserProfiles.findOneByOrFail({
userId: user.id
})
]);
const attachment = [];
if (profile.fields) {
for (const field of profile.fields){
const value = await toHtml(mfm.parse(field.value), profile.mentions, profile.userHost);
attachment.push({
type: "PropertyValue",
name: field.name,
value: value ?? field.value
});
}
}
const emojis = await getEmojis(user.emojis);
const apemojis = emojis.map((emoji)=>renderEmoji(emoji));
const hashtagTags = (user.tags || []).map((tag)=>renderHashtag(tag));
const tag = [
...apemojis,
...hashtagTags
];
const keypair = await getUserKeypair(user.id);
let canBite;
if (user.canBite === "anyone") {
canBite = "https://www.w3.org/ns/activitystreams#Public";
} else if (user.canBite === "followers") {
canBite = user.followersUri ?? `${config.url}/users/${user.id}/followers`;
}
const person = {
type: isSystem ? "Application" : user.isBot ? "Service" : "Person",
id,
inbox: `${id}/inbox`,
outbox: `${id}/outbox`,
followers: `${id}/followers`,
following: `${id}/following`,
featured: `${id}/collections/featured`,
sharedInbox: `${config.url}/inbox`,
endpoints: {
sharedInbox: `${config.url}/inbox`
},
url: `${config.url}/@${user.username}`,
preferredUsername: user.username,
name: user.name,
summary: profile.description ? await toHtml(mfm.parse(profile.description), profile.mentions, profile.userHost) : null,
_misskey_summary: profile.description,
icon: avatar ? renderImage(avatar) : null,
image: banner ? renderImage(banner) : null,
tag,
manuallyApprovesFollowers: user.isLocked,
discoverable: !!user.isExplorable,
publicKey: renderKey(user, keypair, "#main-key"),
isCat: user.isCat,
attachment: attachment.length ? attachment : undefined,
pronouns: profile.pronouns,
canBite
};
if (user.movedToUri) {
person.movedTo = user.movedToUri;
}
if (user.alsoKnownAs) {
person.alsoKnownAs = user.alsoKnownAs;
}
if (profile.birthday) {
person["vcard:bday"] = profile.birthday;
}
if (profile.location) {
person["vcard:Address"] = profile.location;
}
return person;
}
@@ -0,0 +1,18 @@
import config from "../../../config/index.js";
export default async function renderQuestion(user, note, poll) {
const question = {
type: "Question",
id: `${config.url}/questions/${note.id}`,
actor: `${config.url}/users/${user.id}`,
content: note.text || "",
[poll.multiple ? "anyOf" : "oneOf"]: poll.choices.map((text, i)=>({
name: text,
_misskey_votes: poll.votes[i],
replies: {
type: "Collection",
totalItems: poll.votes[i]
}
}))
};
return question;
}
@@ -0,0 +1,9 @@
import config from "../../../config/index.js";
// assumes stamp.note and stamp.targetNote are populated
export default ((stamp)=>({
id: `${config.url}/stamp/${stamp.id}`,
type: "QuoteAuthorization",
attributedTo: `${config.url}/users/${stamp.targetNote.userId}`,
interactingObject: stamp.note.userHost === null ? `${config.url}/notes/${stamp.note.id}` : stamp.note.uri,
interactionTarget: `${config.url}/notes/${stamp.targetNoteId}`
}));
@@ -0,0 +1,9 @@
import config from "../../../config/index.js";
export default function renderQuoteRequest(note, targetNote) {
return {
type: "QuoteRequest",
actor: `${config.url}/users/${note.userId}`,
object: targetNote.uri,
instrument: `${config.url}/notes/${note.id}`
};
}
@@ -0,0 +1,6 @@
import config from "../../../config/index.js";
export const renderReadActivity = (user, message)=>({
type: "Read",
actor: `${config.url}/users/${user.id}`,
object: message.uri
});
@@ -0,0 +1,6 @@
import config from "../../../config/index.js";
export default ((object, user)=>({
type: "Reject",
actor: `${config.url}/users/${user.id}`,
object
}));
@@ -0,0 +1,7 @@
import config from "../../../config/index.js";
export default ((user, target, object)=>({
type: "Remove",
actor: `${config.url}/users/${user.id}`,
target,
object
}));
@@ -0,0 +1,4 @@
export default ((id)=>({
id,
type: "Tombstone"
}));
@@ -0,0 +1,14 @@
import config from "../../../config/index.js";
export default ((object, user)=>{
if (object == null) return null;
const id = typeof object.id === "string" && object.id.startsWith(config.url) ? `${object.id}/undo` : undefined;
return {
type: "Undo",
...id ? {
id
} : {},
actor: `${config.url}/users/${user.id}`,
object,
published: new Date().toISOString()
};
});
@@ -0,0 +1,14 @@
import config from "../../../config/index.js";
export default ((object, user)=>{
const activity = {
id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`,
actor: `${config.url}/users/${user.id}`,
type: "Update",
to: [
"https://www.w3.org/ns/activitystreams#Public"
],
object,
published: new Date().toISOString()
};
return activity;
});
@@ -0,0 +1,22 @@
import config from "../../../config/index.js";
export default async function renderVote(user, vote, note, poll, pollOwner) {
return {
id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`,
actor: `${config.url}/users/${user.id}`,
type: "Create",
to: [
pollOwner.uri
],
published: new Date().toISOString(),
object: {
id: `${config.url}/users/${user.id}#votes/${vote.id}`,
type: "Note",
attributedTo: `${config.url}/users/${user.id}`,
to: [
pollOwner.uri
],
inReplyTo: note.uri,
name: poll.choices[vote.choice]
}
};
}