Move AP methods to own file

This commit is contained in:
Madeline
2023-08-15 13:26:32 +00:00
parent d4f2859957
commit 432750c37b
11 changed files with 171 additions and 117 deletions
BIN
View File
Binary file not shown.
+2
View File
@@ -54,6 +54,7 @@
"@types/nodemailer": "^6.4.9",
"@types/probe-image-size": "^7.2.0",
"@types/sharp": "^0.31.1",
"@types/turndown": "^5.0.1",
"@types/ws": "^8.5.5",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
@@ -105,6 +106,7 @@
"reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tslib": "^2.6.1",
"turndown": "^7.1.2",
"typeorm": "^0.3.17",
"typescript-json-schema": "^0.50.1",
"wretch": "^2.6.0",
@@ -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({
+12
View File
@@ -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");
});
+3
View File
@@ -0,0 +1,3 @@
import { HTTPError } from "lambert-server";
export class APError extends HTTPError {}
+2
View File
@@ -1 +1,3 @@
export * from "./APError";
export * from "./OrderedCollection";
export * from "./transforms/index";
+141
View File
@@ -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(),
});
};
+1
View File
@@ -0,0 +1 @@
export * from "./Message";
+3 -72
View File
@@ -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 {
+1 -44
View File
@@ -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 },