1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-09-19 17:21:35 +02:00

send Follow request to guild when remote invite code used

This commit is contained in:
Madeline 2023-09-28 16:10:57 +00:00
parent 904618e0a7
commit c82b71695d
21 changed files with 378 additions and 126 deletions

23
.vscode/launch.json vendored
View File

@ -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"
}
]
}

View File

@ -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",

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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]

View File

@ -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,12 +35,43 @@ 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;
}
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,
@ -51,8 +79,12 @@ class FederationQueue {
);
const ret = await fetch(receiver, signedActivity);
console.log(ret);
if (!ret.ok) {
console.error(
`Sending activity ${activity.id} to ` +
`${receiver} failed with code ${ret.status} `,
JSON.stringify(await ret.json()),
);
}
}
}

View File

@ -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`,

View File

@ -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, {
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";

View File

@ -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;

View 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
View 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;

View File

@ -406,6 +406,7 @@ router.post(
);
setImmediate(async () => {
if (!Config.get().federation.enabled) return;
const ap = await transformMessageToAnnounceNoce(message);
await Federation.distribute(ap);

View File

@ -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;

View File

@ -19,9 +19,10 @@
import {
APError,
APObjectIsOrganisation,
resolveWebfinger,
splitQualifiedMention,
transformOrganisationToInvite,
tryFederatedGuildJoin,
tryResolveWebfinger,
} from "@spacebar/ap";
import { route } from "@spacebar/api";
import {
@ -68,17 +69,21 @@ 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(
transformOrganisationToInvite(remoteGuild),
await transformOrganisationToInvite(
inputValue,
remoteGuild,
),
);
throw new APError("Remote resource is not a guild");
}
}
}
}
const invite = await Invite.findOneOrFail({
where: { code },
@ -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 },
});

View File

@ -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)),
);

View File

@ -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";

View 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,
};
}
}

View File

@ -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;
};
}

View File

@ -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";

View File

@ -2,20 +2,20 @@ 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)
if (ENABLED) return;
ENABLED = true;
console.log(
red(
`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`,
),
);
HAS_WARNED = true;
app.use(
morgan("combined", {
skip: (req, res) => {