1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-22 02:12:40 +01:00

random bullshit, go!!

This commit is contained in:
Madeline 2023-08-14 12:49:20 +10:00
parent 2cf3833499
commit c560d58f68
No known key found for this signature in database
GPG Key ID: 1958E017C36F2E47
17 changed files with 11727 additions and 4196 deletions

File diff suppressed because it is too large Load Diff

View File

@ -113,7 +113,8 @@
"@spacebar/api": "dist/api",
"@spacebar/cdn": "dist/cdn",
"@spacebar/gateway": "dist/gateway",
"@spacebar/util": "dist/util"
"@spacebar/util": "dist/util",
"@spacebar/ap": "dist/activitypub"
},
"optionalDependencies": {
"erlpack": "^0.1.4",
@ -122,4 +123,4 @@
"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
"sqlite3": "^5.1.6"
}
}
}

64
src/activitypub/Server.ts Normal file
View File

@ -0,0 +1,64 @@
import { BodyParser, CORS, ErrorHandler } from "@spacebar/api";
import {
Config,
JSONReplacer,
initDatabase,
registerRoutes,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { Server, ServerOptions } from "lambert-server";
import path from "path";
import webfinger from "./webfinger";
export class APServer extends Server {
public declare options: ServerOptions;
constructor(opts?: Partial<ServerOptions>) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
super({ ...opts, errorHandler: false, jsonBody: false });
}
async start() {
await initDatabase();
await Config.init();
this.app.set("json replacer", JSONReplacer);
this.app.use(CORS);
this.app.use(BodyParser({ inflate: true, limit: "10mb" }));
const api = Router();
const app = this.app;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// lambert server is lame
this.app = api;
this.routes = await registerRoutes(
this,
path.join(__dirname, "routes", "/"),
);
api.use("*", (req: Request, res: Response) => {
res.status(404).json({
message: "404 endpoint not found",
code: 0,
});
});
this.app = app;
this.app.use("/fed", api);
this.app.get("/fed", (req, res) => {
res.json({ ping: "pong" });
});
this.app.use("/.well-known/webfinger", webfinger);
this.app.use(ErrorHandler);
return super.start();
}
}

1
src/activitypub/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./Server";

View File

@ -0,0 +1,30 @@
import { route } from "@spacebar/api";
import { Channel, Config } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
export default router;
router.get("/", route({}), async (req: Request, res: Response) => {
const id = req.params.id;
const channel = await Channel.findOneOrFail({ where: { id } });
const { webDomain } = Config.get().federation;
return res.json({
"@context": "https://www.w3.org/ns/activitystreams",
type: "Group",
id: `https://${webDomain}/fed/channel/${channel.id}`,
name: channel.name,
preferredUsername: channel.name,
summary: channel.topic,
icon: undefined,
inbox: `https://${webDomain}/fed/channel/${channel.id}/inbox`,
outbox: `https://${webDomain}/fed/channel/${channel.id}/outbox`,
followers: `https://${webDomain}/fed/channel/${channel.id}/followers`,
following: `https://${webDomain}/fed/channel/${channel.id}/following`,
linked: `https://${webDomain}/fed/channel/${channel.id}/likeds`,
});
});

View File

@ -0,0 +1,50 @@
import { route } from "@spacebar/api";
import { Config, Message } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
export default router;
router.get("/", route({}), async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params;
const message = await Message.findOneOrFail({
where: { id: message_id, channel_id },
relations: { author: true, guild: true },
});
const { webDomain } = Config.get().federation;
return res.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: "Announce",
actor: `https://${webDomain}/fed/user/${message.author!.id}`,
published: message.timestamp,
to: ["https://www.w3.org/ns/activitystreams#Public"],
cc: [
message.author?.id
? `https://${webDomain}/fed/users/${message.author.id}`
: undefined,
`https://${webDomain}/fed/channel/${channel_id}/followers`,
],
object: {
id: `https://${webDomain}/fed/channel/${channel_id}/mesages/${message.id}`,
type: "Note",
summary: null,
inReplyTo: undefined, // TODO
published: message.timestamp,
url: `https://app.spacebar.chat/channels${
message.guild?.id ? `/${message.guild.id}` : ""
}/${channel_id}/${message.id}`,
attributedTo: `https://${webDomain}/fed/user/${message.author!.id}`,
to: ["https://www.w3.org/ns/activitystreams#Public"],
cc: [
message.author?.id
? `https://${webDomain}/fed/users/${message.author.id}`
: undefined,
`https://${webDomain}/fed/channel/${channel_id}/followers`,
],
sensitive: false,
content: message.content,
},
});
});

