mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-27 16:24:32 +00:00
Move AP methods to own file
This commit is contained in:
Generated
BIN
Binary file not shown.
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { HTTPError } from "lambert-server";
|
||||
|
||||
export class APError extends HTTPError {}
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./APError";
|
||||
export * from "./OrderedCollection";
|
||||
export * from "./transforms/index";
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./Message";
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user