summary refs log tree commit diff
path: root/src/activitypub/federation
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-09-30 01:33:52 +0000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-09-30 01:33:52 +0000
commit34867e2154467f37b137fafe833d46db63746f93 (patch)
treec113dff7ccf22e606b0537616e14dd0a8298e5ae /src/activitypub/federation
parentAccept Follow for federated guild joins. TODO: Channels and role federation (diff)
downloadserver-34867e2154467f37b137fafe833d46db63746f93.tar.xz
basic channel federation
Diffstat (limited to 'src/activitypub/federation')
-rw-r--r--src/activitypub/federation/OrderedCollection.ts3
-rw-r--r--src/activitypub/federation/inbox/index.ts27
-rw-r--r--src/activitypub/federation/transforms.ts60
-rw-r--r--src/activitypub/federation/utils.ts26
4 files changed, 113 insertions, 3 deletions
diff --git a/src/activitypub/federation/OrderedCollection.ts b/src/activitypub/federation/OrderedCollection.ts
index d530deb8..f3b8feea 100644
--- a/src/activitypub/federation/OrderedCollection.ts
+++ b/src/activitypub/federation/OrderedCollection.ts
@@ -26,6 +26,9 @@ export const makeOrderedCollection = async <T extends AnyAPObject>(opts: {
 
 	const elems = await getElements(before, after);
 
+	// TODO: we need to specify next/prev props
+	// and should probably let the caller of this function specify what they are
+	// along with first/last
 	return {
 		"@context": ACTIVITYSTREAMS_CONTEXT,
 		id: `${id}?page=true`,
diff --git a/src/activitypub/federation/inbox/index.ts b/src/activitypub/federation/inbox/index.ts
index 3146f60a..079c13de 100644
--- a/src/activitypub/federation/inbox/index.ts
+++ b/src/activitypub/federation/inbox/index.ts
@@ -15,15 +15,20 @@ import {
 	ActivityIsFollow,
 	AnyAPObject,
 	ObjectIsNote,
+	ObjectIsOrganization,
 } from "activitypub-types";
 import { Request } from "express";
 import { HttpSig } from "../HttpSig";
 import { federationQueue } from "../queue";
-import { transformNoteToMessage } from "../transforms";
+import {
+	transformNoteToMessage,
+	transformOrganisationToGuild,
+} from "../transforms";
 import { APFollowWithInvite } from "../types";
 import {
 	ACTIVITYSTREAMS_CONTEXT,
 	APError,
+	createChannelsFromGuildFollows,
 	fetchFederatedUser,
 	hasAPContext,
 	resolveAPObject,
@@ -106,13 +111,29 @@ const handlers = {
 		if (typeof inner.object != "string")
 			throw new APError("not implemented");
 
-		const guild = await fetchFederatedUser(inner.object);
+		const apGuild = await resolveAPObject(inner.object);
+		if (!ObjectIsOrganization(apGuild))
+			throw new APError(
+				"Accept Follow received for object other than Organisation ( Guild ), Ignoring",
+			);
+
+		if (!apGuild.following || typeof apGuild.following != "string")
+			throw new APError("Guild must be following channels");
+
+		const guild = await transformOrganisationToGuild(apGuild);
+
+		// create the channels
+
+		await createChannelsFromGuildFollows(
+			apGuild.following + "?page=true", // TODO: wrong
+			guild.id,
+		);
 
 		if (typeof inner.actor != "string")
 			throw new APError("not implemented");
 
 		const { user } = splitQualifiedMention(inner.actor);
-		Member.addToGuild(user, guild.entity.id);
+		Member.addToGuild(user, guild.id);
 	},
 } as Record<string, (activity: AnyAPObject) => Promise<unknown>>;
 
diff --git a/src/activitypub/federation/transforms.ts b/src/activitypub/federation/transforms.ts
index 9d4b11d8..a11cc62b 100644
--- a/src/activitypub/federation/transforms.ts
+++ b/src/activitypub/federation/transforms.ts
@@ -1,6 +1,7 @@
 import {
 	ActorType,
 	Channel,
+	ChannelType,
 	Config,
 	DmChannelDTO,
 	FederationKey,
@@ -8,6 +9,7 @@ import {
 	Invite,
 	Member,
 	Message,
+	Role,
 	Snowflake,
 	User,
 	UserSettings,
@@ -343,7 +345,25 @@ export const transformOrganisationToGuild = async (org: APOrganization) => {
 		owner_id: owner.entity.id,
 	});
 
+	const role = Role.create({
+		id: guild.id,
+		guild_id: guild.id,
+		color: 0,
+		hoist: false,
+		managed: false,
+		// NB: in Spacebar, every role will be non-managed, as we use user-groups instead of roles for managed groups
+		mentionable: false,
+		name: "@everyone",
+		permissions: String("2251804225"),
+		position: 0,
+		icon: undefined,
+		unicode_emoji: undefined,
+		flags: 0, // TODO?
+	});
+
 	await Promise.all([guild.save(), keys.save()]);
+	await role.save();
+
 	return guild;
 };
 
@@ -372,6 +392,7 @@ export const transformGuildToOrganisation = async (
 		inbox: `https://${host}/federation/guilds/${guild.id}/inbox`,
 		outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
 		followers: `https://${host}/federation/guilds/${guild.id}/followers`,
+		following: `https://${host}/federation/guilds/${guild.id}/following`,
 		publicKey: {
 			id: `https://${host}/federation/guilds/${guild.id}#main-key`,
 			owner: `https://${host}/federation/guilds/${guild.id}`,
@@ -379,3 +400,42 @@ export const transformGuildToOrganisation = async (
 		},
 	};
 };
+
+export const transformGroupToChannel = async (
+	group: APGroup,
+	guild_id: string,
+) => {
+	if (!group.id) throw new APError("Channel ( group ) must have ID");
+	if (!group.publicKey || !group.publicKey.publicKeyPem)
+		throw new APError("Federated guild must have public key.");
+
+	const cache = await FederationKey.findOne({
+		where: { federatedId: group.id },
+	});
+	if (cache) return Channel.findOneOrFail({ where: { id: cache.actorId } });
+
+	const keys = FederationKey.create({
+		actorId: Snowflake.generate(),
+		federatedId: group.id,
+		username: group.name,
+		domain: new URL(group.id).hostname,
+		publicKey: group.publicKey.publicKeyPem,
+		type: ActorType.CHANNEL,
+		inbox: group.inbox.toString(),
+		outbox: group.outbox.toString(),
+	});
+
+	const channel = Channel.create({
+		id: keys.actorId,
+		name: group.name,
+		type: ChannelType.GUILD_TEXT, // TODO
+		owner_id: undefined,
+		last_message_id: undefined,
+		position: 0, // TODO
+		guild_id,
+	});
+
+	await Promise.all([keys.save(), channel.save()]);
+
+	return channel;
+};
diff --git a/src/activitypub/federation/utils.ts b/src/activitypub/federation/utils.ts
index 20b37022..e879e863 100644
--- a/src/activitypub/federation/utils.ts
+++ b/src/activitypub/federation/utils.ts
@@ -2,6 +2,7 @@ import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
 import {
 	ActorType,
 	BaseClass,
+	ChannelCreateEvent,
 	Config,
 	Debug,
 	FederationActivity,
@@ -13,9 +14,11 @@ import {
 	User,
 	UserSettings,
 	WebfingerResponse,
+	emitEvent,
 } from "@spacebar/util";
 import {
 	APObject,
+	APOrderedCollection,
 	APPerson,
 	AnyAPObject,
 	ObjectIsGroup,
@@ -27,6 +30,7 @@ import fetch from "node-fetch";
 import { ProxyAgent } from "proxy-agent";
 import TurndownService from "turndown";
 import { federationQueue } from "./queue";
+import { transformGroupToChannel } from "./transforms";
 import { APFollowWithInvite } from "./types";
 
 export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams";
@@ -256,6 +260,28 @@ export const tryFederatedGuildJoin = async (code: string, user_id: string) => {
 	await federationQueue.distribute(follow.toJSON());
 };
 
+export const createChannelsFromGuildFollows = async (
+	endpoint: string,
+	guild_id: string,
+) => {
+	const collection = (await resolveAPObject(endpoint)) as APOrderedCollection; // TODO: validation
+	if (!collection.orderedItems)
+		throw new APError("Guild followers did not contain orderedItems");
+
+	// resolve every channel
+	for (const channel of collection.orderedItems) {
+		if (typeof channel == "string" || !ObjectIsGroup(channel)) continue;
+
+		const guildchannel = await transformGroupToChannel(channel, guild_id);
+
+		await emitEvent({
+			event: "CHANNEL_CREATE",
+			data: guildchannel,
+			guild_id: guildchannel.guild_id,
+		} as ChannelCreateEvent);
+	}
+};
+
 export const APObjectIsSpacebarActor = (
 	object: AnyAPObject,
 ): object is APPerson => {