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 {decode, encode} from "@msgpack/msgpack";
|
||||||
import Logger from "@/services/logger.js";
|
import Logger from "@/services/logger.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import type {IObject} from "@/remote/activitypub/type";
|
|
||||||
|
|
||||||
let client: Socket | null = null;
|
let client: Socket | null = null;
|
||||||
let exponentialBackoff = 0;
|
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}`);
|
logger.debug(`AP GET to: ${url}`);
|
||||||
return await rpcCall("/ap/get", {
|
return await rpcCall("/ap/get", {
|
||||||
user_id: userId,
|
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}`);
|
logger.debug(`AP POST to: ${url}`);
|
||||||
return await rpcCall("/ap/post", {
|
return await rpcCall("/ap/post", {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import Bull from "bull";
|
import Bull from "bull";
|
||||||
import config from "@/config/index.js";
|
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) {
|
export function initialize<T>(name: string, limitPerSec = -1) {
|
||||||
return new Bull<T>(name, {
|
return new Bull<T>(name, {
|
||||||
redis: {
|
redis: {
|
||||||
|
@ -34,7 +43,7 @@ function apBackoff(attemptsMade: number, err: Error) {
|
||||||
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
||||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
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;
|
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 type Bull from "bull";
|
||||||
import httpSignature from "@peertube/http-signature";
|
import httpSignature from "@peertube/http-signature";
|
||||||
import perform from "@/remote/activitypub/perform.js";
|
|
||||||
import Logger from "@/services/logger.js";
|
import Logger from "@/services/logger.js";
|
||||||
import {registerOrFetchInstanceDoc} from "@/services/register-or-fetch-instance-doc.js";
|
import {registerOrFetchInstanceDoc} from "@/services/register-or-fetch-instance-doc.js";
|
||||||
import {Instances} from "@/models/index.js";
|
import {Instances} from "@/models/index.js";
|
||||||
import {
|
import {apRequestChart, federationChart, instanceChart,} from "@/services/chart/index.js";
|
||||||
apRequestChart,
|
|
||||||
federationChart,
|
|
||||||
instanceChart,
|
|
||||||
} from "@/services/chart/index.js";
|
|
||||||
import {fetchMeta} from "@/misc/fetch-meta.js";
|
import {fetchMeta} from "@/misc/fetch-meta.js";
|
||||||
import { toPuny, extractDbHost } from "@/misc/convert-host.js";
|
import {extractDbHost, toPuny} from "@/misc/convert-host.js";
|
||||||
import { getApId } from "@/remote/activitypub/type.js";
|
import {getApId, isCollection, isCollectionOrOrderedCollection} from "@/remote/activitypub/type.js";
|
||||||
import {fetchInstanceMetadata} from "@/services/fetch-instance-metadata.js";
|
import {fetchInstanceMetadata} from "@/services/fetch-instance-metadata.js";
|
||||||
import type {InboxJobData} from "../types.js";
|
import type {InboxJobData} from "../types.js";
|
||||||
import DbResolver from "@/remote/activitypub/db-resolver.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 {StatusError} from "@/misc/fetch.js";
|
||||||
import type {CacheableRemoteUser} from "@/models/entities/user.js";
|
import type {CacheableRemoteUser} from "@/models/entities/user.js";
|
||||||
import type {UserPublickey} from "@/models/entities/user-publickey.js";
|
import type {UserPublickey} from "@/models/entities/user-publickey.js";
|
||||||
import {shouldBlockInstance} from "@/misc/should-block-instance.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");
|
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}`;
|
if (!signature?.keyId) return `Invalid signature: ${signature}`;
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
const host = toPuny(new URL(signature.keyId).hostname);
|
const httpSigKeyHost = toPuny(new URL(signature.keyId).hostname);
|
||||||
|
|
||||||
// interrupt if blocked
|
// interrupt if blocked
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
if (await shouldBlockInstance(host, meta)) {
|
if (await shouldBlockInstance(httpSigKeyHost, meta)) {
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${httpSigKeyHost}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only whitelisted instances in private mode
|
// only whitelisted instances in private mode
|
||||||
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
|
if (meta.privateMode && !meta.allowedHosts.includes(httpSigKeyHost)) {
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${httpSigKeyHost}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyIdLower = signature.keyId.toLowerCase();
|
const keyIdLower = signature.keyId.toLowerCase();
|
||||||
|
@ -59,19 +57,18 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
const dbResolver = new DbResolver();
|
const dbResolver = new DbResolver();
|
||||||
|
|
||||||
// HTTP-Signature keyId from DB
|
// HTTP-Signature keyId from DB
|
||||||
let authUser: {
|
let senderActor: {
|
||||||
user: CacheableRemoteUser;
|
user: CacheableRemoteUser;
|
||||||
key: UserPublickey | null;
|
key: UserPublickey | null;
|
||||||
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
||||||
|
|
||||||
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
|
if (senderActor == null) {
|
||||||
if (authUser == null) {
|
|
||||||
try {
|
try {
|
||||||
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
|
senderActor = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Skip if target is 4xx
|
// Skip if target is 4xx
|
||||||
if (e instanceof StatusError) {
|
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}`;
|
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -81,84 +78,63 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// それでもわからなければ終了
|
if (senderActor == null) {
|
||||||
if (authUser == null) {
|
|
||||||
return "skip: failed to resolve user";
|
return "skip: failed to resolve user";
|
||||||
}
|
}
|
||||||
|
|
||||||
// publicKey がなくても終了
|
if (senderActor.key == null) {
|
||||||
if (authUser.key == null) {
|
|
||||||
return "skip: failed to resolve user publicKey";
|
return "skip: failed to resolve user publicKey";
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP-Signatureの検証
|
|
||||||
const httpSignatureValidated = httpSignature.verifySignature(
|
const httpSignatureValidated = httpSignature.verifySignature(
|
||||||
signature,
|
signature,
|
||||||
authUser.key.keyPem,
|
senderActor.key.keyPem,
|
||||||
);
|
);
|
||||||
|
|
||||||
// また、signatureのsignerは、activity.actorと一致する必要がある
|
if (!httpSignatureValidated) {
|
||||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
return `skip: http-signature verification failed. keyId=${signature.keyId}`;
|
||||||
// 一致しなくても、でも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
|
const signerHost = extractDbHost(senderActor.user.uri!);
|
||||||
// みたいになっててUserを引っ張れば公開キーも入ることを期待する
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
if (activity.signature.creator) {
|
const resolver = new Resolver();
|
||||||
const candicate = activity.signature.creator.replace(/#.*/, "");
|
for (const item of toArray(
|
||||||
await resolvePerson(candicate).catch(() => null);
|
isCollection(activity) ? activity.items : activity.orderedItems,
|
||||||
|
)) {
|
||||||
|
const act = await resolver.resolve(item);
|
||||||
|
|
||||||
|
if (typeof act.id !== "string") {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyIdからLD-Signatureのユーザーを取得
|
const activityIdHost = extractDbHost(act.id);
|
||||||
authUser = await dbResolver.getAuthUserFromKeyId(
|
if (signerHost !== activityIdHost) {
|
||||||
activity.signature.creator,
|
continue;
|
||||||
);
|
|
||||||
if (authUser == null) {
|
|
||||||
return "skip: LD-Signatureのユーザーが取得できませんでした";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authUser.key == null) {
|
try {
|
||||||
return "skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした";
|
await performActivity(senderActor.user, act);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error || typeof err === "string") {
|
||||||
|
apLogger.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LD-Signature検証
|
|
||||||
const ldSignature = new LdSignature();
|
|
||||||
const verified = await ldSignature
|
|
||||||
.verifyRsaSignature2017(activity, authUser.key.keyPem)
|
|
||||||
.catch(() => false);
|
|
||||||
if (!verified) {
|
|
||||||
return "skip: LD-Signatureの検証に失敗しました";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// もう一度actorチェック
|
|
||||||
if (authUser.user.uri !== activity.actor) {
|
|
||||||
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ブロックしてたら中断
|
|
||||||
const ldHost = extractDbHost(authUser.user.uri);
|
|
||||||
if (await shouldBlockInstance(ldHost, meta)) {
|
|
||||||
return `Blocked request: ${ldHost}`;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
|
if (typeof activity.id !== "string") {
|
||||||
}
|
return `skip: activity.id not a valid id`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// activity.idがあればホストが署名者のホストであることを確認する
|
|
||||||
if (typeof activity.id === "string") {
|
|
||||||
const signerHost = extractDbHost(authUser.user.uri!);
|
|
||||||
const activityIdHost = extractDbHost(activity.id);
|
const activityIdHost = extractDbHost(activity.id);
|
||||||
if (signerHost !== activityIdHost) {
|
if (signerHost !== activityIdHost) {
|
||||||
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await performActivity(senderActor.user, activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
registerOrFetchInstanceDoc(authUser.user.host).then((i) => {
|
registerOrFetchInstanceDoc(senderActor.user.host).then((i) => {
|
||||||
Instances.update(i.id, {
|
Instances.update(i.id, {
|
||||||
latestRequestReceivedAt: new Date(),
|
latestRequestReceivedAt: new Date(),
|
||||||
lastCommunicatedAt: new Date(),
|
lastCommunicatedAt: new Date(),
|
||||||
|
@ -172,7 +148,17 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
federationChart.inbox(i.host);
|
federationChart.inbox(i.host);
|
||||||
});
|
});
|
||||||
|
|
||||||
// アクティビティを処理
|
// Update the remote user information if it is out of date
|
||||||
await perform(authUser.user, activity);
|
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";
|
return "ok";
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,66 +1,82 @@
|
||||||
import escapeRegexp from "escape-regexp";
|
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import type {Note} from "@/models/entities/note.js";
|
import type {Note} from "@/models/entities/note.js";
|
||||||
import type {
|
import type {CacheableRemoteUser, CacheableUser,} from "@/models/entities/user.js";
|
||||||
CacheableRemoteUser,
|
|
||||||
CacheableUser,
|
|
||||||
} from "@/models/entities/user.js";
|
|
||||||
import type {UserPublickey} from "@/models/entities/user-publickey.js";
|
import type {UserPublickey} from "@/models/entities/user-publickey.js";
|
||||||
import {
|
import {Notes, UserPublickeys, Users,} from "@/models/index.js";
|
||||||
Notes,
|
|
||||||
Users,
|
|
||||||
UserPublickeys,
|
|
||||||
} from "@/models/index.js";
|
|
||||||
import {Cache} from "@/misc/cache.js";
|
import {Cache} from "@/misc/cache.js";
|
||||||
import {uriPersonCache, userByIdCache} from "@/services/user-cache.js";
|
import {uriPersonCache, userByIdCache} from "@/services/user-cache.js";
|
||||||
import type {IObject} from "./type.js";
|
import type {IObject} from "./type.js";
|
||||||
import {getApId} from "./type.js";
|
import {getApId} from "./type.js";
|
||||||
import {resolvePerson} from "./models/person.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 publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||||
const publicKeyByUserIdCache = 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 =
|
export type UriParseResult =
|
||||||
| {
|
| {
|
||||||
/** wether the URI was generated by us */
|
|
||||||
local: true;
|
local: true;
|
||||||
/** id in DB */
|
object: UriLocalObject
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
/** wether the URI was generated by us */
|
|
||||||
local: false;
|
local: false;
|
||||||
/** uri in DB */
|
uri: URL;
|
||||||
uri: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseUri(value: string | IObject): UriParseResult {
|
export function parseApUri(value: string | IObject): UriParseResult {
|
||||||
const uri = getApId(value);
|
const uri = new URL(getApId(value));
|
||||||
|
if (config.host !== toPuny(uri.host)) {
|
||||||
// the host part of a URL is case insensitive, so use the 'i' flag.
|
|
||||||
const localRegex = new RegExp(
|
|
||||||
`^${escapeRegexp(config.url)}/(\\w+)/(\\w+)(?:/(.+))?`,
|
|
||||||
"i",
|
|
||||||
);
|
|
||||||
const matchLocal = uri.match(localRegex);
|
|
||||||
|
|
||||||
if (matchLocal) {
|
|
||||||
return {
|
|
||||||
local: true,
|
|
||||||
type: matchLocal[1],
|
|
||||||
id: matchLocal[2],
|
|
||||||
rest: matchLocal[3],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
return {
|
||||||
local: false,
|
local: false,
|
||||||
uri,
|
uri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const path = uri.pathname;
|
||||||
|
const [_empty, objectType, id, ...parts] = path.split("/");
|
||||||
|
|
||||||
|
if (!objectType || !id)
|
||||||
|
throw new Error("Invalid local object URI");
|
||||||
|
|
||||||
|
let object: UriLocalObject;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case objectType === "notes" && parts.length == 0:
|
||||||
|
object = { type: parts.length == 1 && parts[0] === "activity" ? "NoteActivity" : "Note", id};
|
||||||
|
break;
|
||||||
|
case objectType === "notes" && parts.length == 1 && parts[0] === "activity":
|
||||||
|
object = { type: "NoteActivity", id};
|
||||||
|
break;
|
||||||
|
case objectType === "users" && parts.length == 0:
|
||||||
|
object = { type: "User", id };
|
||||||
|
break;
|
||||||
|
case objectType === "questions" && parts.length == 0:
|
||||||
|
object = { type: "Question", id };
|
||||||
|
break;
|
||||||
|
case objectType === "likes" && parts.length == 0:
|
||||||
|
object = { type: "Like", id };
|
||||||
|
break;
|
||||||
|
case objectType === "follows" && parts.length == 1:
|
||||||
|
const [followeeId] = parts;
|
||||||
|
object = { type: "Follow", id, followeeId };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown object type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
local: true,
|
||||||
|
object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUriResultLocal(value: UriParseResult): value is UriParseResult & { local: true } {
|
||||||
|
return value.local;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DbResolver {
|
export default class DbResolver {
|
||||||
|
@ -68,17 +84,17 @@ export default class DbResolver {
|
||||||
* AP Note => Misskey Note in DB
|
* AP Note => Misskey Note in DB
|
||||||
*/
|
*/
|
||||||
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
|
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
|
||||||
const parsed = parseUri(value);
|
const parsed = parseApUri(value);
|
||||||
|
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== "notes") return null;
|
if (parsed.object.type !== "Note") return null;
|
||||||
|
|
||||||
return await Notes.findOneBy({
|
return await Notes.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.object.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await Notes.findOneBy({
|
return await Notes.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri.href,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,22 +105,22 @@ export default class DbResolver {
|
||||||
public async getUserFromApId(
|
public async getUserFromApId(
|
||||||
value: string | IObject,
|
value: string | IObject,
|
||||||
): Promise<CacheableUser | null> {
|
): Promise<CacheableUser | null> {
|
||||||
const parsed = parseUri(value);
|
const parsed = parseApUri(value);
|
||||||
|
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== "users") return null;
|
if (parsed.object.type !== "User") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(await userByIdCache.fetchMaybe(parsed.id, () =>
|
(await userByIdCache.fetchMaybe(parsed.object.id, () =>
|
||||||
Users.findOneBy({
|
Users.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.object.id,
|
||||||
}).then((x) => x ?? undefined),
|
}).then((x) => x ?? undefined),
|
||||||
)) ?? null
|
)) ?? null
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return await uriPersonCache.fetch(parsed.uri, () =>
|
return await uriPersonCache.fetch(parsed.uri.href, () =>
|
||||||
Users.findOneBy({
|
Users.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri.href,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||||
import accept from "@/services/following/requests/accept.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 DbResolver from "../../db-resolver.js";
|
||||||
import { relayAccepted } from "@/services/relay.js";
|
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
actor: CacheableRemoteUser,
|
actor: CacheableRemoteUser,
|
||||||
|
@ -21,12 +20,6 @@ export default async (
|
||||||
return "skip: follower is not a local user";
|
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);
|
await accept(actor, follower);
|
||||||
return "ok";
|
return "ok";
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type {CacheableRemoteUser} from "@/models/entities/user.js";
|
import type {CacheableRemoteUser} from "@/models/entities/user.js";
|
||||||
import config from "@/config/index.js";
|
|
||||||
import type {IFlag} from "../../type.js";
|
import type {IFlag} from "../../type.js";
|
||||||
import {getApIds} from "../../type.js";
|
import {getApIds} from "../../type.js";
|
||||||
import {AbuseUserReports, Users} from "@/models/index.js";
|
import {AbuseUserReports, Users} from "@/models/index.js";
|
||||||
import {In} from "typeorm";
|
import {In} from "typeorm";
|
||||||
import {genId} from "@/misc/gen-id.js";
|
import {genId} from "@/misc/gen-id.js";
|
||||||
|
import {isUriResultLocal, parseApUri} from "@/remote/activitypub/db-resolver";
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
actor: CacheableRemoteUser,
|
actor: CacheableRemoteUser,
|
||||||
|
@ -15,9 +15,11 @@ export default async (
|
||||||
// user and it is stored as a comment.
|
// user and it is stored as a comment.
|
||||||
const uris = getApIds(activity.object);
|
const uris = getApIds(activity.object);
|
||||||
|
|
||||||
const userIds = uris
|
const userIds = uris.map(parseApUri)
|
||||||
.filter((uri) => uri.startsWith(`${config.url}/users/`))
|
.filter(isUriResultLocal)
|
||||||
.map((uri) => uri.split("/").pop()!);
|
.filter(obj => obj.object.type === "User")
|
||||||
|
.map(user => user.object.id);
|
||||||
|
|
||||||
const users = await Users.findBy({
|
const users = await Users.findBy({
|
||||||
id: In(userIds),
|
id: In(userIds),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,23 @@
|
||||||
import type {CacheableRemoteUser} from "@/models/entities/user.js";
|
import type {CacheableRemoteUser} from "@/models/entities/user.js";
|
||||||
import { toArray } from "@/prelude/array.js";
|
import type {IObject} from "../type.js";
|
||||||
import {
|
import {
|
||||||
|
getApId,
|
||||||
|
isAccept,
|
||||||
|
isAdd,
|
||||||
|
isAnnounce,
|
||||||
|
isBlock,
|
||||||
isCreate,
|
isCreate,
|
||||||
isDelete,
|
isDelete,
|
||||||
isUpdate,
|
|
||||||
isRead,
|
|
||||||
isFollow,
|
|
||||||
isAccept,
|
|
||||||
isReject,
|
|
||||||
isAdd,
|
|
||||||
isRemove,
|
|
||||||
isAnnounce,
|
|
||||||
isLike,
|
|
||||||
isUndo,
|
|
||||||
isBlock,
|
|
||||||
isCollectionOrOrderedCollection,
|
|
||||||
isCollection,
|
|
||||||
isFlag,
|
isFlag,
|
||||||
|
isFollow,
|
||||||
|
isLike,
|
||||||
isMove,
|
isMove,
|
||||||
getApId,
|
isReject,
|
||||||
|
isRemove,
|
||||||
|
isUndo,
|
||||||
|
isUpdate,
|
||||||
} from "../type.js";
|
} from "../type.js";
|
||||||
import {apLogger} from "../logger.js";
|
import {apLogger} from "../logger.js";
|
||||||
import Resolver from "../resolver.js";
|
|
||||||
import create from "./create/index.js";
|
import create from "./create/index.js";
|
||||||
import performDeleteActivity from "./delete/index.js";
|
import performDeleteActivity from "./delete/index.js";
|
||||||
import performUpdateActivity from "./update/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 block from "./block/index.js";
|
||||||
import flag from "./flag/index.js";
|
import flag from "./flag/index.js";
|
||||||
import move from "./move/index.js";
|
import move from "./move/index.js";
|
||||||
import type { IObject } from "../type.js";
|
|
||||||
import {extractDbHost} from "@/misc/convert-host.js";
|
import {extractDbHost} from "@/misc/convert-host.js";
|
||||||
import {shouldBlockInstance} from "@/misc/should-block-instance.js";
|
import {shouldBlockInstance} from "@/misc/should-block-instance.js";
|
||||||
|
|
||||||
export async function performActivity(
|
export async function performActivity(
|
||||||
actor: CacheableRemoteUser,
|
actor: CacheableRemoteUser,
|
||||||
activity: IObject,
|
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> {
|
): Promise<void> {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
if (typeof activity.id !== "undefined") {
|
|
||||||
const host = extractDbHost(getApId(activity));
|
const host = extractDbHost(getApId(activity));
|
||||||
if (await shouldBlockInstance(host)) return;
|
if (await shouldBlockInstance(host)) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreate(activity)) {
|
if (isCreate(activity)) {
|
||||||
await create(actor, activity);
|
await create(actor, activity);
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||||
import {remoteReject} from "@/services/following/reject.js";
|
import {remoteReject} from "@/services/following/reject.js";
|
||||||
import type {IFollow} from "../../type.js";
|
import type {IFollow} from "../../type.js";
|
||||||
import DbResolver from "../../db-resolver.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 (
|
export default async (
|
||||||
|
@ -22,12 +21,6 @@ export default async (
|
||||||
return "skip: follower is not a local user";
|
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);
|
await remoteReject(actor, follower);
|
||||||
return "ok";
|
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 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 => {
|
export const renderActivity = (x: any): IActivity | null => {
|
||||||
if (x == null) return null;
|
if (x == null) return null;
|
||||||
|
|
||||||
if (typeof x === "object" && x.id == null) {
|
if (typeof x === "object" && x.id == null) {
|
||||||
|
// ???
|
||||||
x.id = `${config.url}/${uuid()}`;
|
x.id = `${config.url}/${uuid()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,22 +55,3 @@ export const renderActivity = (x: any): IActivity | null => {
|
||||||
x,
|
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,8 +1,8 @@
|
||||||
import {In, IsNull} from "typeorm";
|
import {In, IsNull} from "typeorm";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js";
|
import type {IMentionedRemoteUsers, Note} from "@/models/entities/note.js";
|
||||||
import type {DriveFile} from "@/models/entities/drive-file.js";
|
import type {DriveFile} from "@/models/entities/drive-file.js";
|
||||||
import { DriveFiles, Notes, Users, Emojis, Polls } from "@/models/index.js";
|
import {DriveFiles, Emojis, Notes, Polls, Users} from "@/models/index.js";
|
||||||
import type {Emoji} from "@/models/entities/emoji.js";
|
import type {Emoji} from "@/models/entities/emoji.js";
|
||||||
import type {Poll} from "@/models/entities/poll.js";
|
import type {Poll} from "@/models/entities/poll.js";
|
||||||
import toHtml from "../misc/get-note-html.js";
|
import toHtml from "../misc/get-note-html.js";
|
||||||
|
@ -14,7 +14,6 @@ import renderDocument from "./document.js";
|
||||||
export default async function renderNote(
|
export default async function renderNote(
|
||||||
note: Note,
|
note: Note,
|
||||||
dive = true,
|
dive = true,
|
||||||
isTalk = false,
|
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
const getPromisedFiles = async (ids: string[]) => {
|
const getPromisedFiles = async (ids: string[]) => {
|
||||||
if (!ids || ids.length === 0) return [];
|
if (!ids || ids.length === 0) return [];
|
||||||
|
@ -140,12 +139,6 @@ export default async function renderNote(
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
const asTalk = isTalk
|
|
||||||
? {
|
|
||||||
_misskey_talk: true,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${config.url}/notes/${note.id}`,
|
id: `${config.url}/notes/${note.id}`,
|
||||||
type: "Note",
|
type: "Note",
|
||||||
|
@ -168,7 +161,6 @@ export default async function renderNote(
|
||||||
sensitive: note.cw != null || files.some((file) => file.isSensitive),
|
sensitive: note.cw != null || files.some((file) => file.isSensitive),
|
||||||
tag,
|
tag,
|
||||||
...asPoll,
|
...asPoll,
|
||||||
...asTalk,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,16 @@ import {getInstanceActor} from "@/services/instance-actor.js";
|
||||||
import {fetchMeta} from "@/misc/fetch-meta.js";
|
import {fetchMeta} from "@/misc/fetch-meta.js";
|
||||||
import {extractDbHost, isSelfHost} from "@/misc/convert-host.js";
|
import {extractDbHost, isSelfHost} from "@/misc/convert-host.js";
|
||||||
import {signedGet} from "./request.js";
|
import {signedGet} from "./request.js";
|
||||||
import type {ICollection, IObject, IOrderedCollection} from "./type.js";
|
import {
|
||||||
import {getApId, isCollectionOrOrderedCollection} from "./type.js";
|
getApId,
|
||||||
|
ICollection,
|
||||||
|
IObject,
|
||||||
|
IOrderedCollection,
|
||||||
|
isCollectionOrOrderedCollection,
|
||||||
|
isLdApContext
|
||||||
|
} from "./type.js";
|
||||||
import {NoteReactions, Notes, Polls, Users,} from "@/models/index.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 renderNote from "@/remote/activitypub/renderer/note.js";
|
||||||
import {renderLike} from "@/remote/activitypub/renderer/like.js";
|
import {renderLike} from "@/remote/activitypub/renderer/like.js";
|
||||||
import {renderPerson} from "@/remote/activitypub/renderer/person.js";
|
import {renderPerson} from "@/remote/activitypub/renderer/person.js";
|
||||||
|
@ -76,7 +82,11 @@ export default class Resolver {
|
||||||
|
|
||||||
const host = extractDbHost(value);
|
const host = extractDbHost(value);
|
||||||
if (isSelfHost(host)) {
|
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();
|
const meta = await fetchMeta();
|
||||||
|
@ -98,62 +108,53 @@ export default class Resolver {
|
||||||
|
|
||||||
const object = await signedGet(value, this.user);
|
const object = await signedGet(value, this.user);
|
||||||
|
|
||||||
if (
|
if (!isLdApContext(object)) {
|
||||||
object == null ||
|
throw new Error("Invalid AP fetch response");
|
||||||
(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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveLocal(url: string): Promise<IObject> {
|
private async resolveLocal(url: string): Promise<IObject | null> {
|
||||||
const parsed = parseUri(url);
|
const parsed = parseApUri(url);
|
||||||
if (!parsed.local) throw new Error("resolveLocal: not local");
|
if (!parsed.local) throw new Error("resolveLocal: not local");
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.object.type) {
|
||||||
case "notes":
|
case "NoteActivity":
|
||||||
return Notes.findOneByOrFail({ id: parsed.id }).then((note) => {
|
return Notes.findOneByOrFail({ id: parsed.object.id }).then((note) =>
|
||||||
if (parsed.rest === "activity") {
|
renderActivity(renderCreate(renderNote(note, false), note))
|
||||||
// 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),
|
|
||||||
);
|
);
|
||||||
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.
|
// Polls are indexed by the note they are attached to.
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
Notes.findOneByOrFail({ id: parsed.id }),
|
Notes.findOneByOrFail({ id: parsed.object.id }),
|
||||||
Polls.findOneByOrFail({ noteId: parsed.id }),
|
Polls.findOneByOrFail({ noteId: parsed.object.id }),
|
||||||
]).then(([note, poll]) =>
|
]).then(([note, poll]) =>
|
||||||
renderQuestion({ id: note.userId }, note, poll),
|
renderActivity(renderQuestion({id: note.userId}, note, poll)),
|
||||||
);
|
);
|
||||||
case "likes":
|
case "Like":
|
||||||
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(
|
return Promise.all([
|
||||||
(reaction) => renderActivity(renderLike(reaction, { uri: null })),
|
Notes.findOneByOrFail({ id: parsed.object.id }),
|
||||||
|
NoteReactions.findOneByOrFail({ id: parsed.object.id })
|
||||||
|
]).then(
|
||||||
|
([note, reaction]) => renderActivity(renderLike(reaction, note)),
|
||||||
);
|
);
|
||||||
case "follows":
|
case "Follow":
|
||||||
// rest should be <followee id>
|
|
||||||
if (parsed.rest == null || !/^\w+$/.test(parsed.rest))
|
|
||||||
throw new Error("resolveLocal: invalid follow URI");
|
|
||||||
|
|
||||||
return Promise.all(
|
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]) =>
|
).then(([follower, followee]) =>
|
||||||
renderActivity(renderFollow(follower, followee, url)),
|
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 type ApObject = IObject | string | (IObject | string)[];
|
||||||
|
|
||||||
export interface IObject {
|
export interface IObject {
|
||||||
"@context": string | string[] | obj | obj[];
|
"@context": string | string[] | Record<string, any> | Record<string, any>[];
|
||||||
type: string | string[];
|
type: string | string[];
|
||||||
id?: string;
|
id?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
@ -25,6 +24,19 @@ export interface IObject {
|
||||||
sensitive?: boolean;
|
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
|
* Get array of ActivityStreams Objects id
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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_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_inboxDelayed from "./endpoints/admin/queue/inbox-delayed.js";
|
||||||
import * as ep___admin_queue_stats from "./endpoints/admin/queue/stats.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_resetPassword from "./endpoints/admin/reset-password.js";
|
||||||
import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.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";
|
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/deliver-delayed", ep___admin_queue_deliverDelayed],
|
||||||
["admin/queue/inbox-delayed", ep___admin_queue_inboxDelayed],
|
["admin/queue/inbox-delayed", ep___admin_queue_inboxDelayed],
|
||||||
["admin/queue/stats", ep___admin_queue_stats],
|
["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/reset-password", ep___admin_resetPassword],
|
||||||
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
|
["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport],
|
||||||
["admin/search/index-all", ep___admin_search_indexAll],
|
["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.
|
// Wait if blocked.
|
||||||
if (await shouldBlockInstance(extractDbHost(uri))) return null;
|
if (await shouldBlockInstance(extractDbHost(uri))) return null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const dbResolver = new DbResolver();
|
const dbResolver = new DbResolver();
|
||||||
|
|
||||||
const [user, note] = await Promise.all([
|
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 {renderActivity} from "@/remote/activitypub/renderer/index.js";
|
||||||
import renderNote from "@/remote/activitypub/renderer/note.js";
|
import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||||
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
||||||
import {deliverToRelays} from "@/services/relay.js";
|
|
||||||
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||||
import {fetchMeta} from "@/misc/fetch-meta.js";
|
import {fetchMeta} from "@/misc/fetch-meta.js";
|
||||||
|
|
||||||
|
@ -617,11 +616,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliver to relays for public posts.
|
|
||||||
if (["public"].includes(note.visibility)) {
|
|
||||||
deliverToRelays(user, activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GO!
|
// GO!
|
||||||
dm.execute();
|
dm.execute();
|
||||||
})().then();
|
})().then();
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { Notes, UserNotePinings, Users } from "@/models/index.js";
|
||||||
import type {UserNotePining} from "@/models/entities/user-note-pining.js";
|
import type {UserNotePining} from "@/models/entities/user-note-pining.js";
|
||||||
import {genId} from "@/misc/gen-id.js";
|
import {genId} from "@/misc/gen-id.js";
|
||||||
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
|
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
|
||||||
import { deliverToRelays } from "../relay.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指定した投稿をピン留めします
|
* 指定した投稿をピン留めします
|
||||||
|
@ -114,5 +113,4 @@ export async function deliverPinnedChange(
|
||||||
);
|
);
|
||||||
|
|
||||||
deliverToFollowers(user, content);
|
deliverToFollowers(user, content);
|
||||||
deliverToRelays(user, content);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Users } from "@/models/index.js";
|
||||||
import type {User} from "@/models/entities/user.js";
|
import type {User} from "@/models/entities/user.js";
|
||||||
import {renderPerson} from "@/remote/activitypub/renderer/person.js";
|
import {renderPerson} from "@/remote/activitypub/renderer/person.js";
|
||||||
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
|
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
|
||||||
import { deliverToRelays } from "../relay.js";
|
|
||||||
|
|
||||||
export async function publishToFollowers(userId: User["id"]) {
|
export async function publishToFollowers(userId: User["id"]) {
|
||||||
const user = await Users.findOneBy({ id: userId });
|
const user = await Users.findOneBy({ id: userId });
|
||||||
|
@ -16,6 +15,5 @@ export async function publishToFollowers(userId: User["id"]) {
|
||||||
renderUpdate(await renderPerson(user), user),
|
renderUpdate(await renderPerson(user), user),
|
||||||
);
|
);
|
||||||
deliverToFollowers(user, content);
|
deliverToFollowers(user, content);
|
||||||
deliverToRelays(user, content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,6 @@ import {checkHitAntenna} from "@/misc/check-hit-antenna.js";
|
||||||
import {getWordHardMute} from "@/misc/check-word-mute.js";
|
import {getWordHardMute} from "@/misc/check-word-mute.js";
|
||||||
import {addNoteToAntenna} from "../add-note-to-antenna.js";
|
import {addNoteToAntenna} from "../add-note-to-antenna.js";
|
||||||
import {countSameRenotes} from "@/misc/count-same-renotes.js";
|
import {countSameRenotes} from "@/misc/count-same-renotes.js";
|
||||||
import {deliverToRelays, getCachedRelays} from "../relay.js";
|
|
||||||
import {normalizeForSearch} from "@/misc/normalize-for-search.js";
|
import {normalizeForSearch} from "@/misc/normalize-for-search.js";
|
||||||
import {getAntennas} from "@/misc/antenna-cache.js";
|
import {getAntennas} from "@/misc/antenna-cache.js";
|
||||||
import {endedPollNotificationQueue} from "@/queue/queues.js";
|
import {endedPollNotificationQueue} from "@/queue/queues.js";
|
||||||
|
@ -402,39 +401,10 @@ export default async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dontFederateInitially) {
|
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) {
|
if (!note.uri) {
|
||||||
// Publish if the post is local
|
// Publish if the post is local
|
||||||
publishNotesStream(note);
|
publishNotesStream(note);
|
||||||
} else if (boostedByRelay && data.renote?.uri) {
|
} else if (note.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) {
|
|
||||||
// Use Redis transaction for atomicity
|
// Use Redis transaction for atomicity
|
||||||
await redisClient.watch(`publishedNote:${note.uri}`);
|
await redisClient.watch(`publishedNote:${note.uri}`);
|
||||||
const exists = await redisClient.exists(`publishedNote:${note.uri}`);
|
const exists = await redisClient.exists(`publishedNote:${note.uri}`);
|
||||||
|
@ -585,10 +555,6 @@ export default async (
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["public"].includes(note.visibility)) {
|
|
||||||
deliverToRelays(user, noteActivity);
|
|
||||||
}
|
|
||||||
|
|
||||||
dm.execute();
|
dm.execute();
|
||||||
})().then();
|
})().then();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,21 +6,13 @@ 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 renderTombstone from "@/remote/activitypub/renderer/tombstone.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
import type {ILocalUser, IRemoteUser, User} from "@/models/entities/user.js";
|
||||||
import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js";
|
import type {IMentionedRemoteUsers, Note} from "@/models/entities/note.js";
|
||||||
import { Notes, Users, Instances } from "@/models/index.js";
|
import {Instances, Notes, Users} from "@/models/index.js";
|
||||||
import {
|
import {instanceChart, notesChart, perUserNotesChart,} from "@/services/chart/index.js";
|
||||||
notesChart,
|
import {deliverToFollowers, deliverToUser,} from "@/remote/activitypub/deliver-manager.js";
|
||||||
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 {countSameRenotes} from "@/misc/count-same-renotes.js";
|
||||||
import {registerOrFetchInstanceDoc} from "../register-or-fetch-instance-doc.js";
|
import {registerOrFetchInstanceDoc} from "../register-or-fetch-instance-doc.js";
|
||||||
import { deliverToRelays } from "../relay.js";
|
|
||||||
import meilisearch from "@/db/meilisearch.js";
|
import meilisearch from "@/db/meilisearch.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,7 +174,6 @@ async function deliverToConcerned(
|
||||||
content: any,
|
content: any,
|
||||||
) {
|
) {
|
||||||
deliverToFollowers(user, content);
|
deliverToFollowers(user, content);
|
||||||
deliverToRelays(user, content);
|
|
||||||
const remoteUsers = await getMentionedRemoteUsers(note);
|
const remoteUsers = await getMentionedRemoteUsers(note);
|
||||||
for (const remoteUser of remoteUsers) {
|
for (const remoteUser of remoteUsers) {
|
||||||
deliverToUser(user, content, remoteUser);
|
deliverToUser(user, content, remoteUser);
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
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 renderNote from "@/remote/activitypub/renderer/note.js";
|
||||||
import { Users, Notes } from "@/models/index.js";
|
import {Notes, Users} from "@/models/index.js";
|
||||||
import type {Note} from "@/models/entities/note.js";
|
import type {Note} from "@/models/entities/note.js";
|
||||||
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
|
import {deliverToFollowers} from "@/remote/activitypub/deliver-manager.js";
|
||||||
import { deliverToRelays } from "../../relay.js";
|
|
||||||
|
|
||||||
export async function deliverQuestionUpdate(noteId: Note["id"]) {
|
export async function deliverQuestionUpdate(noteId: Note["id"]) {
|
||||||
const note = await Notes.findOneBy({ id: noteId });
|
const note = await Notes.findOneBy({ id: noteId });
|
||||||
|
@ -18,6 +17,5 @@ export async function deliverQuestionUpdate(noteId: Note["id"]) {
|
||||||
renderUpdate(await renderNote(note, false), user),
|
renderUpdate(await renderNote(note, false), user),
|
||||||
);
|
);
|
||||||
deliverToFollowers(user, content);
|
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