diff --git a/scripts/ap/get.js b/scripts/ap/get.js new file mode 100644 index 00000000..ab235427 --- /dev/null +++ b/scripts/ap/get.js @@ -0,0 +1,34 @@ +const nodeFetch = require("node-fetch"); + +const fetch = (url, opts) => + nodeFetch(url, { + ...opts, + headers: { + Accept: "application/activity+json", + ...(opts?.headers || {}), + }, + }).then((x) => x.json()); + +const webfinger = async (domain, user) => { + const query = `https://${domain}/.well-known/webfinger?resource=@${user}@${domain}`; + const json = await fetch(query); + return json.links.find((x) => x.rel == "self").href; +}; + +(async () => { + const userLocation = await webfinger( + "chat.understars.dev", + "1140599542186631381", + ); + console.log(userLocation); + + const user = await fetch(userLocation); + + const outbox = await fetch(user.outbox); + + const firstPage = await fetch(outbox.first); + + const mostRecent = firstPage.orderedItems[0]; + + console.log(mostRecent); +})(); diff --git a/src/activitypub/routes/channel/#channel_id/inbox.ts b/src/activitypub/routes/channel/#channel_id/inbox.ts index 01e267e7..a44944b6 100644 --- a/src/activitypub/routes/channel/#channel_id/inbox.ts +++ b/src/activitypub/routes/channel/#channel_id/inbox.ts @@ -1,9 +1,22 @@ import { route } from "@spacebar/api"; +import { Message, emitEvent } from "@spacebar/util"; import { Router } from "express"; +import { HTTPError } from "lambert-server"; const router = Router(); export default router; router.post("/", route({}), async (req, res) => { - console.log(req.body); + const body = req.body; + + if (body.type != "Create") throw new HTTPError("not implemented"); + + const message = await Message.fromAP(body); + await message.save(); + + await emitEvent({ + event: "MESSAGE_CREATE", + channel_id: message.channel_id, + data: message.toJSON(), + }); }); diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index 07ec8195..d157ba35 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -16,7 +16,13 @@ along with this program. If not, see . */ -import type { APAnnounce, APNote } from "activitypub-types"; +import type { + APAnnounce, + APNote, + APPerson, + AnyAPObject, +} from "activitypub-types"; +import fetch from "node-fetch"; import { Column, CreateDateColumn, @@ -29,7 +35,7 @@ import { OneToMany, RelationId, } from "typeorm"; -import { Config } from ".."; +import { Config, Snowflake } from ".."; import { InteractionType } from "../interfaces/Interaction"; import { Application } from "./Application"; import { Attachment } from "./Attachment"; @@ -269,6 +275,54 @@ export class Message extends BaseClass { content: this.content, }; } + + static async fromAP(data: APNote): Promise { + 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).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; + }); + + 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 }, + }); + + return Message.create({ + id: Snowflake.generate(), + author: await User.fromAP(attrib as APPerson), + content: data.content, // convert html to markdown + timestamp: data.published, + channel_id, + + sticker_items: [], + guild_id: channel.guild_id, + attachments: [], + embeds: [], + reactions: [], + type: 0, + }); + } } export interface MessageComponent { diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 1adeae3a..59713bce 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -37,6 +37,7 @@ 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); @@ -322,6 +323,20 @@ export class User extends BaseClass { }; } + static async fromAP(data: APPerson | string): Promise { + if (typeof data == "string") { + data = (await fetch(data, { + headers: { Accept: "application/activity+json" }, + }).then((x) => x.json())) as APPerson; + } + + return User.create({ + id: Snowflake.generate(), // hm + username: data.preferredUsername, + bio: data.summary, // TODO: convert to markdown + }); + } + static async getPublicUser(user_id: string, opts?: FindOneOptions) { return await User.findOneOrFail({ where: { id: user_id },