mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-25 19:52:36 +01:00
a ton of broken shit and approx 1 nice function
This commit is contained in:
parent
432750c37b
commit
0941df1583
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -15,6 +15,9 @@
|
|||||||
"skipFiles": ["<node_internals>/**"],
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"program": "${workspaceFolder}/src/bundle/start.ts",
|
"program": "${workspaceFolder}/src/bundle/start.ts",
|
||||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
|
"env": {
|
||||||
|
"CONFIG_PATH": "./config.json"
|
||||||
|
},
|
||||||
"preLaunchTask": "tsc: build - tsconfig.json"
|
"preLaunchTask": "tsc: build - tsconfig.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
180228
assets/schemas.json
180228
assets/schemas.json
File diff suppressed because it is too large
Load Diff
6
package-lock.json
generated
6
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"eventemitter2": "^6.4.9",
|
||||||
"exif-be-gone": "^1.3.2",
|
"exif-be-gone": "^1.3.2",
|
||||||
"fast-zlib": "^2.0.1",
|
"fast-zlib": "^2.0.1",
|
||||||
"fido2-lib": "^3.4.1",
|
"fido2-lib": "^3.4.1",
|
||||||
@ -4078,6 +4079,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter2": {
|
||||||
|
"version": "6.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
|
||||||
|
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
|
||||||
|
@ -79,6 +79,7 @@
|
|||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"eventemitter2": "^6.4.9",
|
||||||
"exif-be-gone": "^1.3.2",
|
"exif-be-gone": "^1.3.2",
|
||||||
"fast-zlib": "^2.0.1",
|
"fast-zlib": "^2.0.1",
|
||||||
"fido2-lib": "^3.4.1",
|
"fido2-lib": "^3.4.1",
|
||||||
|
@ -9,6 +9,7 @@ import bodyParser from "body-parser";
|
|||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import { Server, ServerOptions } from "lambert-server";
|
import { Server, ServerOptions } from "lambert-server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { setupListener } from "./listener";
|
||||||
import hostMeta from "./well-known/host-meta";
|
import hostMeta from "./well-known/host-meta";
|
||||||
import webfinger from "./well-known/webfinger";
|
import webfinger from "./well-known/webfinger";
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ export class APServer extends Server {
|
|||||||
async start() {
|
async start() {
|
||||||
await initDatabase();
|
await initDatabase();
|
||||||
await Config.init();
|
await Config.init();
|
||||||
|
setupListener();
|
||||||
|
|
||||||
this.app.set("json replacer", JSONReplacer);
|
this.app.set("json replacer", JSONReplacer);
|
||||||
|
|
||||||
|
136
src/activitypub/listener/index.ts
Normal file
136
src/activitypub/listener/index.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import {
|
||||||
|
APError,
|
||||||
|
APObjectIsPerson,
|
||||||
|
fetchOpts,
|
||||||
|
resolveWebfinger,
|
||||||
|
} from "@spacebar/ap";
|
||||||
|
import {
|
||||||
|
Channel,
|
||||||
|
Config,
|
||||||
|
EVENTEnum,
|
||||||
|
Event,
|
||||||
|
Message,
|
||||||
|
MessageCreateEvent,
|
||||||
|
OrmUtils,
|
||||||
|
RabbitMQ,
|
||||||
|
User,
|
||||||
|
events,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
const sendSignedMessage = async (
|
||||||
|
inbox: string,
|
||||||
|
sender: `${"user" | "channel"}/${string}`,
|
||||||
|
message: object,
|
||||||
|
privateKey: string,
|
||||||
|
) => {
|
||||||
|
const digest = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(JSON.stringify(message))
|
||||||
|
.digest("base64");
|
||||||
|
const signer = crypto.createSign("sha256");
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const url = new URL(inbox);
|
||||||
|
const inboxFrag = url.pathname;
|
||||||
|
const toSign =
|
||||||
|
`(request-target): post ${inboxFrag}\n` +
|
||||||
|
`host: ${url.hostname}\n` +
|
||||||
|
`date: ${now.toUTCString()}\n` +
|
||||||
|
`digest: SHA-256=${digest}`;
|
||||||
|
|
||||||
|
signer.update(toSign);
|
||||||
|
signer.end();
|
||||||
|
|
||||||
|
const signature = signer.sign(privateKey);
|
||||||
|
const sig_b64 = signature.toString("base64");
|
||||||
|
|
||||||
|
const { webDomain } = Config.get().federation;
|
||||||
|
const header =
|
||||||
|
`keyId="https://${webDomain}/fed/${sender}",` +
|
||||||
|
`headers="(request-target) host date digest",` +
|
||||||
|
`signature=${sig_b64}`;
|
||||||
|
|
||||||
|
return await fetch(
|
||||||
|
inbox,
|
||||||
|
OrmUtils.mergeDeep(fetchOpts, {
|
||||||
|
method: "POST",
|
||||||
|
body: message,
|
||||||
|
headers: {
|
||||||
|
Host: url.hostname,
|
||||||
|
Date: now.toUTCString(),
|
||||||
|
Digest: `SHA-256=${digest}`,
|
||||||
|
Signature: header,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage = async (event: MessageCreateEvent) => {
|
||||||
|
const channel_id = event.channel_id;
|
||||||
|
const channel = await Channel.findOneOrFail({
|
||||||
|
where: { id: channel_id },
|
||||||
|
relations: {
|
||||||
|
recipients: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (channel.isDm()) {
|
||||||
|
const message = await Message.findOneOrFail({
|
||||||
|
where: { id: event.data.id },
|
||||||
|
});
|
||||||
|
const apMessage = message.toCreateAP();
|
||||||
|
|
||||||
|
for (const recipient of channel.recipients || []) {
|
||||||
|
if (recipient.user.federatedId) {
|
||||||
|
const user = await resolveWebfinger(recipient.user.federatedId);
|
||||||
|
if (!APObjectIsPerson(user))
|
||||||
|
throw new APError("Cannot deliver message");
|
||||||
|
|
||||||
|
if (!user.id) throw new APError("Receiver ID is null?");
|
||||||
|
|
||||||
|
apMessage.to = [user.id];
|
||||||
|
|
||||||
|
const sender = await User.findOneOrFail({
|
||||||
|
where: { id: event.data.author_id },
|
||||||
|
select: ["privateKey"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof user.inbox != "string")
|
||||||
|
throw new APError("inbox must be URL");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
await sendSignedMessage(
|
||||||
|
user.inbox,
|
||||||
|
`user/${event.data.author_id}`,
|
||||||
|
message,
|
||||||
|
sender.privateKey,
|
||||||
|
).then((x) => x.text()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type ListenerFunc = (event: Event) => Promise<void>;
|
||||||
|
|
||||||
|
const listeners = {
|
||||||
|
MESSAGE_CREATE: onMessage,
|
||||||
|
} as Record<EVENTEnum, ListenerFunc>;
|
||||||
|
|
||||||
|
export const setupListener = () => {
|
||||||
|
if (RabbitMQ.connection)
|
||||||
|
throw new APError("Activitypub module has not implemented RabbitMQ");
|
||||||
|
|
||||||
|
// for (const event in listeners) {
|
||||||
|
// // process.setMaxListeners(process.getMaxListeners() + 1);
|
||||||
|
// // process.addListener("message", (msg) =>
|
||||||
|
// // listener(msg as ProcessEvent, event, listeners[event as EVENTEnum]),
|
||||||
|
// // );
|
||||||
|
|
||||||
|
events.setMaxListeners(events.getMaxListeners() + 1);
|
||||||
|
events.onAny((event, msg) => listeners[msg.event as EVENTEnum]?.(msg));
|
||||||
|
// }
|
||||||
|
};
|
@ -14,7 +14,11 @@ router.post("/", route({}), async (req, res) => {
|
|||||||
|
|
||||||
const message = await messageFromAP(body.object);
|
const message = await messageFromAP(body.object);
|
||||||
|
|
||||||
if ((await Message.count({ where: { id: message.id } })) != 0)
|
if (
|
||||||
|
(await Message.count({
|
||||||
|
where: { federatedId: message.federatedId },
|
||||||
|
})) != 0
|
||||||
|
)
|
||||||
return res.status(200);
|
return res.status(200);
|
||||||
|
|
||||||
await message.save();
|
await message.save();
|
||||||
|
33
src/activitypub/routes/user/#user_id/inbox.ts
Normal file
33
src/activitypub/routes/user/#user_id/inbox.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { messageFromAP } from "@spacebar/ap";
|
||||||
|
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) => {
|
||||||
|
const body = req.body;
|
||||||
|
|
||||||
|
if (body.type != "Create") throw new HTTPError("not implemented");
|
||||||
|
|
||||||
|
const message = await messageFromAP(body.object);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(await Message.count({
|
||||||
|
where: { federatedId: message.federatedId },
|
||||||
|
})) != 0
|
||||||
|
)
|
||||||
|
return res.status(200);
|
||||||
|
|
||||||
|
await message.save();
|
||||||
|
|
||||||
|
await emitEvent({
|
||||||
|
event: "MESSAGE_CREATE",
|
||||||
|
channel_id: message.channel_id,
|
||||||
|
data: message.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200);
|
||||||
|
});
|
@ -5,8 +5,8 @@ import { Request, Response, Router } from "express";
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
router.get("/:id", route({}), async (req: Request, res: Response) => {
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
const id = req.params.id;
|
const id = req.params.user_id;
|
||||||
|
|
||||||
const user = await User.findOneOrFail({ where: { id } });
|
const user = await User.findOneOrFail({ where: { id } });
|
||||||
|
|
@ -1,12 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
8
src/activitypub/util/fetch.ts
Normal file
8
src/activitypub/util/fetch.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
|
||||||
|
import { OrmUtils } from "@spacebar/util";
|
||||||
|
|
||||||
|
export const fetchOpts = OrmUtils.mergeDeep(DEFAULT_FETCH_OPTIONS, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/activity+json",
|
||||||
|
},
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./APError";
|
export * from "./APError";
|
||||||
export * from "./OrderedCollection";
|
export * from "./OrderedCollection";
|
||||||
|
export * from "./fetch";
|
||||||
export * from "./transforms/index";
|
export * from "./transforms/index";
|
||||||
|
@ -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(),
|
|
||||||
});
|
|
||||||
};
|
|
@ -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(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "@spacebar/ap";
|
||||||
export * from "@spacebar/api";
|
export * from "@spacebar/api";
|
||||||
export * from "@spacebar/util";
|
|
||||||
export * from "@spacebar/gateway";
|
|
||||||
export * from "@spacebar/cdn";
|
export * from "@spacebar/cdn";
|
||||||
|
export * from "@spacebar/gateway";
|
||||||
|
export * from "@spacebar/util";
|
||||||
|
@ -387,6 +387,18 @@ export class Channel extends BaseClass {
|
|||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
name = trimSpecial(name);
|
name = trimSpecial(name);
|
||||||
|
|
||||||
|
const { publicKey, privateKey } = await generateKeyPair("rsa", {
|
||||||
|
modulusLength: 4096,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: "spki",
|
||||||
|
format: "pem",
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: "pkcs8",
|
||||||
|
format: "pem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
channel = await Channel.create({
|
channel = await Channel.create({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
@ -403,6 +415,8 @@ export class Channel extends BaseClass {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
|
publicKey,
|
||||||
|
privateKey,
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { APAnnounce, APNote } from "activitypub-types";
|
import type { APAnnounce, APCreate, APNote } from "activitypub-types";
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@ -220,6 +220,9 @@ export class Message extends BaseClass {
|
|||||||
@Column({ type: "simple-json", nullable: true })
|
@Column({ type: "simple-json", nullable: true })
|
||||||
components?: MessageComponent[];
|
components?: MessageComponent[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
federatedId: string;
|
||||||
|
|
||||||
toJSON(): Message {
|
toJSON(): Message {
|
||||||
return {
|
return {
|
||||||
...this,
|
...this,
|
||||||
@ -227,6 +230,7 @@ export class Message extends BaseClass {
|
|||||||
member_id: undefined,
|
member_id: undefined,
|
||||||
webhook_id: undefined,
|
webhook_id: undefined,
|
||||||
application_id: undefined,
|
application_id: undefined,
|
||||||
|
federatedId: undefined,
|
||||||
|
|
||||||
nonce: this.nonce ?? undefined,
|
nonce: this.nonce ?? undefined,
|
||||||
tts: this.tts ?? false,
|
tts: this.tts ?? false,
|
||||||
@ -256,6 +260,19 @@ export class Message extends BaseClass {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toCreateAP(): APCreate {
|
||||||
|
const { webDomain } = Config.get().federation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
type: "Create",
|
||||||
|
id: `https://${webDomain}/fed/channel/${this.channel_id}/messages/${this.id}`,
|
||||||
|
to: [],
|
||||||
|
actor: `https://${webDomain}/fed/user/${this.author_id}`,
|
||||||
|
object: this.toAP(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: move to AP module
|
// TODO: move to AP module
|
||||||
toAP(): APNote {
|
toAP(): APNote {
|
||||||
const { webDomain } = Config.get().federation;
|
const { webDomain } = Config.get().federation;
|
||||||
|
@ -252,6 +252,9 @@ export class User extends BaseClass {
|
|||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
federatedId: string;
|
||||||
|
|
||||||
// TODO: I don't like this method?
|
// TODO: I don't like this method?
|
||||||
validate() {
|
validate() {
|
||||||
if (this.discriminator) {
|
if (this.discriminator) {
|
||||||
|
@ -19,4 +19,7 @@
|
|||||||
export interface MessageAcknowledgeSchema {
|
export interface MessageAcknowledgeSchema {
|
||||||
manual?: boolean;
|
manual?: boolean;
|
||||||
mention_count?: number;
|
mention_count?: number;
|
||||||
|
flags?: number;
|
||||||
|
last_viewed?: number;
|
||||||
|
token?: unknown; // was null
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Channel } from "amqplib";
|
import { Channel } from "amqplib";
|
||||||
import { RabbitMQ } from "./RabbitMQ";
|
import EventEmitter from "eventemitter2";
|
||||||
import EventEmitter from "events";
|
|
||||||
import { EVENT, Event } from "../interfaces";
|
import { EVENT, Event } from "../interfaces";
|
||||||
|
import { RabbitMQ } from "./RabbitMQ";
|
||||||
export const events = new EventEmitter();
|
export const events = new EventEmitter();
|
||||||
|
|
||||||
export async function emitEvent(payload: Omit<Event, "created_at">) {
|
export async function emitEvent(payload: Omit<Event, "created_at">) {
|
||||||
|
Loading…
Reference in New Issue
Block a user