summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/activitypub/routes/channel/#channel_id/inbox.ts7
-rw-r--r--src/activitypub/routes/user/inbox.ts12
-rw-r--r--src/activitypub/routes/user/index.ts (renamed from src/activitypub/routes/user.ts)0
-rw-r--r--src/activitypub/util/APError.ts3
-rw-r--r--src/activitypub/util/index.ts2
-rw-r--r--src/activitypub/util/transforms/Message.ts141
-rw-r--r--src/activitypub/util/transforms/index.ts1
-rw-r--r--src/util/entities/Message.ts75
-rw-r--r--src/util/entities/User.ts45
9 files changed, 169 insertions, 117 deletions
diff --git a/src/activitypub/routes/channel/#channel_id/inbox.ts b/src/activitypub/routes/channel/#channel_id/inbox.ts

index ee7f4519..2dd36143 100644 --- a/src/activitypub/routes/channel/#channel_id/inbox.ts +++ b/src/activitypub/routes/channel/#channel_id/inbox.ts
@@ -1,3 +1,4 @@ +import { messageFromAP } from "@spacebar/ap"; import { route } from "@spacebar/api"; import { Message, emitEvent } from "@spacebar/util"; import { Router } from "express"; @@ -11,7 +12,11 @@ router.post("/", route({}), async (req, res) => { if (body.type != "Create") throw new HTTPError("not implemented"); - const message = await Message.fromAP(body.object); + const message = await messageFromAP(body.object); + + if ((await Message.count({ where: { id: message.id } })) != 0) + return res.status(200); + await message.save(); await emitEvent({ diff --git a/src/activitypub/routes/user/inbox.ts b/src/activitypub/routes/user/inbox.ts new file mode 100644
index 00000000..2065d7f2 --- /dev/null +++ b/src/activitypub/routes/user/inbox.ts
@@ -0,0 +1,12 @@ +import { route } from "@spacebar/api"; +import { Router } from "express"; +import { HTTPError } from "lambert-server"; + +const router = Router(); +export default router; + +router.post("/", route({}), async (req, res) => { + const body = req.body; + + if (body.type != "Create") throw new HTTPError("not implemented"); +}); diff --git a/src/activitypub/routes/user.ts b/src/activitypub/routes/user/index.ts
index 67664dea..67664dea 100644 --- a/src/activitypub/routes/user.ts +++ b/src/activitypub/routes/user/index.ts
diff --git a/src/activitypub/util/APError.ts b/src/activitypub/util/APError.ts new file mode 100644
index 00000000..184ddc32 --- /dev/null +++ b/src/activitypub/util/APError.ts
@@ -0,0 +1,3 @@ +import { HTTPError } from "lambert-server"; + +export class APError extends HTTPError {} diff --git a/src/activitypub/util/index.ts b/src/activitypub/util/index.ts
index 7204e7ae..767e75d9 100644 --- a/src/activitypub/util/index.ts +++ b/src/activitypub/util/index.ts
@@ -1 +1,3 @@ +export * from "./APError"; export * from "./OrderedCollection"; +export * from "./transforms/index"; diff --git a/src/activitypub/util/transforms/Message.ts b/src/activitypub/util/transforms/Message.ts new file mode 100644
index 00000000..3669121c --- /dev/null +++ b/src/activitypub/util/transforms/Message.ts
@@ -0,0 +1,141 @@ +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 new file mode 100644
index 00000000..6596816a --- /dev/null +++ b/src/activitypub/util/transforms/index.ts
@@ -0,0 +1 @@ +export * from "./Message"; diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 65dfa926..bbbb2ac1 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts
@@ -16,13 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import type { - APAnnounce, - APNote, - APPerson, - AnyAPObject, -} from "activitypub-types"; -import fetch from "node-fetch"; +import type { APAnnounce, APNote } from "activitypub-types"; import { Column, CreateDateColumn, @@ -35,7 +29,7 @@ import { OneToMany, RelationId, } from "typeorm"; -import { Config, Snowflake } from ".."; +import { Config } from ".."; import { InteractionType } from "../interfaces/Interaction"; import { Application } from "./Application"; import { Attachment } from "./Attachment"; @@ -262,6 +256,7 @@ export class Message extends BaseClass { }; } + // TODO: move to AP module toAP(): APNote { const { webDomain } = Config.get().federation; @@ -275,70 +270,6 @@ export class Message extends BaseClass { content: this.content, }; } - - static async fromAP(data: APNote): Promise<Message> { - if (!data.attributedTo) - throw new Error("sb Message must have author (attributedTo)"); - - let attrib = Array.isArray(data.attributedTo) - ? data.attributedTo[0] - : data.attributedTo; - if (typeof attrib == "string") { - // fetch it - attrib = (await fetch(attrib, { - headers: { Accept: "application/activity+json" }, - }).then((x) => x.json())) as AnyAPObject; - } - - if (attrib.type != "Person") - throw new Error("only Person can be author of sb Message"); //hm - - let to = data.to; - - if (Array.isArray(to)) - to = to.filter((x) => { - if (typeof x == "string") return x.includes("channel"); - return false; - })[0]; - - if (!to) throw new Error("not deliverable"); - - const channel_id = (to as string).split("/").reverse()[0]; - - const channel = await Channel.findOneOrFail({ - where: { id: channel_id }, - relations: { guild: true }, - }); - - const user = await User.fromAP(attrib as APPerson); - let member; - if ( - (await Member.count({ - where: { id: user.id, guild_id: channel.guild_id }, - })) == 0 - ) - member = await Member.addToGuild(user.id, channel.guild.id); - - return Message.create({ - id: Snowflake.generate(), - author: user, - member, - content: data.content, // convert html to markdown - timestamp: data.published, - channel, - guild: channel.guild, - - sticker_items: [], - guild_id: channel.guild_id, - attachments: [], - embeds: [], - reactions: [], - type: 0, - mentions: [], - mention_roles: [], - mention_channels: [], - }); - } } export interface MessageComponent { diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 25902ba7..f9213693 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts
@@ -37,7 +37,6 @@ import { Session } from "./Session"; import { UserSettings } from "./UserSettings"; import crypto from "crypto"; -import fetch from "node-fetch"; import { promisify } from "util"; const generateKeyPair = promisify(crypto.generateKeyPair); @@ -293,6 +292,7 @@ export class User extends BaseClass { return user as UserPrivate; } + // TODO: move to AP module toAP(): APPersonButMore { const { webDomain } = Config.get().federation; @@ -323,49 +323,6 @@ export class User extends BaseClass { }; } - static async fromAP(data: APPerson | string): Promise<User> { - if (typeof data == "string") { - data = (await fetch(data, { - headers: { Accept: "application/json" }, - }).then((x) => x.json())) as APPerson; - } - - const cache = await User.findOne({ - where: { - email: `${data.preferredUsername}@${ - new URL(data.id!).hostname - }`, - }, - }); - if (cache) return cache; - - return User.create({ - id: Snowflake.generate(), // hm - username: data.preferredUsername, - discriminator: new URL(data.id!).hostname, - premium: false, - bio: data.summary, // TODO: convert to markdown - - email: `${data.preferredUsername}@${new URL(data.id!).hostname}`, - data: { - hash: "#", - valid_tokens_since: new Date(), - }, - extended_settings: "{}", - settings: UserSettings.create(), - publicKey: "", - privateKey: "", - - 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(), - }).save(); - } - static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) { return await User.findOneOrFail({ where: { id: user_id },