diff --git a/src/api/Server.ts b/src/api/Server.ts
index 40d2b6dcb..bea75d7e2 100644
--- a/src/api/Server.ts
+++ b/src/api/Server.ts
@@ -34,7 +34,7 @@ import "missing-native-js-functions";
import morgan from "morgan";
import path from "path";
import { red } from "picocolors";
-import { Authentication, CORS } from "./middlewares/";
+import { Authentication, CORS, ImageProxy } from "./middlewares/";
import { BodyParser } from "./middlewares/BodyParser";
import { ErrorHandler } from "./middlewares/ErrorHandler";
import { initRateLimits } from "./middlewares/RateLimit";
@@ -137,6 +137,8 @@ export class SpacebarServer extends Server {
app.use("/api/v9", api);
app.use("/api", api); // allow unversioned requests
+ app.use("/imageproxy/:hash/:size/:url", ImageProxy);
+
app.get("/", (req, res) =>
res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "index.html")),
);
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index a6cad51cf..ffefee8fb 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -23,37 +23,37 @@ import { HTTPError } from "lambert-server";
export const NO_AUTHORIZATION_ROUTES = [
// Authentication routes
- "/auth/login",
- "/auth/register",
- "/auth/location-metadata",
- "/auth/mfa/totp",
- "/auth/mfa/webauthn",
- "/auth/verify",
- "/auth/forgot",
- "/auth/reset",
+ "POST /auth/login",
+ "POST /auth/register",
+ "GET /auth/location-metadata",
+ "POST /auth/mfa/",
+ "POST /auth/verify",
+ "POST /auth/forgot",
+ "POST /auth/reset",
+ "GET /invites/",
// Routes with a seperate auth system
- /\/webhooks\/\d+\/\w+\/?/, // no token requires auth
+ /POST \/webhooks\/\d+\/\w+\/?/, // no token requires auth
// Public information endpoints
- "/ping",
- "/gateway",
- "/experiments",
- "/updates",
- "/download",
- "/scheduled-maintenances/upcoming.json",
+ "GET /ping",
+ "GET /gateway",
+ "GET /experiments",
+ "GET /updates",
+ "GET /download",
+ "GET /scheduled-maintenances/upcoming.json",
// Public kubernetes integration
- "/-/readyz",
- "/-/healthz",
+ "GET /-/readyz",
+ "GET /-/healthz",
// Client analytics
- "/science",
- "/track",
+ "POST /science",
+ "POST /track",
// Public policy pages
- "/policies/instance",
+ "GET /policies/instance/",
// Oauth callback
"/oauth2/callback",
// Asset delivery
- /\/guilds\/\d+\/widget\.(json|png)/,
+ /GET \/guilds\/\d+\/widget\.(json|png)/,
// Connections
- /\/connections\/\w+\/callback/,
+ /POST \/connections\/\w+\/callback/,
];
export const API_PREFIX = /^\/api(\/v\d+)?/;
@@ -78,11 +78,11 @@ export async function Authentication(
) {
if (req.method === "OPTIONS") return res.sendStatus(204);
const url = req.url.replace(API_PREFIX, "");
- if (url.startsWith("/invites") && req.method === "GET") return next();
if (
NO_AUTHORIZATION_ROUTES.some((x) => {
- if (typeof x === "string") return url.startsWith(x);
- return x.test(url);
+ if (typeof x === "string")
+ return (req.method + " " + url).startsWith(x);
+ return x.test(req.method + " " + url);
})
)
return next();
diff --git a/src/api/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts
index ac8e04325..34433aeb2 100644
--- a/src/api/middlewares/BodyParser.ts
+++ b/src/api/middlewares/BodyParser.ts
@@ -20,6 +20,19 @@ import bodyParser, { OptionsJson } from "body-parser";
import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server";
+const errorMessages: { [key: string]: [string, number] } = {
+ "entity.too.large": ["Request body too large", 413],
+ "entity.parse.failed": ["Invalid JSON body", 400],
+ "entity.verify.failed": ["Entity verification failed", 403],
+ "request.aborted": ["Request aborted", 400],
+ "request.size.invalid": ["Request size did not match content length", 400],
+ "stream.encoding.set": ["Stream encoding should not be set", 500],
+ "stream.not.readable": ["Stream is not readable", 500],
+ "parameters.too.many": ["Too many parameters", 413],
+ "charset.unsupported": ["Unsupported charset", 415],
+ "encoding.unsupported": ["Unsupported content encoding", 415],
+};
+
export function BodyParser(opts?: OptionsJson) {
const jsonParser = bodyParser.json(opts);
@@ -29,8 +42,15 @@ export function BodyParser(opts?: OptionsJson) {
jsonParser(req, res, (err) => {
if (err) {
- // TODO: different errors for body parser (request size limit, wrong body type, invalid body, ...)
- return next(new HTTPError("Invalid Body", 400));
+ const [message, status] = errorMessages[err.type] || [
+ "Invalid Body",
+ 400,
+ ];
+ const errorMessage =
+ message.includes("charset") || message.includes("encoding")
+ ? `${message} "${err.charset || err.encoding}"`
+ : message;
+ return next(new HTTPError(errorMessage, status));
}
next();
});
diff --git a/src/api/middlewares/ImageProxy.ts b/src/api/middlewares/ImageProxy.ts
new file mode 100644
index 000000000..537c5da17
--- /dev/null
+++ b/src/api/middlewares/ImageProxy.ts
@@ -0,0 +1,180 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { Config, JimpType } from "@spacebar/util";
+import { Request, Response } from "express";
+import { yellow } from "picocolors";
+import crypto from "crypto";
+import fetch from "node-fetch";
+
+let sharp: undefined | false | { default: typeof import("sharp") } = undefined;
+
+let Jimp: JimpType | undefined = undefined;
+try {
+ Jimp = require("jimp") as JimpType;
+} catch {
+ // empty
+}
+
+let sentImageProxyWarning = false;
+
+const sharpSupported = new Set([
+ "image/jpeg",
+ "image/png",
+ "image/bmp",
+ "image/tiff",
+ "image/gif",
+ "image/webp",
+ "image/avif",
+ "image/svg+xml",
+]);
+const jimpSupported = new Set([
+ "image/jpeg",
+ "image/png",
+ "image/bmp",
+ "image/tiff",
+ "image/gif",
+]);
+const resizeSupported = new Set([...sharpSupported, ...jimpSupported]);
+
+export async function ImageProxy(req: Request, res: Response) {
+ const path = req.originalUrl.split("/").slice(2);
+
+ // src/api/util/utility/EmbedHandlers.ts getProxyUrl
+ const hash = crypto
+ .createHmac("sha1", Config.get().security.requestSignature)
+ .update(path.slice(1).join("/"))
+ .digest("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_");
+
+ try {
+ if (!crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(path[0])))
+ throw new Error("Invalid signature");
+ } catch {
+ console.log("Invalid signature, expected " + hash + " got " + path[0]);
+ res.status(403).send("Invalid signature");
+ return;
+ }
+
+ const abort = new AbortController();
+ setTimeout(() => abort.abort(), 5000);
+
+ const request = await fetch(path.slice(2).join("/"), {
+ headers: {
+ "User-Agent": "SpacebarImageProxy/1.0.0 (https://spacebar.chat)",
+ },
+ signal: abort.signal,
+ }).catch((e) => {
+ if (e.name === "AbortError") res.status(504).send("Request timed out");
+ else res.status(500).send("Unable to proxy origin: " + e.message);
+ });
+ if (!request) return;
+
+ if (request.status !== 200) {
+ res.status(request.status).send(
+ "Origin failed to respond: " +
+ request.status +
+ " " +
+ request.statusText,
+ );
+ return;
+ }
+
+ if (
+ !request.headers.get("Content-Type") ||
+ !request.headers.get("Content-Length")
+ ) {
+ res.status(500).send(
+ "Origin did not provide a Content-Type or Content-Length header",
+ );
+ return;
+ }
+
+ // @ts-expect-error TS doesn't believe that the header cannot be null (it's checked for falsiness above)
+ if (parseInt(request.headers.get("Content-Length")) > 1024 * 1024 * 10) {
+ res.status(500).send(
+ "Origin provided a Content-Length header that is too large",
+ );
+ return;
+ }
+
+ // @ts-expect-error TS doesn't believe that the header cannot be null (it's checked for falsiness above)
+ let contentType: string = request.headers.get("Content-Type");
+
+ const arrayBuffer = await request.arrayBuffer();
+ let resultBuffer = Buffer.from(arrayBuffer);
+
+ if (
+ !sentImageProxyWarning &&
+ resizeSupported.has(contentType) &&
+ /^\d+x\d+$/.test(path[1])
+ ) {
+ if (sharp !== false) {
+ try {
+ sharp = await import("sharp");
+ } catch {
+ sharp = false;
+ }
+ }
+
+ if (sharp === false && !Jimp) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore Typings don't fit
+ Jimp = await import("jimp");
+ } catch {
+ sentImageProxyWarning = true;
+ console.log(
+ `[ImageProxy] ${yellow(
+ 'Neither "sharp" or "jimp" NPM packages are installed, image resizing will be disabled',
+ )}`,
+ );
+ }
+ }
+
+ const [width, height] = path[1].split("x").map((x) => parseInt(x));
+
+ const buffer = Buffer.from(arrayBuffer);
+ if (sharp && sharpSupported.has(contentType)) {
+ resultBuffer = await sharp
+ .default(buffer)
+ // Sharp doesn't support "scaleToFit"
+ .resize(width)
+ .toBuffer();
+ } else if (Jimp && jimpSupported.has(contentType)) {
+ resultBuffer = await Jimp.read(buffer).then((image) => {
+ contentType = image.getMIME();
+ return (
+ image
+ .scaleToFit(width, height)
+ // @ts-expect-error Jimp is defined at this point
+ .getBufferAsync(Jimp.AUTO)
+ );
+ });
+ }
+ }
+
+ res.header("Content-Type", contentType);
+ res.setHeader(
+ "Cache-Control",
+ "public, max-age=" + Config.get().cdn.proxyCacheHeaderSeconds,
+ );
+
+ res.send(resultBuffer);
+}
diff --git a/src/api/middlewares/index.ts b/src/api/middlewares/index.ts
index 6384e1aad..9fd617f64 100644
--- a/src/api/middlewares/index.ts
+++ b/src/api/middlewares/index.ts
@@ -21,3 +21,4 @@ export * from "./BodyParser";
export * from "./CORS";
export * from "./ErrorHandler";
export * from "./RateLimit";
+export * from "./ImageProxy";
diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts
index 6fa86021c..d5a8a2f40 100644
--- a/src/api/routes/auth/forgot.ts
+++ b/src/api/routes/auth/forgot.ts
@@ -1,31 +1,24 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
-import {
- Config,
- Email,
- FieldErrors,
- ForgotPasswordSchema,
- User,
-} from "@spacebar/util";
+import { Config, Email, ForgotPasswordSchema, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
-import { HTTPError } from "lambert-server";
const router = Router();
router.post(
@@ -37,9 +30,6 @@ router.post(
400: {
body: "APIErrorOrCaptchaResponse",
},
- 500: {
- body: "APIErrorResponse",
- },
},
}),
async (req: Request, res: Response) => {
@@ -71,50 +61,20 @@ router.post(
}
}
- const user = await User.findOneOrFail({
- where: [{ phone: login }, { email: login }],
- select: ["username", "id", "disabled", "deleted", "email"],
- relations: ["security_keys"],
- }).catch(() => {
- throw FieldErrors({
- login: {
- message: req.t("auth:password_reset.EMAIL_DOES_NOT_EXIST"),
- code: "EMAIL_DOES_NOT_EXIST",
- },
- });
- });
+ res.sendStatus(204);
- if (!user.email)
- throw FieldErrors({
- login: {
- message:
- "This account does not have an email address associated with it.",
- code: "NO_EMAIL",
- },
- });
-
- if (user.deleted)
- return res.status(400).json({
- message: "This account is scheduled for deletion.",
- code: 20011,
- });
-
- if (user.disabled)
- return res.status(400).json({
- message: req.t("auth:login.ACCOUNT_DISABLED"),
- code: 20013,
- });
+ const user = await User.findOne({
+ where: [{ phone: login }, { email: login }],
+ select: ["username", "id", "email"],
+ }).catch(() => {});
- return await Email.sendResetPassword(user, user.email)
- .then(() => {
- return res.sendStatus(204);
- })
- .catch((e) => {
+ if (user && user.email) {
+ Email.sendResetPassword(user, user.email).catch((e) => {
console.error(
- `Failed to send password reset email to ${user.username}#${user.discriminator}: ${e}`,
+ `Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`,
);
- throw new HTTPError("Failed to send password reset email", 500);
});
+ }
},
);
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index de1cbd3d8..62152440b 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -287,6 +287,16 @@ router.post(
});
}
+ const { maxUsername } = Config.get().limits.user;
+ if (body.username.length > maxUsername) {
+ throw FieldErrors({
+ username: {
+ code: "BASE_TYPE_BAD_LENGTH",
+ message: `Must be between 2 and ${maxUsername} in length.`,
+ },
+ });
+ }
+
const user = await User.register({ ...body, req });
if (body.invite) {
diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts
index 291b64727..883bb2661 100644
--- a/src/api/routes/channels/#channel_id/index.ts
+++ b/src/api/routes/channels/#channel_id/index.ts
@@ -115,7 +115,7 @@ router.delete(
}
await Promise.all([
- Channel.delete({ id: channel_id }),
+ Channel.deleteChannel(channel),
emitEvent({
event: "CHANNEL_DELETE",
data: channel,
diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts
index 724ebffd4..d43db6ecc 100644
--- a/src/api/routes/channels/#channel_id/pins.ts
+++ b/src/api/routes/channels/#channel_id/pins.ts
@@ -1,30 +1,31 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { route } from "@spacebar/api";
import {
- Channel,
ChannelPinsUpdateEvent,
Config,
DiscordApiErrors,
emitEvent,
Message,
+ MessageCreateEvent,
MessageUpdateEvent,
+ User,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
@@ -60,8 +61,34 @@ router.put(
if (pinned_count >= maxPins)
throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins);
+ message.pinned = true;
+
+ const author = await User.getPublicUser(req.user_id);
+
+ const systemPinMessage = Message.create({
+ timestamp: new Date(),
+ type: 6,
+ guild_id: message.guild_id,
+ channel_id: message.channel_id,
+ author,
+ message_reference: {
+ message_id: message.id,
+ channel_id: message.channel_id,
+ guild_id: message.guild_id,
+ },
+ reactions: [],
+ attachments: [],
+ embeds: [],
+ sticker_items: [],
+ edited_timestamp: undefined,
+ mentions: [],
+ mention_channels: [],
+ mention_roles: [],
+ mention_everyone: false,
+ });
+
await Promise.all([
- Message.update({ id: message_id }, { pinned: true }),
+ message.save(),
emitEvent({
event: "MESSAGE_UPDATE",
channel_id,
@@ -76,6 +103,12 @@ router.put(
last_pin_timestamp: undefined,
},
} as ChannelPinsUpdateEvent),
+ systemPinMessage.save(),
+ emitEvent({
+ event: "MESSAGE_CREATE",
+ channel_id: message.channel_id,
+ data: systemPinMessage,
+ } as MessageCreateEvent),
]);
res.sendStatus(204);
@@ -98,31 +131,27 @@ router.delete(
async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params;
- const channel = await Channel.findOneOrFail({
- where: { id: channel_id },
- });
- if (channel.guild_id) req.permission?.hasThrow("MANAGE_MESSAGES");
-
const message = await Message.findOneOrFail({
where: { id: message_id },
});
+
+ if (message.guild_id) req.permission?.hasThrow("MANAGE_MESSAGES");
+
message.pinned = false;
await Promise.all([
message.save(),
-
emitEvent({
event: "MESSAGE_UPDATE",
channel_id,
data: message,
} as MessageUpdateEvent),
-
emitEvent({
event: "CHANNEL_PINS_UPDATE",
channel_id,
data: {
channel_id,
- guild_id: channel.guild_id,
+ guild_id: message.guild_id,
last_pin_timestamp: undefined,
},
} as ChannelPinsUpdateEvent),
diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
index 839ec363f..75d05c9b3 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -77,8 +77,8 @@ router.patch(
requestBody: "GuildUpdateSchema",
permission: "MANAGE_GUILD",
responses: {
- "200": {
- body: "GuildUpdateSchema",
+ 200: {
+ body: "GuildCreateResponse",
},
401: {
body: "APIErrorResponse",
diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts
index a64ae2c91..34595ca69 100644
--- a/src/api/routes/guilds/#guild_id/vanity-url.ts
+++ b/src/api/routes/guilds/#guild_id/vanity-url.ts
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -108,30 +108,23 @@ router.patch(
});
if (!guild.features.includes("ALIASABLE_NAMES")) {
- await Invite.update(
- { guild_id },
- {
- code: code,
- },
- );
-
- return res.json({ code });
+ await Invite.delete({ guild_id, vanity_url: true });
}
await Invite.create({
vanity_url: true,
- code: code,
+ code,
temporary: false,
uses: 0,
max_uses: 0,
max_age: 0,
created_at: new Date(),
- expires_at: new Date(),
guild_id: guild_id,
channel_id: id,
+ flags: 0,
}).save();
- return res.json({ code: code });
+ return res.json({ code });
},
);
diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index 39f498049..5d9f28c2e 100644
--- a/src/api/routes/guilds/#guild_id/widget.json.ts
+++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -1,25 +1,31 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { random, route } from "@spacebar/api";
-import { Channel, Guild, Invite, Member, Permissions } from "@spacebar/util";
+import {
+ Channel,
+ DiscordApiErrors,
+ Guild,
+ Invite,
+ Member,
+ Permissions,
+} from "@spacebar/util";
import { Request, Response, Router } from "express";
-import { HTTPError } from "lambert-server";
const router: Router = Router();
@@ -47,8 +53,17 @@ router.get(
async (req: Request, res: Response) => {
const { guild_id } = req.params;
- const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
- if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
+ const guild = await Guild.findOneOrFail({
+ where: { id: guild_id },
+ select: {
+ channel_ordering: true,
+ widget_channel_id: true,
+ widget_enabled: true,
+ presence_count: true,
+ name: true,
+ },
+ });
+ if (!guild.widget_enabled) throw DiscordApiErrors.EMBED_DISABLED;
// Fetch existing widget invite for widget channel
let invite = await Invite.findOne({
@@ -71,6 +86,7 @@ router.get(
created_at: new Date(),
guild_id,
channel_id: guild.widget_channel_id,
+ flags: 0,
}).save();
}
diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts
index c9ba8afce..c42b33e07 100644
--- a/src/api/routes/guilds/#guild_id/widget.png.ts
+++ b/src/api/routes/guilds/#guild_id/widget.png.ts
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -19,11 +19,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { route } from "@spacebar/api";
-import { Guild } from "@spacebar/util";
+import { DiscordApiErrors, Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
import fs from "fs";
import { HTTPError } from "lambert-server";
import path from "path";
+import { storage } from "../../../../cdn/util/Storage";
const router: Router = Router();
@@ -48,10 +49,10 @@ router.get(
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
- if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
+ if (!guild.widget_enabled) throw DiscordApiErrors.EMBED_DISABLED;
// Fetch guild information
- const icon = guild.icon;
+ const icon = "avatars/" + guild_id + "/" + guild.icon;
const name = guild.name;
const presence = guild.presence_count + " ONLINE";
@@ -69,8 +70,7 @@ router.get(
}
// Setup canvas
- const { createCanvas } = require("canvas");
- const { loadImage } = require("canvas");
+ const { createCanvas, loadImage } = require("canvas");
const sizeOf = require("image-size");
// TODO: Widget style templates need Spacebar branding
@@ -211,8 +211,8 @@ async function drawIcon(
scale: number,
icon: string,
) {
- const img = new (require("canvas").Image)();
- img.src = icon;
+ const { loadImage } = require("canvas");
+ const img = await loadImage(await storage.get(icon));
// Do some canvas clipping magic!
canvas.save();
diff --git a/src/api/routes/scheduled-maintenances/upcoming_json.ts b/src/api/routes/scheduled-maintenances/upcoming.json.ts
index c1fc0ff39..18f99ec99 100644
--- a/src/api/routes/scheduled-maintenances/upcoming_json.ts
+++ b/src/api/routes/scheduled-maintenances/upcoming.json.ts
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -20,15 +20,11 @@ import { Router, Request, Response } from "express";
import { route } from "@spacebar/api";
const router = Router();
-router.get(
- "/scheduled-maintenances/upcoming.json",
- route({}),
- async (req: Request, res: Response) => {
- res.json({
- page: {},
- scheduled_maintenances: {},
- });
- },
-);
+router.get("/", route({}), async (req: Request, res: Response) => {
+ res.json({
+ page: {},
+ scheduled_maintenances: {},
+ });
+});
export default router;
diff --git a/src/api/routes/teams.ts b/src/api/routes/teams.ts
index 26570165a..1328f6615 100644
--- a/src/api/routes/teams.ts
+++ b/src/api/routes/teams.ts
@@ -1,29 +1,99 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
-
+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
+import {
+ Team,
+ TeamCreateSchema,
+ TeamMember,
+ TeamMemberRole,
+ TeamMemberState,
+ User,
+} from "@spacebar/util";
+import { HTTPError } from "lambert-server";
const router: Router = Router();
-router.get("/", route({}), async (req: Request, res: Response) => {
- //TODO
- res.send([]);
-});
+router.get(
+ "/",
+ route({
+ query: {
+ include_payout_account_status: {
+ type: "boolean",
+ description:
+ "Whether to include team payout account status in the response (default false)",
+ },
+ },
+ responses: {
+ 200: {
+ body: "TeamListResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const teams = await Team.find({
+ where: {
+ owner_user_id: req.user_id,
+ },
+ relations: ["members"],
+ });
+
+ res.send(teams);
+ },
+);
+
+router.post(
+ "/",
+ route({
+ requestBody: "TeamCreateSchema",
+ responses: {
+ 200: {
+ body: "Team",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const user = await User.findOneOrFail({
+ where: [{ id: req.user_id }],
+ select: ["mfa_enabled"],
+ });
+ if (!user.mfa_enabled)
+ throw new HTTPError("You must enable MFA to create a team");
+
+ const body = req.body as TeamCreateSchema;
+
+ const team = Team.create({
+ name: body.name,
+ owner_user_id: req.user_id,
+ });
+ await team.save();
+
+ await TeamMember.create({
+ user_id: req.user_id,
+ team_id: team.id,
+ membership_state: TeamMemberState.ACCEPTED,
+ permissions: ["*"],
+ role: TeamMemberRole.ADMIN,
+ }).save();
+
+ res.json(team);
+ },
+);
export default router;
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index 5caf0d119..9cd8bfda8 100644
--- a/src/api/routes/users/@me/index.ts
+++ b/src/api/routes/users/@me/index.ts
@@ -155,8 +155,8 @@ router.patch(
if (check_username.length > maxUsername) {
throw FieldErrors({
username: {
- code: "USERNAME_INVALID",
- message: `Username must be less than ${maxUsername} in length`,
+ code: "BASE_TYPE_BAD_LENGTH",
+ message: `Must be between 2 and ${maxUsername} in length.`,
},
});
}
|