mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-09 20:22:47 +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>/**"],
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"program": "${workspaceFolder}/src/bundle/start.ts",
|
"program": "${workspaceFolder}/src/bundle/start.ts",
|
||||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
"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": "node dist/bundle/start.js",
|
||||||
"start:api": "node dist/api/start.js",
|
"start:api": "node dist/api/start.js",
|
||||||
"start:cdn": "node dist/cdn/start.js",
|
"start:cdn": "node dist/cdn/start.js",
|
||||||
|
"start:ap": "node dist/activitypub/start.js",
|
||||||
"start:gateway": "node dist/gateway/start.js",
|
"start:gateway": "node dist/gateway/start.js",
|
||||||
"build": "tsc -p .",
|
"build": "tsc -p .",
|
||||||
"test": "node scripts/test.js",
|
"test": "node scripts/test.js",
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
# Spacebar Activitypub
|
# Spacebar Activitypub
|
||||||
|
|
||||||
|
## Activitypub Specification
|
||||||
|
|
||||||
- [Activitystreams vocab](https://www.w3.org/TR/activitystreams-vocabulary)
|
- [Activitystreams vocab](https://www.w3.org/TR/activitystreams-vocabulary)
|
||||||
- [Activitystreams](https://www.w3.org/TR/activitystreams-core)
|
- [Activitystreams](https://www.w3.org/TR/activitystreams-core)
|
||||||
- [Activitypub spec](https://www.w3.org/TR/activitypub/)
|
- [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
|
## Supported Types
|
||||||
|
|
||||||
| Spacebar object | Activitypub |
|
| Spacebar object | Activitypub |
|
||||||
@ -93,6 +110,8 @@ Also contains a collection of [roles](#role-federation).
|
|||||||
|
|
||||||
### Properties Used
|
### Properties Used
|
||||||
|
|
||||||
|
- attributed to is owner
|
||||||
|
|
||||||
## User Federation
|
## User Federation
|
||||||
|
|
||||||
A person. Sends messages to Channels. May also create, modify, or moderate guilds, channels, or roles.
|
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 {
|
import {
|
||||||
Config,
|
Config,
|
||||||
JSONReplacer,
|
JSONReplacer,
|
||||||
@ -13,7 +13,7 @@ import { Server, ServerOptions } from "lambert-server";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import wellknown from "./well-known";
|
import wellknown from "./well-known";
|
||||||
|
|
||||||
export type SpacebarServerOptions = ServerOptions;
|
type SpacebarServerOptions = ServerOptions;
|
||||||
|
|
||||||
export class FederationServer extends Server {
|
export class FederationServer extends Server {
|
||||||
public declare options: SpacebarServerOptions;
|
public declare options: SpacebarServerOptions;
|
||||||
@ -29,18 +29,23 @@ export class FederationServer extends Server {
|
|||||||
await Config.init();
|
await Config.init();
|
||||||
await Sentry.init(this.app);
|
await Sentry.init(this.app);
|
||||||
|
|
||||||
setupMorganLogging(this.app);
|
if (!Config.get().federation.enabled) {
|
||||||
this.app.set("json replacer", JSONReplacer);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Federation is enabled!");
|
||||||
|
|
||||||
|
this.app.set("json replacer", JSONReplacer);
|
||||||
this.app.use(CORS);
|
this.app.use(CORS);
|
||||||
|
this.app.use(bodyParser.json({ inflate: true }));
|
||||||
this.app.use(
|
this.app.use(
|
||||||
BodyParser({
|
bodyParser.json({
|
||||||
inflate: true,
|
|
||||||
limit: "10mb",
|
|
||||||
type: "application/activity+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 app = this.app;
|
||||||
const api = Router();
|
const api = Router();
|
||||||
@ -64,13 +69,6 @@ export class FederationServer extends Server {
|
|||||||
path.join(__dirname, "routes", "/"),
|
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 = app;
|
||||||
|
|
||||||
this.app.use("/federation", api);
|
this.app.use("/federation", api);
|
||||||
@ -80,6 +78,13 @@ export class FederationServer extends Server {
|
|||||||
|
|
||||||
Sentry.errorHandler(this.app);
|
Sentry.errorHandler(this.app);
|
||||||
|
|
||||||
|
api.use("*", (req: Request, res: Response) => {
|
||||||
|
res.status(404).json({
|
||||||
|
message: "404 endpoint not found",
|
||||||
|
code: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return super.start();
|
return super.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { Config, FederationKey, OrmUtils } from "@spacebar/util";
|
|||||||
import { APActivity } from "activitypub-types";
|
import { APActivity } from "activitypub-types";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { IncomingHttpHeaders } from "http";
|
import { IncomingHttpHeaders } from "http";
|
||||||
|
import { RequestInit } from "node-fetch";
|
||||||
import { APError, fetchFederatedUser, fetchOpts } from "./utils";
|
import { APError, fetchFederatedUser, fetchOpts } from "./utils";
|
||||||
|
|
||||||
export class HttpSig {
|
export class HttpSig {
|
||||||
@ -27,8 +28,9 @@ export class HttpSig {
|
|||||||
activity: APActivity,
|
activity: APActivity,
|
||||||
requestHeaders: IncomingHttpHeaders,
|
requestHeaders: IncomingHttpHeaders,
|
||||||
) {
|
) {
|
||||||
const sigheader = requestHeaders["signature"] as string;
|
const sigheader = requestHeaders["signature"]?.toString();
|
||||||
const sigopts: { [key: string]: string } = Object.assign(
|
if (!sigheader) throw new APError("Missing signature");
|
||||||
|
const sigopts: { [key: string]: string | undefined } = Object.assign(
|
||||||
{},
|
{},
|
||||||
...sigheader.split(",").flatMap((keyval) => {
|
...sigheader.split(",").flatMap((keyval) => {
|
||||||
const split = keyval.split("=");
|
const split = keyval.split("=");
|
||||||
@ -43,8 +45,10 @@ export class HttpSig {
|
|||||||
if (!signature || !headers || !keyId)
|
if (!signature || !headers || !keyId)
|
||||||
throw new APError("Invalid signature");
|
throw new APError("Invalid signature");
|
||||||
|
|
||||||
|
const ALLOWED_ALGO = "rsa-sha256";
|
||||||
|
|
||||||
// If it's provided, check it. otherwise just assume it's 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}`);
|
throw new APError(`Unsupported encryption algorithm ${algorithm}`);
|
||||||
|
|
||||||
const url = new URL(keyId);
|
const url = new URL(keyId);
|
||||||
@ -59,7 +63,9 @@ export class HttpSig {
|
|||||||
headers.split(/\s+/),
|
headers.split(/\s+/),
|
||||||
);
|
);
|
||||||
|
|
||||||
const verifier = crypto.createVerify(algorithm.toUpperCase());
|
const verifier = crypto.createVerify(
|
||||||
|
algorithm?.toUpperCase() || ALLOWED_ALGO,
|
||||||
|
);
|
||||||
verifier.write(expected);
|
verifier.write(expected);
|
||||||
verifier.end();
|
verifier.end();
|
||||||
|
|
||||||
@ -113,13 +119,13 @@ export class HttpSig {
|
|||||||
|
|
||||||
return OrmUtils.mergeDeep(fetchOpts, {
|
return OrmUtils.mergeDeep(fetchOpts, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: message,
|
body: JSON.stringify(message),
|
||||||
headers: {
|
headers: {
|
||||||
Host: url.hostname,
|
Host: url.hostname,
|
||||||
Date: now.toUTCString(),
|
Date: now.toUTCString(),
|
||||||
Digest: `SHA-256=${digest}`,
|
Digest: `SHA-256=${digest}`,
|
||||||
Signature: header,
|
Signature: header,
|
||||||
},
|
},
|
||||||
});
|
} as RequestInit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,10 @@ export class Federation {
|
|||||||
throw new APError("Invalid signature");
|
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)
|
const object = Array.isArray(activity.object)
|
||||||
? activity.object[0]
|
? activity.object[0]
|
||||||
|
@ -12,14 +12,11 @@ class FederationQueue {
|
|||||||
private queue: Map<Instance, Array<APActivity>> = new Map();
|
private queue: Map<Instance, Array<APActivity>> = new Map();
|
||||||
|
|
||||||
public async distribute(activity: APActivity) {
|
public async distribute(activity: APActivity) {
|
||||||
let { to, actor } = activity;
|
let { actor } = activity;
|
||||||
|
const { to, object } = activity;
|
||||||
if (!to)
|
|
||||||
throw new APError("Activity with no `to` field is undeliverable.");
|
|
||||||
if (!Array.isArray(to)) to = [to];
|
|
||||||
|
|
||||||
if (!actor)
|
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];
|
if (Array.isArray(actor)) actor = actor[0];
|
||||||
|
|
||||||
// TODO: check if `to` is on our instance?
|
// TODO: check if `to` is on our instance?
|
||||||
@ -38,12 +35,43 @@ class FederationQueue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const receiver of to) {
|
// this is ugly
|
||||||
if (typeof receiver != "string") {
|
for (let recv of [
|
||||||
console.error(receiver);
|
...(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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
const signedActivity = await HttpSig.sign(
|
||||||
receiver.toString(),
|
receiver.toString(),
|
||||||
sender,
|
sender,
|
||||||
@ -51,8 +79,12 @@ class FederationQueue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ret = await fetch(receiver, signedActivity);
|
const ret = await fetch(receiver, signedActivity);
|
||||||
|
if (!ret.ok) {
|
||||||
console.log(ret);
|
console.error(
|
||||||
|
`Sending activity ${activity.id} to ` +
|
||||||
|
`${receiver} failed with code ${ret.status} `,
|
||||||
|
JSON.stringify(await ret.json()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
ACTIVITYSTREAMS_CONTEXT,
|
ACTIVITYSTREAMS_CONTEXT,
|
||||||
APError,
|
APError,
|
||||||
APObjectIsPerson,
|
APObjectIsPerson,
|
||||||
|
fetchFederatedUser,
|
||||||
resolveAPObject,
|
resolveAPObject,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
@ -257,7 +258,7 @@ export const transformPersonToUser = async (person: APPerson) => {
|
|||||||
const keys = await FederationKey.create({
|
const keys = await FederationKey.create({
|
||||||
actorId: Snowflake.generate(),
|
actorId: Snowflake.generate(),
|
||||||
federatedId: url.toString(),
|
federatedId: url.toString(),
|
||||||
username: person.preferredUsername,
|
username: person.name,
|
||||||
domain: url.hostname,
|
domain: url.hostname,
|
||||||
publicKey: person.publicKey?.publicKeyPem,
|
publicKey: person.publicKey?.publicKeyPem,
|
||||||
type: ActorType.USER,
|
type: ActorType.USER,
|
||||||
@ -289,18 +290,63 @@ export const transformPersonToUser = async (person: APPerson) => {
|
|||||||
}).save();
|
}).save();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformOrganisationToInvite = (guild: APOrganization) => {
|
export const transformOrganisationToInvite = async (
|
||||||
|
code: string,
|
||||||
|
org: APOrganization,
|
||||||
|
) => {
|
||||||
|
const guild = await transformOrganisationToGuild(org);
|
||||||
return Invite.create({
|
return Invite.create({
|
||||||
code: guild.id,
|
code,
|
||||||
temporary: false,
|
temporary: false,
|
||||||
uses: -1,
|
uses: -1,
|
||||||
max_uses: 0,
|
max_uses: 0,
|
||||||
max_age: 0,
|
max_age: 0,
|
||||||
created_at: new Date(0),
|
created_at: new Date(0),
|
||||||
flags: 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 (
|
export const transformGuildToOrganisation = async (
|
||||||
guild: Guild,
|
guild: Guild,
|
||||||
): Promise<APOrganization> => {
|
): Promise<APOrganization> => {
|
||||||
@ -317,7 +363,11 @@ export const transformGuildToOrganisation = async (
|
|||||||
|
|
||||||
name: guild.name,
|
name: guild.name,
|
||||||
preferredUsername: guild.id,
|
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`,
|
inbox: `https://${host}/federation/guilds/${guild.id}/inbox`,
|
||||||
outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
|
outbox: `https://${host}/federation/guilds/${guild.id}/outbox`,
|
||||||
|
@ -2,6 +2,7 @@ import { DEFAULT_FETCH_OPTIONS } from "@spacebar/api";
|
|||||||
import {
|
import {
|
||||||
ActorType,
|
ActorType,
|
||||||
Config,
|
Config,
|
||||||
|
FederationActivity,
|
||||||
FederationKey,
|
FederationKey,
|
||||||
OrmUtils,
|
OrmUtils,
|
||||||
Snowflake,
|
Snowflake,
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
APActivity,
|
APActivity,
|
||||||
APAnnounce,
|
APAnnounce,
|
||||||
APCreate,
|
APCreate,
|
||||||
|
APFollow,
|
||||||
APNote,
|
APNote,
|
||||||
APPerson,
|
APPerson,
|
||||||
AnyAPObject,
|
AnyAPObject,
|
||||||
@ -21,14 +23,18 @@ import { HTTPError } from "lambert-server";
|
|||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { ProxyAgent } from "proxy-agent";
|
import { ProxyAgent } from "proxy-agent";
|
||||||
import TurndownService from "turndown";
|
import TurndownService from "turndown";
|
||||||
|
import { Federation } from ".";
|
||||||
|
|
||||||
export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams";
|
export const ACTIVITYSTREAMS_CONTEXT = "https://www.w3.org/ns/activitystreams";
|
||||||
|
|
||||||
export const fetchOpts = OrmUtils.mergeDeep(DEFAULT_FETCH_OPTIONS, {
|
export const fetchOpts = Object.freeze(
|
||||||
|
OrmUtils.mergeDeep(DEFAULT_FETCH_OPTIONS, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/activity+json",
|
Accept: "application/activity+json",
|
||||||
|
"Content-Type": "application/activity+json",
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export class APError extends HTTPError {}
|
export class APError extends HTTPError {}
|
||||||
|
|
||||||
@ -112,6 +118,15 @@ export const resolveWebfinger = async (
|
|||||||
return await resolveAPObject<AnyAPObject>(link.href);
|
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 */
|
/** Fetch from local db, if not found fetch from remote instance and save */
|
||||||
export const fetchFederatedUser = async (actorId: string) => {
|
export const fetchFederatedUser = async (actorId: string) => {
|
||||||
// if we were given webfinger, resolve that first
|
// if we were given webfinger, resolve that first
|
||||||
@ -147,7 +162,7 @@ export const fetchFederatedUser = async (actorId: string) => {
|
|||||||
const keys = FederationKey.create({
|
const keys = FederationKey.create({
|
||||||
actorId: Snowflake.generate(),
|
actorId: Snowflake.generate(),
|
||||||
federatedId: actorId,
|
federatedId: actorId,
|
||||||
username: remoteActor.preferredUsername,
|
username: remoteActor.name,
|
||||||
// this is technically not correct
|
// this is technically not correct
|
||||||
// but it's slightly more difficult to go from actor url -> handle
|
// but it's slightly more difficult to go from actor url -> handle
|
||||||
// so thats a problem for future me
|
// so thats a problem for future me
|
||||||
@ -160,7 +175,7 @@ export const fetchFederatedUser = async (actorId: string) => {
|
|||||||
|
|
||||||
const user = User.create({
|
const user = User.create({
|
||||||
id: keys.actorId,
|
id: keys.actorId,
|
||||||
username: remoteActor.preferredUsername,
|
username: remoteActor.name,
|
||||||
discriminator: "0",
|
discriminator: "0",
|
||||||
bio: new TurndownService().turndown(remoteActor.summary), // html -> markdown
|
bio: new TurndownService().turndown(remoteActor.summary), // html -> markdown
|
||||||
email: `${remoteActor.preferredUsername}@${keys.domain}`,
|
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?
|
// fetch from remote server?
|
||||||
export const APObjectIsPerson = (object: AnyAPObject): object is APPerson => {
|
export const APObjectIsPerson = (object: AnyAPObject): object is APPerson => {
|
||||||
return "type" in object && object.type == "Person";
|
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 { route } from "@spacebar/api";
|
||||||
import { Message, emitEvent } from "@spacebar/util";
|
|
||||||
import { APCreate, APNote } from "activitypub-types";
|
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import { HTTPError } from "lambert-server";
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/", route({}), async (req: Request, res: Response) => {
|
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||||
// TODO: check if the activity exists on the remote server
|
res.json(await Federation.genericInboxHandler(req));
|
||||||
// TODO: refactor
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
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 () => {
|
setImmediate(async () => {
|
||||||
|
if (!Config.get().federation.enabled) return;
|
||||||
const ap = await transformMessageToAnnounceNoce(message);
|
const ap = await transformMessageToAnnounceNoce(message);
|
||||||
|
|
||||||
await Federation.distribute(ap);
|
await Federation.distribute(ap);
|
||||||
|
@ -70,6 +70,14 @@ router.patch(
|
|||||||
|
|
||||||
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
|
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)
|
if (body.enabled != undefined)
|
||||||
guild.welcome_screen.enabled = body.enabled;
|
guild.welcome_screen.enabled = body.enabled;
|
||||||
|
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
import {
|
import {
|
||||||
APError,
|
APError,
|
||||||
APObjectIsOrganisation,
|
APObjectIsOrganisation,
|
||||||
resolveWebfinger,
|
|
||||||
splitQualifiedMention,
|
splitQualifiedMention,
|
||||||
transformOrganisationToInvite,
|
transformOrganisationToInvite,
|
||||||
|
tryFederatedGuildJoin,
|
||||||
|
tryResolveWebfinger,
|
||||||
} from "@spacebar/ap";
|
} from "@spacebar/ap";
|
||||||
import { route } from "@spacebar/api";
|
import { route } from "@spacebar/api";
|
||||||
import {
|
import {
|
||||||
@ -68,17 +69,21 @@ router.get(
|
|||||||
if (domain != accountDomain && domain != host) {
|
if (domain != accountDomain && domain != host) {
|
||||||
// The domain isn't ours
|
// The domain isn't ours
|
||||||
|
|
||||||
const remoteGuild = await resolveWebfinger(inputValue);
|
const remoteGuild = await tryResolveWebfinger(inputValue);
|
||||||
|
if (remoteGuild) {
|
||||||
if (APObjectIsOrganisation(remoteGuild))
|
if (APObjectIsOrganisation(remoteGuild))
|
||||||
return res.json(
|
return res.json(
|
||||||
transformOrganisationToInvite(remoteGuild),
|
await transformOrganisationToInvite(
|
||||||
|
inputValue,
|
||||||
|
remoteGuild,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new APError("Remote resource is not a guild");
|
throw new APError("Remote resource is not a guild");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const invite = await Invite.findOneOrFail({
|
const invite = await Invite.findOneOrFail({
|
||||||
where: { code },
|
where: { code },
|
||||||
@ -110,8 +115,21 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT;
|
if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT;
|
||||||
|
|
||||||
const { code } = req.params;
|
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({
|
const { guild_id } = await Invite.findOneOrFail({
|
||||||
where: { code: code },
|
where: { code: code },
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,12 @@ import { FederationServer } from "@spacebar/ap";
|
|||||||
import * as Api from "@spacebar/api";
|
import * as Api from "@spacebar/api";
|
||||||
import { CDNServer } from "@spacebar/cdn";
|
import { CDNServer } from "@spacebar/cdn";
|
||||||
import * as Gateway from "@spacebar/gateway";
|
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 express from "express";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import { bold, green } from "picocolors";
|
import { bold, green } from "picocolors";
|
||||||
@ -35,9 +40,9 @@ const production = process.env.NODE_ENV == "development" ? false : true;
|
|||||||
server.on("request", app);
|
server.on("request", app);
|
||||||
|
|
||||||
const api = new Api.SpacebarServer({ server, port, production, 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 cdn = new CDNServer({ server, port, production, app });
|
||||||
const gateway = new Gateway.Server({ server, port, production });
|
const gateway = new Gateway.Server({ server, port, production });
|
||||||
const federation = new FederationServer({ server, port, production, app });
|
|
||||||
|
|
||||||
process.on("SIGTERM", async () => {
|
process.on("SIGTERM", async () => {
|
||||||
console.log("Shutting down due to SIGTERM");
|
console.log("Shutting down due to SIGTERM");
|
||||||
@ -53,6 +58,8 @@ async function main() {
|
|||||||
await Config.init();
|
await Config.init();
|
||||||
await Sentry.init(app);
|
await Sentry.init(app);
|
||||||
|
|
||||||
|
setupMorganLogging(app);
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
server.listen({ port }, () => resolve(undefined)),
|
server.listen({ port }, () => resolve(undefined)),
|
||||||
);
|
);
|
||||||
|
@ -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";
|
||||||
|
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 {
|
import {
|
||||||
|
BeforeInsert,
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
@ -83,8 +84,8 @@ export class Guild extends BaseClass {
|
|||||||
@ManyToOne(() => Channel)
|
@ManyToOne(() => Channel)
|
||||||
afk_channel?: Channel;
|
afk_channel?: Channel;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column()
|
||||||
afk_timeout?: number;
|
afk_timeout: number;
|
||||||
|
|
||||||
// * commented out -> use owner instead
|
// * commented out -> use owner instead
|
||||||
// application id of the guild creator if it is bot-created
|
// application id of the guild creator if it is bot-created
|
||||||
@ -135,11 +136,11 @@ export class Guild extends BaseClass {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
max_video_channel_users?: number;
|
max_video_channel_users?: number;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: 0 })
|
||||||
member_count?: number;
|
member_count: number;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: Number, default: 0 })
|
||||||
presence_count?: number; // users online
|
presence_count: number | null; // users online
|
||||||
|
|
||||||
@OneToMany(() => Member, (member: Member) => member.guild, {
|
@OneToMany(() => Member, (member: Member) => member.guild, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
@ -211,8 +212,8 @@ export class Guild extends BaseClass {
|
|||||||
})
|
})
|
||||||
webhooks: Webhook[];
|
webhooks: Webhook[];
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: 0 })
|
||||||
mfa_level?: number;
|
mfa_level: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
name: string;
|
name: string;
|
||||||
@ -225,14 +226,14 @@ export class Guild extends BaseClass {
|
|||||||
@ManyToOne(() => User)
|
@ManyToOne(() => User)
|
||||||
owner?: User; // optional to allow for ownerless guilds
|
owner?: User; // optional to allow for ownerless guilds
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: String, default: "en-US" })
|
||||||
preferred_locale?: string;
|
preferred_locale: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: Number, default: 0 })
|
||||||
premium_subscription_count?: number;
|
premium_subscription_count?: number;
|
||||||
|
|
||||||
@Column()
|
@Column({ default: 0 })
|
||||||
premium_tier?: number; // crowd premium level
|
premium_tier: number; // crowd premium level
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@RelationId((guild: Guild) => guild.public_updates_channel)
|
@RelationId((guild: Guild) => guild.public_updates_channel)
|
||||||
@ -264,17 +265,17 @@ export class Guild extends BaseClass {
|
|||||||
@ManyToOne(() => Channel)
|
@ManyToOne(() => Channel)
|
||||||
system_channel?: Channel;
|
system_channel?: Channel;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: 4 }) // defaults effect: suppress the setup tips to save performance
|
||||||
system_channel_flags?: number;
|
system_channel_flags?: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
unavailable: boolean = false;
|
unavailable: boolean = false;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: 0 })
|
||||||
verification_level?: number;
|
verification_level: number;
|
||||||
|
|
||||||
@Column({ type: "simple-json" })
|
@Column({ type: "simple-json", nullable: true }) // TODO: move this to own table
|
||||||
welcome_screen: GuildWelcomeScreen;
|
welcome_screen: GuildWelcomeScreen | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@RelationId((guild: Guild) => guild.widget_channel)
|
@RelationId((guild: Guild) => guild.widget_channel)
|
||||||
@ -287,8 +288,8 @@ export class Guild extends BaseClass {
|
|||||||
@Column()
|
@Column()
|
||||||
widget_enabled: boolean = true;
|
widget_enabled: boolean = true;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: 0 })
|
||||||
nsfw_level?: number;
|
nsfw_level: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
nsfw: boolean = false;
|
nsfw: boolean = false;
|
||||||
@ -317,32 +318,6 @@ export class Guild extends BaseClass {
|
|||||||
name: body.name || "Spacebar",
|
name: body.name || "Spacebar",
|
||||||
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
|
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
|
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();
|
}).save();
|
||||||
|
|
||||||
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
|
// 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,
|
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 "./EmbedCache";
|
||||||
export * from "./Emoji";
|
export * from "./Emoji";
|
||||||
export * from "./Encryption";
|
export * from "./Encryption";
|
||||||
|
export * from "./FederationActivity";
|
||||||
export * from "./FederationKeys";
|
export * from "./FederationKeys";
|
||||||
export * from "./Guild";
|
export * from "./Guild";
|
||||||
export * from "./Invite";
|
export * from "./Invite";
|
||||||
|
@ -2,20 +2,20 @@ import Express from "express";
|
|||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import { red } from "picocolors";
|
import { red } from "picocolors";
|
||||||
|
|
||||||
let HAS_WARNED = false;
|
let ENABLED = false;
|
||||||
export const setupMorganLogging = (app: Express.Application) => {
|
export const setupMorganLogging = (app: Express.Router) => {
|
||||||
const logRequests = process.env["LOG_REQUESTS"] != undefined;
|
const logRequests = process.env["LOG_REQUESTS"] != undefined;
|
||||||
if (!logRequests) return;
|
if (!logRequests) return;
|
||||||
|
|
||||||
if (!HAS_WARNED)
|
if (ENABLED) return;
|
||||||
|
ENABLED = true;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
red(
|
red(
|
||||||
`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`,
|
`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
HAS_WARNED = true;
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
morgan("combined", {
|
morgan("combined", {
|
||||||
skip: (req, res) => {
|
skip: (req, res) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user