Merge pull request #1032 from spacebarchat/openapi

Better OpenAPI
This commit is contained in:
Madeline 2023-04-29 01:11:22 +10:00 committed by GitHub
commit 009a3ad27f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
167 changed files with 502370 additions and 6557 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -27,34 +27,46 @@ require("missing-native-js-functions");
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json"); const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json"); const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json");
let schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
// const specification = JSON.parse(
for (var schema in schemas) { // fs.readFileSync(openapiPath, { encoding: "utf8" }),
const part = schemas[schema]; // );
for (var key in part.properties) { let specification = {
if (part.properties[key].anyOf) { openapi: "3.1.0",
const nullIndex = part.properties[key].anyOf.findIndex( info: {
(x) => x.type == "null", title: "Spacebar Server",
); description:
if (nullIndex != -1) { "Spacebar is a free open source selfhostable discord compatible chat, voice and video platform",
part.properties[key].nullable = true; license: {
part.properties[key].anyOf.splice(nullIndex, 1); name: "AGPLV3",
url: "https://www.gnu.org/licenses/agpl-3.0.en.html",
if (part.properties[key].anyOf.length == 1) { },
Object.assign( version: "1.0.0",
part.properties[key], },
part.properties[key].anyOf[0], externalDocs: {
); description: "Spacebar Docs",
delete part.properties[key].anyOf; url: "https://docs.spacebar.chat",
} },
} servers: [
} {
} url: "https://old.server.spacebar.chat/api/",
} description: "Official Spacebar Instance",
},
const specification = JSON.parse( ],
fs.readFileSync(openapiPath, { encoding: "utf8" }), components: {
); securitySchemes: {
bearer: {
type: "http",
scheme: "bearer",
description: "Bearer/Bot prefixes are not required.",
bearerFormat: "JWT",
in: "header",
},
},
},
tags: [],
paths: {},
};
function combineSchemas(schemas) { function combineSchemas(schemas) {
var definitions = {}; var definitions = {};
@ -72,6 +84,11 @@ function combineSchemas(schemas) {
} }
for (const key in definitions) { for (const key in definitions) {
const reg = new RegExp(/^[a-zA-Z0-9.\-_]+$/, "gm");
if (!reg.test(key)) {
console.error(`Invalid schema name: ${key} (${reg.test(key)})`);
continue;
}
specification.components = specification.components || {}; specification.components = specification.components || {};
specification.components.schemas = specification.components.schemas =
specification.components.schemas || {}; specification.components.schemas || {};
@ -102,30 +119,20 @@ function getTag(key) {
function apiRoutes() { function apiRoutes() {
const routes = getRouteDescriptions(); const routes = getRouteDescriptions();
const tags = Array.from(routes.keys()).map((x) => getTag(x)); // populate tags
specification.tags = specification.tags || []; const tags = Array.from(routes.keys())
specification.tags = [...specification.tags.map((x) => x.name), ...tags] .map((x) => getTag(x))
.unique() .sort((a, b) => a.localeCompare(b));
.map((x) => ({ name: x })); specification.tags = tags.unique().map((x) => ({ name: x }));
specification.components = specification.components || {};
specification.components.securitySchemes = {
bearer: {
type: "http",
scheme: "bearer",
description: "Bearer/Bot prefixes are not required.",
},
};
routes.forEach((route, pathAndMethod) => { routes.forEach((route, pathAndMethod) => {
const [p, method] = pathAndMethod.split("|"); const [p, method] = pathAndMethod.split("|");
const path = p.replace(/:(\w+)/g, "{$1}"); const path = p.replace(/:(\w+)/g, "{$1}");
specification.paths = specification.paths || {};
let obj = specification.paths[path]?.[method] || {}; let obj = specification.paths[path]?.[method] || {};
obj["x-right-required"] = route.right; obj["x-right-required"] = route.right;
obj["x-permission-required"] = route.permission; obj["x-permission-required"] = route.permission;
obj["x-fires-event"] = route.test?.event; obj["x-fires-event"] = route.event;
if ( if (
!NO_AUTHORIZATION_ROUTES.some((x) => { !NO_AUTHORIZATION_ROUTES.some((x) => {
@ -136,48 +143,56 @@ function apiRoutes() {
obj.security = [{ bearer: [] }]; obj.security = [{ bearer: [] }];
} }
if (route.body) { if (route.description) obj.description = route.description;
if (route.summary) obj.summary = route.summary;
if (route.deprecated) obj.deprecated = route.deprecated;
if (route.requestBody) {
obj.requestBody = { obj.requestBody = {
required: true, required: true,
content: { content: {
"application/json": { "application/json": {
schema: { $ref: `#/components/schemas/${route.body}` }, schema: {
$ref: `#/components/schemas/${route.requestBody}`,
},
}, },
}, },
}.merge(obj.requestBody); }.merge(obj.requestBody);
} }
if (route.test?.response) { if (route.responses) {
const status = route.test.response.status || 200; for (const [k, v] of Object.entries(route.responses)) {
let schema = { let schema = {
allOf: [ $ref: `#/components/schemas/${v.body}`,
{ };
$ref: `#/components/schemas/${route.test.response.body}`,
},
{
example: route.test.body,
},
],
};
if (!route.test.body) schema = schema.allOf[0];
obj.responses = { obj.responses = {
[status]: { [k]: {
...(route.test.response.body ...(v.body
? { ? {
description: description:
obj?.responses?.[status]?.description || "", obj?.responses?.[k]?.description || "",
content: { content: {
"application/json": { "application/json": {
schema: schema, schema: schema,
},
}, },
}, }
} : {
: {}), description: "No description available",
}),
},
}.merge(obj.responses);
}
} else {
obj.responses = {
default: {
description: "No description available",
}, },
}.merge(obj.responses); };
delete obj.responses.default;
} }
// handles path parameters
if (p.includes(":")) { if (p.includes(":")) {
obj.parameters = p.match(/:\w+/g)?.map((x) => ({ obj.parameters = p.match(/:\w+/g)?.map((x) => ({
name: x.replace(":", ""), name: x.replace(":", ""),
@ -187,16 +202,33 @@ function apiRoutes() {
description: x.replace(":", ""), description: x.replace(":", ""),
})); }));
} }
if (route.query) {
// map to array
const query = Object.entries(route.query).map(([k, v]) => ({
name: k,
in: "query",
required: v.required,
schema: { type: v.type },
description: v.description,
}));
obj.parameters = [...(obj.parameters || []), ...query];
}
obj.tags = [...(obj.tags || []), getTag(p)].unique(); obj.tags = [...(obj.tags || []), getTag(p)].unique();
specification.paths[path] = { specification.paths[path] = Object.assign(
...specification.paths[path], specification.paths[path] || {},
[method]: obj, {
}; [method]: obj,
},
);
}); });
} }
function main() { function main() {
console.log("Generating OpenAPI Specification...");
combineSchemas(schemas); combineSchemas(schemas);
apiRoutes(); apiRoutes();

View File

@ -57,6 +57,8 @@ const Excluded = [
"PropertiesSchema", "PropertiesSchema",
"AsyncSchema", "AsyncSchema",
"AnySchema", "AnySchema",
"SMTPConnection.CustomAuthenticationResponse",
"TransportMakeRequestResponse",
]; ];
function modify(obj) { function modify(obj) {
@ -75,14 +77,14 @@ function main() {
const generator = TJS.buildGenerator(program, settings); const generator = TJS.buildGenerator(program, settings);
if (!generator || !program) return; if (!generator || !program) return;
let schemas = generator let schemas = generator.getUserSymbols().filter((x) => {
.getUserSymbols() return (
.filter( (x.endsWith("Schema") ||
(x) => x.endsWith("Response") ||
(x.endsWith("Schema") || x.endsWith("Response")) && x.startsWith("API")) &&
!Excluded.includes(x), !Excluded.includes(x)
); );
console.log(schemas); });
var definitions = {}; var definitions = {};
@ -133,7 +135,7 @@ function main() {
definitions = { ...definitions, [name]: { ...part } }; definitions = { ...definitions, [name]: { ...part } };
} }
modify(definitions); //modify(definitions);
fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4)); fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4));
} }

View File

@ -1,80 +1,64 @@
const express = require("express");
const path = require("path");
const { traverseDirectory } = require("lambert-server");
const RouteUtility = require("../../dist/api/util/handlers/route.js");
const methods = ["get", "post", "put", "delete", "patch"];
const routes = new Map();
let currentFile = "";
let currentPath = "";
/* /*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend. For some reason, if a route exports multiple functions, it won't be registered here!
Copyright (C) 2023 Spacebar and Spacebar Contributors If someone could fix that I'd really appreciate it, but for now just, don't do that :p
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/>.
*/ */
const { traverseDirectory } = require("lambert-server"); const proxy = (file, method, prefix, path, ...args) => {
const path = require("path"); const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true);
const express = require("express"); if (!opts)
const RouteUtility = require("../../dist/api/util/handlers/route.js"); return console.error(
const Router = express.Router; `${file} has route without route() description middleware`,
const routes = new Map();
let currentPath = "";
let currentFile = "";
const methods = ["get", "post", "put", "delete", "patch"];
function registerPath(file, method, prefix, path, ...args) {
const urlPath = prefix + path;
const sourceFile = file.replace("/dist/", "/src/").replace(".js", ".ts");
const opts = args.find((x) => typeof x === "object");
if (opts) {
routes.set(urlPath + "|" + method, opts);
opts.file = sourceFile;
// console.log(method, urlPath, opts);
} else {
console.log(
`${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`,
); );
}
}
function routeOptions(opts) { console.log(prefix + path + " - " + method);
return opts; opts.file = file.replace("/dist/", "/src/").replace(".js", ".ts");
} routes.set(prefix + path + "|" + method, opts());
};
RouteUtility.route = routeOptions; express.Router = () => {
return Object.fromEntries(
methods.map((method) => [
method,
proxy.bind(null, currentFile, method, currentPath),
]),
);
};
express.Router = (opts) => { RouteUtility.route = (opts) => {
const path = currentPath; const func = function () {
const file = currentFile; return opts;
const router = Router(opts); };
func.prototype.OPTS_MARKER = true;
for (const method of methods) { return func;
router[method] = registerPath.bind(null, file, method, path);
}
return router;
}; };
module.exports = function getRouteDescriptions() { module.exports = function getRouteDescriptions() {
const root = path.join(__dirname, "..", "..", "dist", "api", "routes", "/"); const root = path.join(__dirname, "..", "..", "dist", "api", "routes", "/");
traverseDirectory({ dirname: root, recursive: true }, (file) => { traverseDirectory({ dirname: root, recursive: true }, (file) => {
currentFile = file; currentFile = file;
let path = file.replace(root.slice(0, -1), "");
path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path currentPath = file.replace(root.slice(0, -1), "");
path = path.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes currentPath = currentPath.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path
if (path.endsWith("/index")) path = path.slice(0, "/index".length * -1); // delete index from path currentPath = currentPath.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes
currentPath = path; if (currentPath.endsWith("/index"))
currentPath = currentPath.slice(0, "/index".length * -1); // delete index from path
try { try {
require(file); require(file);
} catch (error) { } catch (e) {
console.error("error loading file " + file, error); console.error(e);
} }
}); });
return routes; return routes;
}; };

View File

@ -16,78 +16,114 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Application, Application,
generateToken,
User,
BotModifySchema, BotModifySchema,
handleFile,
DiscordApiErrors, DiscordApiErrors,
User,
generateToken,
handleFile,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { verifyToken } from "node-2fa"; import { verifyToken } from "node-2fa";
const router: Router = Router(); const router: Router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const app = await Application.findOneOrFail({ "/",
where: { id: req.params.id }, route({
relations: ["owner"], responses: {
}); 204: {
body: "TokenOnlyResponse",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const app = await Application.findOneOrFail({
where: { id: req.params.id },
relations: ["owner"],
});
if (app.owner.id != req.user_id) if (app.owner.id != req.user_id)
throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
const user = await User.register({ const user = await User.register({
username: app.name, username: app.name,
password: undefined, password: undefined,
id: app.id, id: app.id,
req, req,
}); });
user.id = app.id; user.id = app.id;
user.premium_since = new Date(); user.premium_since = new Date();
user.bot = true; user.bot = true;
await user.save(); await user.save();
// flags is NaN here? // flags is NaN here?
app.assign({ bot: user, flags: app.flags || 0 }); app.assign({ bot: user, flags: app.flags || 0 });
await app.save(); await app.save();
res.send({ res.send({
token: await generateToken(user.id), token: await generateToken(user.id),
}).status(204); }).status(204);
}); },
);
router.post("/reset", route({}), async (req: Request, res: Response) => { router.post(
const bot = await User.findOneOrFail({ where: { id: req.params.id } }); "/reset",
const owner = await User.findOneOrFail({ where: { id: req.user_id } }); route({
responses: {
200: {
body: "TokenResponse",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const bot = await User.findOneOrFail({ where: { id: req.params.id } });
const owner = await User.findOneOrFail({ where: { id: req.user_id } });
if (owner.id != req.user_id) if (owner.id != req.user_id)
throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
if ( if (
owner.totp_secret && owner.totp_secret &&
(!req.body.code || verifyToken(owner.totp_secret, req.body.code)) (!req.body.code || verifyToken(owner.totp_secret, req.body.code))
) )
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
bot.data = { hash: undefined, valid_tokens_since: new Date() }; bot.data = { hash: undefined, valid_tokens_since: new Date() };
await bot.save(); await bot.save();
const token = await generateToken(bot.id); const token = await generateToken(bot.id);
res.json({ token }).status(200); res.json({ token }).status(200);
}); },
);
router.patch( router.patch(
"/", "/",
route({ body: "BotModifySchema" }), route({
requestBody: "BotModifySchema",
responses: {
200: {
body: "Application",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as BotModifySchema; const body = req.body as BotModifySchema;
if (!body.avatar?.trim()) delete body.avatar; if (!body.avatar?.trim()) delete body.avatar;

View File

@ -16,15 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), (req: Request, res: Response) => { router.get(
// TODO: "/",
//const { exclude_consumed } = req.query; route({
res.status(200).send([]); responses: {
}); 200: {
body: "ApplicationEntitlementsResponse",
},
},
}),
(req: Request, res: Response) => {
// TODO:
//const { exclude_consumed } = req.query;
res.status(200).send([]);
},
);
export default router; export default router;

View File

@ -16,32 +16,55 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Application, Application,
DiscordApiErrors,
ApplicationModifySchema, ApplicationModifySchema,
DiscordApiErrors,
} from "@spacebar/util"; } from "@spacebar/util";
import { verifyToken } from "node-2fa"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { verifyToken } from "node-2fa";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const app = await Application.findOneOrFail({ "/",
where: { id: req.params.id }, route({
relations: ["owner", "bot"], responses: {
}); 200: {
if (app.owner.id != req.user_id) body: "Application",
throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; },
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const app = await Application.findOneOrFail({
where: { id: req.params.id },
relations: ["owner", "bot"],
});
if (app.owner.id != req.user_id)
throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
return res.json(app); return res.json(app);
}); },
);
router.patch( router.patch(
"/", "/",
route({ body: "ApplicationModifySchema" }), route({
requestBody: "ApplicationModifySchema",
responses: {
200: {
body: "Application",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as ApplicationModifySchema; const body = req.body as ApplicationModifySchema;
@ -73,23 +96,35 @@ router.patch(
}, },
); );
router.post("/delete", route({}), async (req: Request, res: Response) => { router.post(
const app = await Application.findOneOrFail({ "/delete",
where: { id: req.params.id }, route({
relations: ["bot", "owner"], responses: {
}); 200: {},
if (app.owner.id != req.user_id) 400: {
throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const app = await Application.findOneOrFail({
where: { id: req.params.id },
relations: ["bot", "owner"],
});
if (app.owner.id != req.user_id)
throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
if ( if (
app.owner.totp_secret && app.owner.totp_secret &&
(!req.body.code || verifyToken(app.owner.totp_secret, req.body.code)) (!req.body.code ||
) verifyToken(app.owner.totp_secret, req.body.code))
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); )
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
await Application.delete({ id: app.id }); await Application.delete({ id: app.id });
res.send().status(200); res.send().status(200);
}); },
);
export default router; export default router;

View File

@ -16,13 +16,23 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
res.json([]).status(200); "/",
}); route({
responses: {
200: {
body: "ApplicationSkusResponse",
},
},
}),
async (req: Request, res: Response) => {
res.json([]).status(200);
},
);
export default router; export default router;

View File

@ -16,14 +16,24 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
//TODO "/",
res.send([]).status(200); route({
}); responses: {
200: {
body: "ApplicationDetectableResponse",
},
},
}),
async (req: Request, res: Response) => {
//TODO
res.send([]).status(200);
},
);
export default router; export default router;

View File

@ -16,28 +16,45 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Application, Application,
ApplicationCreateSchema, ApplicationCreateSchema,
trimSpecial,
User, User,
trimSpecial,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const results = await Application.find({ "/",
where: { owner: { id: req.user_id } }, route({
relations: ["owner", "bot"], responses: {
}); 200: {
res.json(results).status(200); body: "APIApplicationArray",
}); },
},
}),
async (req: Request, res: Response) => {
const results = await Application.find({
where: { owner: { id: req.user_id } },
relations: ["owner", "bot"],
});
res.json(results).status(200);
},
);
router.post( router.post(
"/", "/",
route({ body: "ApplicationCreateSchema" }), route({
requestBody: "ApplicationCreateSchema",
responses: {
200: {
body: "Application",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as ApplicationCreateSchema; const body = req.body as ApplicationCreateSchema;
const user = await User.findOneOrFail({ where: { id: req.user_id } }); const user = await User.findOneOrFail({ where: { id: req.user_id } });

View File

@ -30,7 +30,18 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ body: "ForgotPasswordSchema" }), route({
requestBody: "ForgotPasswordSchema",
responses: {
204: {},
400: {
body: "APIErrorOrCaptchaResponse",
},
500: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { login, captcha_key } = req.body as ForgotPasswordSchema; const { login, captcha_key } = req.body as ForgotPasswordSchema;

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route, random } from "@spacebar/api"; import { random, route } from "@spacebar/api";
import { Config, ValidRegistrationToken } from "@spacebar/util"; import { Config, ValidRegistrationToken } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
@ -25,7 +25,22 @@ export default router;
router.get( router.get(
"/", "/",
route({ right: "OPERATOR" }), route({
query: {
count: {
type: "number",
description:
"The number of registration tokens to generate. Defaults to 1.",
},
length: {
type: "number",
description:
"The length of each registration token. Defaults to 255.",
},
},
right: "OPERATOR",
responses: { 200: { body: "GenerateRegistrationTokensResponse" } },
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const count = req.query.count ? parseInt(req.query.count as string) : 1; const count = req.query.count ? parseInt(req.query.count as string) : 1;
const length = req.query.length const length = req.query.length

View File

@ -16,20 +16,29 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { IPAnalysis, getIpAdress, route } from "@spacebar/api";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
import { getIpAdress, IPAnalysis } from "@spacebar/api";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
//TODO "/",
//Note: It's most likely related to legal. At the moment Discord hasn't finished this too route({
const country_code = (await IPAnalysis(getIpAdress(req))).country_code; responses: {
res.json({ 200: {
consent_required: false, body: "LocationMetadataResponse",
country_code: country_code, },
promotional_email_opt_in: { required: true, pre_checked: false }, },
}); }),
}); async (req: Request, res: Response) => {
//TODO
//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
res.json({
consent_required: false,
country_code: country_code,
promotional_email_opt_in: { required: true, pre_checked: false },
});
},
);
export default router; export default router;

View File

@ -36,7 +36,17 @@ export default router;
router.post( router.post(
"/", "/",
route({ body: "LoginSchema" }), route({
requestBody: "LoginSchema",
responses: {
200: {
body: "LoginResponse",
},
400: {
body: "APIErrorOrCaptchaResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { login, password, captcha_key, undelete } = const { login, password, captcha_key, undelete } =
req.body as LoginSchema; req.body as LoginSchema;

View File

@ -22,14 +22,25 @@ import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
export default router; export default router;
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
if (req.body.provider != null || req.body.voip_provider != null) { "/",
console.log(`[LOGOUT]: provider or voip provider not null!`, req.body); route({
} else { responses: {
delete req.body.provider; 204: {},
delete req.body.voip_provider; },
if (Object.keys(req.body).length != 0) }),
console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body); async (req: Request, res: Response) => {
} if (req.body.provider != null || req.body.voip_provider != null) {
res.status(204).send(); console.log(
}); `[LOGOUT]: provider or voip provider not null!`,
req.body,
);
} else {
delete req.body.provider;
delete req.body.voip_provider;
if (Object.keys(req.body).length != 0)
console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
}
res.status(204).send();
},
);

View File

@ -16,16 +16,26 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { BackupCode, generateToken, User, TotpSchema } from "@spacebar/util"; import { BackupCode, TotpSchema, User, generateToken } from "@spacebar/util";
import { verifyToken } from "node-2fa"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { verifyToken } from "node-2fa";
const router = Router(); const router = Router();
router.post( router.post(
"/", "/",
route({ body: "TotpSchema" }), route({
requestBody: "TotpSchema",
responses: {
200: {
body: "TokenResponse",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// const { code, ticket, gift_code_sku_id, login_source } = // const { code, ticket, gift_code_sku_id, login_source } =
const { code, ticket } = req.body as TotpSchema; const { code, ticket } = req.body as TotpSchema;

View File

@ -41,7 +41,13 @@ function toArrayBuffer(buf: Buffer) {
router.post( router.post(
"/", "/",
route({ body: "WebAuthnTotpSchema" }), route({
requestBody: "WebAuthnTotpSchema",
responses: {
200: { body: "TokenResponse" },
400: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
if (!WebAuthn.fido2) { if (!WebAuthn.fido2) {
// TODO: I did this for typescript and I can't use ! // TODO: I did this for typescript and I can't use !

View File

@ -16,25 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { import {
Config,
generateToken,
Invite,
FieldErrors,
User,
adjustEmail,
RegisterSchema,
ValidRegistrationToken,
} from "@spacebar/util";
import {
route,
getIpAdress,
IPAnalysis, IPAnalysis,
getIpAdress,
isProxy, isProxy,
route,
verifyCaptcha, verifyCaptcha,
} from "@spacebar/api"; } from "@spacebar/api";
import {
Config,
FieldErrors,
Invite,
RegisterSchema,
User,
ValidRegistrationToken,
adjustEmail,
generateToken,
} from "@spacebar/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { MoreThan } from "typeorm"; import { MoreThan } from "typeorm";
@ -42,7 +42,13 @@ const router: Router = Router();
router.post( router.post(
"/", "/",
route({ body: "RegisterSchema" }), route({
requestBody: "RegisterSchema",
responses: {
200: { body: "TokenOnlyResponse" },
400: { body: "APIErrorOrCaptchaResponse" },
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as RegisterSchema; const body = req.body as RegisterSchema;
const { register, security, limits } = Config.get(); const { register, security, limits } = Config.get();

View File

@ -31,9 +31,20 @@ import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
// TODO: the response interface also returns settings, but this route doesn't actually return that.
router.post( router.post(
"/", "/",
route({ body: "PasswordResetSchema" }), route({
requestBody: "PasswordResetSchema",
responses: {
200: {
body: "TokenOnlyResponse",
},
400: {
body: "APIErrorOrCaptchaResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { password, token } = req.body as PasswordResetSchema; const { password, token } = req.body as PasswordResetSchema;

View File

@ -37,9 +37,20 @@ async function getToken(user: User) {
return { token }; return { token };
} }
// TODO: the response interface also returns settings, but this route doesn't actually return that.
router.post( router.post(
"/", "/",
route({ body: "VerifyEmailSchema" }), route({
requestBody: "VerifyEmailSchema",
responses: {
200: {
body: "TokenResponse",
},
400: {
body: "APIErrorOrCaptchaResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { captcha_key, token } = req.body; const { captcha_key, token } = req.body;

View File

@ -24,7 +24,18 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ right: "RESEND_VERIFICATION_EMAIL" }), route({
right: "RESEND_VERIFICATION_EMAIL",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
500: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const user = await User.findOneOrFail({ const user = await User.findOneOrFail({
where: { id: req.user_id }, where: { id: req.user_id },

View File

@ -16,15 +16,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { FieldErrors, User, BackupCodesChallengeSchema } from "@spacebar/util"; import { BackupCodesChallengeSchema, FieldErrors, User } from "@spacebar/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post( router.post(
"/", "/",
route({ body: "BackupCodesChallengeSchema" }), route({
requestBody: "BackupCodesChallengeSchema",
responses: {
200: { body: "BackupCodesChallengeResponse" },
400: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { password } = req.body as BackupCodesChallengeSchema; const { password } = req.body as BackupCodesChallengeSchema;

View File

@ -16,18 +16,18 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
Channel, Channel,
ChannelDeleteEvent, ChannelDeleteEvent,
ChannelModifySchema,
ChannelType, ChannelType,
ChannelUpdateEvent, ChannelUpdateEvent,
emitEvent,
Recipient, Recipient,
emitEvent,
handleFile, handleFile,
ChannelModifySchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
// TODO: delete channel // TODO: delete channel
@ -35,7 +35,15 @@ const router: Router = Router();
router.get( router.get(
"/", "/",
route({ permission: "VIEW_CHANNEL" }), route({
permission: "VIEW_CHANNEL",
responses: {
200: {
body: "Channel",
},
404: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
@ -49,7 +57,15 @@ router.get(
router.delete( router.delete(
"/", "/",
route({ permission: "MANAGE_CHANNELS" }), route({
permission: "MANAGE_CHANNELS",
responses: {
200: {
body: "Channel",
},
404: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
@ -90,7 +106,19 @@ router.delete(
router.patch( router.patch(
"/", "/",
route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), route({
requestBody: "ChannelModifySchema",
permission: "MANAGE_CHANNELS",
responses: {
200: {
body: "Channel",
},
404: {},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const payload = req.body as ChannelModifySchema; const payload = req.body as ChannelModifySchema;
const { channel_id } = req.params; const { channel_id } = req.params;

View File

@ -16,29 +16,37 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { random, route } from "@spacebar/api";
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
import { random } from "@spacebar/api";
import { import {
Channel, Channel,
Guild,
Invite, Invite,
InviteCreateEvent, InviteCreateEvent,
emitEvent,
User,
Guild,
PublicInviteRelation, PublicInviteRelation,
User,
emitEvent,
isTextChannel,
} from "@spacebar/util"; } from "@spacebar/util";
import { isTextChannel } from "./messages"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
router.post( router.post(
"/", "/",
route({ route({
body: "InviteCreateSchema", requestBody: "InviteCreateSchema",
permission: "CREATE_INSTANT_INVITE", permission: "CREATE_INSTANT_INVITE",
right: "CREATE_INVITES", right: "CREATE_INVITES",
responses: {
201: {
body: "Invite",
},
404: {},
400: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { user_id } = req; const { user_id } = req;
@ -84,7 +92,15 @@ router.post(
router.get( router.get(
"/", "/",
route({ permission: "MANAGE_CHANNELS" }), route({
permission: "MANAGE_CHANNELS",
responses: {
200: {
body: "APIInviteArray",
},
404: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ const channel = await Channel.findOneOrFail({

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
emitEvent, emitEvent,
getPermission, getPermission,
@ -23,7 +24,6 @@ import {
ReadState, ReadState,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
const router = Router(); const router = Router();
@ -33,7 +33,13 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ body: "MessageAcknowledgeSchema" }), route({
requestBody: "MessageAcknowledgeSchema",
responses: {
200: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;

View File

@ -16,14 +16,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post( router.post(
"/", "/",
route({ permission: "MANAGE_MESSAGES" }), route({
permission: "MANAGE_MESSAGES",
responses: {
200: {
body: "Message",
},
},
}),
(req: Request, res: Response) => { (req: Request, res: Response) => {
// TODO: // TODO:
res.json({ res.json({

View File

@ -19,24 +19,23 @@
import { import {
Attachment, Attachment,
Channel, Channel,
emitEvent,
SpacebarApiErrors,
getPermission,
getRights,
Message, Message,
MessageCreateEvent, MessageCreateEvent,
MessageCreateSchema,
MessageDeleteEvent, MessageDeleteEvent,
MessageEditSchema,
MessageUpdateEvent, MessageUpdateEvent,
Snowflake, Snowflake,
SpacebarApiErrors,
emitEvent,
getPermission,
getRights,
uploadFile, uploadFile,
MessageCreateSchema,
MessageEditSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { Router, Response, Request } from "express"; import { Request, Response, Router } from "express";
import multer from "multer";
import { route } from "@spacebar/api";
import { handleMessage, postHandleMessage } from "@spacebar/api";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import multer from "multer";
import { handleMessage, postHandleMessage, route } from "../../../../../util";
const router = Router(); const router = Router();
// TODO: message content/embed string length limit // TODO: message content/embed string length limit
@ -53,9 +52,19 @@ const messageUpload = multer({
router.patch( router.patch(
"/", "/",
route({ route({
body: "MessageEditSchema", requestBody: "MessageEditSchema",
permission: "SEND_MESSAGES", permission: "SEND_MESSAGES",
right: "SEND_MESSAGES", right: "SEND_MESSAGES",
responses: {
200: {
body: "Message",
},
400: {
body: "APIErrorResponse",
},
403: {},
404: {},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
@ -143,9 +152,19 @@ router.put(
next(); next();
}, },
route({ route({
body: "MessageCreateSchema", requestBody: "MessageCreateSchema",
permission: "SEND_MESSAGES", permission: "SEND_MESSAGES",
right: "SEND_BACKDATED_EVENTS", right: "SEND_BACKDATED_EVENTS",
responses: {
200: {
body: "Message",
},
400: {
body: "APIErrorResponse",
},
403: {},
404: {},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
@ -230,7 +249,19 @@ router.put(
router.get( router.get(
"/", "/",
route({ permission: "VIEW_CHANNEL" }), route({
permission: "VIEW_CHANNEL",
responses: {
200: {
body: "Message",
},
400: {
body: "APIErrorResponse",
},
403: {},
404: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
@ -252,38 +283,54 @@ router.get(
}, },
); );
router.delete("/", route({}), async (req: Request, res: Response) => { router.delete(
const { message_id, channel_id } = req.params; "/",
route({
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); responses: {
const message = await Message.findOneOrFail({ where: { id: message_id } }); 204: {},
400: {
const rights = await getRights(req.user_id); body: "APIErrorResponse",
},
if (message.author_id !== req.user_id) { 404: {},
if (!rights.has("MANAGE_MESSAGES")) {
const permission = await getPermission(
req.user_id,
channel.guild_id,
channel_id,
);
permission.hasThrow("MANAGE_MESSAGES");
}
} else rights.hasThrow("SELF_DELETE_MESSAGES");
await Message.delete({ id: message_id });
await emitEvent({
event: "MESSAGE_DELETE",
channel_id,
data: {
id: message_id,
channel_id,
guild_id: channel.guild_id,
}, },
} as MessageDeleteEvent); }),
async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params;
res.sendStatus(204); const channel = await Channel.findOneOrFail({
}); where: { id: channel_id },
});
const message = await Message.findOneOrFail({
where: { id: message_id },
});
const rights = await getRights(req.user_id);
if (message.author_id !== req.user_id) {
if (!rights.has("MANAGE_MESSAGES")) {
const permission = await getPermission(
req.user_id,
channel.guild_id,
channel_id,
);
permission.hasThrow("MANAGE_MESSAGES");
}
} else rights.hasThrow("SELF_DELETE_MESSAGES");
await Message.delete({ id: message_id });
await emitEvent({
event: "MESSAGE_DELETE",
channel_id,
data: {
id: message_id,
channel_id,
guild_id: channel.guild_id,
},
} as MessageDeleteEvent);
res.sendStatus(204);
},
);
export default router; export default router;

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
Channel, Channel,
emitEvent, emitEvent,
@ -32,8 +33,7 @@ import {
PublicUserProjection, PublicUserProjection,
User, User,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
import { Router, Response, Request } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { In } from "typeorm"; import { In } from "typeorm";
@ -57,7 +57,17 @@ function getEmoji(emoji: string): PartialEmoji {
router.delete( router.delete(
"/", "/",
route({ permission: "MANAGE_MESSAGES" }), route({
permission: "MANAGE_MESSAGES",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
@ -83,7 +93,17 @@ router.delete(
router.delete( router.delete(
"/:emoji", "/:emoji",
route({ permission: "MANAGE_MESSAGES" }), route({
permission: "MANAGE_MESSAGES",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
const emoji = getEmoji(req.params.emoji); const emoji = getEmoji(req.params.emoji);
@ -120,7 +140,19 @@ router.delete(
router.get( router.get(
"/:emoji", "/:emoji",
route({ permission: "VIEW_CHANNEL" }), route({
permission: "VIEW_CHANNEL",
responses: {
200: {
body: "PublicUser",
},
400: {
body: "APIErrorResponse",
},
404: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
const emoji = getEmoji(req.params.emoji); const emoji = getEmoji(req.params.emoji);
@ -148,7 +180,18 @@ router.get(
router.put( router.put(
"/:emoji/:user_id", "/:emoji/:user_id",
route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), route({
permission: "READ_MESSAGE_HISTORY",
right: "SELF_ADD_REACTIONS",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { message_id, channel_id, user_id } = req.params; const { message_id, channel_id, user_id } = req.params;
if (user_id !== "@me") throw new HTTPError("Invalid user"); if (user_id !== "@me") throw new HTTPError("Invalid user");
@ -219,7 +262,16 @@ router.put(
router.delete( router.delete(
"/:emoji/:user_id", "/:emoji/:user_id",
route({}), route({
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
let { user_id } = req.params; let { user_id } = req.params;
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;

View File

@ -16,18 +16,18 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { route } from "@spacebar/api";
import { import {
Channel, Channel,
Config, Config,
emitEvent, emitEvent,
getPermission, getPermission,
getRights, getRights,
MessageDeleteBulkEvent,
Message, Message,
MessageDeleteBulkEvent,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
@ -38,7 +38,17 @@ export default router;
// https://discord.com/developers/docs/resources/channel#bulk-delete-messages // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
router.post( router.post(
"/", "/",
route({ body: "BulkDeleteSchema" }), route({
requestBody: "BulkDeleteSchema",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {},
404: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ const channel = await Channel.findOneOrFail({

View File

@ -16,165 +16,171 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { handleMessage, postHandleMessage, route } from "@spacebar/api";
import { import {
Attachment, Attachment,
Channel, Channel,
ChannelType, ChannelType,
Config, Config,
DmChannelDTO, DmChannelDTO,
emitEvent,
FieldErrors, FieldErrors,
getPermission, Member,
Message, Message,
MessageCreateEvent, MessageCreateEvent,
Snowflake,
uploadFile,
Member,
MessageCreateSchema, MessageCreateSchema,
Reaction,
ReadState, ReadState,
Rights, Rights,
Reaction, Snowflake,
User, User,
emitEvent,
getPermission,
isTextChannel,
uploadFile,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { handleMessage, postHandleMessage, route } from "@spacebar/api";
import multer from "multer"; import multer from "multer";
import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm"; import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
import { URL } from "url"; import { URL } from "url";
const router: Router = Router(); const router: Router = Router();
export default router;
export function isTextChannel(type: ChannelType): boolean {
switch (type) {
case ChannelType.GUILD_STORE:
case ChannelType.GUILD_VOICE:
case ChannelType.GUILD_STAGE_VOICE:
case ChannelType.GUILD_CATEGORY:
case ChannelType.GUILD_FORUM:
case ChannelType.DIRECTORY:
throw new HTTPError("not a text channel", 400);
case ChannelType.DM:
case ChannelType.GROUP_DM:
case ChannelType.GUILD_NEWS:
case ChannelType.GUILD_NEWS_THREAD:
case ChannelType.GUILD_PUBLIC_THREAD:
case ChannelType.GUILD_PRIVATE_THREAD:
case ChannelType.GUILD_TEXT:
case ChannelType.ENCRYPTED:
case ChannelType.ENCRYPTED_THREAD:
return true;
default:
throw new HTTPError("unimplemented", 400);
}
}
// https://discord.com/developers/docs/resources/channel#create-message // https://discord.com/developers/docs/resources/channel#create-message
// get messages // get messages
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const channel_id = req.params.channel_id; "/",
const channel = await Channel.findOneOrFail({ route({
where: { id: channel_id }, query: {
}); around: {
if (!channel) throw new HTTPError("Channel not found", 404); type: "string",
},
before: {
type: "string",
},
after: {
type: "string",
},
limit: {
type: "number",
description:
"max number of messages to return (1-100). defaults to 50",
},
},
responses: {
200: {
body: "APIMessageArray",
},
400: {
body: "APIErrorResponse",
},
403: {},
404: {},
},
}),
async (req: Request, res: Response) => {
const channel_id = req.params.channel_id;
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (!channel) throw new HTTPError("Channel not found", 404);
isTextChannel(channel.type); isTextChannel(channel.type);
const around = req.query.around ? `${req.query.around}` : undefined; const around = req.query.around ? `${req.query.around}` : undefined;
const before = req.query.before ? `${req.query.before}` : undefined; const before = req.query.before ? `${req.query.before}` : undefined;
const after = req.query.after ? `${req.query.after}` : undefined; const after = req.query.after ? `${req.query.after}` : undefined;
const limit = Number(req.query.limit) || 50; const limit = Number(req.query.limit) || 50;
if (limit < 1 || limit > 100) if (limit < 1 || limit > 100)
throw new HTTPError("limit must be between 1 and 100", 422); throw new HTTPError("limit must be between 1 and 100", 422);
const halfLimit = Math.floor(limit / 2); const halfLimit = Math.floor(limit / 2);
const permissions = await getPermission( const permissions = await getPermission(
req.user_id, req.user_id,
channel.guild_id, channel.guild_id,
channel_id, channel_id,
); );
permissions.hasThrow("VIEW_CHANNEL"); permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
const query: FindManyOptions<Message> & { const query: FindManyOptions<Message> & {
where: { id?: FindOperator<string> | FindOperator<string>[] }; where: { id?: FindOperator<string> | FindOperator<string>[] };
} = { } = {
order: { timestamp: "DESC" }, order: { timestamp: "DESC" },
take: limit, take: limit,
where: { channel_id }, where: { channel_id },
relations: [ relations: [
"author", "author",
"webhook", "webhook",
"application", "application",
"mentions", "mentions",
"mention_roles", "mention_roles",
"mention_channels", "mention_channels",
"sticker_items", "sticker_items",
"attachments", "attachments",
], ],
}; };
if (after) { if (after) {
if (BigInt(after) > BigInt(Snowflake.generate())) if (BigInt(after) > BigInt(Snowflake.generate()))
return res.status(422); return res.status(422);
query.where.id = MoreThan(after); query.where.id = MoreThan(after);
} else if (before) { } else if (before) {
if (BigInt(before) < BigInt(req.params.channel_id)) if (BigInt(before) < BigInt(req.params.channel_id))
return res.status(422); return res.status(422);
query.where.id = LessThan(before); query.where.id = LessThan(before);
} else if (around) { } else if (around) {
query.where.id = [ query.where.id = [
MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
LessThan((BigInt(around) + BigInt(halfLimit)).toString()), LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
]; ];
return res.json([]); // TODO: fix around return res.json([]); // TODO: fix around
} }
const messages = await Message.find(query); const messages = await Message.find(query);
const endpoint = Config.get().cdn.endpointPublic; const endpoint = Config.get().cdn.endpointPublic;
return res.json( return res.json(
messages.map((x: Partial<Message>) => { messages.map((x: Partial<Message>) => {
(x.reactions || []).forEach((y: Partial<Reaction>) => { (x.reactions || []).forEach((y: Partial<Reaction>) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
if ((y.user_ids || []).includes(req.user_id)) y.me = true; if ((y.user_ids || []).includes(req.user_id)) y.me = true;
delete y.user_ids; delete y.user_ids;
}); });
if (!x.author) if (!x.author)
x.author = User.create({ x.author = User.create({
id: "4", id: "4",
discriminator: "0000", discriminator: "0000",
username: "Spacebar Ghost", username: "Spacebar Ghost",
public_flags: 0, public_flags: 0,
});
x.attachments?.forEach((y: Attachment) => {
// dynamically set attachment proxy_url in case the endpoint changed
const uri = y.proxy_url.startsWith("http")
? y.proxy_url
: `https://example.org${y.proxy_url}`;
y.proxy_url = `${endpoint == null ? "" : endpoint}${
new URL(uri).pathname
}`;
}); });
x.attachments?.forEach((y: Attachment) => {
// dynamically set attachment proxy_url in case the endpoint changed
const uri = y.proxy_url.startsWith("http")
? y.proxy_url
: `https://example.org${y.proxy_url}`;
y.proxy_url = `${endpoint == null ? "" : endpoint}${
new URL(uri).pathname
}`;
});
/** /**
Some clients ( discord.js ) only check if a property exists within the response, Some clients ( discord.js ) only check if a property exists within the response,
which causes errors when, say, the `application` property is `null`. which causes errors when, say, the `application` property is `null`.
**/ **/
// for (var curr in x) { // for (var curr in x) {
// if (x[curr] === null) // if (x[curr] === null)
// delete x[curr]; // delete x[curr];
// } // }
return x; return x;
}), }),
); );
}); },
);
// TODO: config max upload size // TODO: config max upload size
const messageUpload = multer({ const messageUpload = multer({
@ -205,9 +211,19 @@ router.post(
next(); next();
}, },
route({ route({
body: "MessageCreateSchema", requestBody: "MessageCreateSchema",
permission: "SEND_MESSAGES", permission: "SEND_MESSAGES",
right: "SEND_MESSAGES", right: "SEND_MESSAGES",
responses: {
200: {
body: "Message",
},
400: {
body: "APIErrorResponse",
},
403: {},
404: {},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
@ -366,3 +382,5 @@ router.post(
return res.json(message); return res.json(message);
}, },
); );
export default router;

View File

@ -19,13 +19,13 @@
import { import {
Channel, Channel,
ChannelPermissionOverwrite, ChannelPermissionOverwrite,
ChannelPermissionOverwriteSchema,
ChannelUpdateEvent, ChannelUpdateEvent,
emitEvent, emitEvent,
Member, Member,
Role, Role,
ChannelPermissionOverwriteSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { Router, Response, Request } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
@ -36,8 +36,14 @@ const router: Router = Router();
router.put( router.put(
"/:overwrite_id", "/:overwrite_id",
route({ route({
body: "ChannelPermissionOverwriteSchema", requestBody: "ChannelPermissionOverwriteSchema",
permission: "MANAGE_ROLES", permission: "MANAGE_ROLES",
responses: {
204: {},
404: {},
501: {},
400: { body: "APIErrorResponse" },
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, overwrite_id } = req.params; const { channel_id, overwrite_id } = req.params;
@ -92,7 +98,7 @@ router.put(
// TODO: check permission hierarchy // TODO: check permission hierarchy
router.delete( router.delete(
"/:overwrite_id", "/:overwrite_id",
route({ permission: "MANAGE_ROLES" }), route({ permission: "MANAGE_ROLES", responses: { 204: {}, 404: {} } }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, overwrite_id } = req.params; const { channel_id, overwrite_id } = req.params;

View File

@ -16,23 +16,33 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
Channel, Channel,
ChannelPinsUpdateEvent, ChannelPinsUpdateEvent,
Config, Config,
DiscordApiErrors,
emitEvent, emitEvent,
Message, Message,
MessageUpdateEvent, MessageUpdateEvent,
DiscordApiErrors,
} from "@spacebar/util"; } from "@spacebar/util";
import { Router, Request, Response } from "express"; import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
router.put( router.put(
"/:message_id", "/:message_id",
route({ permission: "VIEW_CHANNEL" }), route({
permission: "VIEW_CHANNEL",
responses: {
204: {},
403: {},
404: {},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
@ -74,7 +84,17 @@ router.put(
router.delete( router.delete(
"/:message_id", "/:message_id",
route({ permission: "VIEW_CHANNEL" }), route({
permission: "VIEW_CHANNEL",
responses: {
204: {},
403: {},
404: {},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
@ -114,7 +134,17 @@ router.delete(
router.get( router.get(
"/", "/",
route({ permission: ["READ_MESSAGE_HISTORY"] }), route({
permission: ["READ_MESSAGE_HISTORY"],
responses: {
200: {
body: "APIMessageArray",
},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;

View File

@ -16,20 +16,20 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { isTextChannel } from "./messages";
import { FindManyOptions, Between, Not, FindOperator } from "typeorm";
import { import {
Channel, Channel,
emitEvent,
getPermission,
getRights,
Message, Message,
MessageDeleteBulkEvent, MessageDeleteBulkEvent,
PurgeSchema, PurgeSchema,
emitEvent,
getPermission,
getRights,
isTextChannel,
} from "@spacebar/util"; } from "@spacebar/util";
import { Router, Response, Request } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { Between, FindManyOptions, FindOperator, Not } from "typeorm";
const router: Router = Router(); const router: Router = Router();
@ -42,6 +42,14 @@ router.post(
"/", "/",
route({ route({
/*body: "PurgeSchema",*/ /*body: "PurgeSchema",*/
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {},
403: {},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { route } from "@spacebar/api";
import { import {
Channel, Channel,
ChannelRecipientAddEvent, ChannelRecipientAddEvent,
@ -28,80 +28,98 @@ import {
Recipient, Recipient,
User, User,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.put("/:user_id", route({}), async (req: Request, res: Response) => { router.put(
const { channel_id, user_id } = req.params; "/:user_id",
const channel = await Channel.findOneOrFail({ route({
where: { id: channel_id }, responses: {
relations: ["recipients"], 201: {},
}); 404: {},
},
}),
async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients"],
});
if (channel.type !== ChannelType.GROUP_DM) { if (channel.type !== ChannelType.GROUP_DM) {
const recipients = [ const recipients = [
...(channel.recipients?.map((r) => r.user_id) || []), ...(channel.recipients?.map((r) => r.user_id) || []),
user_id, user_id,
].unique(); ].unique();
const new_channel = await Channel.createDMChannel( const new_channel = await Channel.createDMChannel(
recipients, recipients,
req.user_id, req.user_id,
); );
return res.status(201).json(new_channel); return res.status(201).json(new_channel);
} else { } else {
if (channel.recipients?.map((r) => r.user_id).includes(user_id)) { if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
}
channel.recipients?.push(
Recipient.create({ channel_id: channel_id, user_id: user_id }),
);
await channel.save();
await emitEvent({
event: "CHANNEL_CREATE",
data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id,
});
await emitEvent({
event: "CHANNEL_RECIPIENT_ADD",
data: {
channel_id: channel_id,
user: await User.findOneOrFail({
where: { id: user_id },
select: PublicUserProjection,
}),
},
channel_id: channel_id,
} as ChannelRecipientAddEvent);
return res.sendStatus(204);
}
},
);
router.delete(
"/:user_id",
route({
responses: {
204: {},
404: {},
},
}),
async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients"],
});
if (
!(
channel.type === ChannelType.GROUP_DM &&
(channel.owner_id === req.user_id || user_id === req.user_id)
)
)
throw DiscordApiErrors.MISSING_PERMISSIONS;
if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
} }
channel.recipients?.push( await Channel.removeRecipientFromChannel(channel, user_id);
Recipient.create({ channel_id: channel_id, user_id: user_id }),
);
await channel.save();
await emitEvent({
event: "CHANNEL_CREATE",
data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id,
});
await emitEvent({
event: "CHANNEL_RECIPIENT_ADD",
data: {
channel_id: channel_id,
user: await User.findOneOrFail({
where: { id: user_id },
select: PublicUserProjection,
}),
},
channel_id: channel_id,
} as ChannelRecipientAddEvent);
return res.sendStatus(204); return res.sendStatus(204);
} },
}); );
router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients"],
});
if (
!(
channel.type === ChannelType.GROUP_DM &&
(channel.owner_id === req.user_id || user_id === req.user_id)
)
)
throw DiscordApiErrors.MISSING_PERMISSIONS;
if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
}
await Channel.removeRecipientFromChannel(channel, user_id);
return res.sendStatus(204);
});
export default router; export default router;

View File

@ -16,15 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Router, Request, Response } from "express"; import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.post( router.post(
"/", "/",
route({ permission: "SEND_MESSAGES" }), route({
permission: "SEND_MESSAGES",
responses: {
204: {},
404: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const user_id = req.user_id; const user_id = req.user_id;

View File

@ -16,34 +16,56 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Channel, Channel,
Config, Config,
handleFile, DiscordApiErrors,
trimSpecial,
User, User,
Webhook, Webhook,
WebhookCreateSchema, WebhookCreateSchema,
WebhookType, WebhookType,
handleFile,
trimSpecial,
isTextChannel,
} from "@spacebar/util"; } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { isTextChannel } from "./messages/index";
import { DiscordApiErrors } from "@spacebar/util";
import crypto from "crypto"; import crypto from "crypto";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
//TODO: implement webhooks //TODO: implement webhooks
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
res.json([]); "/",
}); route({
responses: {
200: {
body: "APIWebhookArray",
},
},
}),
async (req: Request, res: Response) => {
res.json([]);
},
);
// TODO: use Image Data Type for avatar instead of String // TODO: use Image Data Type for avatar instead of String
router.post( router.post(
"/", "/",
route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), route({
requestBody: "WebhookCreateSchema",
permission: "MANAGE_WEBHOOKS",
responses: {
200: {
body: "WebhookCreateResponse",
},
400: {
body: "APIErrorResponse",
},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const channel_id = req.params.channel_id; const channel_id = req.params.channel_id;
const channel = await Channel.findOneOrFail({ const channel = await Channel.findOneOrFail({

View File

@ -29,7 +29,7 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ body: "ConnectionCallbackSchema" }), route({ requestBody: "ConnectionCallbackSchema" }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { connection_name } = req.params; const { connection_name } = req.params;
const connection = ConnectionStore.connections.get(connection_name); const connection = ConnectionStore.connections.get(connection_name);

View File

@ -16,49 +16,61 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Guild, Config } from "@spacebar/util"; import { Config, Guild } from "@spacebar/util";
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
import { Like } from "typeorm"; import { Like } from "typeorm";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { offset, limit, categories } = req.query; "/",
const showAllGuilds = Config.get().guild.discovery.showAllGuilds; route({
const configLimit = Config.get().guild.discovery.limit; responses: {
let guilds; 200: {
if (categories == undefined) { body: "DiscoverableGuildsResponse",
guilds = showAllGuilds },
? await Guild.find({ take: Math.abs(Number(limit || configLimit)) }) },
: await Guild.find({ }),
where: { features: Like(`%DISCOVERABLE%`) }, async (req: Request, res: Response) => {
take: Math.abs(Number(limit || configLimit)), const { offset, limit, categories } = req.query;
}); const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
} else { const configLimit = Config.get().guild.discovery.limit;
guilds = showAllGuilds let guilds;
? await Guild.find({ if (categories == undefined) {
where: { primary_category_id: categories.toString() }, guilds = showAllGuilds
take: Math.abs(Number(limit || configLimit)), ? await Guild.find({
}) take: Math.abs(Number(limit || configLimit)),
: await Guild.find({ })
where: { : await Guild.find({
primary_category_id: categories.toString(), where: { features: Like(`%DISCOVERABLE%`) },
features: Like("%DISCOVERABLE%"), take: Math.abs(Number(limit || configLimit)),
}, });
take: Math.abs(Number(limit || configLimit)), } else {
}); guilds = showAllGuilds
} ? await Guild.find({
where: { primary_category_id: categories.toString() },
take: Math.abs(Number(limit || configLimit)),
})
: await Guild.find({
where: {
primary_category_id: categories.toString(),
features: Like("%DISCOVERABLE%"),
},
take: Math.abs(Number(limit || configLimit)),
});
}
const total = guilds ? guilds.length : undefined; const total = guilds ? guilds.length : undefined;
res.send({ res.send({
total: total, total: total,
guilds: guilds, guilds: guilds,
offset: Number(offset || Config.get().guild.discovery.offset), offset: Number(offset || Config.get().guild.discovery.offset),
limit: Number(limit || configLimit), limit: Number(limit || configLimit),
}); });
}); },
);
export default router; export default router;

View File

@ -16,24 +16,34 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Categories } from "@spacebar/util";
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Categories } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/categories", route({}), async (req: Request, res: Response) => { router.get(
// TODO: "/categories",
// Get locale instead route({
responses: {
200: {
body: "APIDiscoveryCategoryArray",
},
},
}),
async (req: Request, res: Response) => {
// TODO:
// Get locale instead
// const { locale, primary_only } = req.query; // const { locale, primary_only } = req.query;
const { primary_only } = req.query; const { primary_only } = req.query;
const out = primary_only const out = primary_only
? await Categories.find() ? await Categories.find()
: await Categories.find({ where: { is_primary: true } }); : await Categories.find({ where: { is_primary: true } });
res.send(out); res.send(out);
}); },
);
export default router; export default router;

View File

@ -16,32 +16,43 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { FieldErrors, Release } from "@spacebar/util"; import { FieldErrors, Release } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { platform } = req.query; "/",
route({
if (!platform) responses: {
throw FieldErrors({ 302: {},
platform: { 404: {
code: "BASE_TYPE_REQUIRED", body: "APIErrorResponse",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
}, },
},
}),
async (req: Request, res: Response) => {
const { platform } = req.query;
if (!platform)
throw FieldErrors({
platform: {
code: "BASE_TYPE_REQUIRED",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
},
});
const release = await Release.findOneOrFail({
where: {
enabled: true,
platform: platform as string,
},
order: { pub_date: "DESC" },
}); });
const release = await Release.findOneOrFail({ res.redirect(release.url);
where: { },
enabled: true, );
platform: platform as string,
},
order: { pub_date: "DESC" },
});
res.redirect(release.url);
});
export default router; export default router;

View File

@ -16,32 +16,34 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { Config } from "@spacebar/util"; import { Config } from "@spacebar/util";
import { Router, Response, Request } from "express"; import { Request, Response, Router } from "express";
import { route, RouteOptions } from "@spacebar/api";
const router = Router(); const router = Router();
const options: RouteOptions = { router.get(
test: { "/",
response: { route({
body: "GatewayBotResponse", responses: {
200: {
body: "GatewayBotResponse",
},
}, },
}),
(req: Request, res: Response) => {
const { endpointPublic } = Config.get().gateway;
res.json({
url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
shards: 1,
session_start_limit: {
total: 1000,
remaining: 999,
reset_after: 14400000,
max_concurrency: 1,
},
});
}, },
}; );
router.get("/", route(options), (req: Request, res: Response) => {
const { endpointPublic } = Config.get().gateway;
res.json({
url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
shards: 1,
session_start_limit: {
total: 1000,
remaining: 999,
reset_after: 14400000,
max_concurrency: 1,
},
});
});
export default router; export default router;

View File

@ -16,25 +16,27 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { Config } from "@spacebar/util"; import { Config } from "@spacebar/util";
import { Router, Response, Request } from "express"; import { Request, Response, Router } from "express";
import { route, RouteOptions } from "@spacebar/api";
const router = Router(); const router = Router();
const options: RouteOptions = { router.get(
test: { "/",
response: { route({
body: "GatewayResponse", responses: {
200: {
body: "GatewayResponse",
},
}, },
}),
(req: Request, res: Response) => {
const { endpointPublic } = Config.get().gateway;
res.json({
url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
});
}, },
}; );
router.get("/", route(options), (req: Request, res: Response) => {
const { endpointPublic } = Config.get().gateway;
res.json({
url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
});
});
export default router; export default router;

View File

@ -16,34 +16,62 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { route } from "@spacebar/api";
import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
import { Request, Response, Router } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from "proxy-agent"; import ProxyAgent from "proxy-agent";
import { route } from "@spacebar/api";
import { getGifApiKey, parseGifResult } from "./trending";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
// TODO: Custom providers "/",
const { q, media_format, locale } = req.query; route({
query: {
const apiKey = getGifApiKey(); q: {
type: "string",
const agent = new ProxyAgent(); required: true,
description: "Search query",
const response = await fetch( },
`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, media_format: {
{ type: "string",
agent, description: "Media format",
method: "get", values: Object.keys(TenorMediaTypes).filter((key) =>
headers: { "Content-Type": "application/json" }, isNaN(Number(key)),
),
},
locale: {
type: "string",
description: "Locale",
},
}, },
); responses: {
200: {
body: "TenorGifsResponse",
},
},
}),
async (req: Request, res: Response) => {
// TODO: Custom providers
const { q, media_format, locale } = req.query;
const { results } = await response.json(); const apiKey = getGifApiKey();
res.json(results.map(parseGifResult)).status(200); const agent = new ProxyAgent();
});
const response = await fetch(
`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`,
{
agent,
method: "get",
headers: { "Content-Type": "application/json" },
},
);
const { results } = await response.json();
res.json(results.map(parseGifResult)).status(200);
},
);
export default router; export default router;

View File

@ -16,34 +16,57 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { route } from "@spacebar/api";
import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
import { Request, Response, Router } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from "proxy-agent"; import ProxyAgent from "proxy-agent";
import { route } from "@spacebar/api";
import { getGifApiKey, parseGifResult } from "./trending";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
// TODO: Custom providers "/",
const { media_format, locale } = req.query; route({
query: {
const apiKey = getGifApiKey(); media_format: {
type: "string",
const agent = new ProxyAgent(); description: "Media format",
values: Object.keys(TenorMediaTypes).filter((key) =>
const response = await fetch( isNaN(Number(key)),
`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, ),
{ },
agent, locale: {
method: "get", type: "string",
headers: { "Content-Type": "application/json" }, description: "Locale",
},
}, },
); responses: {
200: {
body: "TenorGifsResponse",
},
},
}),
async (req: Request, res: Response) => {
// TODO: Custom providers
const { media_format, locale } = req.query;
const { results } = await response.json(); const apiKey = getGifApiKey();
res.json(results.map(parseGifResult)).status(200); const agent = new ProxyAgent();
});
const response = await fetch(
`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`,
{
agent,
method: "get",
headers: { "Content-Type": "application/json" },
},
);
const { results } = await response.json();
res.json(results.map(parseGifResult)).status(200);
},
);
export default router; export default router;

View File

@ -16,126 +16,76 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { route } from "@spacebar/api";
import {
TenorCategoriesResults,
TenorTrendingResults,
getGifApiKey,
parseGifResult,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from "proxy-agent"; import ProxyAgent from "proxy-agent";
import { route } from "@spacebar/api";
import { Config } from "@spacebar/util";
import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
// TODO: Move somewhere else router.get(
enum TENOR_GIF_TYPES { "/",
gif, route({
mediumgif, query: {
tinygif, locale: {
nanogif, type: "string",
mp4, description: "Locale",
loopedmp4,
tinymp4,
nanomp4,
webm,
tinywebm,
nanowebm,
}
type TENOR_MEDIA = {
preview: string;
url: string;
dims: number[];
size: number;
};
type TENOR_GIF = {
created: number;
hasaudio: boolean;
id: string;
media: { [type in keyof typeof TENOR_GIF_TYPES]: TENOR_MEDIA }[];
tags: string[];
title: string;
itemurl: string;
hascaption: boolean;
url: string;
};
type TENOR_CATEGORY = {
searchterm: string;
path: string;
image: string;
name: string;
};
type TENOR_CATEGORIES_RESULTS = {
tags: TENOR_CATEGORY[];
};
type TENOR_TRENDING_RESULTS = {
next: string;
results: TENOR_GIF[];
};
export function parseGifResult(result: TENOR_GIF) {
return {
id: result.id,
title: result.title,
url: result.itemurl,
src: result.media[0].mp4.url,
gif_src: result.media[0].gif.url,
width: result.media[0].mp4.dims[0],
height: result.media[0].mp4.dims[1],
preview: result.media[0].mp4.preview,
};
}
export function getGifApiKey() {
const { enabled, provider, apiKey } = Config.get().gif;
if (!enabled) throw new HTTPError(`Gifs are disabled`);
if (provider !== "tenor" || !apiKey)
throw new HTTPError(`${provider} gif provider not supported`);
return apiKey;
}
router.get("/", route({}), async (req: Request, res: Response) => {
// TODO: Custom providers
// TODO: return gifs as mp4
// const { media_format, locale } = req.query;
const { locale } = req.query;
const apiKey = getGifApiKey();
const agent = new ProxyAgent();
const [responseSource, trendGifSource] = await Promise.all([
fetch(
`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
{
agent,
method: "get",
headers: { "Content-Type": "application/json" },
}, },
), },
fetch( responses: {
`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, 200: {
{ body: "TenorTrendingResponse",
agent,
method: "get",
headers: { "Content-Type": "application/json" },
}, },
), },
]); }),
async (req: Request, res: Response) => {
// TODO: Custom providers
// TODO: return gifs as mp4
// const { media_format, locale } = req.query;
const { locale } = req.query;
const { tags } = (await responseSource.json()) as TENOR_CATEGORIES_RESULTS; const apiKey = getGifApiKey();
const { results } = (await trendGifSource.json()) as TENOR_TRENDING_RESULTS;
res.json({ const agent = new ProxyAgent();
categories: tags.map((x) => ({
name: x.searchterm, const [responseSource, trendGifSource] = await Promise.all([
src: x.image, fetch(
})), `https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
gifs: [parseGifResult(results[0])], {
}).status(200); agent,
}); method: "get",
headers: { "Content-Type": "application/json" },
},
),
fetch(
`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`,
{
agent,
method: "get",
headers: { "Content-Type": "application/json" },
},
),
]);
const { tags } =
(await responseSource.json()) as TenorCategoriesResults;
const { results } =
(await trendGifSource.json()) as TenorTrendingResults;
res.json({
categories: tags.map((x) => ({
name: x.searchterm,
src: x.image,
})),
gifs: [parseGifResult(results[0])],
}).status(200);
},
);
export default router; export default router;

View File

@ -16,34 +16,44 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Guild, Config } from "@spacebar/util"; import { Config, Guild } from "@spacebar/util";
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
import { Like } from "typeorm"; import { Like } from "typeorm";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
// const { limit, personalization_disabled } = req.query; "/",
const { limit } = req.query; route({
const showAllGuilds = Config.get().guild.discovery.showAllGuilds; responses: {
200: {
body: "GuildRecommendationsResponse",
},
},
}),
async (req: Request, res: Response) => {
// const { limit, personalization_disabled } = req.query;
const { limit } = req.query;
const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
const genLoadId = (size: number) => const genLoadId = (size: number) =>
[...Array(size)] [...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16)) .map(() => Math.floor(Math.random() * 16).toString(16))
.join(""); .join("");
const guilds = showAllGuilds const guilds = showAllGuilds
? await Guild.find({ take: Math.abs(Number(limit || 24)) }) ? await Guild.find({ take: Math.abs(Number(limit || 24)) })
: await Guild.find({ : await Guild.find({
where: { features: Like("%DISCOVERABLE%") }, where: { features: Like("%DISCOVERABLE%") },
take: Math.abs(Number(limit || 24)), take: Math.abs(Number(limit || 24)),
}); });
res.send({ res.send({
recommended_guilds: guilds, recommended_guilds: guilds,
load_id: `server_recs/${genLoadId(32)}`, load_id: `server_recs/${genLoadId(32)}`,
}).status(200); }).status(200);
}); },
);
export default router; export default router;

View File

@ -16,20 +16,20 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { getIpAdress, route } from "@spacebar/api";
import { import {
Ban,
BanModeratorSchema,
BanRegistrySchema,
DiscordApiErrors, DiscordApiErrors,
emitEvent,
GuildBanAddEvent, GuildBanAddEvent,
GuildBanRemoveEvent, GuildBanRemoveEvent,
Ban,
User,
Member, Member,
BanRegistrySchema, User,
BanModeratorSchema, emitEvent,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { getIpAdress, route } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
@ -37,7 +37,17 @@ const router: Router = Router();
router.get( router.get(
"/", "/",
route({ permission: "BAN_MEMBERS" }), route({
permission: "BAN_MEMBERS",
responses: {
200: {
body: "GuildBansResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
@ -73,7 +83,20 @@ router.get(
router.get( router.get(
"/:user", "/:user",
route({ permission: "BAN_MEMBERS" }), route({
permission: "BAN_MEMBERS",
responses: {
200: {
body: "BanModeratorSchema",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const user_id = req.params.ban; const user_id = req.params.ban;
@ -97,7 +120,21 @@ router.get(
router.put( router.put(
"/:user_id", "/:user_id",
route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), route({
requestBody: "BanCreateSchema",
permission: "BAN_MEMBERS",
responses: {
200: {
body: "Ban",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const banned_user_id = req.params.user_id; const banned_user_id = req.params.user_id;
@ -143,7 +180,20 @@ router.put(
router.put( router.put(
"/@me", "/@me",
route({ body: "BanCreateSchema" }), route({
requestBody: "BanCreateSchema",
responses: {
200: {
body: "Ban",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
@ -182,7 +232,18 @@ router.put(
router.delete( router.delete(
"/:user_id", "/:user_id",
route({ permission: "BAN_MEMBERS" }), route({
permission: "BAN_MEMBERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, user_id } = req.params; const { guild_id, user_id } = req.params;

View File

@ -16,28 +16,52 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { route } from "@spacebar/api";
import { import {
Channel, Channel,
ChannelUpdateEvent,
emitEvent,
ChannelModifySchema, ChannelModifySchema,
ChannelReorderSchema, ChannelReorderSchema,
ChannelUpdateEvent,
emitEvent,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
const channels = await Channel.find({ where: { guild_id } }); route({
responses: {
201: {
body: "APIChannelArray",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const channels = await Channel.find({ where: { guild_id } });
res.json(channels); res.json(channels);
}); },
);
router.post( router.post(
"/", "/",
route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), route({
requestBody: "ChannelModifySchema",
permission: "MANAGE_CHANNELS",
responses: {
201: {
body: "Channel",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel // creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
const { guild_id } = req.params; const { guild_id } = req.params;
@ -54,7 +78,19 @@ router.post(
router.patch( router.patch(
"/", "/",
route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), route({
requestBody: "ChannelReorderSchema",
permission: "MANAGE_CHANNELS",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// changes guild channel position // changes guild channel position
const { guild_id } = req.params; const { guild_id } = req.params;

View File

@ -16,37 +16,51 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { emitEvent, GuildDeleteEvent, Guild } from "@spacebar/util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
// discord prefixes this route with /delete instead of using the delete method // discord prefixes this route with /delete instead of using the delete method
// docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const { guild_id } = req.params; "/",
route({
const guild = await Guild.findOneOrFail({ responses: {
where: { id: guild_id }, 204: {},
select: ["owner_id"], 401: {
}); body: "APIErrorResponse",
if (guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([
Guild.delete({ id: guild_id }), // this will also delete all guild related data
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
}, },
guild_id: guild_id, 404: {
} as GuildDeleteEvent), body: "APIErrorResponse",
]); },
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
return res.sendStatus(204); const guild = await Guild.findOneOrFail({
}); where: { id: guild_id },
select: ["owner_id"],
});
if (guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([
Guild.delete({ id: guild_id }), // this will also delete all guild related data
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
},
guild_id: guild_id,
} as GuildDeleteEvent),
]);
return res.sendStatus(204);
},
);
export default router; export default router;

View File

@ -16,40 +16,50 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
// TODO: route({
// Load from database responses: {
// Admin control, but for now it allows anyone to be discoverable 200: {
body: "GuildDiscoveryRequirementsResponse",
res.send({ },
guild_id: guild_id,
safe_environment: true,
healthy: true,
health_score_pending: false,
size: true,
nsfw_properties: {},
protected: true,
sufficient: true,
sufficient_without_grace_period: true,
valid_rules_channel: true,
retention_healthy: true,
engagement_healthy: true,
age: true,
minimum_age: 0,
health_score: {
avg_nonnew_participators: 0,
avg_nonnew_communicators: 0,
num_intentful_joiners: 0,
perc_ret_w1_intentful: 0,
}, },
minimum_size: 0, }),
}); async (req: Request, res: Response) => {
}); const { guild_id } = req.params;
// TODO:
// Load from database
// Admin control, but for now it allows anyone to be discoverable
res.send({
guild_id: guild_id,
safe_environment: true,
healthy: true,
health_score_pending: false,
size: true,
nsfw_properties: {},
protected: true,
sufficient: true,
sufficient_without_grace_period: true,
valid_rules_channel: true,
retention_healthy: true,
engagement_healthy: true,
age: true,
minimum_age: 0,
health_score: {
avg_nonnew_participators: 0,
avg_nonnew_communicators: 0,
num_intentful_joiners: 0,
perc_ret_w1_intentful: 0,
},
minimum_size: 0,
});
},
);
export default router; export default router;

View File

@ -16,55 +16,95 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { route } from "@spacebar/api";
import { import {
Config, Config,
DiscordApiErrors, DiscordApiErrors,
emitEvent,
Emoji, Emoji,
EmojiCreateSchema,
EmojiModifySchema,
GuildEmojisUpdateEvent, GuildEmojisUpdateEvent,
handleFile,
Member, Member,
Snowflake, Snowflake,
User, User,
EmojiCreateSchema, emitEvent,
EmojiModifySchema, handleFile,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
route({
responses: {
200: {
body: "APIEmojiArray",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
const emojis = await Emoji.find({ const emojis = await Emoji.find({
where: { guild_id: guild_id }, where: { guild_id: guild_id },
relations: ["user"], relations: ["user"],
}); });
return res.json(emojis); return res.json(emojis);
}); },
);
router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id, emoji_id } = req.params; "/:emoji_id",
route({
responses: {
200: {
body: "Emoji",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, emoji_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
const emoji = await Emoji.findOneOrFail({ const emoji = await Emoji.findOneOrFail({
where: { guild_id: guild_id, id: emoji_id }, where: { guild_id: guild_id, id: emoji_id },
relations: ["user"], relations: ["user"],
}); });
return res.json(emoji); return res.json(emoji);
}); },
);
router.post( router.post(
"/", "/",
route({ route({
body: "EmojiCreateSchema", requestBody: "EmojiCreateSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS", permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
201: {
body: "Emoji",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
@ -113,8 +153,16 @@ router.post(
router.patch( router.patch(
"/:emoji_id", "/:emoji_id",
route({ route({
body: "EmojiModifySchema", requestBody: "EmojiModifySchema",
permission: "MANAGE_EMOJIS_AND_STICKERS", permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
200: {
body: "Emoji",
},
403: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { emoji_id, guild_id } = req.params; const { emoji_id, guild_id } = req.params;
@ -141,7 +189,15 @@ router.patch(
router.delete( router.delete(
"/:emoji_id", "/:emoji_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { emoji_id, guild_id } = req.params; const { emoji_id, guild_id } = req.params;

View File

@ -16,46 +16,79 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { route } from "@spacebar/api";
import { import {
DiscordApiErrors, DiscordApiErrors,
Guild,
GuildUpdateEvent,
GuildUpdateSchema,
Member,
SpacebarApiErrors,
emitEvent, emitEvent,
getPermission, getPermission,
getRights, getRights,
Guild,
GuildUpdateEvent,
handleFile, handleFile,
Member,
GuildUpdateSchema,
SpacebarApiErrors,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
route({
responses: {
"200": {
body: "APIGuildWithJoinedAt",
},
401: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const [guild, member] = await Promise.all([ const [guild, member] = await Promise.all([
Guild.findOneOrFail({ where: { id: guild_id } }), Guild.findOneOrFail({ where: { id: guild_id } }),
Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }), Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
]); ]);
if (!member) if (!member)
throw new HTTPError( throw new HTTPError(
"You are not a member of the guild you are trying to access", "You are not a member of the guild you are trying to access",
401, 401,
); );
return res.send({ return res.send({
...guild, ...guild,
joined_at: member?.joined_at, joined_at: member?.joined_at,
}); });
}); },
);
router.patch( router.patch(
"/", "/",
route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), route({
requestBody: "GuildUpdateSchema",
permission: "MANAGE_GUILD",
responses: {
"200": {
body: "GuildUpdateSchema",
},
401: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as GuildUpdateSchema; const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params; const { guild_id } = req.params;

View File

@ -16,15 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Invite, PublicInviteRelation } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Invite, PublicInviteRelation } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get( router.get(
"/", "/",
route({ permission: "MANAGE_GUILD" }), route({
permission: "MANAGE_GUILD",
responses: {
200: {
body: "APIInviteArray",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;

View File

@ -16,17 +16,27 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
// TODO: member verification "/",
route({
responses: {
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
// TODO: member verification
res.status(404).json({ res.status(404).json({
message: "Unknown Guild Member Verification Form", message: "Unknown Guild Member Verification Form",
code: 10068, code: 10068,
}); });
}); },
);
export default router; export default router;

View File

@ -16,38 +16,70 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { route } from "@spacebar/api";
import { import {
Member, emitEvent,
Emoji,
getPermission, getPermission,
getRights, getRights,
Role,
GuildMemberUpdateEvent,
emitEvent,
Sticker,
Emoji,
Guild, Guild,
GuildMemberUpdateEvent,
handleFile, handleFile,
Member,
MemberChangeSchema, MemberChangeSchema,
Role,
Sticker,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id, member_id } = req.params; "/",
await Member.IsInGuildOrFail(req.user_id, guild_id); route({
responses: {
200: {
body: "Member",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
const member = await Member.findOneOrFail({ const member = await Member.findOneOrFail({
where: { id: member_id, guild_id }, where: { id: member_id, guild_id },
}); });
return res.json(member); return res.json(member);
}); },
);
router.patch( router.patch(
"/", "/",
route({ body: "MemberChangeSchema" }), route({
requestBody: "MemberChangeSchema",
responses: {
200: {
body: "Member",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const member_id = const member_id =
@ -119,54 +151,81 @@ router.patch(
}, },
); );
router.put("/", route({}), async (req: Request, res: Response) => { router.put(
// TODO: Lurker mode "/",
route({
responses: {
200: {
body: "MemberJoinGuildResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
// TODO: Lurker mode
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
const { guild_id } = req.params; const { guild_id } = req.params;
let { member_id } = req.params; let { member_id } = req.params;
if (member_id === "@me") { if (member_id === "@me") {
member_id = req.user_id; member_id = req.user_id;
rights.hasThrow("JOIN_GUILDS"); rights.hasThrow("JOIN_GUILDS");
} else { } else {
// TODO: join others by controller // TODO: join others by controller
} }
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
where: { id: guild_id }, where: { id: guild_id },
}); });
const emoji = await Emoji.find({ const emoji = await Emoji.find({
where: { guild_id: guild_id }, where: { guild_id: guild_id },
}); });
const roles = await Role.find({ const roles = await Role.find({
where: { guild_id: guild_id }, where: { guild_id: guild_id },
}); });
const stickers = await Sticker.find({ const stickers = await Sticker.find({
where: { guild_id: guild_id }, where: { guild_id: guild_id },
}); });
await Member.addToGuild(member_id, guild_id); await Member.addToGuild(member_id, guild_id);
res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers }); res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
}); },
);
router.delete("/", route({}), async (req: Request, res: Response) => { router.delete(
const { guild_id, member_id } = req.params; "/",
const permission = await getPermission(req.user_id, guild_id); route({
const rights = await getRights(req.user_id); responses: {
if (member_id === "@me" || member_id === req.user_id) { 204: {},
// TODO: unless force-joined 403: {
rights.hasThrow("SELF_LEAVE_GROUPS"); body: "APIErrorResponse",
} else { },
rights.hasThrow("KICK_BAN_MEMBERS"); },
permission.hasThrow("KICK_MEMBERS"); }),
} async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
const permission = await getPermission(req.user_id, guild_id);
const rights = await getRights(req.user_id);
if (member_id === "@me" || member_id === req.user_id) {
// TODO: unless force-joined
rights.hasThrow("SELF_LEAVE_GROUPS");
} else {
rights.hasThrow("KICK_BAN_MEMBERS");
permission.hasThrow("KICK_MEMBERS");
}
await Member.removeFromGuild(member_id, guild_id); await Member.removeFromGuild(member_id, guild_id);
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -16,15 +16,26 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.patch( router.patch(
"/", "/",
route({ body: "MemberNickChangeSchema" }), route({
requestBody: "MemberNickChangeSchema",
responses: {
200: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
let permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";

View File

@ -16,15 +16,23 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Member } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Member } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.delete( router.delete(
"/", "/",
route({ permission: "MANAGE_ROLES" }), route({
permission: "MANAGE_ROLES",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params; const { guild_id, role_id, member_id } = req.params;
@ -35,7 +43,13 @@ router.delete(
router.put( router.put(
"/", "/",
route({ permission: "MANAGE_ROLES" }), route({
permission: "MANAGE_ROLES",
responses: {
204: {},
403: {},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params; const { guild_id, role_id, member_id } = req.params;

View File

@ -16,35 +16,58 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { Member, PublicMemberProjection } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { MoreThan } from "typeorm"; import { Member, PublicMemberProjection } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { MoreThan } from "typeorm";
const router = Router(); const router = Router();
// TODO: send over websocket // TODO: send over websocket
// TODO: check for GUILD_MEMBERS intent // TODO: check for GUILD_MEMBERS intent
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
const limit = Number(req.query.limit) || 1; route({
if (limit > 1000 || limit < 1) query: {
throw new HTTPError("Limit must be between 1 and 1000"); limit: {
const after = `${req.query.after}`; type: "number",
const query = after ? { id: MoreThan(after) } : {}; description:
"max number of members to return (1-1000). default 1",
},
after: {
type: "string",
},
},
responses: {
200: {
body: "APIMemberArray",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const limit = Number(req.query.limit) || 1;
if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const after = `${req.query.after}`;
const query = after ? { id: MoreThan(after) } : {};
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
const members = await Member.find({ const members = await Member.find({
where: { guild_id, ...query }, where: { guild_id, ...query },
select: PublicMemberProjection, select: PublicMemberProjection,
take: limit, take: limit,
order: { id: "ASC" }, order: { id: "ASC" },
}); });
return res.json(members); return res.json(members);
}); },
);
export default router; export default router;

View File

@ -18,140 +18,159 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { getPermission, FieldErrors, Message, Channel } from "@spacebar/util"; import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { FindManyOptions, In, Like } from "typeorm"; import { FindManyOptions, In, Like } from "typeorm";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { "/",
channel_id, route({
content, responses: {
// include_nsfw, // TODO 200: {
offset, body: "GuildMessagesSearchResponse",
sort_order, },
// sort_by, // TODO: Handle 'relevance' 403: {
limit, body: "APIErrorResponse",
author_id, },
} = req.query; 422: {
body: "APIErrorResponse",
const parsedLimit = Number(limit) || 50;
if (parsedLimit < 1 || parsedLimit > 100)
throw new HTTPError("limit must be between 1 and 100", 422);
if (sort_order) {
if (
typeof sort_order != "string" ||
["desc", "asc"].indexOf(sort_order) == -1
)
throw FieldErrors({
sort_order: {
message: "Value must be one of ('desc', 'asc').",
code: "BASE_TYPE_CHOICES",
},
}); // todo this is wrong
}
const permissions = await getPermission(
req.user_id,
req.params.guild_id,
channel_id as string | undefined,
);
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 });
const query: FindManyOptions<Message> = {
order: {
timestamp: sort_order
? (sort_order.toUpperCase() as "ASC" | "DESC")
: "DESC",
},
take: parsedLimit || 0,
where: {
guild: {
id: req.params.guild_id,
}, },
}, },
relations: [ }),
"author", async (req: Request, res: Response) => {
"webhook", const {
"application", channel_id,
"mentions", content,
"mention_roles", // include_nsfw, // TODO
"mention_channels", offset,
"sticker_items", sort_order,
"attachments", // sort_by, // TODO: Handle 'relevance'
], limit,
skip: offset ? Number(offset) : 0, author_id,
}; } = req.query;
//@ts-ignore
if (channel_id) query.where.channel = { id: channel_id };
else {
// get all channel IDs that this user can access
const channels = await Channel.find({
where: { guild_id: req.params.guild_id },
select: ["id"],
});
const ids = [];
for (const channel of channels) { const parsedLimit = Number(limit) || 50;
const perm = await getPermission( if (parsedLimit < 1 || parsedLimit > 100)
req.user_id, throw new HTTPError("limit must be between 1 and 100", 422);
req.params.guild_id,
channel.id, if (sort_order) {
); if (
if (!perm.has("VIEW_CHANNEL") || !perm.has("READ_MESSAGE_HISTORY")) typeof sort_order != "string" ||
continue; ["desc", "asc"].indexOf(sort_order) == -1
ids.push(channel.id); )
throw FieldErrors({
sort_order: {
message: "Value must be one of ('desc', 'asc').",
code: "BASE_TYPE_CHOICES",
},
}); // todo this is wrong
} }
//@ts-ignore const permissions = await getPermission(
query.where.channel = { id: In(ids) }; req.user_id,
} req.params.guild_id,
//@ts-ignore channel_id as string | undefined,
if (author_id) query.where.author = { id: author_id }; );
//@ts-ignore permissions.hasThrow("VIEW_CHANNEL");
if (content) query.where.content = Like(`%${content}%`); if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 });
const messages: Message[] = await Message.find(query); const query: FindManyOptions<Message> = {
order: {
const messagesDto = messages.map((x) => [ timestamp: sort_order
{ ? (sort_order.toUpperCase() as "ASC" | "DESC")
id: x.id, : "DESC",
type: x.type,
content: x.content,
channel_id: x.channel_id,
author: {
id: x.author?.id,
username: x.author?.username,
avatar: x.author?.avatar,
avatar_decoration: null,
discriminator: x.author?.discriminator,
public_flags: x.author?.public_flags,
}, },
attachments: x.attachments, take: parsedLimit || 0,
embeds: x.embeds, where: {
mentions: x.mentions, guild: {
mention_roles: x.mention_roles, id: req.params.guild_id,
pinned: x.pinned, },
mention_everyone: x.mention_everyone, },
tts: x.tts, relations: [
timestamp: x.timestamp, "author",
edited_timestamp: x.edited_timestamp, "webhook",
flags: x.flags, "application",
components: x.components, "mentions",
hit: true, "mention_roles",
}, "mention_channels",
]); "sticker_items",
"attachments",
],
skip: offset ? Number(offset) : 0,
};
//@ts-ignore
if (channel_id) query.where.channel = { id: channel_id };
else {
// get all channel IDs that this user can access
const channels = await Channel.find({
where: { guild_id: req.params.guild_id },
select: ["id"],
});
const ids = [];
return res.json({ for (const channel of channels) {
messages: messagesDto, const perm = await getPermission(
total_results: messages.length, req.user_id,
}); req.params.guild_id,
}); channel.id,
);
if (
!perm.has("VIEW_CHANNEL") ||
!perm.has("READ_MESSAGE_HISTORY")
)
continue;
ids.push(channel.id);
}
//@ts-ignore
query.where.channel = { id: In(ids) };
}
//@ts-ignore
if (author_id) query.where.author = { id: author_id };
//@ts-ignore
if (content) query.where.content = Like(`%${content}%`);
const messages: Message[] = await Message.find(query);
const messagesDto = messages.map((x) => [
{
id: x.id,
type: x.type,
content: x.content,
channel_id: x.channel_id,
author: {
id: x.author?.id,
username: x.author?.username,
avatar: x.author?.avatar,
avatar_decoration: null,
discriminator: x.author?.discriminator,
public_flags: x.author?.public_flags,
},
attachments: x.attachments,
embeds: x.embeds,
mentions: x.mentions,
mention_roles: x.mention_roles,
pinned: x.pinned,
mention_everyone: x.mention_everyone,
tts: x.tts,
timestamp: x.timestamp,
edited_timestamp: x.edited_timestamp,
flags: x.flags,
components: x.components,
hit: true,
},
]);
return res.json({
messages: messagesDto,
total_results: messages.length,
});
},
);
export default router; export default router;

View File

@ -31,7 +31,20 @@ const router = Router();
router.patch( router.patch(
"/:member_id", "/:member_id",
route({ body: "MemberChangeProfileSchema" }), route({
requestBody: "MemberChangeProfileSchema",
responses: {
200: {
body: "Member",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
// const member_id = // const member_id =

View File

@ -16,14 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { Guild, Member, Snowflake } from "@spacebar/util";
import { LessThan, IsNull } from "typeorm";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Guild, Member, Snowflake } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { IsNull, LessThan } from "typeorm";
const router = Router(); const router = Router();
//Returns all inactive members, respecting role hierarchy //Returns all inactive members, respecting role hierarchy
export const inactiveMembers = async ( const inactiveMembers = async (
guild_id: string, guild_id: string,
user_id: string, user_id: string,
days: number, days: number,
@ -80,25 +80,46 @@ export const inactiveMembers = async (
return members; return members;
}; };
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const days = parseInt(req.query.days as string); "/",
route({
responses: {
"200": {
body: "GuildPruneResponse",
},
},
}),
async (req: Request, res: Response) => {
const days = parseInt(req.query.days as string);
let roles = req.query.include_roles; let roles = req.query.include_roles;
if (typeof roles === "string") roles = [roles]; //express will return array otherwise if (typeof roles === "string") roles = [roles]; //express will return array otherwise
const members = await inactiveMembers( const members = await inactiveMembers(
req.params.guild_id, req.params.guild_id,
req.user_id, req.user_id,
days, days,
roles as string[], roles as string[],
); );
res.send({ pruned: members.length }); res.send({ pruned: members.length });
}); },
);
router.post( router.post(
"/", "/",
route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), route({
permission: "KICK_MEMBERS",
right: "KICK_BAN_MEMBERS",
responses: {
200: {
body: "GuildPurgeResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const days = parseInt(req.body.days); const days = parseInt(req.body.days);

View File

@ -16,22 +16,35 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
import { Guild } from "@spacebar/util"; import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { getVoiceRegions, route, getIpAdress } from "@spacebar/api";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); route({
//TODO we should use an enum for guild's features and not hardcoded strings responses: {
return res.json( 200: {
await getVoiceRegions( body: "APIGuildVoiceRegion",
getIpAdress(req), },
guild.features.includes("VIP_REGIONS"), 404: {
), body: "APIErrorResponse",
); },
}); },
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
//TODO we should use an enum for guild's features and not hardcoded strings
return res.json(
await getVoiceRegions(
getIpAdress(req),
guild.features.includes("VIP_REGIONS"),
),
);
},
);
export default router; export default router;

View File

@ -16,31 +16,63 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { route } from "@spacebar/api";
import { import {
Role,
Member,
GuildRoleUpdateEvent,
GuildRoleDeleteEvent,
emitEvent, emitEvent,
GuildRoleDeleteEvent,
GuildRoleUpdateEvent,
handleFile, handleFile,
Member,
Role,
RoleModifySchema, RoleModifySchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id, role_id } = req.params; "/",
await Member.IsInGuildOrFail(req.user_id, guild_id); route({
const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } }); responses: {
return res.json(role); 200: {
}); body: "Role",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
const role = await Role.findOneOrFail({
where: { guild_id, id: role_id },
});
return res.json(role);
},
);
router.delete( router.delete(
"/", "/",
route({ permission: "MANAGE_ROLES" }), route({
permission: "MANAGE_ROLES",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params; const { guild_id, role_id } = req.params;
if (role_id === guild_id) if (role_id === guild_id)
@ -69,7 +101,24 @@ router.delete(
router.patch( router.patch(
"/", "/",
route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), route({
requestBody: "RoleModifySchema",
permission: "MANAGE_ROLES",
responses: {
200: {
body: "Role",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { role_id, guild_id } = req.params; const { role_id, guild_id } = req.params;
const body = req.body as RoleModifySchema; const body = req.body as RoleModifySchema;

View File

@ -16,21 +16,20 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { route } from "@spacebar/api";
import { import {
Role,
getPermission,
Member,
GuildRoleCreateEvent,
GuildRoleUpdateEvent,
emitEvent,
Config, Config,
DiscordApiErrors, DiscordApiErrors,
emitEvent,
GuildRoleCreateEvent,
GuildRoleUpdateEvent,
Member,
Role,
RoleModifySchema, RoleModifySchema,
RolePositionUpdateSchema, RolePositionUpdateSchema,
Snowflake, Snowflake,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
import { Not } from "typeorm"; import { Not } from "typeorm";
const router: Router = Router(); const router: Router = Router();
@ -47,7 +46,21 @@ router.get("/", route({}), async (req: Request, res: Response) => {
router.post( router.post(
"/", "/",
route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), route({
requestBody: "RoleModifySchema",
permission: "MANAGE_ROLES",
responses: {
200: {
body: "Role",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const guild_id = req.params.guild_id; const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema; const body = req.body as RoleModifySchema;
@ -104,14 +117,25 @@ router.post(
router.patch( router.patch(
"/", "/",
route({ body: "RolePositionUpdateSchema" }), route({
requestBody: "RolePositionUpdateSchema",
permission: "MANAGE_ROLES",
responses: {
200: {
body: "APIRoleArray",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as RolePositionUpdateSchema; const body = req.body as RolePositionUpdateSchema;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
await Promise.all( await Promise.all(
body.map(async (x) => body.map(async (x) =>
Role.update({ guild_id, id: x.id }, { position: x.position }), Role.update({ guild_id, id: x.id }, { position: x.position }),

View File

@ -16,29 +16,42 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
emitEvent,
GuildStickersUpdateEvent, GuildStickersUpdateEvent,
Member, Member,
ModifyGuildStickerSchema,
Snowflake, Snowflake,
Sticker, Sticker,
StickerFormatType, StickerFormatType,
StickerType, StickerType,
emitEvent,
uploadFile, uploadFile,
ModifyGuildStickerSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { Router, Request, Response } from "express"; import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
import multer from "multer";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import multer from "multer";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
await Member.IsInGuildOrFail(req.user_id, guild_id); route({
responses: {
200: {
body: "APIStickerArray",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(await Sticker.find({ where: { guild_id } })); res.json(await Sticker.find({ where: { guild_id } }));
}); },
);
const bodyParser = multer({ const bodyParser = multer({
limits: { limits: {
@ -54,7 +67,18 @@ router.post(
bodyParser, bodyParser,
route({ route({
permission: "MANAGE_EMOJIS_AND_STICKERS", permission: "MANAGE_EMOJIS_AND_STICKERS",
body: "ModifyGuildStickerSchema", requestBody: "ModifyGuildStickerSchema",
responses: {
200: {
body: "Sticker",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
if (!req.file) throw new HTTPError("missing file"); if (!req.file) throw new HTTPError("missing file");
@ -81,7 +105,7 @@ router.post(
}, },
); );
export function getStickerFormat(mime_type: string) { function getStickerFormat(mime_type: string) {
switch (mime_type) { switch (mime_type) {
case "image/apng": case "image/apng":
return StickerFormatType.APNG; return StickerFormatType.APNG;
@ -98,20 +122,46 @@ export function getStickerFormat(mime_type: string) {
} }
} }
router.get("/:sticker_id", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id, sticker_id } = req.params; "/:sticker_id",
await Member.IsInGuildOrFail(req.user_id, guild_id); route({
responses: {
200: {
body: "Sticker",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json( res.json(
await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }), await Sticker.findOneOrFail({
); where: { guild_id, id: sticker_id },
}); }),
);
},
);
router.patch( router.patch(
"/:sticker_id", "/:sticker_id",
route({ route({
body: "ModifyGuildStickerSchema", requestBody: "ModifyGuildStickerSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS", permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
200: {
body: "Sticker",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;
@ -141,7 +191,15 @@ async function sendStickerUpdateEvent(guild_id: string) {
router.delete( router.delete(
"/:sticker_id", "/:sticker_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;

View File

@ -16,11 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { generateCode, route } from "@spacebar/api";
import { Guild, Template } from "@spacebar/util"; import { Guild, Template } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
import { generateCode } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
@ -41,19 +40,46 @@ const TemplateGuildProjection: (keyof Guild)[] = [
"icon", "icon",
]; ];
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
route({
responses: {
200: {
body: "APITemplateArray",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const templates = await Template.find({ const templates = await Template.find({
where: { source_guild_id: guild_id }, where: { source_guild_id: guild_id },
}); });
return res.json(templates); return res.json(templates);
}); },
);
router.post( router.post(
"/", "/",
route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), route({
requestBody: "TemplateCreateSchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "Template",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
@ -81,7 +107,13 @@ router.post(
router.delete( router.delete(
"/:code", "/:code",
route({ permission: "MANAGE_GUILD" }), route({
permission: "MANAGE_GUILD",
responses: {
200: { body: "Template" },
403: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { code, guild_id } = req.params; const { code, guild_id } = req.params;
@ -96,7 +128,13 @@ router.delete(
router.put( router.put(
"/:code", "/:code",
route({ permission: "MANAGE_GUILD" }), route({
permission: "MANAGE_GUILD",
responses: {
200: { body: "Template" },
403: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { code, guild_id } = req.params; const { code, guild_id } = req.params;
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
@ -115,7 +153,14 @@ router.put(
router.patch( router.patch(
"/:code", "/:code",
route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), route({
requestBody: "TemplateModifySchema",
permission: "MANAGE_GUILD",
responses: {
200: { body: "Template" },
403: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { code, guild_id } = req.params; const { code, guild_id } = req.params;
const { name, description } = req.body; const { name, description } = req.body;

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
Channel, Channel,
ChannelType, ChannelType,
@ -23,8 +24,7 @@ import {
Invite, Invite,
VanityUrlSchema, VanityUrlSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { Router, Request, Response } from "express"; import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
@ -33,7 +33,20 @@ const InviteRegex = /\W/g;
router.get( router.get(
"/", "/",
route({ permission: "MANAGE_GUILD" }), route({
permission: "MANAGE_GUILD",
responses: {
200: {
body: "GuildVanityUrlResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
@ -60,7 +73,21 @@ router.get(
router.patch( router.patch(
"/", "/",
route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), route({
requestBody: "VanityUrlSchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "GuildVanityUrlCreateResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as VanityUrlSchema; const body = req.body as VanityUrlSchema;

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { route } from "@spacebar/api";
import { import {
Channel, Channel,
ChannelType, ChannelType,
@ -26,7 +27,6 @@ import {
VoiceStateUpdateEvent, VoiceStateUpdateEvent,
VoiceStateUpdateSchema, VoiceStateUpdateSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
@ -34,7 +34,21 @@ const router = Router();
router.patch( router.patch(
"/", "/",
route({ body: "VoiceStateUpdateSchema" }), route({
requestBody: "VoiceStateUpdateSchema",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as VoiceStateUpdateSchema; const body = req.body as VoiceStateUpdateSchema;
const { guild_id } = req.params; const { guild_id } = req.params;

View File

@ -16,27 +16,49 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Guild, GuildUpdateWelcomeScreenSchema, Member } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const guild_id = req.params.guild_id; "/",
route({
responses: {
200: {
body: "GuildWelcomeScreen",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(guild.welcome_screen); res.json(guild.welcome_screen);
}); },
);
router.patch( router.patch(
"/", "/",
route({ route({
body: "GuildUpdateWelcomeScreenSchema", requestBody: "GuildUpdateWelcomeScreenSchema",
permission: "MANAGE_GUILD", permission: "MANAGE_GUILD",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const guild_id = req.params.guild_id; const guild_id = req.params.guild_id;

View File

@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { Permissions, Guild, Invite, Channel, Member } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { random, route } from "@spacebar/api"; import { random, route } from "@spacebar/api";
import { Channel, Guild, Invite, Member, Permissions } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
@ -32,77 +32,90 @@ const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget // https://discord.com/developers/docs/resources/guild#get-guild-widget
// TODO: Cache the response for a guild for 5 minutes regardless of response // TODO: Cache the response for a guild for 5 minutes regardless of response
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
route({
responses: {
200: {
body: "GuildWidgetJsonResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
// Fetch existing widget invite for widget channel // Fetch existing widget invite for widget channel
let invite = await Invite.findOne({ let invite = await Invite.findOne({
where: { channel_id: guild.widget_channel_id }, where: { channel_id: guild.widget_channel_id },
}); });
if (guild.widget_channel_id && !invite) { if (guild.widget_channel_id && !invite) {
// Create invite for channel if none exists // Create invite for channel if none exists
// TODO: Refactor invite create code to a shared function // TODO: Refactor invite create code to a shared function
const max_age = 86400; // 24 hours const max_age = 86400; // 24 hours
const expires_at = new Date(max_age * 1000 + Date.now()); const expires_at = new Date(max_age * 1000 + Date.now());
invite = await Invite.create({ invite = await Invite.create({
code: random(), code: random(),
temporary: false, temporary: false,
uses: 0, uses: 0,
max_uses: 0, max_uses: 0,
max_age: max_age, max_age: max_age,
expires_at, expires_at,
created_at: new Date(), created_at: new Date(),
guild_id, guild_id,
channel_id: guild.widget_channel_id, channel_id: guild.widget_channel_id,
}).save(); }).save();
}
// Fetch voice channels, and the @everyone permissions object
const channels: { id: string; name: string; position: number }[] = [];
(
await Channel.find({
where: { guild_id: guild_id, type: 2 },
order: { position: "ASC" },
})
).filter((doc) => {
// Only return channels where @everyone has the CONNECT permission
if (
doc.permission_overwrites === undefined ||
Permissions.channelPermission(
doc.permission_overwrites,
Permissions.FLAGS.CONNECT,
) === Permissions.FLAGS.CONNECT
) {
channels.push({
id: doc.id,
name: doc.name ?? "Unknown channel",
position: doc.position ?? 0,
});
} }
});
// Fetch members // Fetch voice channels, and the @everyone permissions object
// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) const channels: { id: string; name: string; position: number }[] = [];
const members = await Member.find({ where: { guild_id: guild_id } });
// Construct object to respond with (
const data = { await Channel.find({
id: guild_id, where: { guild_id: guild_id, type: 2 },
name: guild.name, order: { position: "ASC" },
instant_invite: invite?.code, })
channels: channels, ).filter((doc) => {
members: members, // Only return channels where @everyone has the CONNECT permission
presence_count: guild.presence_count, if (
}; doc.permission_overwrites === undefined ||
Permissions.channelPermission(
doc.permission_overwrites,
Permissions.FLAGS.CONNECT,
) === Permissions.FLAGS.CONNECT
) {
channels.push({
id: doc.id,
name: doc.name ?? "Unknown channel",
position: doc.position ?? 0,
});
}
});
res.set("Cache-Control", "public, max-age=300"); // Fetch members
return res.json(data); // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
}); const members = await Member.find({ where: { guild_id: guild_id } });
// Construct object to respond with
const data = {
id: guild_id,
name: guild.name,
instant_invite: invite?.code,
channels: channels,
members: members,
presence_count: guild.presence_count,
};
res.set("Cache-Control", "public, max-age=300");
return res.json(data);
},
);
export default router; export default router;

View File

@ -18,11 +18,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response, Router } from "express";
import { Guild } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
import fs from "fs"; import fs from "fs";
import { HTTPError } from "lambert-server";
import path from "path"; import path from "path";
const router: Router = Router(); const router: Router = Router();
@ -31,130 +31,178 @@ const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-image // https://discord.com/developers/docs/resources/guild#get-guild-widget-image
// TODO: Cache the response // TODO: Cache the response
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
route({
responses: {
200: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
// Fetch guild information // Fetch guild information
const icon = guild.icon; const icon = guild.icon;
const name = guild.name; const name = guild.name;
const presence = guild.presence_count + " ONLINE"; const presence = guild.presence_count + " ONLINE";
// Fetch parameter // Fetch parameter
const style = req.query.style?.toString() || "shield"; const style = req.query.style?.toString() || "shield";
if ( if (
!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style) !["shield", "banner1", "banner2", "banner3", "banner4"].includes(
) { style,
throw new HTTPError( )
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", ) {
400,
);
}
// Setup canvas
const { createCanvas } = require("canvas");
const { loadImage } = require("canvas");
const sizeOf = require("image-size");
// TODO: Widget style templates need Spacebar branding
const source = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"assets",
"widget",
`${style}.png`,
);
if (!fs.existsSync(source)) {
throw new HTTPError("Widget template does not exist.", 400);
}
// Create base template image for parameter
const { width, height } = await sizeOf(source);
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
const template = await loadImage(source);
ctx.drawImage(template, 0, 0);
// Add the guild specific information to the template asset image
switch (style) {
case "shield":
ctx.textAlign = "center";
await drawText(
ctx,
73,
13,
"#FFFFFF",
"thin 10px Verdana",
presence,
);
break;
case "banner1":
if (icon) await drawIcon(ctx, 20, 27, 50, icon);
await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
await drawText(
ctx,
83,
66,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner2":
if (icon) await drawIcon(ctx, 13, 19, 36, icon);
await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
await drawText(
ctx,
62,
49,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner3":
if (icon) await drawIcon(ctx, 20, 20, 50, icon);
await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
await drawText(
ctx,
83,
58,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner4":
if (icon) await drawIcon(ctx, 21, 136, 50, icon);
await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
await drawText(
ctx,
84,
171,
"#C9D2F0FF",
"thin 12px Verdana",
presence,
);
break;
default:
throw new HTTPError( throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", "Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400, 400,
); );
} }
// Return final image // Setup canvas
const buffer = canvas.toBuffer("image/png"); const { createCanvas } = require("canvas");
res.set("Content-Type", "image/png"); const { loadImage } = require("canvas");
res.set("Cache-Control", "public, max-age=3600"); const sizeOf = require("image-size");
return res.send(buffer);
}); // TODO: Widget style templates need Spacebar branding
const source = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"assets",
"widget",
`${style}.png`,
);
if (!fs.existsSync(source)) {
throw new HTTPError("Widget template does not exist.", 400);
}
// Create base template image for parameter
const { width, height } = await sizeOf(source);
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
const template = await loadImage(source);
ctx.drawImage(template, 0, 0);
// Add the guild specific information to the template asset image
switch (style) {
case "shield":
ctx.textAlign = "center";
await drawText(
ctx,
73,
13,
"#FFFFFF",
"thin 10px Verdana",
presence,
);
break;
case "banner1":
if (icon) await drawIcon(ctx, 20, 27, 50, icon);
await drawText(
ctx,
83,
51,
"#FFFFFF",
"12px Verdana",
name,
22,
);
await drawText(
ctx,
83,
66,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner2":
if (icon) await drawIcon(ctx, 13, 19, 36, icon);
await drawText(
ctx,
62,
34,
"#FFFFFF",
"12px Verdana",
name,
15,
);
await drawText(
ctx,
62,
49,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner3":
if (icon) await drawIcon(ctx, 20, 20, 50, icon);
await drawText(
ctx,
83,
44,
"#FFFFFF",
"12px Verdana",
name,
27,
);
await drawText(
ctx,
83,
58,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner4":
if (icon) await drawIcon(ctx, 21, 136, 50, icon);
await drawText(
ctx,
84,
156,
"#FFFFFF",
"13px Verdana",
name,
27,
);
await drawText(
ctx,
84,
171,
"#C9D2F0FF",
"thin 12px Verdana",
presence,
);
break;
default:
throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400,
);
}
// Return final image
const buffer = canvas.toBuffer("image/png");
res.set("Content-Type", "image/png");
res.set("Cache-Control", "public, max-age=3600");
return res.send(buffer);
},
);
async function drawIcon( async function drawIcon(
canvas: any, canvas: any,

View File

@ -16,28 +16,55 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { Guild, WidgetModifySchema } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Guild, WidgetModifySchema } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-settings // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
route({
responses: {
200: {
body: "GuildWidgetSettingsResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
return res.json({ return res.json({
enabled: guild.widget_enabled || false, enabled: guild.widget_enabled || false,
channel_id: guild.widget_channel_id || null, channel_id: guild.widget_channel_id || null,
}); });
}); },
);
// https://discord.com/developers/docs/resources/guild#modify-guild-widget // https://discord.com/developers/docs/resources/guild#modify-guild-widget
router.patch( router.patch(
"/", "/",
route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), route({
requestBody: "WidgetModifySchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "WidgetModifySchema",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as WidgetModifySchema; const body = req.body as WidgetModifySchema;
const { guild_id } = req.params; const { guild_id } = req.params;

View File

@ -16,16 +16,16 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import {
Guild,
Config,
getRights,
Member,
DiscordApiErrors,
GuildCreateSchema,
} from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import {
Config,
DiscordApiErrors,
Guild,
GuildCreateSchema,
Member,
getRights,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
@ -33,7 +33,21 @@ const router: Router = Router();
router.post( router.post(
"/", "/",
route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), route({
requestBody: "GuildCreateSchema",
right: "CREATE_GUILDS",
responses: {
201: {
body: "GuildCreateResponse",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema; const body = req.body as GuildCreateSchema;

View File

@ -16,72 +16,91 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { route } from "@spacebar/api";
import { import {
Template, Config,
DiscordApiErrors,
Guild, Guild,
GuildTemplateCreateSchema,
Member,
Role, Role,
Snowflake, Snowflake,
Config, Template,
Member,
GuildTemplateCreateSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
import { DiscordApiErrors } from "@spacebar/util";
import fetch from "node-fetch"; import fetch from "node-fetch";
const router: Router = Router(); const router: Router = Router();
router.get("/:code", route({}), async (req: Request, res: Response) => { router.get(
const { allowDiscordTemplates, allowRaws, enabled } = "/:code",
Config.get().templates; route({
if (!enabled) responses: {
res.json({ 200: {
code: 403, body: "Template",
message: "Template creation & usage is disabled on this instance.",
}).sendStatus(403);
const { code } = req.params;
if (code.startsWith("discord:")) {
if (!allowDiscordTemplates)
return res
.json({
code: 403,
message:
"Discord templates cannot be used on this instance.",
})
.sendStatus(403);
const discordTemplateID = code.split("discord:", 2)[1];
const discordTemplateData = await fetch(
`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
{
method: "get",
headers: { "Content-Type": "application/json" },
}, },
); 403: {
return res.json(await discordTemplateData.json()); body: "APIErrorResponse",
} },
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { allowDiscordTemplates, allowRaws, enabled } =
Config.get().templates;
if (!enabled)
res.json({
code: 403,
message:
"Template creation & usage is disabled on this instance.",
}).sendStatus(403);
if (code.startsWith("external:")) { const { code } = req.params;
if (!allowRaws)
return res
.json({
code: 403,
message: "Importing raws is disabled on this instance.",
})
.sendStatus(403);
return res.json(code.split("external:", 2)[1]); if (code.startsWith("discord:")) {
} if (!allowDiscordTemplates)
return res
.json({
code: 403,
message:
"Discord templates cannot be used on this instance.",
})
.sendStatus(403);
const discordTemplateID = code.split("discord:", 2)[1];
const template = await Template.findOneOrFail({ where: { code: code } }); const discordTemplateData = await fetch(
res.json(template); `https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
}); {
method: "get",
headers: { "Content-Type": "application/json" },
},
);
return res.json(await discordTemplateData.json());
}
if (code.startsWith("external:")) {
if (!allowRaws)
return res
.json({
code: 403,
message: "Importing raws is disabled on this instance.",
})
.sendStatus(403);
return res.json(code.split("external:", 2)[1]);
}
const template = await Template.findOneOrFail({
where: { code: code },
});
res.json(template);
},
);
router.post( router.post(
"/:code", "/:code",
route({ body: "GuildTemplateCreateSchema" }), route({ requestBody: "GuildTemplateCreateSchema" }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { const {
enabled, enabled,

View File

@ -16,35 +16,64 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { route } from "@spacebar/api";
import { import {
emitEvent, emitEvent,
getPermission, getPermission,
Guild, Guild,
Invite, Invite,
InviteDeleteEvent, InviteDeleteEvent,
User,
PublicInviteRelation, PublicInviteRelation,
User,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
router.get("/:code", route({}), async (req: Request, res: Response) => { router.get(
const { code } = req.params; "/:code",
route({
responses: {
"200": {
body: "Invite",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { code } = req.params;
const invite = await Invite.findOneOrFail({ const invite = await Invite.findOneOrFail({
where: { code }, where: { code },
relations: PublicInviteRelation, relations: PublicInviteRelation,
}); });
res.status(200).send(invite); res.status(200).send(invite);
}); },
);
router.post( router.post(
"/:code", "/:code",
route({ right: "USE_MASS_INVITES" }), route({
right: "USE_MASS_INVITES",
responses: {
"200": {
body: "Invite",
},
401: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { code } = req.params; const { code } = req.params;
const { guild_id } = await Invite.findOneOrFail({ const { guild_id } = await Invite.findOneOrFail({
@ -75,33 +104,56 @@ router.post(
); );
// * cant use permission of route() function because path doesn't have guild_id/channel_id // * cant use permission of route() function because path doesn't have guild_id/channel_id
router.delete("/:code", route({}), async (req: Request, res: Response) => { router.delete(
const { code } = req.params; "/:code",
const invite = await Invite.findOneOrFail({ where: { code } }); route({
const { guild_id, channel_id } = invite; responses: {
"200": {
body: "Invite",
},
401: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { code } = req.params;
const invite = await Invite.findOneOrFail({ where: { code } });
const { guild_id, channel_id } = invite;
const permission = await getPermission(req.user_id, guild_id, channel_id); const permission = await getPermission(
req.user_id,
if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS")) guild_id,
throw new HTTPError( channel_id,
"You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
401,
); );
await Promise.all([ if (
Invite.delete({ code }), !permission.has("MANAGE_GUILD") &&
emitEvent({ !permission.has("MANAGE_CHANNELS")
event: "INVITE_DELETE", )
guild_id: guild_id, throw new HTTPError(
data: { "You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
channel_id: channel_id, 401,
guild_id: guild_id, );
code: code,
},
} as InviteDeleteEvent),
]);
res.json({ invite: invite }); await Promise.all([
}); Invite.delete({ code }),
emitEvent({
event: "INVITE_DELETE",
guild_id: guild_id,
data: {
channel_id: channel_id,
guild_id: guild_id,
code: code,
},
} as InviteDeleteEvent),
]);
res.json({ invite: invite });
},
);
export default router; export default router;

View File

@ -16,126 +16,168 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
ApiError, ApiError,
Application, Application,
ApplicationAuthorizeSchema, ApplicationAuthorizeSchema,
getPermission,
DiscordApiErrors, DiscordApiErrors,
Member, Member,
Permissions, Permissions,
User, User,
getPermission,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
// TODO: scopes, other oauth types // TODO: scopes, other oauth types
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
// const { client_id, scope, response_type, redirect_url } = req.query; "/",
const { client_id } = req.query; route({
responses: {
const app = await Application.findOne({ // TODO: I really didn't feel like typing all of it out
where: { 200: {},
id: client_id as string, 400: {
}, body: "APIErrorResponse",
relations: ["bot"], },
}); 404: {
body: "APIErrorResponse",
// TODO: use DiscordApiErrors
// findOneOrFail throws code 404
if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
const bot = app.bot;
delete app.bot;
const user = await User.findOneOrFail({
where: {
id: req.user_id,
bot: false,
},
select: ["id", "username", "avatar", "discriminator", "public_flags"],
});
const guilds = await Member.find({
where: {
user: {
id: req.user_id,
}, },
}, },
relations: ["guild", "roles"], }),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment async (req: Request, res: Response) => {
//@ts-ignore // const { client_id, scope, response_type, redirect_url } = req.query;
// prettier-ignore const { client_id } = req.query;
select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"],
});
const guildsWithPermissions = guilds.map((x) => { const app = await Application.findOne({
const perms = where: {
x.guild.owner_id === user.id id: client_id as string,
? new Permissions(Permissions.FLAGS.ADMINISTRATOR) },
: Permissions.finalPermission({ relations: ["bot"],
user: { });
id: user.id,
roles: x.roles?.map((x) => x.id) || [],
},
guild: {
roles: x?.roles || [],
},
});
return { // TODO: use DiscordApiErrors
id: x.guild.id, // findOneOrFail throws code 404
name: x.guild.name, if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
icon: x.guild.icon, if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
mfa_level: x.guild.mfa_level,
permissions: perms.bitfield.toString(),
};
});
return res.json({ const bot = app.bot;
guilds: guildsWithPermissions, delete app.bot;
user: {
id: user.id, const user = await User.findOneOrFail({
username: user.username, where: {
avatar: user.avatar, id: req.user_id,
avatar_decoration: null, // TODO bot: false,
discriminator: user.discriminator, },
public_flags: user.public_flags, select: [
}, "id",
application: { "username",
id: app.id, "avatar",
name: app.name, "discriminator",
icon: app.icon, "public_flags",
description: app.description, ],
summary: app.summary, });
type: app.type,
hook: app.hook, const guilds = await Member.find({
guild_id: null, // TODO support guilds where: {
bot_public: app.bot_public, user: {
bot_require_code_grant: app.bot_require_code_grant, id: req.user_id,
verify_key: app.verify_key, },
flags: app.flags, },
}, relations: ["guild", "roles"],
bot: { // eslint-disable-next-line @typescript-eslint/ban-ts-comment
id: bot.id, //@ts-ignore
username: bot.username, // prettier-ignore
avatar: bot.avatar, select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"],
avatar_decoration: null, // TODO });
discriminator: bot.discriminator,
public_flags: bot.public_flags, const guildsWithPermissions = guilds.map((x) => {
bot: true, const perms =
approximated_guild_count: 0, // TODO x.guild.owner_id === user.id
}, ? new Permissions(Permissions.FLAGS.ADMINISTRATOR)
authorized: false, : Permissions.finalPermission({
}); user: {
}); id: user.id,
roles: x.roles?.map((x) => x.id) || [],
},
guild: {
roles: x?.roles || [],
},
});
return {
id: x.guild.id,
name: x.guild.name,
icon: x.guild.icon,
mfa_level: x.guild.mfa_level,
permissions: perms.bitfield.toString(),
};
});
return res.json({
guilds: guildsWithPermissions,
user: {
id: user.id,
username: user.username,
avatar: user.avatar,
avatar_decoration: null, // TODO
discriminator: user.discriminator,
public_flags: user.public_flags,
},
application: {
id: app.id,
name: app.name,
icon: app.icon,
description: app.description,
summary: app.summary,
type: app.type,
hook: app.hook,
guild_id: null, // TODO support guilds
bot_public: app.bot_public,
bot_require_code_grant: app.bot_require_code_grant,
verify_key: app.verify_key,
flags: app.flags,
},
bot: {
id: bot.id,
username: bot.username,
avatar: bot.avatar,
avatar_decoration: null, // TODO
discriminator: bot.discriminator,
public_flags: bot.public_flags,
bot: true,
approximated_guild_count: 0, // TODO
},
authorized: false,
});
},
);
router.post( router.post(
"/", "/",
route({ body: "ApplicationAuthorizeSchema" }), route({
requestBody: "ApplicationAuthorizeSchema",
query: {
client_id: {
type: "string",
},
},
responses: {
200: {
body: "OAuthAuthorizeResponse",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as ApplicationAuthorizeSchema; const body = req.body as ApplicationAuthorizeSchema;
// const { client_id, scope, response_type, redirect_url } = req.query; // const { client_id, scope, response_type, redirect_url } = req.query;

View File

@ -16,29 +16,39 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Config } from "@spacebar/util"; import { Config } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), (req: Request, res: Response) => { router.get(
const { general } = Config.get(); "/",
res.send({ route({
ping: "pong!", responses: {
instance: { 200: {
id: general.instanceId, body: "InstancePingResponse",
name: general.instanceName, },
description: general.instanceDescription,
image: general.image,
correspondenceEmail: general.correspondenceEmail,
correspondenceUserID: general.correspondenceUserID,
frontPage: general.frontPage,
tosPage: general.tosPage,
}, },
}); }),
}); (req: Request, res: Response) => {
const { general } = Config.get();
res.send({
ping: "pong!",
instance: {
id: general.instanceId,
name: general.instanceName,
description: general.instanceDescription,
image: general.image,
correspondenceEmail: general.correspondenceEmail,
correspondenceUserID: general.correspondenceUserID,
frontPage: general.frontPage,
tosPage: general.tosPage,
},
});
},
);
export default router; export default router;

View File

@ -16,25 +16,38 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Config } from "@spacebar/util"; import { Config } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { cdn, gateway, api } = Config.get(); "/",
route({
responses: {
200: {
body: "InstanceDomainsResponse",
},
},
}),
async (req: Request, res: Response) => {
const { cdn, gateway, api } = Config.get();
const IdentityForm = { const IdentityForm = {
cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001", cdn:
gateway: cdn.endpointPublic ||
gateway.endpointPublic || process.env.CDN ||
process.env.GATEWAY || "http://localhost:3001",
"ws://localhost:3001", gateway:
defaultApiVersion: api.defaultVersion ?? 9, gateway.endpointPublic ||
apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/", process.env.GATEWAY ||
}; "ws://localhost:3001",
defaultApiVersion: api.defaultVersion ?? 9,
apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/",
};
res.json(IdentityForm); res.json(IdentityForm);
}); },
);
export default router; export default router;

View File

@ -16,14 +16,24 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Config } from "@spacebar/util"; import { Config } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { general } = Config.get(); "/",
res.json(general); route({
}); responses: {
200: {
body: "APIGeneralConfiguration",
},
},
}),
async (req: Request, res: Response) => {
const { general } = Config.get();
res.json(general);
},
);
export default router; export default router;

View File

@ -16,14 +16,24 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Config } from "@spacebar/util"; import { Config } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { limits } = Config.get(); "/",
res.json(limits); route({
}); responses: {
200: {
body: "APILimitsConfiguration",
},
},
}),
async (req: Request, res: Response) => {
const { limits } = Config.get();
res.json(limits);
},
);
export default router; export default router;

View File

@ -28,20 +28,33 @@ import {
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
if (!Config.get().security.statsWorldReadable) { "/",
const rights = await getRights(req.user_id); route({
rights.hasThrow("VIEW_SERVER_STATS"); responses: {
} 200: {
body: "InstanceStatsResponse",
res.json({ },
counts: { 403: {
user: await User.count(), body: "APIErrorResponse",
guild: await Guild.count(), },
message: await Message.count(),
members: await Member.count(),
}, },
}); }),
}); async (req: Request, res: Response) => {
if (!Config.get().security.statsWorldReadable) {
const rights = await getRights(req.user_id);
rights.hasThrow("VIEW_SERVER_STATS");
}
res.json({
counts: {
user: await User.count(),
guild: await Guild.count(),
message: await Message.count(),
members: await Member.count(),
},
});
},
);
export default router; export default router;

View File

@ -16,14 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { AckBulkSchema, ReadState } from "@spacebar/util"; import { AckBulkSchema, ReadState } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post( router.post(
"/", "/",
route({ body: "AckBulkSchema" }), route({
requestBody: "AckBulkSchema",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as AckBulkSchema; const body = req.body as AckBulkSchema;

View File

@ -16,14 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post("/", route({}), (req: Request, res: Response) => { router.post(
// TODO: "/",
res.sendStatus(204); route({
}); responses: {
204: {},
},
}),
(req: Request, res: Response) => {
// TODO:
res.sendStatus(204);
},
);
export default router; export default router;

View File

@ -16,16 +16,28 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { StickerPack } from "@spacebar/util"; import { StickerPack } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const sticker_packs = await StickerPack.find({ relations: ["stickers"] }); "/",
route({
responses: {
200: {
body: "APIStickerPackArray",
},
},
}),
async (req: Request, res: Response) => {
const sticker_packs = await StickerPack.find({
relations: ["stickers"],
});
res.json({ sticker_packs }); res.json({ sticker_packs });
}); },
);
export default router; export default router;

View File

@ -16,15 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Sticker } from "@spacebar/util";
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Sticker } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { sticker_id } = req.params; "/",
route({
responses: {
200: {
body: "Sticker",
},
},
}),
async (req: Request, res: Response) => {
const { sticker_id } = req.params;
res.json(await Sticker.find({ where: { id: sticker_id } })); res.json(await Sticker.find({ where: { id: sticker_id } }));
}); },
);
export default router; export default router;

View File

@ -16,14 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.post( router.post(
"/", "/",
route({ right: "OPERATOR" }), route({
right: "OPERATOR",
responses: {
200: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
console.log(`/stop was called by ${req.user_id} at ${new Date()}`); console.log(`/stop was called by ${req.user_id} at ${new Date()}`);
res.sendStatus(200); res.sendStatus(200);

View File

@ -16,37 +16,53 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { FieldErrors, Release } from "@spacebar/util"; import { FieldErrors, Release } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const platform = req.query.platform; "/",
route({
if (!platform) responses: {
throw FieldErrors({ 200: {
platform: { body: "UpdatesResponse",
code: "BASE_TYPE_REQUIRED",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
}, },
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const platform = req.query.platform;
if (!platform)
throw FieldErrors({
platform: {
code: "BASE_TYPE_REQUIRED",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
},
});
const release = await Release.findOneOrFail({
where: {
enabled: true,
platform: platform as string,
},
order: { pub_date: "DESC" },
}); });
const release = await Release.findOneOrFail({ res.json({
where: { name: release.name,
enabled: true, pub_date: release.pub_date,
platform: platform as string, url: release.url,
}, notes: release.notes,
order: { pub_date: "DESC" }, });
}); },
);
res.json({
name: release.name,
pub_date: release.pub_date,
url: release.url,
notes: release.notes,
});
});
export default router; export default router;

View File

@ -30,7 +30,18 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ right: "MANAGE_USERS" }), route({
right: "MANAGE_USERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
await User.findOneOrFail({ await User.findOneOrFail({
where: { id: req.params.id }, where: { id: req.params.id },

View File

@ -16,16 +16,26 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { User } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { id } = req.params; "/",
route({
responses: {
200: {
body: "APIPublicUser",
},
},
}),
async (req: Request, res: Response) => {
const { id } = req.params;
res.json(await User.getPublicUser(id)); res.json(await User.getPublicUser(id));
}); },
);
export default router; export default router;

View File

@ -16,23 +16,23 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import {
User,
Member,
UserProfileModifySchema,
handleFile,
PrivateUserProjection,
emitEvent,
UserUpdateEvent,
} from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import {
Member,
PrivateUserProjection,
User,
UserProfileModifySchema,
UserUpdateEvent,
emitEvent,
handleFile,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get( router.get(
"/", "/",
route({ test: { response: { body: "UserProfileResponse" } } }), route({ responses: { 200: { body: "UserProfileResponse" } } }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
if (req.params.id === "@me") req.params.id = req.user_id; if (req.params.id === "@me") req.params.id = req.user_id;
@ -151,7 +151,7 @@ router.get(
router.patch( router.patch(
"/", "/",
route({ body: "UserProfileModifySchema" }), route({ requestBody: "UserProfileModifySchema" }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as UserProfileModifySchema; const body = req.body as UserProfileModifySchema;

View File

@ -16,17 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { User } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User, UserRelationsResponse } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get( router.get(
"/", "/",
route({ test: { response: { body: "UserRelationsResponse" } } }), route({
responses: {
200: { body: "UserRelationsResponse" },
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const mutual_relations: object[] = []; const mutual_relations: UserRelationsResponse = [];
const requested_relations = await User.findOneOrFail({ const requested_relations = await User.findOneOrFail({
where: { id: req.params.id }, where: { id: req.params.id },
relations: ["relationships"], relations: ["relationships"],

View File

@ -16,32 +16,51 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express"; import { route } from "@spacebar/api";
import { import {
Recipient,
DmChannelDTO,
Channel, Channel,
DmChannelCreateSchema, DmChannelCreateSchema,
DmChannelDTO,
Recipient,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const recipients = await Recipient.find({ "/",
where: { user_id: req.user_id, closed: false }, route({
relations: ["channel", "channel.recipients"], responses: {
}); 200: {
res.json( body: "APIDMChannelArray",
await Promise.all( },
recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])), },
), }),
); async (req: Request, res: Response) => {
}); const recipients = await Recipient.find({
where: { user_id: req.user_id, closed: false },
relations: ["channel", "channel.recipients"],
});
res.json(
await Promise.all(
recipients.map((r) =>
DmChannelDTO.from(r.channel, [req.user_id]),
),
),
);
},
);
router.post( router.post(
"/", "/",
route({ body: "DmChannelCreateSchema" }), route({
requestBody: "DmChannelCreateSchema",
responses: {
200: {
body: "DmChannelDTO",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as DmChannelCreateSchema; const body = req.body as DmChannelCreateSchema;
res.json( res.json(

View File

@ -29,7 +29,7 @@ const router = Router();
// TODO: connection update schema // TODO: connection update schema
router.patch( router.patch(
"/", "/",
route({ body: "ConnectionUpdateSchema" }), route({ requestBody: "ConnectionUpdateSchema" }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { connection_name, connection_id } = req.params; const { connection_name, connection_id } = req.params;
const body = req.body as ConnectionUpdateSchema; const body = req.body as ConnectionUpdateSchema;

View File

@ -16,41 +16,58 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { Member, User } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Member, User } from "@spacebar/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const user = await User.findOneOrFail({ "/",
where: { id: req.user_id }, route({
select: ["data"], responses: {
}); //User object 204: {},
let correctpass = true; 401: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["data"],
}); //User object
let correctpass = true;
if (user.data.hash) { if (user.data.hash) {
// guest accounts can delete accounts without password // guest accounts can delete accounts without password
correctpass = await bcrypt.compare(req.body.password, user.data.hash); correctpass = await bcrypt.compare(
if (!correctpass) { req.body.password,
throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); user.data.hash,
);
if (!correctpass) {
throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
}
} }
}
// TODO: decrement guild member count // TODO: decrement guild member count
if (correctpass) { if (correctpass) {
await Promise.all([ await Promise.all([
User.delete({ id: req.user_id }), User.delete({ id: req.user_id }),
Member.delete({ id: req.user_id }), Member.delete({ id: req.user_id }),
]); ]);
res.sendStatus(204); res.sendStatus(204);
} else { } else {
res.sendStatus(401); res.sendStatus(401);
} }
}); },
);
export default router; export default router;

View File

@ -16,35 +16,52 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { User } from "@spacebar/util";
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User } from "@spacebar/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const user = await User.findOneOrFail({ "/",
where: { id: req.user_id }, route({
select: ["data"], responses: {
}); //User object 204: {},
let correctpass = true; 400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["data"],
}); //User object
let correctpass = true;
if (user.data.hash) { if (user.data.hash) {
// guest accounts can delete accounts without password // guest accounts can delete accounts without password
correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/ correctpass = await bcrypt.compare(
} req.body.password,
user.data.hash,
); //Not sure if user typed right password :/
}
if (correctpass) { if (correctpass) {
await User.update({ id: req.user_id }, { disabled: true }); await User.update({ id: req.user_id }, { disabled: true });
res.sendStatus(204); res.sendStatus(204);
} else { } else {
res.status(400).json({ res.status(400).json({
message: "Password does not match", message: "Password does not match",
code: 50018, code: 50018,
}); });
} }
}); },
);
export default router; export default router;

View File

@ -16,79 +16,106 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { route } from "@spacebar/api";
import { import {
Config,
Guild, Guild,
Member,
User,
GuildDeleteEvent, GuildDeleteEvent,
GuildMemberRemoveEvent, GuildMemberRemoveEvent,
Member,
User,
emitEvent, emitEvent,
Config,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const members = await Member.find({ "/",
relations: ["guild"], route({
where: { id: req.user_id }, responses: {
}); 200: {
body: "APIGuildArray",
},
},
}),
async (req: Request, res: Response) => {
const members = await Member.find({
relations: ["guild"],
where: { id: req.user_id },
});
let guild = members.map((x) => x.guild); let guild = members.map((x) => x.guild);
if ("with_counts" in req.query && req.query.with_counts == "true") { if ("with_counts" in req.query && req.query.with_counts == "true") {
guild = []; // TODO: Load guilds with user role permissions number guild = []; // TODO: Load guilds with user role permissions number
} }
res.json(guild); res.json(guild);
}); },
);
// user send to leave a certain guild // user send to leave a certain guild
router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { router.delete(
const { autoJoin } = Config.get().guild; "/:guild_id",
const { guild_id } = req.params; route({
const guild = await Guild.findOneOrFail({ responses: {
where: { id: guild_id }, 204: {},
select: ["owner_id"], 400: {
}); body: "APIErrorResponse",
},
if (!guild) throw new HTTPError("Guild doesn't exist", 404); 404: {
if (guild.owner_id === req.user_id) body: "APIErrorResponse",
throw new HTTPError("You can't leave your own guild", 400);
if (
autoJoin.enabled &&
autoJoin.guilds.includes(guild_id) &&
!autoJoin.canLeave
) {
throw new HTTPError("You can't leave instance auto join guilds", 400);
}
await Promise.all([
Member.delete({ id: req.user_id, guild_id: guild_id }),
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
}, },
user_id: req.user_id,
} as GuildDeleteEvent),
]);
const user = await User.getPublicUser(req.user_id);
await emitEvent({
event: "GUILD_MEMBER_REMOVE",
data: {
guild_id: guild_id,
user: user,
}, },
guild_id: guild_id, }),
} as GuildMemberRemoveEvent); async (req: Request, res: Response) => {
const { autoJoin } = Config.get().guild;
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: ["owner_id"],
});
return res.sendStatus(204); if (!guild) throw new HTTPError("Guild doesn't exist", 404);
}); if (guild.owner_id === req.user_id)
throw new HTTPError("You can't leave your own guild", 400);
if (
autoJoin.enabled &&
autoJoin.guilds.includes(guild_id) &&
!autoJoin.canLeave
) {
throw new HTTPError(
"You can't leave instance auto join guilds",
400,
);
}
await Promise.all([
Member.delete({ id: req.user_id, guild_id: guild_id }),
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
},
user_id: req.user_id,
} as GuildDeleteEvent),
]);
const user = await User.getPublicUser(req.user_id);
await emitEvent({
event: "GUILD_MEMBER_REMOVE",
data: {
guild_id: guild_id,
user: user,
},
guild_id: guild_id,
} as GuildMemberRemoveEvent);
return res.sendStatus(204);
},
);
export default router; export default router;

View File

@ -16,29 +16,49 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express"; import { route } from "@spacebar/api";
import { import {
Channel, Channel,
Member, Member,
OrmUtils, OrmUtils,
UserGuildSettingsSchema, UserGuildSettingsSchema,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
// GET doesn't exist on discord.com // GET doesn't exist on discord.com
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const user = await Member.findOneOrFail({ "/",
where: { id: req.user_id, guild_id: req.params.guild_id }, route({
select: ["settings"], responses: {
}); 200: {},
return res.json(user.settings); 404: {},
}); },
}),
async (req: Request, res: Response) => {
const user = await Member.findOneOrFail({
where: { id: req.user_id, guild_id: req.params.guild_id },
select: ["settings"],
});
return res.json(user.settings);
},
);
router.patch( router.patch(
"/", "/",
route({ body: "UserGuildSettingsSchema" }), route({
requestBody: "UserGuildSettingsSchema",
responses: {
200: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as UserGuildSettingsSchema; const body = req.body as UserGuildSettingsSchema;

View File

@ -16,36 +16,59 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express"; import { route } from "@spacebar/api";
import { import {
User,
PrivateUserProjection,
emitEvent,
UserUpdateEvent,
handleFile,
FieldErrors,
adjustEmail, adjustEmail,
Config, Config,
UserModifySchema, emitEvent,
FieldErrors,
generateToken, generateToken,
handleFile,
PrivateUserProjection,
User,
UserModifySchema,
UserUpdateEvent,
} from "@spacebar/util"; } from "@spacebar/util";
import { route } from "@spacebar/api";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
res.json( "/",
await User.findOne({ route({
select: PrivateUserProjection, responses: {
where: { id: req.user_id }, 200: {
}), body: "APIPrivateUser",
); },
}); },
}),
async (req: Request, res: Response) => {
res.json(
await User.findOne({
select: PrivateUserProjection,
where: { id: req.user_id },
}),
);
},
);
router.patch( router.patch(
"/", "/",
route({ body: "UserModifySchema" }), route({
requestBody: "UserModifySchema",
responses: {
200: {
body: "UserUpdateResponse",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as UserModifySchema; const body = req.body as UserModifySchema;

View File

@ -16,21 +16,34 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
BackupCode, BackupCode,
generateMfaBackupCodes,
User,
CodesVerificationSchema, CodesVerificationSchema,
DiscordApiErrors, DiscordApiErrors,
User,
generateMfaBackupCodes,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post( router.post(
"/", "/",
route({ body: "CodesVerificationSchema" }), route({
requestBody: "CodesVerificationSchema",
responses: {
200: {
body: "APIBackupCodeArray",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// const { key, nonce, regenerate } = req.body as CodesVerificationSchema; // const { key, nonce, regenerate } = req.body as CodesVerificationSchema;
const { regenerate } = req.body as CodesVerificationSchema; const { regenerate } = req.body as CodesVerificationSchema;

Some files were not shown because too many files have changed in this diff Show More