Removed relay jank, fixed local object rendering in resolution, and refactored local URI parsing
ci/woodpecker/tag/ociImageTag Pipeline was successful Details

This commit is contained in:
Natty 2024-11-25 04:13:48 +01:00
parent e845eda898
commit e77ba36953
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
28 changed files with 319 additions and 1384 deletions

View File

@ -4,7 +4,6 @@ import {SmartBuffer} from "smart-buffer";
import {decode, encode} from "@msgpack/msgpack";
import Logger from "@/services/logger.js";
import config from "@/config/index.js";
import type {IObject} from "@/remote/activitypub/type";
let client: Socket | null = null;
let exponentialBackoff = 0;
@ -162,7 +161,7 @@ async function rpcCall<D, T>(method: string, data: D): Promise<T> {
}
export async function magApGet(userId: string, url: string): Promise<IObject> {
export async function magApGet(userId: string, url: string): Promise<any> {
logger.debug(`AP GET to: ${url}`);
return await rpcCall("/ap/get", {
user_id: userId,
@ -170,7 +169,7 @@ export async function magApGet(userId: string, url: string): Promise<IObject> {
});
}
export async function magApPost(userId: string, url: string, body: any): Promise<IObject> {
export async function magApPost(userId: string, url: string, body: any): Promise<any> {
logger.debug(`AP POST to: ${url}`);
return await rpcCall("/ap/post", {
user_id: userId,

View File

@ -1,6 +1,15 @@
import Bull from "bull";
import config from "@/config/index.js";
export class RetriableLater extends Error {
constructor(message: string) {
super(message);
this.name = "RetriableLater";
Object.setPrototypeOf(this, RetriableLater.prototype);
}
}
export function initialize<T>(name: string, limitPerSec = -1) {
return new Bull<T>(name, {
redis: {
@ -34,7 +43,7 @@ function apBackoff(attemptsMade: number, err: Error) {
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
if (err?.message?.startsWith("RetriableLater")) {
if (err instanceof RetriableLater) {
backoff += (Math.pow(1.5, attemptsMade) - 1) * baseDelay * 10 * attemptsMade;
}

View File

@ -1,27 +1,25 @@
import {URL} from "node:url";
import type Bull from "bull";
import httpSignature from "@peertube/http-signature";
import perform from "@/remote/activitypub/perform.js";
import Logger from "@/services/logger.js";
import {registerOrFetchInstanceDoc} from "@/services/register-or-fetch-instance-doc.js";
import {Instances} from "@/models/index.js";
import {
apRequestChart,
federationChart,
instanceChart,
} from "@/services/chart/index.js";
import {apRequestChart, federationChart, instanceChart,} from "@/services/chart/index.js";
import {fetchMeta} from "@/misc/fetch-meta.js";
import { toPuny, extractDbHost } from "@/misc/convert-host.js";
import { getApId } from "@/remote/activitypub/type.js";
import {extractDbHost, toPuny} from "@/misc/convert-host.js";
import {getApId, isCollection, isCollectionOrOrderedCollection} from "@/remote/activitypub/type.js";
import {fetchInstanceMetadata} from "@/services/fetch-instance-metadata.js";
import type {InboxJobData} from "../types.js";
import DbResolver from "@/remote/activitypub/db-resolver.js";
import { resolvePerson } from "@/remote/activitypub/models/person.js";
import { LdSignature } from "@/remote/activitypub/misc/ld-signature.js";
import {StatusError} from "@/misc/fetch.js";
import type {CacheableRemoteUser} from "@/models/entities/user.js";
import type {UserPublickey} from "@/models/entities/user-publickey.js";
import {shouldBlockInstance} from "@/misc/should-block-instance.js";
import {updatePerson} from "@/remote/activitypub/models/person";
import {performActivity} from "@/remote/activitypub/kernel";
import Resolver from "@/remote/activitypub/resolver";
import {toArray} from "@/prelude/array";
import {apLogger} from "@/remote/activitypub/logger";
const logger = new Logger("inbox");
@ -38,17 +36,17 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
if (!signature?.keyId) return `Invalid signature: ${signature}`;
//#endregion
const host = toPuny(new URL(signature.keyId).hostname);
const httpSigKeyHost = toPuny(new URL(signature.keyId).hostname);
// interrupt if blocked
const meta = await fetchMeta();
if (await shouldBlockInstance(host, meta)) {
return `Blocked request: ${host}`;
if (await shouldBlockInstance(httpSigKeyHost, meta)) {
return `Blocked request: ${httpSigKeyHost}`;
}
// only whitelisted instances in private mode
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
return `Blocked request: ${host}`;
if (meta.privateMode && !meta.allowedHosts.includes(httpSigKeyHost)) {
return `Blocked request: ${httpSigKeyHost}`;
}
const keyIdLower = signature.keyId.toLowerCase();
@ -59,19 +57,18 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
const dbResolver = new DbResolver();
// HTTP-Signature keyId from DB
let authUser: {
let senderActor: {
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {
if (senderActor == null) {
try {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
senderActor = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
} catch (e) {
// Skip if target is 4xx
if (e instanceof StatusError) {
if (e.isClientError) {
if (e.isClientError && e.statusCode != 429) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
}
throw new Error(
@ -81,84 +78,63 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
}
}
// それでもわからなければ終了
if (authUser == null) {
if (senderActor == null) {
return "skip: failed to resolve user";
}
// publicKey がなくても終了
if (authUser.key == null) {
if (senderActor.key == null) {
return "skip: failed to resolve user publicKey";
}
// HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature(
signature,
authUser.key.keyPem,
senderActor.key.keyPem,
);
// また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
if (activity.signature) {
if (activity.signature.type !== "RsaSignature2017") {
return `skip: unsupported LD-signature type ${activity.signature.type}`;
if (!httpSignatureValidated) {
return `skip: http-signature verification failed. keyId=${signature.keyId}`;
}
// activity.signature.creator: https://example.oom/users/user#main-key
// みたいになっててUserを引っ張れば公開キーも入ることを期待する
if (activity.signature.creator) {
const candicate = activity.signature.creator.replace(/#.*/, "");
await resolvePerson(candicate).catch(() => null);
const signerHost = extractDbHost(senderActor.user.uri!);
if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver();
for (const item of toArray(
isCollection(activity) ? activity.items : activity.orderedItems,
)) {
const act = await resolver.resolve(item);
if (typeof act.id !== "string") {
continue;
}
// keyIdからLD-Signatureのユーザーを取得
authUser = await dbResolver.getAuthUserFromKeyId(
activity.signature.creator,
);
if (authUser == null) {
return "skip: LD-Signatureのユーザーが取得できませんでした";
const activityIdHost = extractDbHost(act.id);
if (signerHost !== activityIdHost) {
continue;
}
if (authUser.key == null) {
return "skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした";
try {
await performActivity(senderActor.user, act);
} catch (err) {
if (err instanceof Error || typeof err === "string") {
apLogger.error(err);
}
// LD-Signature検証
const ldSignature = new LdSignature();
const verified = await ldSignature
.verifyRsaSignature2017(activity, authUser.key.keyPem)
.catch(() => false);
if (!verified) {
return "skip: LD-Signatureの検証に失敗しました";
}
// もう一度actorチェック
if (authUser.user.uri !== activity.actor) {
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
}
// ブロックしてたら中断
const ldHost = extractDbHost(authUser.user.uri);
if (await shouldBlockInstance(ldHost, meta)) {
return `Blocked request: ${ldHost}`;
}
} else {
return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
}
if (typeof activity.id !== "string") {
return `skip: activity.id not a valid id`;
}
// activity.idがあればホストが署名者のホストであることを確認する
if (typeof activity.id === "string") {
const signerHost = extractDbHost(authUser.user.uri!);
const activityIdHost = extractDbHost(activity.id);
if (signerHost !== activityIdHost) {
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
}
await performActivity(senderActor.user, activity);
}
// Update stats
registerOrFetchInstanceDoc(authUser.user.host).then((i) => {
registerOrFetchInstanceDoc(senderActor.user.host).then((i) => {
Instances.update(i.id, {
latestRequestReceivedAt: new Date(),
lastCommunicatedAt: new Date(),
@ -172,7 +148,17 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
federationChart.inbox(i.host);
});
// アクティビティを処理
await perform(authUser.user, activity);
// Update the remote user information if it is out of date
if (senderActor.user.uri) {
if (
senderActor.user.lastFetchedAt == null ||
Date.now() - senderActor.user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24
) {
setImmediate(() => {
updatePerson(senderActor?.user.uri!);
});
}
}
return "ok";
};

View File

@ -1,66 +1,82 @@
import escapeRegexp from "escape-regexp";
import config from "@/config/index.js";
import type {Note} from "@/models/entities/note.js";
import type {
CacheableRemoteUser,
CacheableUser,
} from "@/models/entities/user.js";
import type {CacheableRemoteUser, CacheableUser,} from "@/models/entities/user.js";
import type {UserPublickey} from "@/models/entities/user-publickey.js";
import {
Notes,
Users,
UserPublickeys,
} from "@/models/index.js";
import {Notes, UserPublickeys, Users,} from "@/models/index.js";
import {Cache} from "@/misc/cache.js";
import {uriPersonCache, userByIdCache} from "@/services/user-cache.js";
import type {IObject} from "./type.js";
import {getApId} from "./type.js";
import {resolvePerson} from "./models/person.js";
import {URL} from "node:url";
import {toPuny} from "@/misc/convert-host";
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export type UriLocalObject =
({ type: "Note" } | { type: "NoteActivity" } | { type: "User" } | { type: "Question" } | { type: "Like" } | { type: "Follow", followeeId: string })
& { id: string }
export type UriParseResult =
| {
/** wether the URI was generated by us */
local: true;
/** id in DB */
id: string;
/** hint of type, e.g. "notes", "users" */
type: string;
/** any remaining text after type and id, not including the slash after id. undefined if empty */
rest?: string;
object: UriLocalObject
}
| {
/** wether the URI was generated by us */
local: false;
/** uri in DB */
uri: string;
uri: URL;
};
export function parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
// the host part of a URL is case insensitive, so use the 'i' flag.
const localRegex = new RegExp(
`^${escapeRegexp(config.url)}/(\\w+)/(\\w+)(?:/(.+))?`,
"i",
);
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
local: true,
type: matchLocal[1],
id: matchLocal[2],
rest: matchLocal[3],
};
} else {
export function parseApUri(value: string | IObject): UriParseResult {
const uri = new URL(getApId(value));
if (config.host !== toPuny(uri.host)) {
return {
local: false,
uri,
};
}
const path = uri.pathname;
const [_empty, objectType, id, ...parts] = path.split("/");
if (!objectType || !id)
throw new Error("Invalid local object URI");
let object: UriLocalObject;
switch (true) {
case objectType === "notes" && parts.length == 0:
object = { type: parts.length == 1 && parts[0] === "activity" ? "NoteActivity" : "Note", id};
break;
case objectType === "notes" && parts.length == 1 && parts[0] === "activity":
object = { type: "NoteActivity", id};
break;
case objectType === "users" && parts.length == 0:
object = { type: "User", id };
break;
case objectType === "questions" && parts.length == 0:
object = { type: "Question", id };
break;
case objectType === "likes" && parts.length == 0:
object = { type: "Like", id };
break;
case objectType === "follows" && parts.length == 1:
const [followeeId] = parts;
object = { type: "Follow", id, followeeId };
break;
default:
throw new Error("Unknown object type");
}
return {
local: true,
object
}
}
export function isUriResultLocal(value: UriParseResult): value is UriParseResult & { local: true } {
return value.local;
}
export default class DbResolver {
@ -68,17 +84,17 @@ export default class DbResolver {
* AP Note => Misskey Note in DB
*/
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
const parsed = parseUri(value);
const parsed = parseApUri(value);
if (parsed.local) {
if (parsed.type !== "notes") return null;
if (parsed.object.type !== "Note") return null;
return await Notes.findOneBy({
id: parsed.id,
id: parsed.object.id,
});
} else {
return await Notes.findOneBy({
uri: parsed.uri,
uri: parsed.uri.href,
});
}
}
@ -89,22 +105,22 @@ export default class DbResolver {
public async getUserFromApId(
value: string | IObject,
): Promise<CacheableUser | null> {
const parsed = parseUri(value);
const parsed = parseApUri(value);
if (parsed.local) {
if (parsed.type !== "users") return null;
if (parsed.object.type !== "User") return null;
return (
(await userByIdCache.fetchMaybe(parsed.id, () =>
(await userByIdCache.fetchMaybe(parsed.object.id, () =>
Users.findOneBy({
id: parsed.id,
id: parsed.object.id,
}).then((x) => x ?? undefined),
)) ?? null
);
} else {
return await uriPersonCache.fetch(parsed.uri, () =>
return await uriPersonCache.fetch(parsed.uri.href, () =>
Users.findOneBy({
uri: parsed.uri,
uri: parsed.uri.href,
}),
);
}

View File

@ -2,7 +2,6 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
import accept from "@/services/following/requests/accept.js";
import type {IFollow} from "../../type.js";
import DbResolver from "../../db-resolver.js";
import { relayAccepted } from "@/services/relay.js";
export default async (
actor: CacheableRemoteUser,
@ -21,12 +20,6 @@ export default async (
return "skip: follower is not a local user";
}
// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await relayAccepted(match[1]);
}
await accept(actor, follower);
return "ok";
};

View File

@ -1,10 +1,10 @@
import type {CacheableRemoteUser} from "@/models/entities/user.js";
import config from "@/config/index.js";
import type {IFlag} from "../../type.js";
import {getApIds} from "../../type.js";
import {AbuseUserReports, Users} from "@/models/index.js";
import {In} from "typeorm";
import {genId} from "@/misc/gen-id.js";
import {isUriResultLocal, parseApUri} from "@/remote/activitypub/db-resolver";
export default async (
actor: CacheableRemoteUser,
@ -15,9 +15,11 @@ export default async (
// user and it is stored as a comment.
const uris = getApIds(activity.object);
const userIds = uris
.filter((uri) => uri.startsWith(`${config.url}/users/`))
.map((uri) => uri.split("/").pop()!);
const userIds = uris.map(parseApUri)
.filter(isUriResultLocal)
.filter(obj => obj.object.type === "User")
.map(user => user.object.id);
const users = await Users.findBy({
id: In(userIds),
});

View File

@ -1,27 +1,23 @@
import type {CacheableRemoteUser} from "@/models/entities/user.js";
import { toArray } from "@/prelude/array.js";
import type {IObject} from "../type.js";
import {
getApId,
isAccept,
isAdd,
isAnnounce,
isBlock,
isCreate,
isDelete,
isUpdate,
isRead,
isFollow,
isAccept,
isReject,
isAdd,
isRemove,
isAnnounce,
isLike,
isUndo,
isBlock,
isCollectionOrOrderedCollection,
isCollection,
isFlag,
isFollow,
isLike,
isMove,
getApId,
isReject,
isRemove,
isUndo,
isUpdate,
} from "../type.js";
import {apLogger} from "../logger.js";
import Resolver from "../resolver.js";
import create from "./create/index.js";
import performDeleteActivity from "./delete/index.js";
import performUpdateActivity from "./update/index.js";
@ -36,43 +32,17 @@ import remove from "./remove/index.js";
import block from "./block/index.js";
import flag from "./flag/index.js";
import move from "./move/index.js";
import type { IObject } from "../type.js";
import {extractDbHost} from "@/misc/convert-host.js";
import {shouldBlockInstance} from "@/misc/should-block-instance.js";
export async function performActivity(
actor: CacheableRemoteUser,
activity: IObject,
) {
if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver();
for (const item of toArray(
isCollection(activity) ? activity.items : activity.orderedItems,
)) {
const act = await resolver.resolve(item);
try {
await performOneActivity(actor, act);
} catch (err) {
if (err instanceof Error || typeof err === "string") {
apLogger.error(err);
}
}
}
} else {
await performOneActivity(actor, activity);
}
}
async function performOneActivity(
actor: CacheableRemoteUser,
activity: IObject,
): Promise<void> {
if (actor.isSuspended) return;
if (typeof activity.id !== "undefined") {
const host = extractDbHost(getApId(activity));
if (await shouldBlockInstance(host)) return;
}
if (isCreate(activity)) {
await create(actor, activity);

View File

@ -2,7 +2,6 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
import {remoteReject} from "@/services/following/reject.js";
import type {IFollow} from "../../type.js";
import DbResolver from "../../db-resolver.js";
import { relayRejected } from "@/services/relay.js";
import {Users} from "@/models/index.js";
export default async (
@ -22,12 +21,6 @@ export default async (
return "skip: follower is not a local user";
}
// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await relayRejected(match[1]);
}
await remoteReject(actor, follower);
return "ok";
};

View File

@ -1,525 +0,0 @@
const id_v1 = {
"@context": {
id: "@id",
type: "@type",
cred: "https://w3id.org/credentials#",
dc: "http://purl.org/dc/terms/",
identity: "https://w3id.org/identity#",
perm: "https://w3id.org/permissions#",
ps: "https://w3id.org/payswarm#",
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
sec: "https://w3id.org/security#",
schema: "http://schema.org/",
xsd: "http://www.w3.org/2001/XMLSchema#",
Group: "https://www.w3.org/ns/activitystreams#Group",
claim: { "@id": "cred:claim", "@type": "@id" },
credential: { "@id": "cred:credential", "@type": "@id" },
issued: { "@id": "cred:issued", "@type": "xsd:dateTime" },
issuer: { "@id": "cred:issuer", "@type": "@id" },
recipient: { "@id": "cred:recipient", "@type": "@id" },
Credential: "cred:Credential",
CryptographicKeyCredential: "cred:CryptographicKeyCredential",
about: { "@id": "schema:about", "@type": "@id" },
address: { "@id": "schema:address", "@type": "@id" },
addressCountry: "schema:addressCountry",
addressLocality: "schema:addressLocality",
addressRegion: "schema:addressRegion",
comment: "rdfs:comment",
created: { "@id": "dc:created", "@type": "xsd:dateTime" },
creator: { "@id": "dc:creator", "@type": "@id" },
description: "schema:description",
email: "schema:email",
familyName: "schema:familyName",
givenName: "schema:givenName",
image: { "@id": "schema:image", "@type": "@id" },
label: "rdfs:label",
name: "schema:name",
postalCode: "schema:postalCode",
streetAddress: "schema:streetAddress",
title: "dc:title",
url: { "@id": "schema:url", "@type": "@id" },
Person: "schema:Person",
PostalAddress: "schema:PostalAddress",
Organization: "schema:Organization",
identityService: { "@id": "identity:identityService", "@type": "@id" },
idp: { "@id": "identity:idp", "@type": "@id" },
Identity: "identity:Identity",
paymentProcessor: "ps:processor",
preferences: { "@id": "ps:preferences", "@type": "@vocab" },
cipherAlgorithm: "sec:cipherAlgorithm",
cipherData: "sec:cipherData",
cipherKey: "sec:cipherKey",
digestAlgorithm: "sec:digestAlgorithm",
digestValue: "sec:digestValue",
domain: "sec:domain",
expires: { "@id": "sec:expiration", "@type": "xsd:dateTime" },
initializationVector: "sec:initializationVector",
member: { "@id": "schema:member", "@type": "@id" },
memberOf: { "@id": "schema:memberOf", "@type": "@id" },
nonce: "sec:nonce",
normalizationAlgorithm: "sec:normalizationAlgorithm",
owner: { "@id": "sec:owner", "@type": "@id" },
password: "sec:password",
privateKey: { "@id": "sec:privateKey", "@type": "@id" },
privateKeyPem: "sec:privateKeyPem",
publicKey: { "@id": "sec:publicKey", "@type": "@id" },
publicKeyPem: "sec:publicKeyPem",
publicKeyService: { "@id": "sec:publicKeyService", "@type": "@id" },
revoked: { "@id": "sec:revoked", "@type": "xsd:dateTime" },
signature: "sec:signature",
signatureAlgorithm: "sec:signatureAlgorithm",
signatureValue: "sec:signatureValue",
CryptographicKey: "sec:Key",
EncryptedMessage: "sec:EncryptedMessage",
GraphSignature2012: "sec:GraphSignature2012",
LinkedDataSignature2015: "sec:LinkedDataSignature2015",
accessControl: { "@id": "perm:accessControl", "@type": "@id" },
writePermission: { "@id": "perm:writePermission", "@type": "@id" },
},
};
const security_v1 = {
"@context": {
id: "@id",
type: "@type",
dc: "http://purl.org/dc/terms/",
sec: "https://w3id.org/security#",
xsd: "http://www.w3.org/2001/XMLSchema#",
EcdsaKoblitzSignature2016: "sec:EcdsaKoblitzSignature2016",
Ed25519Signature2018: "sec:Ed25519Signature2018",
EncryptedMessage: "sec:EncryptedMessage",
GraphSignature2012: "sec:GraphSignature2012",
LinkedDataSignature2015: "sec:LinkedDataSignature2015",
LinkedDataSignature2016: "sec:LinkedDataSignature2016",
CryptographicKey: "sec:Key",
authenticationTag: "sec:authenticationTag",
canonicalizationAlgorithm: "sec:canonicalizationAlgorithm",
cipherAlgorithm: "sec:cipherAlgorithm",
cipherData: "sec:cipherData",
cipherKey: "sec:cipherKey",
created: { "@id": "dc:created", "@type": "xsd:dateTime" },
creator: { "@id": "dc:creator", "@type": "@id" },
digestAlgorithm: "sec:digestAlgorithm",
digestValue: "sec:digestValue",
domain: "sec:domain",
encryptionKey: "sec:encryptionKey",
expiration: { "@id": "sec:expiration", "@type": "xsd:dateTime" },
expires: { "@id": "sec:expiration", "@type": "xsd:dateTime" },
initializationVector: "sec:initializationVector",
iterationCount: "sec:iterationCount",
nonce: "sec:nonce",
normalizationAlgorithm: "sec:normalizationAlgorithm",
owner: { "@id": "sec:owner", "@type": "@id" },
password: "sec:password",
privateKey: { "@id": "sec:privateKey", "@type": "@id" },
privateKeyPem: "sec:privateKeyPem",
publicKey: { "@id": "sec:publicKey", "@type": "@id" },
publicKeyBase58: "sec:publicKeyBase58",
publicKeyPem: "sec:publicKeyPem",
publicKeyWif: "sec:publicKeyWif",
publicKeyService: { "@id": "sec:publicKeyService", "@type": "@id" },
revoked: { "@id": "sec:revoked", "@type": "xsd:dateTime" },
salt: "sec:salt",
signature: "sec:signature",
signatureAlgorithm: "sec:signingAlgorithm",
signatureValue: "sec:signatureValue",
},
};
const activitystreams = {
"@context": {
"@vocab": "_:",
xsd: "http://www.w3.org/2001/XMLSchema#",
as: "https://www.w3.org/ns/activitystreams#",
ldp: "http://www.w3.org/ns/ldp#",
vcard: "http://www.w3.org/2006/vcard/ns#",
id: "@id",
type: "@type",
Accept: "as:Accept",
Activity: "as:Activity",
IntransitiveActivity: "as:IntransitiveActivity",
Add: "as:Add",
Announce: "as:Announce",
Application: "as:Application",
Arrive: "as:Arrive",
Article: "as:Article",
Audio: "as:Audio",
Block: "as:Block",
Collection: "as:Collection",
CollectionPage: "as:CollectionPage",
Relationship: "as:Relationship",
Create: "as:Create",
Delete: "as:Delete",
Dislike: "as:Dislike",
Document: "as:Document",
Event: "as:Event",
Follow: "as:Follow",
Flag: "as:Flag",
Group: "as:Group",
Ignore: "as:Ignore",
Image: "as:Image",
Invite: "as:Invite",
Join: "as:Join",
Leave: "as:Leave",
Like: "as:Like",
Link: "as:Link",
Mention: "as:Mention",
Note: "as:Note",
Object: "as:Object",
Offer: "as:Offer",
OrderedCollection: "as:OrderedCollection",
OrderedCollectionPage: "as:OrderedCollectionPage",
Organization: "as:Organization",
Page: "as:Page",
Person: "as:Person",
Place: "as:Place",
Profile: "as:Profile",
Question: "as:Question",
Reject: "as:Reject",
Remove: "as:Remove",
Service: "as:Service",
TentativeAccept: "as:TentativeAccept",
TentativeReject: "as:TentativeReject",
Tombstone: "as:Tombstone",
Undo: "as:Undo",
Update: "as:Update",
Video: "as:Video",
View: "as:View",
Listen: "as:Listen",
Read: "as:Read",
Move: "as:Move",
Travel: "as:Travel",
IsFollowing: "as:IsFollowing",
IsFollowedBy: "as:IsFollowedBy",
IsContact: "as:IsContact",
IsMember: "as:IsMember",
subject: {
"@id": "as:subject",
"@type": "@id",
},
relationship: {
"@id": "as:relationship",
"@type": "@id",
},
actor: {
"@id": "as:actor",
"@type": "@id",
},
attributedTo: {
"@id": "as:attributedTo",
"@type": "@id",
},
attachment: {
"@id": "as:attachment",
"@type": "@id",
},
bcc: {
"@id": "as:bcc",
"@type": "@id",
},
bto: {
"@id": "as:bto",
"@type": "@id",
},
cc: {
"@id": "as:cc",
"@type": "@id",
},
context: {
"@id": "as:context",
"@type": "@id",
},
current: {
"@id": "as:current",
"@type": "@id",
},
first: {
"@id": "as:first",
"@type": "@id",
},
generator: {
"@id": "as:generator",
"@type": "@id",
},
icon: {
"@id": "as:icon",
"@type": "@id",
},
image: {
"@id": "as:image",
"@type": "@id",
},
inReplyTo: {
"@id": "as:inReplyTo",
"@type": "@id",
},
items: {
"@id": "as:items",
"@type": "@id",
},
instrument: {
"@id": "as:instrument",
"@type": "@id",
},
orderedItems: {
"@id": "as:items",
"@type": "@id",
"@container": "@list",
},
last: {
"@id": "as:last",
"@type": "@id",
},
location: {
"@id": "as:location",
"@type": "@id",
},
next: {
"@id": "as:next",
"@type": "@id",
},
object: {
"@id": "as:object",
"@type": "@id",
},
oneOf: {
"@id": "as:oneOf",
"@type": "@id",
},
anyOf: {
"@id": "as:anyOf",
"@type": "@id",
},
closed: {
"@id": "as:closed",
"@type": "xsd:dateTime",
},
origin: {
"@id": "as:origin",
"@type": "@id",
},
accuracy: {
"@id": "as:accuracy",
"@type": "xsd:float",
},
prev: {
"@id": "as:prev",
"@type": "@id",
},
preview: {
"@id": "as:preview",
"@type": "@id",
},
replies: {
"@id": "as:replies",
"@type": "@id",
},
result: {
"@id": "as:result",
"@type": "@id",
},
audience: {
"@id": "as:audience",
"@type": "@id",
},
partOf: {
"@id": "as:partOf",
"@type": "@id",
},
tag: {
"@id": "as:tag",
"@type": "@id",
},
target: {
"@id": "as:target",
"@type": "@id",
},
to: {
"@id": "as:to",
"@type": "@id",
},
url: {
"@id": "as:url",
"@type": "@id",
},
altitude: {
"@id": "as:altitude",
"@type": "xsd:float",
},
content: "as:content",
contentMap: {
"@id": "as:content",
"@container": "@language",
},
name: "as:name",
nameMap: {
"@id": "as:name",
"@container": "@language",
},
duration: {
"@id": "as:duration",
"@type": "xsd:duration",
},
endTime: {
"@id": "as:endTime",
"@type": "xsd:dateTime",
},
height: {
"@id": "as:height",
"@type": "xsd:nonNegativeInteger",
},
href: {
"@id": "as:href",
"@type": "@id",
},
hreflang: "as:hreflang",
latitude: {
"@id": "as:latitude",
"@type": "xsd:float",
},
longitude: {
"@id": "as:longitude",
"@type": "xsd:float",
},
mediaType: "as:mediaType",
published: {
"@id": "as:published",
"@type": "xsd:dateTime",
},
radius: {
"@id": "as:radius",
"@type": "xsd:float",
},
rel: "as:rel",
startIndex: {
"@id": "as:startIndex",
"@type": "xsd:nonNegativeInteger",
},
startTime: {
"@id": "as:startTime",
"@type": "xsd:dateTime",
},
summary: "as:summary",
summaryMap: {
"@id": "as:summary",
"@container": "@language",
},
totalItems: {
"@id": "as:totalItems",
"@type": "xsd:nonNegativeInteger",
},
units: "as:units",
updated: {
"@id": "as:updated",
"@type": "xsd:dateTime",
},
width: {
"@id": "as:width",
"@type": "xsd:nonNegativeInteger",
},
describes: {
"@id": "as:describes",
"@type": "@id",
},
formerType: {
"@id": "as:formerType",
"@type": "@id",
},
deleted: {
"@id": "as:deleted",
"@type": "xsd:dateTime",
},
inbox: {
"@id": "ldp:inbox",
"@type": "@id",
},
outbox: {
"@id": "as:outbox",
"@type": "@id",
},
following: {
"@id": "as:following",
"@type": "@id",
},
followers: {
"@id": "as:followers",
"@type": "@id",
},
streams: {
"@id": "as:streams",
"@type": "@id",
},
preferredUsername: "as:preferredUsername",
endpoints: {
"@id": "as:endpoints",
"@type": "@id",
},
uploadMedia: {
"@id": "as:uploadMedia",
"@type": "@id",
},
proxyUrl: {
"@id": "as:proxyUrl",
"@type": "@id",
},
liked: {
"@id": "as:liked",
"@type": "@id",
},
oauthAuthorizationEndpoint: {
"@id": "as:oauthAuthorizationEndpoint",
"@type": "@id",
},
oauthTokenEndpoint: {
"@id": "as:oauthTokenEndpoint",
"@type": "@id",
},
provideClientKey: {
"@id": "as:provideClientKey",
"@type": "@id",
},
signClientKey: {
"@id": "as:signClientKey",
"@type": "@id",
},
sharedInbox: {
"@id": "as:sharedInbox",
"@type": "@id",
},
Public: {
"@id": "as:Public",
"@type": "@id",
},
source: "as:source",
likes: {
"@id": "as:likes",
"@type": "@id",
},
shares: {
"@id": "as:shares",
"@type": "@id",
},
alsoKnownAs: {
"@id": "as:alsoKnownAs",
"@type": "@id",
},
},
};
export const CONTEXTS: Record<string, unknown> = {
"https://w3id.org/identity/v1": id_v1,
"https://w3id.org/security/v1": security_v1,
"https://www.w3.org/ns/activitystreams": activitystreams,
};

View File

@ -1,141 +0,0 @@
import * as crypto from "node:crypto";
import jsonld from "jsonld";
import { CONTEXTS } from "./contexts.js";
import fetch from "node-fetch";
import { httpAgent, httpsAgent } from "@/misc/fetch.js";
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
export class LdSignature {
public debug = false;
public preLoad = true;
public loderTimeout = 10 * 1000;
public async signRsaSignature2017(
data: any,
privateKey: string,
creator: string,
domain?: string,
created?: Date,
): Promise<any> {
const options = {
type: "RsaSignature2017",
creator,
domain,
nonce: crypto.randomBytes(16).toString("hex"),
created: (created || new Date()).toISOString(),
} as {
type: string;
creator: string;
domain?: string;
nonce: string;
created: string;
};
if (!domain) {
options.domain = undefined;
}
const toBeSigned = await this.createVerifyData(data, options);
const signer = crypto.createSign("sha256");
signer.update(toBeSigned);
signer.end();
const signature = signer.sign(privateKey);
return {
...data,
signature: {
...options,
signatureValue: signature.toString("base64"),
},
};
}
public async verifyRsaSignature2017(
data: any,
publicKey: string,
): Promise<boolean> {
const toBeSigned = await this.createVerifyData(data, data.signature);
const verifier = crypto.createVerify("sha256");
verifier.update(toBeSigned);
return verifier.verify(publicKey, data.signature.signatureValue, "base64");
}
public async createVerifyData(data: any, options: any) {
const transformedOptions = {
...options,
"@context": "https://w3id.org/identity/v1",
};
delete transformedOptions["type"];
delete transformedOptions["id"];
delete transformedOptions["signatureValue"];
const canonizedOptions = await this.normalize(transformedOptions);
const optionsHash = this.sha256(canonizedOptions);
const transformedData = { ...data };
delete transformedData["signature"];
const cannonidedData = await this.normalize(transformedData);
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
const documentHash = this.sha256(cannonidedData);
const verifyData = `${optionsHash}${documentHash}`;
return verifyData;
}
public async normalize(data: any) {
const customLoader = this.getLoader();
return await jsonld.normalize(data, {
documentLoader: customLoader,
});
}
private getLoader() {
return async (url: string): Promise<any> => {
if (!url.match("^https?://")) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) {
if (url in CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`);
return {
contextUrl: null,
document: CONTEXTS[url],
documentUrl: url,
};
}
}
if (this.debug) console.debug(`MISS: ${url}`);
const document = await this.fetchDocument(url);
return {
contextUrl: null,
document: document,
documentUrl: url,
};
};
}
private async fetchDocument(url: string) {
const json = await fetch(url, {
headers: {
Accept: "application/ld+json, application/json",
},
// TODO
//timeout: this.loderTimeout,
agent: (u) => (u.protocol === "http:" ? httpAgent : httpsAgent),
}).then((res) => {
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
} else {
return res.json();
}
});
return json;
}
public sha256(data: string): string {
const hash = crypto.createHash("sha256");
hash.update(data);
return hash.digest("hex");
}
}

View File

@ -1,23 +0,0 @@
import type { IObject } from "./type.js";
import type { CacheableRemoteUser } from "@/models/entities/user.js";
import { performActivity } from "./kernel/index.js";
import { updatePerson } from "./models/person.js";
export default async (
actor: CacheableRemoteUser,
activity: IObject,
): Promise<void> => {
await performActivity(actor, activity);
// Update the remote user information if it is out of date
if (actor.uri) {
if (
actor.lastFetchedAt == null ||
Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24
) {
setImmediate(() => {
updatePerson(actor.uri!);
});
}
}
};

View File

@ -1,14 +0,0 @@
import config from "@/config/index.js";
import type { Relay } from "@/models/entities/relay.js";
import type { ILocalUser } from "@/models/entities/user.js";
export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
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;
}

View File

@ -1,14 +1,17 @@
import {v4 as uuid} from "uuid";
import config from "@/config/index.js";
import { getUserKeypair } from "@/misc/keypair-store.js";
import type { User } from "@/models/entities/user.js";
import { LdSignature } from "../misc/ld-signature.js";
import type {IActivity} from "../type.js";
/**
* Unfortunately named function that adds a context to AP objects
*
* @param x An input AP object
*/
export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null;
if (typeof x === "object" && x.id == null) {
// ???
x.id = `${config.url}/${uuid()}`;
}
@ -52,22 +55,3 @@ export const renderActivity = (x: any): IActivity | null => {
x,
);
};
export const attachLdSignature = async (
activity: any,
user: { id: User["id"]; host: null },
): Promise<IActivity | null> => {
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;
};

View File

@ -1,8 +1,8 @@
import {In, IsNull} from "typeorm";
import config from "@/config/index.js";
import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js";
import type {IMentionedRemoteUsers, Note} from "@/models/entities/note.js";
import type {DriveFile} from "@/models/entities/drive-file.js";
import { DriveFiles, Notes, Users, Emojis, Polls } from "@/models/index.js";
import {DriveFiles, Emojis, Notes, Polls, Users} from "@/models/index.js";
import type {Emoji} from "@/models/entities/emoji.js";
import type {Poll} from "@/models/entities/poll.js";
import toHtml from "../misc/get-note-html.js";
@ -14,7 +14,6 @@ import renderDocument from "./document.js";
export default async function renderNote(
note: Note,
dive = true,
isTalk = false,
): Promise<Record<string, unknown>> {
const getPromisedFiles = async (ids: string[]) => {
if (!ids || ids.length === 0) return [];
@ -140,12 +139,6 @@ export default async function renderNote(
}
: {};
const asTalk = isTalk
? {
_misskey_talk: true,
}
: {};
return {
id: `${config.url}/notes/${note.id}`,
type: "Note",
@ -168,7 +161,6 @@ export default async function renderNote(
sensitive: note.cw != null || files.some((file) => file.isSensitive),
tag,
...asPoll,
...asTalk,
};
}

View File

@ -4,10 +4,16 @@ import {getInstanceActor} from "@/services/instance-actor.js";
import {fetchMeta} from "@/misc/fetch-meta.js";
import {extractDbHost, isSelfHost} from "@/misc/convert-host.js";
import {signedGet} from "./request.js";
import type {ICollection, IObject, IOrderedCollection} from "./type.js";
import {getApId, isCollectionOrOrderedCollection} from "./type.js";
import {
getApId,
ICollection,
IObject,
IOrderedCollection,
isCollectionOrOrderedCollection,
isLdApContext
} from "./type.js";
import {NoteReactions, Notes, Polls, Users,} from "@/models/index.js";
import {parseUri} from "./db-resolver.js";
import {parseApUri} from "./db-resolver.js";
import renderNote from "@/remote/activitypub/renderer/note.js";
import {renderLike} from "@/remote/activitypub/renderer/like.js";
import {renderPerson} from "@/remote/activitypub/renderer/person.js";
@ -76,7 +82,11 @@ export default class Resolver {
const host = extractDbHost(value);
if (isSelfHost(host)) {
return await this.resolveLocal(value);
const rendered = await this.resolveLocal(value);
if (rendered == null) {
throw new Error("Invalid locally rendered AP object");
}
return rendered;
}
const meta = await fetchMeta();
@ -98,62 +108,53 @@ export default class Resolver {
const object = await signedGet(value, this.user);
if (
object == null ||
(Array.isArray(object["@context"])
? !(object["@context"] as unknown[]).includes(
"https://www.w3.org/ns/activitystreams",
)
: object["@context"] !== "https://www.w3.org/ns/activitystreams")
) {
throw new Error("invalid response");
if (!isLdApContext(object)) {
throw new Error("Invalid AP fetch response");
}
return object;
}
private resolveLocal(url: string): Promise<IObject> {
const parsed = parseUri(url);
private async resolveLocal(url: string): Promise<IObject | null> {
const parsed = parseApUri(url);
if (!parsed.local) throw new Error("resolveLocal: not local");
switch (parsed.type) {
case "notes":
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => {
if (parsed.rest === "activity") {
// this refers to the create activity and not the note itself
return renderActivity(renderCreate(renderNote(note)));
} else {
return renderNote(note);
}
});
case "users":
return Users.findOneByOrFail({ id: parsed.id }).then((user) =>
renderPerson(user as ILocalUser),
switch (parsed.object.type) {
case "NoteActivity":
return Notes.findOneByOrFail({ id: parsed.object.id }).then((note) =>
renderActivity(renderCreate(renderNote(note, false), note))
);
case "questions":
case "Note":
return Notes.findOneByOrFail({ id: parsed.object.id }).then((note) =>
renderActivity(renderNote(note))
);
case "User":
return Users.findOneByOrFail({ id: parsed.object.id }).then((user) =>
renderActivity(renderPerson(user as ILocalUser))
);
case "Question":
// Polls are indexed by the note they are attached to.
return Promise.all([
Notes.findOneByOrFail({ id: parsed.id }),
Polls.findOneByOrFail({ noteId: parsed.id }),
Notes.findOneByOrFail({ id: parsed.object.id }),
Polls.findOneByOrFail({ noteId: parsed.object.id }),
]).then(([note, poll]) =>
renderQuestion({ id: note.userId }, note, poll),
renderActivity(renderQuestion({id: note.userId}, note, poll)),
);
case "likes":
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(
(reaction) => renderActivity(renderLike(reaction, { uri: null })),
case "Like":
return Promise.all([
Notes.findOneByOrFail({ id: parsed.object.id }),
NoteReactions.findOneByOrFail({ id: parsed.object.id })
]).then(
([note, reaction]) => renderActivity(renderLike(reaction, note)),
);
case "follows":
// rest should be <followee id>
if (parsed.rest == null || !/^\w+$/.test(parsed.rest))
throw new Error("resolveLocal: invalid follow URI");
case "Follow":
return Promise.all(
[parsed.id, parsed.rest].map((id) => Users.findOneByOrFail({ id })),
[parsed.object.id, parsed.object.followeeId].map(
(id) => Users.findOneByOrFail({ id })
)
).then(([follower, followee]) =>
renderActivity(renderFollow(follower, followee, url)),
);
default:
throw new Error(`resolveLocal: type ${type} unhandled`);
}
}
}

View File

@ -1,8 +1,7 @@
export type obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[];
export interface IObject {
"@context": string | string[] | obj | obj[];
"@context": string | string[] | Record<string, any> | Record<string, any>[];
type: string | string[];
id?: string;
summary?: string;
@ -25,6 +24,19 @@ export interface IObject {
sensitive?: boolean;
}
export function isLdApContext(object: any): object is IObject {
if (object == null || typeof object !== "object") {
return false;
}
if (object["@context"] === "https://www.w3.org/ns/activitystreams") {
return true;
}
return Array.isArray(object["@context"])
&& (object["@context"] as unknown[]).includes("https://www.w3.org/ns/activitystreams");
}
/**
* Get array of ActivityStreams Objects id
*/

View File

@ -43,9 +43,6 @@ import * as ep___admin_queue_clear from "./endpoints/admin/queue/clear.js";
import * as ep___admin_queue_deliverDelayed from "./endpoints/admin/queue/deliver-delayed.js";
import * as ep___admin_queue_inboxDelayed from "./endpoints/admin/queue/inbox-delayed.js";
import * as ep___admin_queue_stats from "./endpoints/admin/queue/stats.js";
import * as ep___admin_relays_add from "./endpoints/admin/relays/add.js";
import * as ep___admin_relays_list from "./endpoints/admin/relays/list.js";
import * as ep___admin_relays_remove from "./endpoints/admin/relays/remove.js";
import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js";
import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js";
import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all.js";
@ -332,9 +329,6 @@ const eps = [
["admin/queue/deliver-delayed", ep___admin_queue_deliverDelayed],
["admin/queue/inbox-delayed", ep___admin_queue_inboxDelayed],
["admin/queue/stats", ep___admin_queue_stats],
["admin/relays/add", ep___admin_relays_add],
["admin/relays/list", ep___admin_relays_list],
["admin/relays/remove", ep___admin_relays_remove],
["admin/reset-password", ep___admin_resetPassword],
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
["admin/search/index-all", ep___admin_search_indexAll],

View File

@ -1,65 +0,0 @@
import { URL } from "node:url";
import define from "../../../define.js";
import { addRelay } from "@/services/relay.js";
import { ApiError } from "../../../error.js";
export const meta = {
tags: ["admin"],
requireCredential: true,
requireModerator: true,
errors: {
invalidUrl: {
message: "Invalid URL",
code: "INVALID_URL",
id: "fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c",
},
},
res: {
type: "object",
optional: false,
nullable: false,
properties: {
id: {
type: "string",
optional: false,
nullable: false,
format: "id",
},
inbox: {
description: "URL of the inbox, must be a https scheme URL",
type: "string",
optional: false,
nullable: false,
format: "url",
},
status: {
type: "string",
optional: false,
nullable: false,
default: "requesting",
enum: ["requesting", "accepted", "rejected"],
},
},
},
} as const;
export const paramDef = {
type: "object",
properties: {
inbox: { type: "string" },
},
required: ["inbox"],
} as const;
export default define(meta, paramDef, async (ps, user) => {
try {
if (new URL(ps.inbox).protocol !== "https:") throw new Error("https only");
} catch {
throw new ApiError(meta.errors.invalidUrl);
}
return await addRelay(ps.inbox);
});

View File

@ -1,51 +0,0 @@
import define from "../../../define.js";
import { listRelay } from "@/services/relay.js";
export const meta = {
tags: ["admin"],
requireCredential: true,
requireModerator: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
properties: {
id: {
type: "string",
optional: false,
nullable: false,
format: "id",
},
inbox: {
type: "string",
optional: false,
nullable: false,
format: "url",
},
status: {
type: "string",
optional: false,
nullable: false,
default: "requesting",
enum: ["requesting", "accepted", "rejected"],
},
},
},
},
} as const;
export const paramDef = {
type: "object",
properties: {},
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
return await listRelay();
});

View File

@ -1,21 +0,0 @@
import define from "../../../define.js";
import { removeRelay } from "@/services/relay.js";
export const meta = {
tags: ["admin"],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: "object",
properties: {
inbox: { type: "string" },
},
required: ["inbox"],
} as const;
export default define(meta, paramDef, async (ps, user) => {
return await removeRelay(ps.inbox);
});

View File

@ -108,6 +108,8 @@ async function fetchAny(
// Wait if blocked.
if (await shouldBlockInstance(extractDbHost(uri))) return null;
const dbResolver = new DbResolver();
const [user, note] = await Promise.all([

View File

@ -20,7 +20,6 @@ import DeliverManager from "@/remote/activitypub/deliver-manager.js";
import {renderActivity} from "@/remote/activitypub/renderer/index.js";
import renderNote from "@/remote/activitypub/renderer/note.js";
import renderUpdate from "@/remote/activitypub/renderer/update.js";
import {deliverToRelays} from "@/services/relay.js";
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
import {fetchMeta} from "@/misc/fetch-meta.js";
@ -617,11 +616,6 @@ export default define(meta, paramDef, async (ps, user) => {
dm.addFollowersRecipe();
}
// Deliver to relays for public posts.
if (["public"].includes(note.visibility)) {
deliverToRelays(user, activity);
}
// GO!
dm.execute();
})().then();

View File

@ -9,7 +9,6 @@ import { Notes, UserNotePinings, Users } from "@/models/index.js";
import type {UserNotePining} from "@/models/entities/user-note-pining.js";
import {genId} from "@/misc/gen-id.js";
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
import { deliverToRelays } from "../relay.js";
/**
* 稿
@ -114,5 +113,4 @@ export async function deliverPinnedChange(
);
deliverToFollowers(user, content);
deliverToRelays(user, content);
}

View File

@ -4,7 +4,6 @@ import { Users } from "@/models/index.js";
import type {User} from "@/models/entities/user.js";
import {renderPerson} from "@/remote/activitypub/renderer/person.js";
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
import { deliverToRelays } from "../relay.js";
export async function publishToFollowers(userId: User["id"]) {
const user = await Users.findOneBy({ id: userId });
@ -16,6 +15,5 @@ export async function publishToFollowers(userId: User["id"]) {
renderUpdate(await renderPerson(user), user),
);
deliverToFollowers(user, content);
deliverToRelays(user, content);
}
}

View File

@ -42,7 +42,6 @@ import {checkHitAntenna} from "@/misc/check-hit-antenna.js";
import {getWordHardMute} from "@/misc/check-word-mute.js";
import {addNoteToAntenna} from "../add-note-to-antenna.js";
import {countSameRenotes} from "@/misc/count-same-renotes.js";
import {deliverToRelays, getCachedRelays} from "../relay.js";
import {normalizeForSearch} from "@/misc/normalize-for-search.js";
import {getAntennas} from "@/misc/antenna-cache.js";
import {endedPollNotificationQueue} from "@/queue/queues.js";
@ -402,39 +401,10 @@ export default async (
}
if (!dontFederateInitially) {
const relays = await getCachedRelays();
// Some relays (e.g., aode-relay) deliver posts by boosting them as
// Announce activities. In that case, user is the relay's actor.
const boostedByRelay =
!!user.inbox &&
relays.map((relay) => relay.inbox).includes(user.inbox);
if (!note.uri) {
// Publish if the post is local
publishNotesStream(note);
} else if (boostedByRelay && data.renote?.uri) {
// Use Redis transaction for atomicity
await redisClient.watch(`publishedNote:${data.renote.uri}`);
const exists = await redisClient.exists(
`publishedNote:${data.renote.uri}`,
);
if (exists === 0) {
// Start the transaction
const transaction = redisClient.multi();
const key = `publishedNote:${data.renote.uri}`;
transaction.set(key, 1, "EX", 30);
// Execute the transaction
transaction.exec((err, replies) => {
// Publish after setting the key in Redis
if (!err && data.renote) {
publishNotesStream(data.renote);
}
});
} else {
// Abort the transaction
redisClient.unwatch();
}
} else if (!boostedByRelay && note.uri) {
} else if (note.uri) {
// Use Redis transaction for atomicity
await redisClient.watch(`publishedNote:${note.uri}`);
const exists = await redisClient.exists(`publishedNote:${note.uri}`);
@ -585,10 +555,6 @@ export default async (
dm.addFollowersRecipe();
}
if (["public"].includes(note.visibility)) {
deliverToRelays(user, noteActivity);
}
dm.execute();
})().then();
}

View File

@ -6,21 +6,13 @@ import renderUndo from "@/remote/activitypub/renderer/undo.js";
import {renderActivity} from "@/remote/activitypub/renderer/index.js";
import renderTombstone from "@/remote/activitypub/renderer/tombstone.js";
import config from "@/config/index.js";
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js";
import { Notes, Users, Instances } from "@/models/index.js";
import {
notesChart,
perUserNotesChart,
instanceChart,
} from "@/services/chart/index.js";
import {
deliverToFollowers,
deliverToUser,
} from "@/remote/activitypub/deliver-manager.js";
import type {ILocalUser, IRemoteUser, User} from "@/models/entities/user.js";
import type {IMentionedRemoteUsers, Note} from "@/models/entities/note.js";
import {Instances, Notes, Users} from "@/models/index.js";
import {instanceChart, notesChart, perUserNotesChart,} from "@/services/chart/index.js";
import {deliverToFollowers, deliverToUser,} from "@/remote/activitypub/deliver-manager.js";
import {countSameRenotes} from "@/misc/count-same-renotes.js";
import {registerOrFetchInstanceDoc} from "../register-or-fetch-instance-doc.js";
import { deliverToRelays } from "../relay.js";
import meilisearch from "@/db/meilisearch.js";
/**
@ -182,7 +174,6 @@ async function deliverToConcerned(
content: any,
) {
deliverToFollowers(user, content);
deliverToRelays(user, content);
const remoteUsers = await getMentionedRemoteUsers(note);
for (const remoteUser of remoteUsers) {
deliverToUser(user, content, remoteUser);

View File

@ -1,10 +1,9 @@
import renderUpdate from "@/remote/activitypub/renderer/update.js";
import {renderActivity} from "@/remote/activitypub/renderer/index.js";
import renderNote from "@/remote/activitypub/renderer/note.js";
import { Users, Notes } from "@/models/index.js";
import {Notes, Users} from "@/models/index.js";
import type {Note} from "@/models/entities/note.js";
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
import { deliverToRelays } from "../../relay.js";
export async function deliverQuestionUpdate(noteId: Note["id"]) {
const note = await Notes.findOneBy({ id: noteId });
@ -18,6 +17,5 @@ export async function deliverQuestionUpdate(noteId: Note["id"]) {
renderUpdate(await renderNote(note, false), user),
);
deliverToFollowers(user, content);
deliverToRelays(user, content);
}
}

View File

@ -1,123 +0,0 @@
import { IsNull } from "typeorm";
import { renderFollowRelay } from "@/remote/activitypub/renderer/follow-relay.js";
import {
renderActivity,
attachLdSignature,
} from "@/remote/activitypub/renderer/index.js";
import renderUndo from "@/remote/activitypub/renderer/undo.js";
import { deliver } from "@/queue/index.js";
import type { ILocalUser, User } from "@/models/entities/user.js";
import { Users, Relays } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import { Cache } from "@/misc/cache.js";
import type { Relay } from "@/models/entities/relay.js";
import { createSystemUser } from "./create-system-user.js";
const ACTOR_USERNAME = "relay.actor" as const;
const relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
export async function getRelayActor(): Promise<ILocalUser> {
const user = await Users.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
});
if (user) return user as ILocalUser;
const created = await createSystemUser(ACTOR_USERNAME);
return created as ILocalUser;
}
export async function addRelay(inbox: string) {
const relay = await Relays.insert({
id: genId(),
inbox,
status: "requesting",
}).then((x) => Relays.findOneByOrFail(x.identifiers[0]));
const relayActor = await getRelayActor();
const follow = renderFollowRelay(relay, relayActor);
const activity = renderActivity(follow);
deliver(relayActor, activity, relay.inbox);
return relay;
}
export async function removeRelay(inbox: string) {
const relay = await Relays.findOneBy({
inbox,
});
if (relay == null) {
throw new Error("relay not found");
}
const relayActor = await getRelayActor();
const follow = renderFollowRelay(relay, relayActor);
const undo = renderUndo(follow, relayActor);
const activity = renderActivity(undo);
deliver(relayActor, activity, relay.inbox);
await Relays.delete(relay.id);
await updateRelaysCache();
}
export async function listRelay() {
const relays = await Relays.find();
return relays;
}
export async function getCachedRelays(): Promise<Relay[]> {
return await relaysCache.fetch(null, () =>
Relays.findBy({
status: "accepted",
}),
);
}
export async function relayAccepted(id: string) {
const result = await Relays.update(id, {
status: "accepted",
});
await updateRelaysCache();
return JSON.stringify(result);
}
async function updateRelaysCache() {
const relays = await Relays.findBy({
status: "accepted",
});
relaysCache.set(null, relays);
}
export async function relayRejected(id: string) {
const result = await Relays.update(id, {
status: "rejected",
});
return JSON.stringify(result);
}
export async function deliverToRelays(
user: { id: User["id"]; host: null },
activity: any,
) {
if (activity == null) return;
const relays = await getCachedRelays();
if (relays.length === 0) return;
// TODO
//const copy = structuredClone(activity);
const copy = JSON.parse(JSON.stringify(activity));
if (!copy.to) copy.to = ["https://www.w3.org/ns/activitystreams#Public"];
const signed = await attachLdSignature(copy, user);
for (const relay of relays) {
deliver(user, signed, relay.inbox);
}
}