mirror of
https://github.com/spacebarchat/server.git
synced 2026-04-03 00:15:47 +00:00
switch to own activitypub types library
This commit is contained in:
Binary file not shown.
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@@ -69,7 +69,7 @@
|
||||
"@aws-sdk/client-s3": "^3.385.0",
|
||||
"@sentry/integrations": "^7.66.0",
|
||||
"@sentry/node": "^7.66.0",
|
||||
"activitypub-core-types": "^0.3.2",
|
||||
"activitypub-types": "github:spacebarchat/activitypub-types#dist",
|
||||
"ajv": "8.6.2",
|
||||
"ajv-formats": "2.1.1",
|
||||
"amqplib": "^0.10.3",
|
||||
@@ -99,6 +99,7 @@
|
||||
"node-fetch": "^2.6.12",
|
||||
"node-os-utils": "^1.3.7",
|
||||
"nodemailer": "^6.9.4",
|
||||
"pg": "^8.11.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"proxy-agent": "^6.3.0",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { AP } from "activitypub-core-types";
|
||||
import { APOrderedCollection, AnyAPObject } from "activitypub-types";
|
||||
import { ACTIVITYSTREAMS_CONTEXT } from "./utils";
|
||||
|
||||
export const makeOrderedCollection = async <T extends AP.CoreObject>(opts: {
|
||||
export const makeOrderedCollection = async <T extends AnyAPObject>(opts: {
|
||||
page: boolean;
|
||||
min_id?: string;
|
||||
max_id?: string;
|
||||
id: URL;
|
||||
id: string;
|
||||
getTotalElements: () => Promise<number>;
|
||||
getElements: (before?: string, after?: string) => Promise<T[]>;
|
||||
}): Promise<AP.OrderedCollection> => {
|
||||
}): Promise<APOrderedCollection> => {
|
||||
const { page, min_id, max_id, id, getTotalElements, getElements } = opts;
|
||||
|
||||
if (!page)
|
||||
@@ -28,7 +28,7 @@ export const makeOrderedCollection = async <T extends AP.CoreObject>(opts: {
|
||||
|
||||
return {
|
||||
"@context": ACTIVITYSTREAMS_CONTEXT,
|
||||
id: new URL(`${id}?page=true`),
|
||||
id: `${id}?page=true`,
|
||||
type: "OrderedCollection",
|
||||
first: new URL(`${id}?page=true`),
|
||||
last: new URL(`${id}?page=true&min_id=0`),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Responsible for dispatching activitypub events to external instances
|
||||
*/
|
||||
|
||||
import { AP } from "activitypub-core-types";
|
||||
import { APActivity } from "activitypub-types";
|
||||
import { federationQueue } from "./queue";
|
||||
|
||||
export * from "./OrderedCollection";
|
||||
@@ -11,7 +11,7 @@ export * from "./transforms";
|
||||
export * from "./utils";
|
||||
|
||||
export class Federation {
|
||||
static async distribute(activity: AP.Activity) {
|
||||
static async distribute(activity: APActivity) {
|
||||
await federationQueue.distribute(activity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Config, FederationKey } from "@spacebar/util";
|
||||
import { AP } from "activitypub-core-types";
|
||||
import fetch from "node-fetch";
|
||||
import { APError, signActivity, splitQualifiedMention } from "./utils";
|
||||
import { APActivity } from "activitypub-types";
|
||||
|
||||
//
|
||||
type Instance = string;
|
||||
|
||||
class FederationQueue {
|
||||
// TODO: queue messages and send them to shared inbox
|
||||
private queue: Map<Instance, Array<AP.Activity>> = new Map();
|
||||
private queue: Map<Instance, Array<APActivity>> = new Map();
|
||||
|
||||
public async distribute(activity: AP.Activity) {
|
||||
public async distribute(activity: APActivity) {
|
||||
let { to, actor } = activity;
|
||||
|
||||
if (!to)
|
||||
|
||||
@@ -10,20 +10,19 @@ import {
|
||||
User,
|
||||
UserSettings,
|
||||
} from "@spacebar/util";
|
||||
import { AP } from "activitypub-core-types";
|
||||
import TurndownService from "turndown";
|
||||
import { In } from "typeorm";
|
||||
import {
|
||||
ACTIVITYSTREAMS_CONTEXT,
|
||||
APError,
|
||||
APObjectIsPerson,
|
||||
APObjectIsSpacebarActor,
|
||||
resolveAPObject,
|
||||
} from "./utils";
|
||||
import { APAnnounce, APGroup, APNote, APPerson } from "activitypub-types";
|
||||
|
||||
export const transformMessageToAnnounceNoce = async (
|
||||
message: Message,
|
||||
): Promise<AP.Announce> => {
|
||||
): Promise<APAnnounce> => {
|
||||
const { host } = Config.get().federation;
|
||||
|
||||
const channel = await Channel.findOneOrFail({
|
||||
@@ -34,10 +33,9 @@ export const transformMessageToAnnounceNoce = async (
|
||||
});
|
||||
|
||||
let to = [
|
||||
new URL(
|
||||
`https://${host}/federation/channels/${message.channel_id}/followers`,
|
||||
),
|
||||
`https://${host}/federation/channels/${message.channel_id}/followers`,
|
||||
];
|
||||
|
||||
if (channel.isDm()) {
|
||||
const otherUsers = channel.recipients?.filter(
|
||||
(x) => x.user_id != message.author_id,
|
||||
@@ -48,27 +46,25 @@ export const transformMessageToAnnounceNoce = async (
|
||||
});
|
||||
|
||||
to = remoteUsersKeys.map((x) =>
|
||||
x.inbox ? new URL(x.inbox!) : new URL(`${x.federatedId}/inbox`),
|
||||
x.inbox ? x.inbox! : `${x.federatedId}/inbox`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
"@context": ACTIVITYSTREAMS_CONTEXT,
|
||||
type: "Announce",
|
||||
id: new URL(
|
||||
`https://${host}/federation/channels/${message.channel_id}/messages/${message.id}`,
|
||||
),
|
||||
id: `https://${host}/federation/channels/${message.channel_id}/messages/${message.id}`,
|
||||
// this is wrong for remote users
|
||||
actor: new URL(`https://${host}/federation/users/${message.author_id}`),
|
||||
actor: `https://${host}/federation/users/${message.author_id}`,
|
||||
published: message.timestamp,
|
||||
to,
|
||||
object: await transformMessageToNote(message),
|
||||
};
|
||||
} as APAnnounce;
|
||||
};
|
||||
|
||||
export const transformMessageToNote = async (
|
||||
message: Message,
|
||||
): Promise<AP.Note> => {
|
||||
): Promise<APNote> => {
|
||||
const { host } = Config.get().federation;
|
||||
|
||||
const referencedMessage = message.message_reference
|
||||
@@ -78,23 +74,18 @@ export const transformMessageToNote = async (
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: new URL(`https://${host}/federation/messages/${message.id}`),
|
||||
id: `https://${host}/federation/messages/${message.id}`,
|
||||
type: "Note",
|
||||
content: message.content, // TODO: convert markdown to html
|
||||
inReplyTo: referencedMessage
|
||||
? await transformMessageToNote(referencedMessage)
|
||||
: undefined,
|
||||
published: message.timestamp,
|
||||
attributedTo: new URL(
|
||||
`https://${host}/federation/users/${message.author_id}`,
|
||||
),
|
||||
to: [
|
||||
new URL(
|
||||
`https://${host}/federation/channels/${message.channel_id}`,
|
||||
),
|
||||
],
|
||||
attributedTo: `https://${host}/federation/users/${message.author_id}`,
|
||||
|
||||
to: [`https://${host}/federation/channels/${message.channel_id}`],
|
||||
tag: message.mentions?.map(
|
||||
(x) => new URL(`https://${host}/federation/users/${x.id}`),
|
||||
(x) => `https://${host}/federation/users/${x.id}`,
|
||||
),
|
||||
attachment: [],
|
||||
// replies: [],
|
||||
@@ -105,7 +96,7 @@ export const transformMessageToNote = async (
|
||||
};
|
||||
|
||||
// TODO: this was copied from the previous implemention. refactor it.
|
||||
export const transformNoteToMessage = async (note: AP.Note) => {
|
||||
export const transformNoteToMessage = async (note: APNote) => {
|
||||
if (!note.id) throw new APError("Note must have ID");
|
||||
if (note.type != "Note") throw new APError("Message must be Note");
|
||||
|
||||
@@ -177,7 +168,7 @@ export const transformNoteToMessage = async (note: AP.Note) => {
|
||||
|
||||
export const transformChannelToGroup = async (
|
||||
channel: Channel,
|
||||
): Promise<AP.Group> => {
|
||||
): Promise<APGroup> => {
|
||||
const { host, accountDomain } = Config.get().federation;
|
||||
|
||||
const keys = await FederationKey.findOneOrFail({
|
||||
@@ -187,7 +178,7 @@ export const transformChannelToGroup = async (
|
||||
return {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
type: "Group",
|
||||
id: new URL(`https://${host}/fed/channels/${channel.id}`),
|
||||
id: `https://${host}/fed/channels/${channel.id}`,
|
||||
name: channel.name,
|
||||
preferredUsername: channel.id,
|
||||
summary: channel.topic,
|
||||
@@ -200,15 +191,13 @@ export const transformChannelToGroup = async (
|
||||
publicKeyPem: keys.publicKey,
|
||||
},
|
||||
|
||||
inbox: new URL(`https://${host}/fed/channels/${channel.id}/inbox`),
|
||||
outbox: new URL(`https://${host}/fed/channels/${channel.id}/outbox`),
|
||||
followers: new URL(
|
||||
`https://${host}/fed/channels/${channel.id}/followers`,
|
||||
),
|
||||
inbox: `https://${host}/fed/channels/${channel.id}/inbox`,
|
||||
outbox: `https://${host}/fed/channels/${channel.id}/outbox`,
|
||||
followers: `https://${host}/fed/channels/${channel.id}/followers`,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformUserToPerson = async (user: User): Promise<AP.Person> => {
|
||||
export const transformUserToPerson = async (user: User): Promise<APPerson> => {
|
||||
const { host, accountDomain } = Config.get().federation;
|
||||
|
||||
const keys = await FederationKey.findOneOrFail({
|
||||
@@ -218,7 +207,7 @@ export const transformUserToPerson = async (user: User): Promise<AP.Person> => {
|
||||
return {
|
||||
"@context": ACTIVITYSTREAMS_CONTEXT,
|
||||
type: "Person",
|
||||
id: new URL(`https://${host}/federation/users/${user.id}`),
|
||||
id: `https://${host}/federation/users/${user.id}`,
|
||||
|
||||
name: user.username,
|
||||
preferredUsername: user.id,
|
||||
@@ -233,11 +222,9 @@ export const transformUserToPerson = async (user: User): Promise<AP.Person> => {
|
||||
]
|
||||
: undefined,
|
||||
|
||||
inbox: new URL(`https://${host}/federation/users/${user.id}/inbox`),
|
||||
outbox: new URL(`https://${host}/federation/users/${user.id}/outbox`),
|
||||
followers: new URL(
|
||||
`https://${host}/federation/users/${user.id}/followers`,
|
||||
),
|
||||
inbox: `https://${host}/federation/users/${user.id}/inbox`,
|
||||
outbox: `https://${host}/federation/users/${user.id}/outbox`,
|
||||
followers: `https://${host}/federation/users/${user.id}/followers`,
|
||||
publicKey: {
|
||||
id: `https://${host}/federation/users/${user.id}#main-key`,
|
||||
owner: `https://${host}/federation/users/${user.id}`,
|
||||
@@ -247,7 +234,7 @@ export const transformUserToPerson = async (user: User): Promise<AP.Person> => {
|
||||
};
|
||||
|
||||
// TODO: this was copied from previous implementation. refactor.
|
||||
export const transformPersonToUser = async (person: AP.Person) => {
|
||||
export const transformPersonToUser = async (person: APPerson) => {
|
||||
if (!person.id) throw new APError("User must have ID");
|
||||
|
||||
const url = new URL(person.id.toString());
|
||||
|
||||
@@ -5,7 +5,13 @@ import {
|
||||
OrmUtils,
|
||||
WebfingerResponse,
|
||||
} from "@spacebar/util";
|
||||
import { AP } from "activitypub-core-types";
|
||||
import {
|
||||
APActivity,
|
||||
APActor,
|
||||
APObject,
|
||||
APPerson,
|
||||
AnyAPObject,
|
||||
} from "activitypub-types";
|
||||
import crypto from "crypto";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fetch from "node-fetch";
|
||||
@@ -30,7 +36,7 @@ export const hasAPContext = (data: object) => {
|
||||
return context == activitystreams;
|
||||
};
|
||||
|
||||
export const resolveAPObject = async <T extends object>(
|
||||
export const resolveAPObject = async <T extends AnyAPObject>(
|
||||
data: string | T,
|
||||
): Promise<T> => {
|
||||
// we were already given an AP object
|
||||
@@ -79,7 +85,7 @@ export const splitQualifiedMention = (lookup: string) => {
|
||||
|
||||
export const resolveWebfinger = async (
|
||||
lookup: string,
|
||||
): Promise<AP.CoreObject> => {
|
||||
): Promise<AnyAPObject> => {
|
||||
const { domain } = splitQualifiedMention(lookup);
|
||||
|
||||
const agent = new ProxyAgent();
|
||||
@@ -94,7 +100,7 @@ export const resolveWebfinger = async (
|
||||
const link = wellknown.links.find((x) => x.rel == "self");
|
||||
if (!link) throw new APError(".well-known did not contain rel=self link");
|
||||
|
||||
return await resolveAPObject<AP.CoreObject>(link.href);
|
||||
return await resolveAPObject<AnyAPObject>(link.href);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -107,7 +113,7 @@ export const resolveWebfinger = async (
|
||||
export const signActivity = async (
|
||||
inbox: string,
|
||||
sender: FederationKey,
|
||||
message: AP.Activity,
|
||||
message: APActivity,
|
||||
) => {
|
||||
if (!sender.privateKey)
|
||||
throw new APError("cannot sign without private key");
|
||||
@@ -152,27 +158,23 @@ export const signActivity = async (
|
||||
};
|
||||
|
||||
// fetch from remote server?
|
||||
export const APObjectIsPerson = (
|
||||
object: AP.EntityReference,
|
||||
): object is AP.Person => {
|
||||
export const APObjectIsPerson = (object: AnyAPObject): object is APPerson => {
|
||||
return "type" in object && object.type == "Person";
|
||||
};
|
||||
|
||||
export const APObjectIsGroup = (
|
||||
object: AP.EntityReference,
|
||||
): object is AP.Person => {
|
||||
export const APObjectIsGroup = (object: AnyAPObject): object is APPerson => {
|
||||
return "type" in object && object.type == "Group";
|
||||
};
|
||||
|
||||
export const APObjectIsOrganisation = (
|
||||
object: AP.EntityReference,
|
||||
): object is AP.Person => {
|
||||
object: AnyAPObject,
|
||||
): object is APPerson => {
|
||||
return "type" in object && object.type == "Organization";
|
||||
};
|
||||
|
||||
export const APObjectIsSpacebarActor = (
|
||||
object: AP.EntityReference,
|
||||
): object is AP.Person => {
|
||||
object: AnyAPObject,
|
||||
): object is APPerson => {
|
||||
return (
|
||||
APObjectIsGroup(object) ||
|
||||
APObjectIsOrganisation(object) ||
|
||||
|
||||
@@ -1,35 +1,14 @@
|
||||
import { transformNoteToMessage } from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Message, emitEvent } from "@spacebar/util";
|
||||
import { AP } from "activitypub-core-types";
|
||||
import { APCreate, APNote } from "activitypub-types";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
const router = Router();
|
||||
|
||||
// TODO: check if the activity exists on the remote server
|
||||
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||
const body = req.body as AP.Create;
|
||||
|
||||
if (body.type != "Create") throw new HTTPError("not implemented");
|
||||
|
||||
const object = Array.isArray(body.object) ? body.object[0] : body.object;
|
||||
if (!object) return res.status(400);
|
||||
if (!("type" in object) || object.type != "Note")
|
||||
throw new HTTPError("must be Note");
|
||||
const message = await transformNoteToMessage(object as AP.Note);
|
||||
|
||||
if ((await Message.count({ where: { nonce: object.id!.toString() } })) != 0)
|
||||
return res.status(200);
|
||||
|
||||
await message.save();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_CREATE",
|
||||
channel_id: message.channel_id,
|
||||
data: message.toJSON(),
|
||||
});
|
||||
|
||||
return res.status(200);
|
||||
// TODO: check if the activity exists on the remote server
|
||||
// TODO: refactor
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Config, Message, Snowflake } from "@spacebar/util";
|
||||
import { AP } from "activitypub-core-types";
|
||||
import { APAnnounce } from "activitypub-types";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
|
||||
const router = Router();
|
||||
@@ -19,9 +19,9 @@ router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
page: page != undefined,
|
||||
min_id: min_id?.toString(),
|
||||
max_id: max_id?.toString(),
|
||||
id: new URL(`https://${host}/federation/channels/${channel_id}/outbox`),
|
||||
id: `https://${host}/federation/channels/${channel_id}/outbox`,
|
||||
getTotalElements: () => Message.count({ where: { channel_id } }),
|
||||
getElements: async (before, after): Promise<AP.Announce[]> => {
|
||||
getElements: async (before, after): Promise<APAnnounce[]> => {
|
||||
const query: FindManyOptions<Message> & {
|
||||
where: { id?: FindOperator<string> | FindOperator<string>[] };
|
||||
} = {
|
||||
|
||||
@@ -1,36 +1,10 @@
|
||||
import { transformNoteToMessage } from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Message, emitEvent } from "@spacebar/util";
|
||||
import { AP } from "activitypub-core-types";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
const router = Router();
|
||||
|
||||
// TODO: check if the activity exists on the remote server
|
||||
// TODO: support lemmy ChatMessage type?
|
||||
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||
const body = req.body as AP.Create;
|
||||
|
||||
if (body.type != "Create") throw new HTTPError("not implemented");
|
||||
|
||||
const object = Array.isArray(body.object) ? body.object[0] : body.object;
|
||||
if (!object) return res.status(400);
|
||||
if (!("type" in object) || object.type != "Note")
|
||||
throw new HTTPError("must be Note");
|
||||
const message = await transformNoteToMessage(object as AP.Note);
|
||||
|
||||
if ((await Message.count({ where: { nonce: object.id!.toString() } })) != 0)
|
||||
return res.status(200);
|
||||
|
||||
await message.save();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_CREATE",
|
||||
channel_id: message.channel_id,
|
||||
data: message.toJSON(),
|
||||
});
|
||||
|
||||
return res.status(200);
|
||||
// TODO: support lemmy ChatMessage type?
|
||||
// TODO: check if the activity exists on the remote server
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user