View File

@ -0,0 +1,76 @@
import { route } from "@spacebar/api";
import { Config, Message, Snowflake } from "@spacebar/util";
import { Router } from "express";
import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
const router = Router();
export default router;
router.get("/", route({}), async (req, res) => {
// TODO: authentication
const { channel_id } = req.params;
const { page, min_id, max_id } = req.query;
const { webDomain } = Config.get().federation;
if (!page)
return res.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `https://${webDomain}/fed/users/${channel_id}/outbox`,
type: "OrderedCollection",
first: `https://${webDomain}/fed/users/${channel_id}/outbox?page=true`,
last: `https://${webDomain}/fed/users/${channel_id}/outbox?page=true&min_id=0`,
});
const after = min_id ? `${min_id}` : undefined;
const before = max_id ? `${max_id}` : undefined;
const query: FindManyOptions<Message> & {
where: { id?: FindOperator<string> | FindOperator<string>[] };
} = {
order: { timestamp: "DESC" },
take: 20,
where: { channel_id: channel_id },
relations: ["author"],
};
if (after) {
if (BigInt(after) > BigInt(Snowflake.generate()))
return res.status(422);
query.where.id = MoreThan(after);
} else if (before) {
if (BigInt(before) > BigInt(Snowflake.generate()))
return res.status(422);
query.where.id = LessThan(before);
}
const messages = await Message.find(query);
return res.json({
"@context": "https://www.w3.org/ns/activitystreams",
id: `https://${webDomain}/fed/channel/${channel_id}/outbox?page=true`,
type: "OrderedCollection",
next: `https://${webDomain}/fed/channel/${channel_id}/outbox?page=true&max_id=${
messages[0]?.id || "0"
}`,
prev: `https://${webDomain}/fed/channel/${channel_id}/outbox?page=true&max_id=${
messages[messages.length - 1]?.id || "0"
}`,
partOf: `https://${webDomain}/fed/channel/${channel_id}/outbox`,
orderedItems: messages.map((message) => ({
id: `https://${webDomain}/fed/channel/${channel_id}/message/${message.id}`,
type: "Announce", // hmm
actor: `https://${webDomain}/fed/channel/${channel_id}`,
published: message.timestamp,
to: ["https://www.w3.org/ns/activitystreams#Public"],
cc: [
message.author?.id
? `https://${webDomain}/fed/users/${message.author.id}`
: undefined,
`https://${webDomain}/fed/channel/${channel_id}/followers`,
],
object: `https://${webDomain}/fed/channel/${channel_id}/messages/${message.id}`,
})),
});
});

View File

