From b993216651336afa4f2b62217d201a2898385165 Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Fri, 29 Sep 2023 04:59:12 +0000 Subject: [PATCH] send accept when follow is received --- src/activitypub/federation/inbox/index.ts | 82 ++++++++++++++++++++++- src/activitypub/federation/queue.ts | 15 +++-- src/activitypub/federation/transforms.ts | 2 +- src/activitypub/federation/types.ts | 5 ++ src/activitypub/federation/utils.ts | 72 ++++++++++++-------- 5 files changed, 141 insertions(+), 35 deletions(-) create mode 100644 src/activitypub/federation/types.ts diff --git a/src/activitypub/federation/inbox/index.ts b/src/activitypub/federation/inbox/index.ts index 6972b559..2b924d52 100644 --- a/src/activitypub/federation/inbox/index.ts +++ b/src/activitypub/federation/inbox/index.ts @@ -1,9 +1,33 @@ -import { Message, MessageCreateEvent, emitEvent } from "@spacebar/util"; -import { APCreate, AnyAPObject, ObjectIsNote } from "activitypub-types"; +import { + ActorType, + FederationActivity, + FederationKey, + Invite, + Member, + Message, + MessageCreateEvent, + emitEvent, +} from "@spacebar/util"; +import { + APAccept, + APCreate, + APFollow, + AnyAPObject, + ObjectIsNote, +} from "activitypub-types"; import { Request } from "express"; import { HttpSig } from "../HttpSig"; +import { federationQueue } from "../queue"; import { transformNoteToMessage } from "../transforms"; -import { APError, hasAPContext, resolveAPObject } from "../utils"; +import { APFollowWithInvite } from "../types"; +import { + ACTIVITYSTREAMS_CONTEXT, + APError, + fetchFederatedUser, + hasAPContext, + resolveAPObject, + splitQualifiedMention, +} from "../utils"; /** * Key names are derived from the object type names @@ -37,6 +61,27 @@ const handlers = { message.save(), ]); }, + + Follow: async (activity: APFollow) => { + // dummy: send back Accept regardless + + if (typeof activity.object != "string") + throw new APError("not implemented"); + const mention = splitQualifiedMention(activity.object); + + const keys = await FederationKey.findOneOrFail({ + where: { domain: mention.domain, actorId: mention.user }, + }); + + switch (keys.type) { + case ActorType.GUILD: + if (typeof activity.actor != "string") + throw new APError("not implemented"); + return addRemoteUserToGuild(activity.actor, keys, activity); + default: + throw new APError("not implemented"); + } + }, } as Record Promise>; export const genericInboxHandler = async (req: Request) => { @@ -78,3 +123,34 @@ export const genericInboxHandler = async (req: Request) => { console.warn(`Activity of type ${type} not implemented`); throw new APError(`Activity of type ${type} not implemented`); }; + +const addRemoteUserToGuild = async ( + actor: string, + guild: FederationKey, + follow: APFollow, +) => { + const invite = (follow as APFollowWithInvite).invite; + if (!invite) throw new APError("Requires invite"); + + await Invite.findOneOrFail({ + where: { + guild_id: guild.actorId, + code: splitQualifiedMention(invite).user, + }, + }); + + const { entity, keys } = await fetchFederatedUser(actor); + + await Member.addToGuild(entity.id, guild.actorId); + + const accept = await FederationActivity.create({ + data: { + type: "Accept", + "@context": ACTIVITYSTREAMS_CONTEXT, + actor: guild.federatedId, + object: follow, + } as APAccept, + }).save(); + + federationQueue.distribute(accept.toJSON()); +}; diff --git a/src/activitypub/federation/queue.ts b/src/activitypub/federation/queue.ts index 0f08dd85..290cbd35 100644 --- a/src/activitypub/federation/queue.ts +++ b/src/activitypub/federation/queue.ts @@ -1,8 +1,8 @@ -import { Config, FederationKey } from "@spacebar/util"; -import { APActivity } from "activitypub-types"; +import { Config, Debug, FederationKey } from "@spacebar/util"; +import { APActivity, ActivityIsFollow } from "activitypub-types"; import fetch from "node-fetch"; import { HttpSig } from "./HttpSig"; -import { APError, splitQualifiedMention } from "./utils"; +import { APError, LOG_NAMES, splitQualifiedMention } from "./utils"; // type Instance = string; @@ -15,6 +15,8 @@ class FederationQueue { let { actor } = activity; const { to, object } = activity; + Debug(LOG_NAMES.remote, `distributing activity ${activity.id}`); + if (!actor) throw new APError("Activity with no actor cannot be signed."); if (Array.isArray(actor)) actor = actor[0]; @@ -43,7 +45,11 @@ class FederationQueue { if (!recv) continue; // this is wrong? - if (typeof recv != "string") continue; + if (typeof recv != "string") { + if (ActivityIsFollow(recv)) { + recv = recv.actor!.toString(); + } else continue; + } if (recv == "https://www.w3.org/ns/activitystreams#Public") { console.debug(`TODO: Skipping sending activity to #Public`); @@ -58,6 +64,7 @@ class FederationQueue { // TODO: this is bad if (!recv.includes("/inbox")) recv = `${recv}/inbox`; + Debug(LOG_NAMES.remote, `sending activity to ${recv}`); await this.signAndSend(activity, sender, recv); } } diff --git a/src/activitypub/federation/transforms.ts b/src/activitypub/federation/transforms.ts index 41b76148..9d4b11d8 100644 --- a/src/activitypub/federation/transforms.ts +++ b/src/activitypub/federation/transforms.ts @@ -340,7 +340,7 @@ export const transformOrganisationToGuild = async (org: APOrganization) => { const guild = Guild.create({ id: keys.actorId, name: org.name, - owner_id: owner.user.id, + owner_id: owner.entity.id, }); await Promise.all([guild.save(), keys.save()]); diff --git a/src/activitypub/federation/types.ts b/src/activitypub/federation/types.ts new file mode 100644 index 00000000..267c093f --- /dev/null +++ b/src/activitypub/federation/types.ts @@ -0,0 +1,5 @@ +import { APFollow } from "activitypub-types"; + +export type APFollowWithInvite = APFollow & { + invite: string; +}; diff --git a/src/activitypub/federation/utils.ts b/src/activitypub/federation/utils.ts index 520c292e..20b37022 100644 --- a/src/activitypub/federation/utils.ts +++ b/src/activitypub/federation/utils.ts @@ -1,11 +1,13 @@ import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api"; import { ActorType, + BaseClass, Config, Debug, FederationActivity, FederationCache, FederationKey, + Guild, OrmUtils, Snowflake, User, @@ -13,7 +15,6 @@ import { WebfingerResponse, } from "@spacebar/util"; import { - APFollow, APObject, APPerson, AnyAPObject, @@ -26,6 +27,7 @@ import fetch from "node-fetch"; import { ProxyAgent } from "proxy-agent"; import TurndownService from "turndown"; import { federationQueue } from "./queue"; +import { APFollowWithInvite } from "./types"; export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams"; export const LOG_NAMES = { @@ -142,7 +144,9 @@ export const tryResolveWebfinger = async (lookup: string) => { }; /** Fetch from local db, if not found fetch from remote instance and save */ -export const fetchFederatedUser = async (actorId: string) => { +export const fetchFederatedUser = async ( + actorId: string, +): Promise<{ keys: FederationKey; entity: BaseClass }> => { // if we were given webfinger, resolve that first const mention = splitQualifiedMention(actorId); const cache = await FederationKey.findOne({ @@ -151,7 +155,7 @@ export const fetchFederatedUser = async (actorId: string) => { if (cache) { return { keys: cache, - user: await User.findOneOrFail({ where: { id: cache.actorId } }), + entity: await User.findOneOrFail({ where: { id: cache.actorId } }), }; } @@ -187,33 +191,46 @@ export const fetchFederatedUser = async (actorId: string) => { outbox: remoteActor.outbox, }); - const user = User.create({ - id: keys.actorId, - username: remoteActor.name, - discriminator: "0", - bio: new TurndownService().turndown(remoteActor.summary), // html -> markdown - email: `${remoteActor.preferredUsername}@${keys.domain}`, - data: { - hash: "#", - valid_tokens_since: new Date(), - }, - extended_settings: "{}", - settings: UserSettings.create(), - premium: false, + let entity: BaseClass | undefined = undefined; + if (type == ActorType.USER) + entity = User.create({ + id: keys.actorId, + username: remoteActor.name, + discriminator: "0", + bio: new TurndownService().turndown(remoteActor.summary), // html -> markdown + email: `${remoteActor.preferredUsername}@${keys.domain}`, + data: { + hash: "#", + valid_tokens_since: new Date(), + }, + extended_settings: "{}", + settings: UserSettings.create(), + premium: false, - premium_since: Config.get().defaults.user.premium - ? new Date() - : undefined, - rights: Config.get().register.defaultRights, - premium_type: Config.get().defaults.user.premiumType ?? 0, - verified: Config.get().defaults.user.verified ?? true, - created_at: new Date(), - }); + premium_since: Config.get().defaults.user.premium + ? new Date() + : undefined, + rights: Config.get().register.defaultRights, + premium_type: Config.get().defaults.user.premiumType ?? 0, + verified: Config.get().defaults.user.verified ?? true, + created_at: new Date(), + }); - await Promise.all([keys.save(), user.save()]); + if (type == ActorType.GUILD) + entity = Guild.create({ + id: keys.actorId, + name: remoteActor.name, + owner_id: ( + await fetchFederatedUser(remoteActor.attributedTo!.toString()) + ).entity.id, + }); + + if (!entity) throw new APError("not possible :3"); + + await Promise.all([keys.save(), entity.save()]); return { keys, - user, + entity, }; }; @@ -232,7 +249,8 @@ export const tryFederatedGuildJoin = async (code: string, user_id: string) => { type: "Follow", actor: `https://${host}/federation/users/${user_id}`, object: guild.id, - } as APFollow, + invite: code, + } as APFollowWithInvite, }).save(); await federationQueue.distribute(follow.toJSON());