mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-22 02:12:40 +01:00
send Follow request to guild when remote invite code used
This commit is contained in:
parent
904618e0a7
commit
c82b71695d
23
.vscode/launch.json
vendored
23
.vscode/launch.json
vendored
@ -15,7 +15,28 @@
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/src/bundle/start.ts",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "tsc: build - tsconfig.json"
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "API",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/src/api/start.ts",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Activitypub",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/src/activitypub/start.ts",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
"start": "node dist/bundle/start.js",
|
||||
"start:api": "node dist/api/start.js",
|
||||
"start:cdn": "node dist/cdn/start.js",
|
||||
"start:ap": "node dist/activitypub/start.js",
|
||||
"start:gateway": "node dist/gateway/start.js",
|
||||
"build": "tsc -p .",
|
||||
"test": "node scripts/test.js",
|
||||
@ -126,4 +127,4 @@
|
||||
"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
|
||||
"sqlite3": "^5.1.6"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,26 @@
|
||||
# Spacebar Activitypub
|
||||
|
||||
## Activitypub Specification
|
||||
|
||||
- [Activitystreams vocab](https://www.w3.org/TR/activitystreams-vocabulary)
|
||||
- [Activitystreams](https://www.w3.org/TR/activitystreams-core)
|
||||
- [Activitypub spec](https://www.w3.org/TR/activitypub/)
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood)
|
||||
- [Guide for new ActivityPub implementers](https://socialhub.activitypub.rocks/t/guide-for-new-activitypub-implementers/479)
|
||||
- Understanding activitypub
|
||||
[part 1](https://seb.jambor.dev/posts/understanding-activitypub/),
|
||||
[part 2](https://seb.jambor.dev/posts/understanding-activitypub-part-2-lemmy/),
|
||||
[part 3](https://seb.jambor.dev/posts/understanding-activitypub-part-3-the-state-of-mastodon/)
|
||||
- [Nodejs Express Activitypub sample implementation](https://github.com/dariusk/express-activitypub)
|
||||
- [Reading Activitypub](https://tinysubversions.com/notes/reading-activitypub/#the-ultimate-tl-dr)
|
||||
|
||||
# Spacebar Activitypub Docs
|
||||
|
||||
Incomplete documentation.
|
||||
|
||||
## Supported Types
|
||||
|
||||
| Spacebar object | Activitypub |
|
||||
@ -93,6 +110,8 @@ Also contains a collection of [roles](#role-federation).
|
||||
|
||||
### Properties Used
|
||||
|
||||
- attributed to is owner
|
||||
|
||||
## User Federation
|
||||
|
||||
A person. Sends messages to Channels. May also create, modify, or moderate guilds, channels, or roles.
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BodyParser, CORS, ErrorHandler } from "@spacebar/api";
|
||||
import { CORS, ErrorHandler } from "@spacebar/api";
|
||||
import {
|
||||
Config,
|
||||
JSONReplacer,
|
||||
@ -13,7 +13,7 @@ import { Server, ServerOptions } from "lambert-server";
|
||||
import path from "path";
|
||||
import wellknown from "./well-known";
|
||||
|
||||
export type SpacebarServerOptions = ServerOptions;
|
||||
type SpacebarServerOptions = ServerOptions;
|
||||
|
||||
export class FederationServer extends Server {
|
||||
public declare options: SpacebarServerOptions;
|
||||
@ -29,18 +29,23 @@ export class FederationServer extends Server {
|
||||
await Config.init();
|
||||
await Sentry.init(this.app);
|
||||
|
||||
setupMorganLogging(this.app);
|
||||
this.app.set("json replacer", JSONReplacer);
|
||||
if (!Config.get().federation.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Federation is enabled!");
|
||||
|
||||
this.app.set("json replacer", JSONReplacer);
|
||||
this.app.use(CORS);
|
||||
this.app.use(bodyParser.json({ inflate: true }));
|
||||
this.app.use(
|
||||
BodyParser({
|
||||
inflate: true,
|
||||
limit: "10mb",
|
||||
bodyParser.json({
|
||||
type: "application/activity+json",
|
||||
}),
|
||||
);
|
||||
this.app.use(bodyParser.urlencoded({ extended: true }));
|
||||
this.app.use(bodyParser.urlencoded({ inflate: true, extended: true }));
|
||||
|
||||
setupMorganLogging(this.app);
|
||||
|
||||
const app = this.app;
|
||||
const api = Router();
|
||||
@ -64,13 +69,6 @@ export class FederationServer extends Server {
|
||||
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("/federation", api);
|
||||
@ -80,6 +78,13 @@ export class FederationServer extends Server {
|
||||
|
||||
Sentry.errorHandler(this.app);
|
||||
|
||||
api.use("*", (req: Request, res: Response) => {
|
||||
res.status(404).json({
|
||||
message: "404 endpoint not found",
|
||||
code: 0,
|
||||
});
|
||||
});
|
||||
|
||||
return super.start();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Config, FederationKey, OrmUtils } from "@spacebar/util";
|
||||
import { APActivity } from "activitypub-types";
|
||||
import crypto from "crypto";
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
import { RequestInit } from "node-fetch";
|
||||
import { APError, fetchFederatedUser, fetchOpts } from "./utils";
|
||||
|
||||
export class HttpSig {
|
||||
@ -27,8 +28,9 @@ export class HttpSig {
|
||||
activity: APActivity,
|
||||
requestHeaders: IncomingHttpHeaders,
|
||||
) {
|
||||
const sigheader = requestHeaders["signature"] as string;
|
||||
const sigopts: { [key: string]: string } = Object.assign(
|
||||
const sigheader = requestHeaders["signature"]?.toString();
|
||||
if (!sigheader) throw new APError("Missing signature");
|
||||
const sigopts: { [key: string]: string | undefined } = Object.assign(
|
||||
{},
|
||||
...sigheader.split(",").flatMap((keyval) => {
|
||||
const split = keyval.split("=");
|
||||
@ -43,8 +45,10 @@ export class HttpSig {
|
||||
if (!signature || !headers || !keyId)
|
||||
throw new APError("Invalid signature");
|
||||
|
||||
const ALLOWED_ALGO = "rsa-sha256";
|
||||
|
||||
// If it's provided, check it. otherwise just assume it's sha256
|
||||
if (algorithm && algorithm != "rsa-sha256")
|
||||
if (algorithm && algorithm != ALLOWED_ALGO)
|
||||
throw new APError(`Unsupported encryption algorithm ${algorithm}`);
|
||||
|
||||
const url = new URL(keyId);
|
||||
@ -59,7 +63,9 @@ export class HttpSig {
|
||||
headers.split(/\s+/),
|
||||
);
|
||||
|
||||
const verifier = crypto.createVerify(algorithm.toUpperCase());
|
||||
const verifier = crypto.createVerify(
|
||||
algorithm?.toUpperCase() || ALLOWED_ALGO,
|
||||
);
|
||||
verifier.write(expected);
|
||||
verifier.end();
|
||||
|
||||
@ -113,13 +119,13 @@ export class HttpSig {
|
||||
|
||||
return OrmUtils.mergeDeep(fetchOpts, {
|
||||
method: "POST",
|
||||
body: message,
|
||||
body: JSON.stringify(message),
|
||||
headers: {
|
||||
Host: url.hostname,
|
||||
Date: now.toUTCString(),
|
||||
Digest: `SHA-256=${digest}`,
|
||||
Signature: header,
|
||||
},
|
||||
});
|
||||
} as RequestInit);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,10 @@ export class Federation {
|
||||
throw new APError("Invalid signature");
|
||||
}
|
||||
|
||||
if (!APActivityIsCreate(activity)) throw new APError("not implemented");
|
||||
if (!APActivityIsCreate(activity))
|
||||
throw new APError(
|
||||
`activity of type ${activity.type} not implemented`,
|
||||
);
|
||||
|
||||
const object = Array.isArray(activity.object)
|
||||
? activity.object[0]
|
||||
|
@ -12,14 +12,11 @@ class FederationQueue {
|
||||
private queue: Map<Instance, Array<APActivity>> = new Map();
|
||||
|
||||
public async distribute(activity: APActivity) {
|
||||
let { to, actor } = activity;
|
||||
|
||||
if (!to)
|
||||
throw new APError("Activity with no `to` field is undeliverable.");
|
||||
if (!Array.isArray(to)) to = [to];
|
||||
let { actor } = activity;
|
||||
const { to, object } = activity;
|
||||
|
||||
if (!actor)
|
||||
throw new APError("Activity with no `to` field is undeliverable.");
|
||||
throw new APError("Activity with no actor cannot be signed.");
|
||||
if (Array.isArray(actor)) actor = actor[0];
|
||||
|
||||
// TODO: check if `to` is on our instance?
|
||||
@ -38,21 +35,56 @@ class FederationQueue {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const receiver of to) {
|
||||
if (typeof receiver != "string") {
|
||||
console.error(receiver);
|
||||
// this is ugly
|
||||
for (let recv of [
|
||||
...(Array.isArray(to) ? to : [to]),
|
||||
...(Array.isArray(object) ? object : [object]),
|
||||
]) {
|
||||
if (!recv) continue;
|
||||
|
||||
if (typeof recv != "string") {
|
||||
console.warn(
|
||||
`TODO: activity with non-string destination was not sent`,
|
||||
recv,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const signedActivity = await HttpSig.sign(
|
||||
receiver.toString(),
|
||||
sender,
|
||||
activity,
|
||||
if (recv == "https://www.w3.org/ns/activitystreams#Public") {
|
||||
console.debug(`TODO: Skipping sending activity to #Public`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recv.includes("/followers")) {
|
||||
console.warn("sending to /followers is not implemented");
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: this is bad
|
||||
if (!recv.includes("/inbox")) recv = `${recv}/inbox`;
|
||||
|
||||
await this.signAndSend(activity, sender, recv);
|
||||
}
|
||||
}
|
||||
|
||||
private async signAndSend(
|
||||
activity: APActivity,
|
||||
sender: FederationKey,
|
||||
receiver: string,
|
||||
) {
|
||||
const signedActivity = await HttpSig.sign(
|
||||
receiver.toString(),
|
||||
sender,
|
||||
activity,
|
||||
);
|
||||
|
||||
const ret = await fetch(receiver, signedActivity);
|
||||
if (!ret.ok) {
|
||||
console.error(
|
||||
`Sending activity ${activity.id} to ` +
|
||||
`${receiver} failed with code ${ret.status} `,
|
||||
JSON.stringify(await ret.json()),
|
||||
);
|
||||
|
||||
const ret = await fetch(receiver, signedActivity);
|
||||
|
||||
console.log(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
ACTIVITYSTREAMS_CONTEXT,
|
||||
APError,
|
||||
APObjectIsPerson,
|
||||
fetchFederatedUser,
|
||||
resolveAPObject,
|
||||
} from "./utils";
|
||||
|
||||
@ -257,7 +258,7 @@ export const transformPersonToUser = async (person: APPerson) => {
|
||||
const keys = await FederationKey.create({
|
||||
actorId: Snowflake.generate(),
|
||||
federatedId: url.toString(),
|
||||
username: person.preferredUsername,
|
||||
username: person.name,
|
||||
domain: url.hostname,
|
||||
publicKey: person.publicKey?.publicKeyPem,
|
||||
type: ActorType.USER,
|
||||
@ -289,18 +290,63 @@ export const transformPersonToUser = async (person: APPerson) => {
|
||||
}).save();
|
||||
};
|
||||
|
||||
export const transformOrganisationToInvite = (guild: APOrganization) => {
|
||||
export const transformOrganisationToInvite = async (
|
||||
code: string,
|
||||
org: APOrganization,
|
||||
) => {
|
||||
const guild = await transformOrganisationToGuild(org);
|
||||
return Invite.create({
|
||||
code: guild.id,
|
||||
code,
|
||||
temporary: false,
|
||||
uses: -1,
|
||||
max_uses: 0,
|
||||
max_age: 0,
|
||||
created_at: new Date(0),
|
||||
flags: 0,
|
||||
guild,
|
||||
channel: Channel.create({}),
|
||||
inviter: guild.owner,
|
||||
});
|
||||
};
|
||||
|
||||
export const transformOrganisationToGuild = async (org: APOrganization) => {
|
||||
if (!org.id) throw new APError("Federated guild must have ID");
|
||||
if (!org.publicKey || !org.publicKey.publicKeyPem)
|
||||
throw new APError("Federated guild must have public key.");
|
||||
|
||||
const cache = await FederationKey.findOne({
|
||||
where: { federatedId: org.id },
|
||||
});
|
||||
if (cache) {
|
||||
return await Guild.findOneOrFail({ where: { id: cache.actorId } });
|
||||
}
|
||||
|
||||
const keys = FederationKey.create({
|
||||
actorId: Snowflake.generate(),
|
||||
federatedId: org.id,
|
||||
username: org.name,
|
||||
domain: new URL(org.id).hostname,
|
||||
publicKey: org.publicKey.publicKeyPem,
|
||||
type: ActorType.GUILD,
|
||||
inbox: org.inbox.toString(),
|
||||
outbox: org.outbox.toString(),
|
||||
});
|
||||
|
||||
if (typeof org.attributedTo != "string")
|
||||
throw new APError("attributedTo must be string");
|
||||
|
||||
const owner = await fetchFederatedUser(org.attributedTo);
|
||||
|
||||
const guild = Guild.create({
|
||||
id: keys.actorId,
|
||||
name: org.name,
|
||||
owner_id: owner.user.id,
|
||||
});
|
||||
|
||||
await Promise.all([guild.save(), keys.save()]);
|
||||
return guild;
|
||||
};
|
||||
|
||||
export const transformGuildToOrganisation = async (
|
||||
guild: Guild,
|
||||
): Promise<APOrganization> => {
|
||||
@ -317,7 +363,11 @@ export const transformGuildToOrganisation = async (
|
||||
|
||||
name: guild.name,
|
||||
preferredUsername: guild.id,
|
||||
icon: undefined,
|
||||
icon: guild.icon
|
||||
? `${Config.get().cdn.endpointPublic}/icons/${guild.icon}`
|
||||
: undefined,
|
||||
|
||||
attributedTo: `https://${host}/federation/users/${guild.owner_id}`,
|
||||
|
||||
inbox: `https://${host}/federation/guilds/${guild.id}/inbox`,
|
||||
outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
|
||||
|
@ -2,6 +2,7 @@ import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
|
||||
import {
|
||||
ActorType,
|
||||
Config,
|
||||
FederationActivity,
|
||||
FederationKey,
|
||||
OrmUtils,
|
||||
Snowflake,
|
||||
@ -13,6 +14,7 @@ import {
|
||||
APActivity,
|
||||
APAnnounce,
|
||||
APCreate,
|
||||
APFollow,
|
||||
APNote,
|
||||
APPerson,
|
||||
AnyAPObject,
|
||||
@ -21,14 +23,18 @@ import { HTTPError } from "lambert-server";
|
||||
import fetch from "node-fetch";
|
||||
import { ProxyAgent } from "proxy-agent";
|
||||
import TurndownService from "turndown";
|
||||
import { Federation } from ".";
|
||||
|
||||
export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams";
|
||||
|
||||
export const fetchOpts = OrmUtils.mergeDeep(DEFAULT_FETCH_OPTIONS, {
|
||||
headers: {
|
||||
Accept: "application/activity+json",
|
||||
},
|
||||
});
|
||||
export const fetchOpts = Object.freeze(
|
||||
OrmUtils.mergeDeep(DEFAULT_FETCH_OPTIONS, {
|
||||
headers: {
|
||||
Accept: "application/activity+json",
|
||||
"Content-Type": "application/activity+json",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export class APError extends HTTPError {}
|
||||
|
||||
@ -112,6 +118,15 @@ export const resolveWebfinger = async (
|
||||
return await resolveAPObject<AnyAPObject>(link.href);
|
||||
};
|
||||
|
||||
export const tryResolveWebfinger = async (lookup: string) => {
|
||||
try {
|
||||
return await resolveWebfinger(lookup);
|
||||
} catch (e) {
|
||||
console.error(`Error resolving webfinger ${lookup}`, e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** Fetch from local db, if not found fetch from remote instance and save */
|
||||
export const fetchFederatedUser = async (actorId: string) => {
|
||||
// if we were given webfinger, resolve that first
|
||||
@ -147,7 +162,7 @@ export const fetchFederatedUser = async (actorId: string) => {
|
||||
const keys = FederationKey.create({
|
||||
actorId: Snowflake.generate(),
|
||||
federatedId: actorId,
|
||||
username: remoteActor.preferredUsername,
|
||||
username: remoteActor.name,
|
||||
// this is technically not correct
|
||||
// but it's slightly more difficult to go from actor url -> handle
|
||||
// so thats a problem for future me
|
||||
@ -160,7 +175,7 @@ export const fetchFederatedUser = async (actorId: string) => {
|
||||
|
||||
const user = User.create({
|
||||
id: keys.actorId,
|
||||
username: remoteActor.preferredUsername,
|
||||
username: remoteActor.name,
|
||||
discriminator: "0",
|
||||
bio: new TurndownService().turndown(remoteActor.summary), // html -> markdown
|
||||
email: `${remoteActor.preferredUsername}@${keys.domain}`,
|
||||
@ -188,6 +203,27 @@ export const fetchFederatedUser = async (actorId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const tryFederatedGuildJoin = async (code: string, user_id: string) => {
|
||||
const guild = await tryResolveWebfinger(code);
|
||||
if (!guild || !APObjectIsOrganisation(guild))
|
||||
throw new APError(
|
||||
`Invite code did not produce Guild on remote server ${code}`,
|
||||
);
|
||||
|
||||
const { host } = Config.get().federation;
|
||||
|
||||
const follow = await FederationActivity.create({
|
||||
data: {
|
||||
"@context": ACTIVITYSTREAMS_CONTEXT,
|
||||
type: "Follow",
|
||||
actor: `https://${host}/federation/users/${user_id}`,
|
||||
object: guild.id,
|
||||
} as APFollow,
|
||||
}).save();
|
||||
|
||||
await Federation.distribute(follow.toJSON());
|
||||
};
|
||||
|
||||
// fetch from remote server?
|
||||
export const APObjectIsPerson = (object: AnyAPObject): object is APPerson => {
|
||||
return "type" in object && object.type == "Person";
|
||||
|
@ -1,14 +1,10 @@
|
||||
import { transformNoteToMessage } from "@spacebar/ap";
|
||||
import { Federation } from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Message, emitEvent } from "@spacebar/util";
|
||||
import { APCreate, APNote } from "activitypub-types";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
const router = Router();
|
||||
|
||||
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||
// TODO: check if the activity exists on the remote server
|
||||
// TODO: refactor
|
||||
res.json(await Federation.genericInboxHandler(req));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
13
src/activitypub/routes/guilds/#guild_id/inbox.ts
Normal file
13
src/activitypub/routes/guilds/#guild_id/inbox.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Federation } from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import { Request, Response, Router } from "express";
|
||||
const router = Router();
|
||||
|
||||
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||
// TODO: support lemmy ChatMessage type?
|
||||
// TODO: check if the activity exists on the remote server
|
||||
|
||||
res.json(await Federation.genericInboxHandler(req));
|
||||
});
|
||||
|
||||
export default router;
|
13
src/activitypub/start.ts
Normal file
13
src/activitypub/start.ts
Normal file
@ -0,0 +1,13 @@
|
||||
require("module-alias/register");
|
||||
import "dotenv/config";
|
||||
|
||||
import { FederationServer } from "./Server";
|
||||
const server = new FederationServer({ port: Number(process.env.PORT) || 3003 });
|
||||
server
|
||||
.start()
|
||||
.then(() => {
|
||||
console.log("[Server] started on :" + server.options.port);
|
||||
})
|
||||
.catch((e) => console.error("[Server] Error starting: ", e));
|
||||
|
||||
module.exports = server;
|
@ -406,6 +406,7 @@ router.post(
|
||||
);
|
||||
|
||||
setImmediate(async () => {
|
||||
if (!Config.get().federation.enabled) return;
|
||||
const ap = await transformMessageToAnnounceNoce(message);
|
||||
|
||||
await Federation.distribute(ap);
|
||||
|
@ -70,6 +70,14 @@ router.patch(
|
||||
|
||||
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
|
||||
|
||||
// TODO: move this
|
||||
if (!guild.welcome_screen)
|
||||
guild.welcome_screen = {
|
||||
enabled: false,
|
||||
description: "",
|
||||
welcome_channels: [],
|
||||
};
|
||||
|
||||
if (body.enabled != undefined)
|
||||
guild.welcome_screen.enabled = body.enabled;
|
||||
|
||||
|
@ -19,9 +19,10 @@
|
||||
import {
|
||||
APError,
|
||||
APObjectIsOrganisation,
|
||||
resolveWebfinger,
|
||||
splitQualifiedMention,
|
||||
transformOrganisationToInvite,
|
||||
tryFederatedGuildJoin,
|
||||
tryResolveWebfinger,
|
||||
} from "@spacebar/ap";
|
||||
import { route } from "@spacebar/api";
|
||||
import {
|
||||
@ -68,14 +69,18 @@ router.get(
|
||||
if (domain != accountDomain && domain != host) {
|
||||
// The domain isn't ours
|
||||
|
||||
const remoteGuild = await resolveWebfinger(inputValue);
|
||||
const remoteGuild = await tryResolveWebfinger(inputValue);
|
||||
if (remoteGuild) {
|
||||
if (APObjectIsOrganisation(remoteGuild))
|
||||
return res.json(
|
||||
await transformOrganisationToInvite(
|
||||
inputValue,
|
||||
remoteGuild,
|
||||
),
|
||||
);
|
||||
|
||||
if (APObjectIsOrganisation(remoteGuild))
|
||||
return res.json(
|
||||
transformOrganisationToInvite(remoteGuild),
|
||||
);
|
||||
|
||||
throw new APError("Remote resource is not a guild");
|
||||
throw new APError("Remote resource is not a guild");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -110,8 +115,21 @@ router.post(
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT;
|
||||
|
||||
const { code } = req.params;
|
||||
|
||||
// Federation
|
||||
const mention = splitQualifiedMention(code);
|
||||
if (mention.user.length && Config.get().federation.enabled) {
|
||||
const { domain } = mention;
|
||||
const { accountDomain, host } = Config.get().federation;
|
||||
if (domain != accountDomain && domain != host) {
|
||||
// this domain isn't ours, try a federated join
|
||||
// send a follow request to the guild
|
||||
|
||||
return res.json(await tryFederatedGuildJoin(code, req.user_id));
|
||||
}
|
||||
}
|
||||
|
||||
const { guild_id } = await Invite.findOneOrFail({
|
||||
where: { code: code },
|
||||
});
|
||||
|
@ -23,7 +23,12 @@ import { FederationServer } from "@spacebar/ap";
|
||||
import * as Api from "@spacebar/api";
|
||||
import { CDNServer } from "@spacebar/cdn";
|
||||
import * as Gateway from "@spacebar/gateway";
|
||||
import { Config, Sentry, initDatabase } from "@spacebar/util";
|
||||
import {
|
||||
Config,
|
||||
Sentry,
|
||||
initDatabase,
|
||||
setupMorganLogging,
|
||||
} from "@spacebar/util";
|
||||
import express from "express";
|
||||
import http from "http";
|
||||
import { bold, green } from "picocolors";
|
||||
@ -35,9 +40,9 @@ const production = process.env.NODE_ENV == "development" ? false : true;
|
||||
server.on("request", app);
|
||||
|
||||
const api = new Api.SpacebarServer({ server, port, production, app });
|
||||
const federation = new FederationServer({ server, port, production, app });
|
||||
const cdn = new CDNServer({ server, port, production, app });
|
||||
const gateway = new Gateway.Server({ server, port, production });
|
||||
const federation = new FederationServer({ server, port, production, app });
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("Shutting down due to SIGTERM");
|
||||
@ -53,6 +58,8 @@ async function main() {
|
||||
await Config.init();
|
||||
await Sentry.init(app);
|
||||
|
||||
setupMorganLogging(app);
|
||||
|
||||
await new Promise((resolve) =>
|
||||
server.listen({ port }, () => resolve(undefined)),
|
||||
);
|
||||
|
@ -16,7 +16,8 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "@spacebar/ap";
|
||||
export * from "@spacebar/api";
|
||||
export * from "@spacebar/util";
|
||||
export * from "@spacebar/gateway";
|
||||
export * from "@spacebar/cdn";
|
||||
export * from "@spacebar/gateway";
|
||||
export * from "@spacebar/util";
|
||||
|
18
src/util/entities/FederationActivity.ts
Normal file
18
src/util/entities/FederationActivity.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { APActivity } from "activitypub-types";
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { Config } from "..";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
|
||||
@Entity("federation_activities")
|
||||
export class FederationActivity extends BaseClass {
|
||||
@Column({ type: "simple-json" })
|
||||
data: APActivity;
|
||||
|
||||
toJSON(): APActivity {
|
||||
const { host } = Config.get().federation;
|
||||
return {
|
||||
id: `https://${host}/federation/activities/${this.id}`,
|
||||
...this.data,
|
||||
};
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
@ -83,8 +84,8 @@ export class Guild extends BaseClass {
|
||||
@ManyToOne(() => Channel)
|
||||
afk_channel?: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
afk_timeout?: number;
|
||||
@Column()
|
||||
afk_timeout: number;
|
||||
|
||||
// * commented out -> use owner instead
|
||||
// application id of the guild creator if it is bot-created
|
||||
@ -135,11 +136,11 @@ export class Guild extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
max_video_channel_users?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
member_count?: number;
|
||||
@Column({ default: 0 })
|
||||
member_count: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
presence_count?: number; // users online
|
||||
@Column({ nullable: true, type: Number, default: 0 })
|
||||
presence_count: number | null; // users online
|
||||
|
||||
@OneToMany(() => Member, (member: Member) => member.guild, {
|
||||
cascade: true,
|
||||
@ -211,8 +212,8 @@ export class Guild extends BaseClass {
|
||||
})
|
||||
webhooks: Webhook[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
mfa_level?: number;
|
||||
@Column({ default: 0 })
|
||||
mfa_level: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
@ -225,14 +226,14 @@ export class Guild extends BaseClass {
|
||||
@ManyToOne(() => User)
|
||||
owner?: User; // optional to allow for ownerless guilds
|
||||
|
||||
@Column({ nullable: true })
|
||||
preferred_locale?: string;
|
||||
@Column({ nullable: true, type: String, default: "en-US" })
|
||||
preferred_locale: string | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ nullable: true, type: Number, default: 0 })
|
||||
premium_subscription_count?: number;
|
||||
|
||||
@Column()
|
||||
premium_tier?: number; // crowd premium level
|
||||
@Column({ default: 0 })
|
||||
premium_tier: number; // crowd premium level
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.public_updates_channel)
|
||||
@ -264,17 +265,17 @@ export class Guild extends BaseClass {
|
||||
@ManyToOne(() => Channel)
|
||||
system_channel?: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ default: 4 }) // defaults effect: suppress the setup tips to save performance
|
||||
system_channel_flags?: number;
|
||||
|
||||
@Column()
|
||||
unavailable: boolean = false;
|
||||
|
||||
@Column({ nullable: true })
|
||||
verification_level?: number;
|
||||
@Column({ default: 0 })
|
||||
verification_level: number;
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
welcome_screen: GuildWelcomeScreen;
|
||||
@Column({ type: "simple-json", nullable: true }) // TODO: move this to own table
|
||||
welcome_screen: GuildWelcomeScreen | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.widget_channel)
|
||||
@ -287,8 +288,8 @@ export class Guild extends BaseClass {
|
||||
@Column()
|
||||
widget_enabled: boolean = true;
|
||||
|
||||
@Column({ nullable: true })
|
||||
nsfw_level?: number;
|
||||
@Column({ default: 0 })
|
||||
nsfw_level: number;
|
||||
|
||||
@Column()
|
||||
nsfw: boolean = false;
|
||||
@ -317,32 +318,6 @@ export class Guild extends BaseClass {
|
||||
name: body.name || "Spacebar",
|
||||
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
|
||||
owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
|
||||
presence_count: 0,
|
||||
member_count: 0, // will automatically be increased by addMember()
|
||||
mfa_level: 0,
|
||||
preferred_locale: "en-US",
|
||||
premium_subscription_count: 0,
|
||||
premium_tier: 0,
|
||||
system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
|
||||
nsfw_level: 0,
|
||||
verification_level: 0,
|
||||
welcome_screen: {
|
||||
enabled: false,
|
||||
description: "",
|
||||
welcome_channels: [],
|
||||
},
|
||||
|
||||
afk_timeout: Config.get().defaults.guild.afkTimeout,
|
||||
default_message_notifications:
|
||||
Config.get().defaults.guild.defaultMessageNotifications,
|
||||
explicit_content_filter:
|
||||
Config.get().defaults.guild.explicitContentFilter,
|
||||
features: Config.get().guild.defaultFeatures,
|
||||
max_members: Config.get().limits.guild.maxMembers,
|
||||
max_presences: Config.get().defaults.guild.maxPresences,
|
||||
max_video_channel_users:
|
||||
Config.get().defaults.guild.maxVideoChannelUsers,
|
||||
region: Config.get().regions.default,
|
||||
}).save();
|
||||
|
||||
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
|
||||
@ -414,4 +389,32 @@ export class Guild extends BaseClass {
|
||||
unavailable: this.unavailable == false ? undefined : true,
|
||||
};
|
||||
}
|
||||
|
||||
@BeforeInsert()
|
||||
__set_defaults = () => {
|
||||
if (!this.afk_timeout)
|
||||
this.afk_timeout = Config.get().defaults.guild.afkTimeout;
|
||||
|
||||
if (!this.default_message_notifications)
|
||||
this.default_message_notifications =
|
||||
Config.get().defaults.guild.defaultMessageNotifications;
|
||||
|
||||
if (!this.explicit_content_filter)
|
||||
this.explicit_content_filter =
|
||||
Config.get().defaults.guild.explicitContentFilter;
|
||||
|
||||
if (!this.features) this.features = Config.get().guild.defaultFeatures;
|
||||
|
||||
if (!this.max_members)
|
||||
this.max_members = Config.get().limits.guild.maxMembers;
|
||||
|
||||
if (!this.max_presences)
|
||||
this.afk_timeout = Config.get().defaults.guild.maxPresences;
|
||||
|
||||
if (!this.max_video_channel_users)
|
||||
this.max_video_channel_users =
|
||||
Config.get().defaults.guild.maxVideoChannelUsers;
|
||||
|
||||
if (!this.region) this.region = Config.get().regions.default;
|
||||
};
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export * from "./ConnectionConfigEntity";
|
||||
export * from "./EmbedCache";
|
||||
export * from "./Emoji";
|
||||
export * from "./Encryption";
|
||||
export * from "./FederationActivity";
|
||||
export * from "./FederationKeys";
|
||||
export * from "./Guild";
|
||||
export * from "./Invite";
|
||||
|
@ -2,19 +2,19 @@ import Express from "express";
|
||||
import morgan from "morgan";
|
||||
import { red } from "picocolors";
|
||||
|
||||
let HAS_WARNED = false;
|
||||
export const setupMorganLogging = (app: Express.Application) => {
|
||||
let ENABLED = false;
|
||||
export const setupMorganLogging = (app: Express.Router) => {
|
||||
const logRequests = process.env["LOG_REQUESTS"] != undefined;
|
||||
if (!logRequests) return;
|
||||
|
||||
if (!HAS_WARNED)
|
||||
console.log(
|
||||
red(
|
||||
`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`,
|
||||
),
|
||||
);
|
||||
if (ENABLED) return;
|
||||
ENABLED = true;
|
||||
|
||||
HAS_WARNED = true;
|
||||
console.log(
|
||||
red(
|
||||
`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`,
|
||||
),
|
||||
);
|
||||
|
||||
app.use(
|
||||
morgan("combined", {
|
||||
|
Loading…
Reference in New Issue
Block a user