@ -0,0 +1,36 @@
import { route } from "@spacebar/api";
import { Config, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
export default router;
router.get("/:id", route({}), async (req: Request, res: Response) => {
const id = req.params.name;
const user = await User.findOneOrFail({ where: { id } });
const { webDomain } = Config.get().federation;
return res.json({
"@context": "https://www.w3.org/ns/activitystreams",
type: "Person",
id: `https://${webDomain}/fed/user/${user.id}`,
name: user.username,
preferredUsername: user.username,
summary: user.bio,
icon: user.avatar
? [
`${Config.get().cdn.endpointPublic}/avatars/${user.id}/${
user.avatar
}`,
]
: undefined,
inbox: `https://${webDomain}/fed/user/${user.id}/inbox`,
outbox: `https://${webDomain}/fed/user/${user.id}/outbox`,
followers: `https://${webDomain}/fed/user/${user.id}/followers`,
following: `https://${webDomain}/fed/user/${user.id}/following`,
linked: `https://${webDomain}/fed/user/${user.id}/likeds`,
});
});

7
src/activitypub/start.ts Normal file
View File

@ -0,0 +1,7 @@
require("module-alias/register");
import "dotenv/config";
import { APServer } from "./Server";
const port = Number(process.env.PORT) || 3005;
const server = new APServer({ port });
server.start().catch(console.error);

View File

View File

@ -0,0 +1,63 @@
import { route } from "@spacebar/api";
import { Channel, Config, User, WebfingerResponse } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router();
export default router;
router.get(
"/",
route({
query: {
resource: {
type: "string",
description: "Resource to locate",
},
},
responses: {
200: {
body: "WebfingerResponse",
},
},
}),
async (req: Request, res: Response<WebfingerResponse>) => {
let resource = req.query.resource as string | undefined;
if (!resource) throw new HTTPError("Must specify resource");
// we know what you mean, bro
resource = resource.replace("acct:", "");
const [resourceId, resourceDomain] = resource.split("@");
const { webDomain } = Config.get().federation;
if (resourceDomain != webDomain)
throw new HTTPError("Resource could not be found", 404);
const found =
(await User.findOne({
where: { id: resourceId },
select: ["id"],
})) ||
(await Channel.findOne({
where: { id: resourceId },
select: ["id"],
}));
if (!found) throw new HTTPError("Resource could not be found", 404);
const type = found instanceof Channel ? "channel" : "user";
return res.json({
subject: `acct:${resourceId}@${webDomain}`, // mastodon always returns acct so might as well
aliases: [`https://${webDomain}/fed/${type}/${resourceId}`],
links: [
{
rel: "self",
type: "application/activity+json",
href: `https://${webDomain}/fed/${type}/${resourceId}`,
},
],
});
},
);

View File

@ -19,13 +19,14 @@
process.on("unhandledRejection", console.error);
process.on("uncaughtException", console.error);
import http from "http";
import { APServer } from "@spacebar/ap";
import * as Api from "@spacebar/api";
import * as Gateway from "@spacebar/gateway";
import { CDNServer } from "@spacebar/cdn";
import * as Gateway from "@spacebar/gateway";
import { Config, Sentry, initDatabase } from "@spacebar/util";
import express from "express";
import { green, bold } from "picocolors";
import { Config, initDatabase, Sentry } from "@spacebar/util";
import http from "http";
import { bold, green } from "picocolors";
const app = express();
const server = http.createServer();
@ -36,12 +37,14 @@ server.on("request", app);
const api = new Api.SpacebarServer({ server, port, production, app });
const cdn = new CDNServer({ server, port, production, app });
const gateway = new Gateway.Server({ server, port, production });
const activitypub = new APServer({ server, port, production, app });
process.on("SIGTERM", async () => {
console.log("Shutting down due to SIGTERM");
await gateway.stop();
await cdn.stop();
await api.stop();
activitypub.stop();
server.close();
Sentry.close();
});
@ -54,7 +57,12 @@ async function main() {
await new Promise((resolve) =>
server.listen({ port }, () => resolve(undefined)),
);
await Promise.all([api.start(), cdn.start(), gateway.start()]);
await Promise.all([
api.start(),
cdn.start(),
gateway.start(),
activitypub.start(),
]);
Sentry.errorHandler(app);

View File

@ -38,6 +38,7 @@ import {
SentryConfiguration,
TemplateConfiguration,
} from "../config";
import { FederationConfiguration } from "./types/FederationConfiguration";
export class ConfigValue {
gateway: EndpointConfiguration = new EndpointConfiguration();
@ -61,4 +62,5 @@ export class ConfigValue {
email: EmailConfiguration = new EmailConfiguration();
passwordReset: PasswordResetConfiguration =
new PasswordResetConfiguration();
federation = new FederationConfiguration();
}

View File

@ -0,0 +1,5 @@
export class FederationConfiguration {
enabled: boolean = false;
localDomain: string | null = null;
webDomain: string | null = null;
}

View File

@ -0,0 +1,12 @@
interface WebfingerLink {
rel: string;
type: string;
href: string;
template?: string;
}
export interface WebfingerResponse {
subject: string;
aliases: string[];
links: WebfingerLink[];
}

View File

@ -28,7 +28,8 @@ export * from "./TypedResponses";
export * from "./UpdatesResponse";
export * from "./UserNoteResponse";
export * from "./UserProfileResponse";
export * from "./UserRelationshipsResponse";
export * from "./UserRelationsResponse";
export * from "./UserRelationshipsResponse";
export * from "./WebAuthnCreateResponse";
export * from "./WebfingerResponse";
export * from "./WebhookCreateResponse";

View File

@ -37,7 +37,8 @@
"@spacebar/api*": ["./api"],
"@spacebar/gateway*": ["./gateway"],
"@spacebar/cdn*": ["./cdn"],
"@spacebar/util*": ["./util"]
"@spacebar/util*": ["./util"],
"@spacebar/ap*": ["./activitypub"]
} /* Specify a set of entries that re-map imports to additional lookup locations. */,
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */