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:
parent
2cf3833499
commit
c560d58f68
15548
assets/schemas.json
15548
assets/schemas.json
File diff suppressed because it is too large
Load Diff
@ -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
64
src/activitypub/Server.ts
Normal 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
1
src/activitypub/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Server";
|
30
src/activitypub/routes/channel/#channel_id/index.ts
Normal file
30
src/activitypub/routes/channel/#channel_id/index.ts
Normal 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`,
|
||||
});
|
||||
});
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
76
src/activitypub/routes/channel/#channel_id/outbox.ts
Normal file
76
src/activitypub/routes/channel/#channel_id/outbox.ts
Normal 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}`,
|
||||
})),
|
||||
});
|
||||
});
|
36
src/activitypub/routes/user.ts
Normal file
36
src/activitypub/routes/user.ts
Normal 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
7
src/activitypub/start.ts
Normal 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);
|
0
src/activitypub/util/actor.ts
Normal file
0
src/activitypub/util/actor.ts
Normal file
63
src/activitypub/webfinger/index.ts
Normal file
63
src/activitypub/webfinger/index.ts
Normal 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}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
5
src/util/config/types/FederationConfiguration.ts
Normal file
5
src/util/config/types/FederationConfiguration.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class FederationConfiguration {
|
||||
enabled: boolean = false;
|
||||
localDomain: string | null = null;
|
||||
webDomain: string | null = null;
|
||||
}
|
12
src/util/schemas/responses/WebfingerResponse.ts
Normal file
12
src/util/schemas/responses/WebfingerResponse.ts
Normal file
@ -0,0 +1,12 @@
|
||||
interface WebfingerLink {
|
||||
rel: string;
|
||||
type: string;
|
||||
href: string;
|
||||
template?: string;
|
||||
}
|
||||
|
||||
export interface WebfingerResponse {
|
||||
subject: string;
|
||||
aliases: string[];
|
||||
links: WebfingerLink[];
|
||||
}
|
@ -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";
|
||||
|
@ -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'. */
|
||||
|
Loading…
Reference in New Issue
Block a user