Rewrite getRouteDescriptions, fix message route not appearing in openapi spec

This commit is contained in:
Madeline 2023-04-16 01:51:52 +10:00
parent a263ebb1e5
commit b438f2b071
No known key found for this signature in database
GPG Key ID: 1958E017C36F2E47
14 changed files with 247 additions and 120 deletions

View File

@ -14646,6 +14646,147 @@
] ]
} }
}, },
"/channels/{channel_id}/messages/": {
"get": {
"security": [
{
"bearer": []
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIMessageArray"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"403": {
"description": "No description available"
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "channel_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "channel_id"
},
{
"name": "around",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"type": "number"
},
"description": "max number of messages to return (1-100). defaults to 50"
}
],
"tags": [
"channels"
]
},
"post": {
"x-right-required": "SEND_MESSAGES",
"x-permission-required": "SEND_MESSAGES",
"security": [
{
"bearer": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageCreateSchema"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Message"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"403": {
"description": "No description available"
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "channel_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "channel_id"
}
],
"tags": [
"channels"
]
}
},
"/channels/{channel_id}/messages/bulk-delete/": { "/channels/{channel_id}/messages/bulk-delete/": {
"post": { "post": {
"security": [ "security": [

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

@ -25,10 +25,10 @@ import {
PublicInviteRelation, PublicInviteRelation,
User, User,
emitEvent, emitEvent,
isTextChannel,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { isTextChannel } from "./messages";
const router: Router = Router(); const router: Router = Router();

View File

@ -35,6 +35,7 @@ import {
User, User,
emitEvent, emitEvent,
getPermission, getPermission,
isTextChannel,
uploadFile, uploadFile,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
@ -45,32 +46,6 @@ 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( router.get(
@ -407,3 +382,5 @@ router.post(
return res.json(message); return res.json(message);
}, },
); );
export default router;

View File

@ -25,11 +25,11 @@ import {
emitEvent, emitEvent,
getPermission, getPermission,
getRights, getRights,
isTextChannel,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { Between, FindManyOptions, FindOperator, Not } from "typeorm"; import { Between, FindManyOptions, FindOperator, Not } from "typeorm";
import { isTextChannel } from "./messages";
const router: Router = Router(); const router: Router = Router();

View File

@ -27,11 +27,11 @@ import {
WebhookType, WebhookType,
handleFile, handleFile,
trimSpecial, trimSpecial,
isTextChannel,
} from "@spacebar/util"; } from "@spacebar/util";
import crypto from "crypto"; import crypto from "crypto";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { isTextChannel } from "./messages/index";
const router: Router = Router(); const router: Router = Router();

View File

@ -17,11 +17,10 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { TenorMediaTypes } from "@spacebar/util"; import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
import { Request, Response, Router } from "express"; 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 { getGifApiKey, parseGifResult } from "./trending";
const router = Router(); const router = Router();

View File

@ -17,11 +17,10 @@
*/ */
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { TenorMediaTypes } from "@spacebar/util"; import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
import { Request, Response, Router } from "express"; 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 { getGifApiKey, parseGifResult } from "./trending";
const router = Router(); const router = Router();

View File

@ -18,40 +18,17 @@
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Config,
TenorCategoriesResults, TenorCategoriesResults,
TenorGif,
TenorTrendingResults, TenorTrendingResults,
getGifApiKey,
parseGifResult,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from "proxy-agent"; import ProxyAgent from "proxy-agent";
const router = Router(); const router = Router();
export function parseGifResult(result: TenorGif) {
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( router.get(
"/", "/",
route({ route({

View File

@ -23,7 +23,7 @@ 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,

View File

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

View File

@ -482,3 +482,27 @@ export enum ChannelPermissionOverwriteType {
member = 1, member = 1,
group = 2, group = 2,
} }
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);
}
}

25
src/util/util/Gifs.ts Normal file
View File

@ -0,0 +1,25 @@
import { HTTPError } from "lambert-server";
import { Config } from "./Config";
import { TenorGif } from "..";
export function parseGifResult(result: TenorGif) {
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;
}

View File

@ -41,3 +41,4 @@ export * from "./String";
export * from "./Token"; export * from "./Token";
export * from "./TraverseDirectory"; export * from "./TraverseDirectory";
export * from "./WebAuthn"; export * from "./WebAuthn";
export * from "./Gifs";