summary refs log tree commit diff
path: root/src/activitypub/util/transforms
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-08-16 10:13:22 +0000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-08-16 10:17:56 +0000
commit0941df15831c64d5321bbbf381e09e4340b44268 (patch)
treeb320cb5b3a39270abcabc865f57f893715078e3f /src/activitypub/util/transforms
parentMove AP methods to own file (diff)
downloadserver-0941df15831c64d5321bbbf381e09e4340b44268.tar.xz
a ton of broken shit and approx 1 nice function
Diffstat (limited to 'src/activitypub/util/transforms')
-rw-r--r--src/activitypub/util/transforms/Message.ts141
-rw-r--r--src/activitypub/util/transforms/index.ts197
2 files changed, 196 insertions, 142 deletions
diff --git a/src/activitypub/util/transforms/Message.ts b/src/activitypub/util/transforms/Message.ts
deleted file mode 100644

index 3669121c..00000000 --- a/src/activitypub/util/transforms/Message.ts +++ /dev/null
@@ -1,141 +0,0 @@ -import { APError } from "@spacebar/ap"; -import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api"; -import { - Channel, - Config, - Member, - Message, - OrmUtils, - Snowflake, - User, - UserSettings, -} from "@spacebar/util"; -import { APNote, APPerson, AnyAPObject } from "activitypub-types"; -import fetch from "node-fetch"; -import { ProxyAgent } from "proxy-agent"; -import TurndownService from "turndown"; - -const fetchOpts = OrmUtils.mergeDeep(DEFAULT_FETCH_OPTIONS, { - headers: { - Accept: "application/activity+json", - }, -}); - -const hasAPContext = (data: object) => { - if (!("@context" in data)) return false; - const context = data["@context"]; - const activitystreams = "https://www.w3.org/ns/activitystreams"; - if (Array.isArray(context)) - return context.find((x) => x == activitystreams); - return context == activitystreams; -}; - -export const resolveAPObject = async <T>(data: string | T): Promise<T> => { - // we were already given an AP object - if (typeof data != "string") return data; - - const agent = new ProxyAgent(); - const ret = await fetch(data, { - ...fetchOpts, - agent, - }); - - const json = await ret.json(); - - if (!hasAPContext(json)) throw new APError("Object is not APObject"); - - return json; -}; - -export const messageFromAP = async (data: APNote): Promise<Message> => { - if (!data.id) throw new APError("Message must have ID"); - if (data.type != "Note") throw new APError("Message must be Note"); - - const to = Array.isArray(data.to) - ? data.to.filter((x) => - typeof x == "string" ? x.includes("channel") : false, - )[0] - : data.to; - if (!to || typeof to != "string") - throw new APError("Message not deliverable"); - - // TODO: use a regex - const channel_id = to.split("/").reverse()[0]; - const channel = await Channel.findOneOrFail({ - where: { id: channel_id }, - relations: { guild: true }, - }); - - if (!data.attributedTo) - throw new APError("Message must have author (attributedTo)"); - const attrib = await resolveAPObject( - Array.isArray(data.attributedTo) - ? data.attributedTo[0] // hmm - : data.attributedTo, - ); - - if (!APObjectIsPerson(attrib)) - throw new APError("Message attributedTo must be Person"); - - const user = await userFromAP(attrib); - const member = channel.guild - ? await Member.findOneOrFail({ - where: { id: user.id, guild_id: channel.guild.id }, - }) - : undefined; - - return Message.create({ - id: data.id, - content: new TurndownService().turndown(data.content), - timestamp: data.published, - author: user, - guild: channel.guild, - member, - channel, - - type: 0, - sticker_items: [], - attachments: [], - embeds: [], - reactions: [], - mentions: [], - mention_roles: [], - mention_channels: [], - }); -}; - -export const APObjectIsPerson = (object: AnyAPObject): object is APPerson => { - return object.type == "Person"; -}; - -export const userFromAP = async (data: APPerson): Promise<User> => { - if (!data.id) throw new APError("User must have ID"); - - const url = new URL(data.id); - const email = `${url.pathname.split("/").reverse()[0]}@${url.hostname}`; - - return User.create({ - id: Snowflake.generate(), - username: data.preferredUsername, - discriminator: url.hostname, - bio: new TurndownService().turndown(data.summary), - email, - data: { - hash: "#", - valid_tokens_since: new Date(), - }, - extended_settings: "{}", - settings: UserSettings.create(), - publicKey: "", - privateKey: "", - 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(), - }); -}; diff --git a/src/activitypub/util/transforms/index.ts b/src/activitypub/util/transforms/index.ts
index 6596816a..e8107ca0 100644 --- a/src/activitypub/util/transforms/index.ts +++ b/src/activitypub/util/transforms/index.ts
@@ -1 +1,196 @@ -export * from "./Message"; +import { APError, fetchOpts } from "@spacebar/ap"; +import { + Channel, + Config, + DmChannelDTO, + Member, + Message, + Snowflake, + User, + UserSettings, + WebfingerResponse, +} from "@spacebar/util"; +import { APNote, APPerson, AnyAPObject } from "activitypub-types"; +import fetch from "node-fetch"; +import { ProxyAgent } from "proxy-agent"; +import TurndownService from "turndown"; + +const hasAPContext = (data: object) => { + if (!("@context" in data)) return false; + const context = data["@context"]; + const activitystreams = "https://www.w3.org/ns/activitystreams"; + if (Array.isArray(context)) + return context.find((x) => x == activitystreams); + return context == activitystreams; +}; + +export const resolveAPObject = async <T>(data: string | T): Promise<T> => { + // we were already given an AP object + if (typeof data != "string") return data; + + const agent = new ProxyAgent(); + const ret = await fetch(data, { + ...fetchOpts, + agent, + }); + + const json = await ret.json(); + + if (!hasAPContext(json)) throw new APError("Object is not APObject"); + + return json; +}; + +export const resolveWebfinger = async ( + lookup: string, +): Promise<AnyAPObject> => { + let domain: string, user: string; + if (lookup.includes("@")) { + // lookup a @handle + + if (lookup[0] == "@") lookup = lookup.slice(1); + [domain, user] = lookup.split("@"); + } else { + // lookup was a URL ( hopefully ) + const url = new URL(lookup); + domain = url.hostname; + user = url.pathname.split("/").reverse()[0]; + } + + const agent = new ProxyAgent(); + const wellknown = (await fetch( + `https://${domain}/.well-known/webfinger?resource=${lookup}`, + { + agent, + ...fetchOpts, + }, + ).then((x) => x.json())) as WebfingerResponse; + + 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<AnyAPObject>(link.href); +}; + +export const messageFromAP = async (data: APNote): Promise<Message> => { + if (!data.id) throw new APError("Message must have ID"); + if (data.type != "Note") throw new APError("Message must be Note"); + + if (!data.attributedTo) + throw new APError("Message must have author (attributedTo)"); + const attrib = await resolveAPObject( + Array.isArray(data.attributedTo) + ? data.attributedTo[0] // hmm + : data.attributedTo, + ); + + if (!APObjectIsPerson(attrib)) + throw new APError("Message attributedTo must be Person"); + + const user = await userFromAP(attrib); + + const to = Array.isArray(data.to) + ? data.to.filter((x) => + typeof x == "string" + ? x.includes("channel") || x.includes("user") + : false, + )[0] + : data.to; + if (!to || typeof to != "string") + throw new APError("Message not deliverable"); + + // TODO: use a regex + + let channel: Channel | DmChannelDTO; + const to_id = to.split("/").reverse()[0]; + if (to.includes("user")) { + // this is a DM channel + const toUser = await User.findOneOrFail({ where: { id: to_id } }); + + // Channel.createDMCHannel does a .save() so the author must be present + await user.save(); + + // const cache = await Channel.findOne({ where: { recipients: []}}) + + channel = await Channel.createDMChannel( + [toUser.id, user.id], + toUser.id, + ); + } else { + channel = await Channel.findOneOrFail({ + where: { id: to_id }, + relations: { guild: true }, + }); + } + + const member = + channel instanceof Channel + ? await Member.findOneOrFail({ + where: { id: user.id, guild_id: channel.guild.id }, + }) + : undefined; + + return Message.create({ + id: Snowflake.generate(), + federatedId: data.id, + content: new TurndownService().turndown(data.content), + timestamp: data.published, + author: user, + guild: channel instanceof Channel ? channel.guild : undefined, + member, + channel_id: channel.id, + + type: 0, + sticker_items: [], + attachments: [], + embeds: [], + reactions: [], + mentions: [], + mention_roles: [], + mention_channels: [], + }); +}; + +export const APObjectIsPerson = (object: AnyAPObject): object is APPerson => { + return object.type == "Person"; +}; + +export const userFromAP = async (data: APPerson): Promise<User> => { + if (!data.id) throw new APError("User must have ID"); + + const url = new URL(data.id); + const email = `${url.pathname.split("/").reverse()[0]}@${url.hostname}`; + + // don't like this + // the caching should probably be done elsewhere + // this function should only be for converting AP to SB (ideally) + const cache = await User.findOne({ + where: { federatedId: url.toString() }, + }); + if (cache) return cache; + + return User.create({ + federatedId: url.toString(), + username: data.preferredUsername, + discriminator: url.hostname, + bio: new TurndownService().turndown(data.summary), + email, + data: { + hash: "#", + valid_tokens_since: new Date(), + }, + extended_settings: "{}", + settings: UserSettings.create(), + publicKey: "", + privateKey: "", + 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(), + }); +};