From b438f2b071dbaa82371016168ab843f24be5063e Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Sun, 16 Apr 2023 01:51:52 +1000 Subject: [PATCH] Rewrite getRouteDescriptions, fix message route not appearing in openapi spec --- assets/openapi.json | 141 ++++++++++++++++++ scripts/util/getRouteDescriptions.js | 104 ++++++------- .../routes/channels/#channel_id/invites.ts | 2 +- .../channels/#channel_id/messages/index.ts | 29 +--- src/api/routes/channels/#channel_id/purge.ts | 2 +- .../routes/channels/#channel_id/webhooks.ts | 2 +- src/api/routes/gifs/search.ts | 3 +- src/api/routes/gifs/trending-gifs.ts | 3 +- src/api/routes/gifs/trending.ts | 27 +--- src/api/routes/guilds/#guild_id/prune.ts | 2 +- src/api/routes/guilds/#guild_id/stickers.ts | 2 +- src/util/entities/Channel.ts | 24 +++ src/util/util/Gifs.ts | 25 ++++ src/util/util/index.ts | 1 + 14 files changed, 247 insertions(+), 120 deletions(-) create mode 100644 src/util/util/Gifs.ts diff --git a/assets/openapi.json b/assets/openapi.json index 567d4bb8..8c1920f9 100644 --- a/assets/openapi.json +++ b/assets/openapi.json @@ -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/": { "post": { "security": [ diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js index fe36c238..a79dac96 100644 --- a/scripts/util/getRouteDescriptions.js +++ b/scripts/util/getRouteDescriptions.js @@ -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. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + For some reason, if a route exports multiple functions, it won't be registered here! + If someone could fix that I'd really appreciate it, but for now just, don't do that :p */ -const { traverseDirectory } = require("lambert-server"); -const path = require("path"); -const express = require("express"); -const RouteUtility = require("../../dist/api/util/handlers/route.js"); -const Router = express.Router; - -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`, +const proxy = (file, method, prefix, path, ...args) => { + const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true); + if (!opts) + return console.error( + `${file} has route without route() description middleware`, ); - } -} -function routeOptions(opts) { - return opts; -} + console.log(prefix + path + " - " + method); + 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) => { - const path = currentPath; - const file = currentFile; - const router = Router(opts); - - for (const method of methods) { - router[method] = registerPath.bind(null, file, method, path); - } - - return router; +RouteUtility.route = (opts) => { + const func = function () { + return opts; + }; + func.prototype.OPTS_MARKER = true; + return func; }; module.exports = function getRouteDescriptions() { const root = path.join(__dirname, "..", "..", "dist", "api", "routes", "/"); traverseDirectory({ dirname: root, recursive: true }, (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 - path = path.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes - if (path.endsWith("/index")) path = path.slice(0, "/index".length * -1); // delete index from path - currentPath = path; + + currentPath = file.replace(root.slice(0, -1), ""); + currentPath = currentPath.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path + currentPath = currentPath.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes + if (currentPath.endsWith("/index")) + currentPath = currentPath.slice(0, "/index".length * -1); // delete index from path try { require(file); - } catch (error) { - console.error("error loading file " + file, error); + } catch (e) { + console.error(e); } }); + return routes; }; diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index f608cca2..b02f65d3 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -25,10 +25,10 @@ import { PublicInviteRelation, User, emitEvent, + isTextChannel, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { isTextChannel } from "./messages"; const router: Router = Router(); diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 811d2b4c..f031fa75 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -35,6 +35,7 @@ import { User, emitEvent, getPermission, + isTextChannel, uploadFile, } from "@spacebar/util"; import { Request, Response, Router } from "express"; @@ -45,32 +46,6 @@ import { URL } from "url"; 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 // get messages router.get( @@ -407,3 +382,5 @@ router.post( return res.json(message); }, ); + +export default router; diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts index cbd46bd0..012fec1c 100644 --- a/src/api/routes/channels/#channel_id/purge.ts +++ b/src/api/routes/channels/#channel_id/purge.ts @@ -25,11 +25,11 @@ import { emitEvent, getPermission, getRights, + isTextChannel, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { Between, FindManyOptions, FindOperator, Not } from "typeorm"; -import { isTextChannel } from "./messages"; const router: Router = Router(); diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 6b81298f..d54756a1 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -27,11 +27,11 @@ import { WebhookType, handleFile, trimSpecial, + isTextChannel, } from "@spacebar/util"; import crypto from "crypto"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { isTextChannel } from "./messages/index"; const router: Router = Router(); diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts index b51bba37..f125a463 100644 --- a/src/api/routes/gifs/search.ts +++ b/src/api/routes/gifs/search.ts @@ -17,11 +17,10 @@ */ import { route } from "@spacebar/api"; -import { TenorMediaTypes } from "@spacebar/util"; +import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util"; import { Request, Response, Router } from "express"; import fetch from "node-fetch"; import ProxyAgent from "proxy-agent"; -import { getGifApiKey, parseGifResult } from "./trending"; const router = Router(); diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts index 899250cf..d6fa89ac 100644 --- a/src/api/routes/gifs/trending-gifs.ts +++ b/src/api/routes/gifs/trending-gifs.ts @@ -17,11 +17,10 @@ */ import { route } from "@spacebar/api"; -import { TenorMediaTypes } from "@spacebar/util"; +import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util"; import { Request, Response, Router } from "express"; import fetch from "node-fetch"; import ProxyAgent from "proxy-agent"; -import { getGifApiKey, parseGifResult } from "./trending"; const router = Router(); diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts index 3c2ab6ab..e3d6e974 100644 --- a/src/api/routes/gifs/trending.ts +++ b/src/api/routes/gifs/trending.ts @@ -18,40 +18,17 @@ import { route } from "@spacebar/api"; import { - Config, TenorCategoriesResults, - TenorGif, TenorTrendingResults, + getGifApiKey, + parseGifResult, } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; import fetch from "node-fetch"; import ProxyAgent from "proxy-agent"; 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( "/", route({ diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts index 92ea91fc..2c77340d 100644 --- a/src/api/routes/guilds/#guild_id/prune.ts +++ b/src/api/routes/guilds/#guild_id/prune.ts @@ -23,7 +23,7 @@ import { IsNull, LessThan } from "typeorm"; const router = Router(); //Returns all inactive members, respecting role hierarchy -export const inactiveMembers = async ( +const inactiveMembers = async ( guild_id: string, user_id: string, days: number, diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts index 2da9a21e..88f9a40e 100644 --- a/src/api/routes/guilds/#guild_id/stickers.ts +++ b/src/api/routes/guilds/#guild_id/stickers.ts @@ -105,7 +105,7 @@ router.post( }, ); -export function getStickerFormat(mime_type: string) { +function getStickerFormat(mime_type: string) { switch (mime_type) { case "image/apng": return StickerFormatType.APNG; diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 9ce04848..e23d93db 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -482,3 +482,27 @@ export enum ChannelPermissionOverwriteType { member = 1, 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); + } +} diff --git a/src/util/util/Gifs.ts b/src/util/util/Gifs.ts new file mode 100644 index 00000000..a5a5e64c --- /dev/null +++ b/src/util/util/Gifs.ts @@ -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; +} diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 838239b7..3a98be15 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -41,3 +41,4 @@ export * from "./String"; export * from "./Token"; export * from "./TraverseDirectory"; export * from "./WebAuthn"; +export * from "./Gifs";