Removed relay jank, fixed local object rendering in resolution, and refactored local URI parsing
ci/woodpecker/tag/ociImageTag Pipeline was successful
Details
ci/woodpecker/tag/ociImageTag Pipeline was successful
Details
This commit is contained in:
parent
e845eda898
commit
e77ba36953
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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(
|
||||
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<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";
|
||||
};
|
||||
|
|
|
@ -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<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);
|
||||
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<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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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<void> {
|
||||
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);
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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!);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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<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;
|
||||
};
|
||||
|
|
|
@ -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<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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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([
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue