diff --git a/packages/backend/src/mag/rpc-client.ts b/packages/backend/src/mag/rpc-client.ts index d4799bce25..514b29c430 100644 --- a/packages/backend/src/mag/rpc-client.ts +++ b/packages/backend/src/mag/rpc-client.ts @@ -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(method: string, data: D): Promise { } -export async function magApGet(userId: string, url: string): Promise { +export async function magApGet(userId: string, url: string): Promise { 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 { }); } -export async function magApPost(userId: string, url: string, body: any): Promise { +export async function magApPost(userId: string, url: string, body: any): Promise { logger.debug(`AP POST to: ${url}`); return await rpcCall("/ap/post", { user_id: userId, diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts index a6164d50fb..a21b215537 100644 --- a/packages/backend/src/queue/initialize.ts +++ b/packages/backend/src/queue/initialize.ts @@ -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(name: string, limitPerSec = -1) { return new Bull(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; } diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index b319fbd381..512c545418 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -1,27 +1,25 @@ -import { URL } from "node:url"; +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 { fetchMeta } from "@/misc/fetch-meta.js"; -import { toPuny, extractDbHost } from "@/misc/convert-host.js"; -import { getApId } from "@/remote/activitypub/type.js"; -import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; -import type { InboxJobData } from "../types.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 {fetchMeta} from "@/misc/fetch-meta.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 {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): Promise => { 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): Promise => { 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): Promise => { } } - // それでもわからなければ終了 - 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( + 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}`; - } - - // 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); - } - - // keyIdからLD-Signatureのユーザーを取得 - authUser = await dbResolver.getAuthUserFromKeyId( - activity.signature.creator, - ); - if (authUser == null) { - return "skip: LD-Signatureのユーザーが取得できませんでした"; - } - - if (authUser.key == null) { - return "skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした"; - } - - // 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 (!httpSignatureValidated) { + return `skip: http-signature verification failed. keyId=${signature.keyId}`; } - // 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}`; + 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; + } + + const activityIdHost = extractDbHost(act.id); + if (signerHost !== activityIdHost) { + continue; + } + + try { + await performActivity(senderActor.user, act); + } catch (err) { + if (err instanceof Error || typeof err === "string") { + apLogger.error(err); + } + } } + } else { + if (typeof activity.id !== "string") { + return `skip: activity.id not a valid id`; + } + + 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): Promise => { 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"; }; diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 19ed96814a..7ac3aaf247 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -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 { UserPublickey } from "@/models/entities/user-publickey.js"; -import { - Notes, - Users, - UserPublickeys, -} 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 type {Note} from "@/models/entities/note.js"; +import type {CacheableRemoteUser, CacheableUser,} from "@/models/entities/user.js"; +import type {UserPublickey} from "@/models/entities/user-publickey.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(Infinity); const publicKeyByUserIdCache = new Cache(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); +export function parseApUri(value: string | IObject): UriParseResult { + const uri = new URL(getApId(value)); + if (config.host !== toPuny(uri.host)) { + return { + local: false, + uri, + }; + } - // 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); + const path = uri.pathname; + const [_empty, objectType, id, ...parts] = path.split("/"); - if (matchLocal) { - return { - local: true, - type: matchLocal[1], - id: matchLocal[2], - rest: matchLocal[3], - }; - } else { - return { - local: false, - uri, - }; - } + 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 { - 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 { - 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, }), ); } diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts index e430bbf576..77e7a5368c 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts @@ -1,8 +1,7 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; +import type {CacheableRemoteUser} from "@/models/entities/user.js"; import accept from "@/services/following/requests/accept.js"; -import type { IFollow } from "../../type.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"; }; diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts index 39ba8b3f4f..ea9d43b204 100644 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts @@ -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 type {CacheableRemoteUser} from "@/models/entities/user.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), }); diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts index 8e70355ca7..177af64902 100644 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/index.ts @@ -1,27 +1,23 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { toArray } from "@/prelude/array.js"; +import type {CacheableRemoteUser} from "@/models/entities/user.js"; +import type {IObject} from "../type.js"; import { - isCreate, - isDelete, - isUpdate, - isRead, - isFollow, - isAccept, - isReject, - isAdd, - isRemove, - isAnnounce, - isLike, - isUndo, - isBlock, - isCollectionOrOrderedCollection, - isCollection, - isFlag, - isMove, - getApId, + getApId, + isAccept, + isAdd, + isAnnounce, + isBlock, + isCreate, + isDelete, + isFlag, + isFollow, + isLike, + isMove, + isReject, + isRemove, + isUndo, + isUpdate, } from "../type.js"; -import { apLogger } from "../logger.js"; -import Resolver from "../resolver.js"; +import {apLogger} from "../logger.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"; +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 { if (actor.isSuspended) return; - if (typeof activity.id !== "undefined") { - const host = extractDbHost(getApId(activity)); - if (await shouldBlockInstance(host)) return; - } + const host = extractDbHost(getApId(activity)); + if (await shouldBlockInstance(host)) return; if (isCreate(activity)) { await create(actor, activity); diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts index 670c1556fd..c8baf90667 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts @@ -1,9 +1,8 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { remoteReject } from "@/services/following/reject.js"; -import type { IFollow } from "../../type.js"; +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"; +import {Users} from "@/models/index.js"; export default async ( actor: CacheableRemoteUser, @@ -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"; }; diff --git a/packages/backend/src/remote/activitypub/misc/contexts.ts b/packages/backend/src/remote/activitypub/misc/contexts.ts deleted file mode 100644 index 8c97b59729..0000000000 --- a/packages/backend/src/remote/activitypub/misc/contexts.ts +++ /dev/null @@ -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 = { - "https://w3id.org/identity/v1": id_v1, - "https://w3id.org/security/v1": security_v1, - "https://www.w3.org/ns/activitystreams": activitystreams, -}; diff --git a/packages/backend/src/remote/activitypub/misc/ld-signature.ts b/packages/backend/src/remote/activitypub/misc/ld-signature.ts deleted file mode 100644 index 62707624be..0000000000 --- a/packages/backend/src/remote/activitypub/misc/ld-signature.ts +++ /dev/null @@ -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 { - 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 { - 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 => { - 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"); - } -} diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts deleted file mode 100644 index 0d2cdb4a5e..0000000000 --- a/packages/backend/src/remote/activitypub/perform.ts +++ /dev/null @@ -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 => { - 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!); - }); - } - } -}; diff --git a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts b/packages/backend/src/remote/activitypub/renderer/follow-relay.ts deleted file mode 100644 index ad7f05bf84..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts +++ /dev/null @@ -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; -} diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index 7b98cf2d77..84c9613111 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -1,14 +1,17 @@ -import { v4 as uuid } from "uuid"; +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"; +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 => { - 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; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 2ad2fec9fb..cda7386c17 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,10 +1,10 @@ -import { In, IsNull } from "typeorm"; +import {In, IsNull} from "typeorm"; import config from "@/config/index.js"; -import type { Note, IMentionedRemoteUsers } 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 type { Emoji } from "@/models/entities/emoji.js"; -import type { Poll } from "@/models/entities/poll.js"; +import type {IMentionedRemoteUsers, Note} from "@/models/entities/note.js"; +import type {DriveFile} from "@/models/entities/drive-file.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"; import renderEmoji from "./emoji.js"; import renderMention from "./mention.js"; @@ -14,7 +14,6 @@ import renderDocument from "./document.js"; export default async function renderNote( note: Note, dive = true, - isTalk = false, ): Promise> { 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, }; } diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 1011914972..a4a4e23293 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -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 { - const parsed = parseUri(url); + private async resolveLocal(url: string): Promise { + 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 - 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`); } } } diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index b0bdb0a8b4..edd9779bf6 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -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 | Record[]; 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 */ diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index b15cdd14fd..821ee9a323 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -29,7 +29,7 @@ import * as ep___admin_emoji_setLicenseBulk from "./endpoints/admin/emoji/set-li import * as ep___admin_emoji_update from "./endpoints/admin/emoji/update.js"; import * as ep___admin_federation_deleteAllFiles from "./endpoints/admin/federation/delete-all-files.js"; import * as ep___admin_federation_refreshRemoteInstanceMetadata - from "./endpoints/admin/federation/refresh-remote-instance-metadata.js"; + from "./endpoints/admin/federation/refresh-remote-instance-metadata.js"; import * as ep___admin_federation_removeAllFollowing from "./endpoints/admin/federation/remove-all-following.js"; import * as ep___admin_federation_updateInstance from "./endpoints/admin/federation/update-instance.js"; import * as ep___admin_getIndexStats from "./endpoints/admin/get-index-stats.js"; @@ -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], diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts deleted file mode 100644 index bb56216a74..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ /dev/null @@ -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); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts deleted file mode 100644 index 4c294ba9b2..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ /dev/null @@ -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(); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts deleted file mode 100644 index 1b3d90628b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ /dev/null @@ -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); -}); diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 16408ea377..d81ac61c63 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -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([ diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 067019bea3..141d62f0d9 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -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(); diff --git a/packages/backend/src/services/i/pin.ts b/packages/backend/src/services/i/pin.ts index 97045a9fa9..ad7d6b4c59 100644 --- a/packages/backend/src/services/i/pin.ts +++ b/packages/backend/src/services/i/pin.ts @@ -1,15 +1,14 @@ import config from "@/config/index.js"; import renderAdd from "@/remote/activitypub/renderer/add.js"; import renderRemove from "@/remote/activitypub/renderer/remove.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -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"; +import {renderActivity} from "@/remote/activitypub/renderer/index.js"; +import {IdentifiableError} from "@/misc/identifiable-error.js"; +import type {User} from "@/models/entities/user.js"; +import type {Note} from "@/models/entities/note.js"; +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"; /** * 指定した投稿をピン留めします @@ -114,5 +113,4 @@ export async function deliverPinnedChange( ); deliverToFollowers(user, content); - deliverToRelays(user, content); } diff --git a/packages/backend/src/services/i/update.ts b/packages/backend/src/services/i/update.ts index cc950ac85a..c9509fe309 100644 --- a/packages/backend/src/services/i/update.ts +++ b/packages/backend/src/services/i/update.ts @@ -1,10 +1,9 @@ import renderUpdate from "@/remote/activitypub/renderer/update.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -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"; +import {renderActivity} from "@/remote/activitypub/renderer/index.js"; +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"; 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); } } diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 92b2e5a800..4ea51bed26 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -19,14 +19,14 @@ import {extractHashtags} from "@/misc/extract-hashtags.js"; import type {IMentionedRemoteUsers} from "@/models/entities/note.js"; import {Note} from "@/models/entities/note.js"; import { - Instances, - MutedNotes, - Mutings, - Notes, - NoteThreadMutings, - NoteWatchings, - UserProfiles, - Users, + Instances, + MutedNotes, + Mutings, + Notes, + NoteThreadMutings, + NoteWatchings, + UserProfiles, + Users, } from "@/models/index.js"; import type {DriveFile} from "@/models/entities/drive-file.js"; import type {App} from "@/models/entities/app.js"; @@ -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(); } diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 90175ccdc4..7db43cefd3 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,26 +1,18 @@ -import { Brackets, In } from "typeorm"; -import { publishNoteStream } from "@/services/stream.js"; +import {Brackets, In} from "typeorm"; +import {publishNoteStream} from "@/services/stream.js"; import renderDelete from "@/remote/activitypub/renderer/delete.js"; import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.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 { countSameRenotes } from "@/misc/count-same-renotes.js"; -import { registerOrFetchInstanceDoc } from "../register-or-fetch-instance-doc.js"; -import { deliverToRelays } from "../relay.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 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); diff --git a/packages/backend/src/services/note/polls/update.ts b/packages/backend/src/services/note/polls/update.ts index e02d48d055..2b743dc2dc 100644 --- a/packages/backend/src/services/note/polls/update.ts +++ b/packages/backend/src/services/note/polls/update.ts @@ -1,10 +1,9 @@ import renderUpdate from "@/remote/activitypub/renderer/update.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.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 type { Note } from "@/models/entities/note.js"; -import { deliverToFollowers } from "@/remote/activitypub/deliver-manager.js"; -import { deliverToRelays } from "../../relay.js"; +import {Notes, Users} from "@/models/index.js"; +import type {Note} from "@/models/entities/note.js"; +import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.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); } } diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts deleted file mode 100644 index bec4b1f86b..0000000000 --- a/packages/backend/src/services/relay.ts +++ /dev/null @@ -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(1000 * 60 * 10); - -export async function getRelayActor(): Promise { - 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 { - 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); - } -}