diff --git a/bun.lockb b/bun.lockb index c94c7c9..840c710 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 08e62eb..8a80fc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "legica-dana", - "version": "0.7.0", + "version": "0.7.5", "main": "src/app.ts", "scripts": { "start": "bun src/app.ts" @@ -17,6 +17,7 @@ "dotenv": "^8.2.0", "express": "^4.18.2", "express-basic-auth": "^1.2.1", + "redoc-express": "^2.1.0", "typescript": "^4.1.5" }, "devDependencies": { diff --git a/src/app.ts b/src/app.ts index 008d177..191ba59 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,9 +3,10 @@ import { Chat } from "@common"; import { Controller } from "@core"; import { ClientController } from "@controllers"; import express from "express"; -import { config } from "@constants"; -import basicAuth from "express-basic-auth"; +import { APP_VERSION, config } from "@constants"; import bodyParser from "body-parser"; +import redoc from "redoc-express"; +import path from "path"; const client: Client = new Client(); const chat: Chat = new Chat(client); @@ -13,16 +14,47 @@ const app = express(); app.use(bodyParser.json()); -app.use( - basicAuth({ - users: { - admin: config.PASSWORD, +app.get("/docs/swagger.json", (req, res) => { + res.sendFile("swagger.json", { root: path.join(__dirname, "..") }); +}); +app.get( + "/docs", + redoc({ + title: "API Docs", + specUrl: "/docs/swagger.json", + nonce: "", + redocOptions: { + theme: { + colors: { + primary: { + main: "#6EC5AB", + }, + }, + typography: { + fontFamily: `"museo-sans", 'Helvetica Neue', Helvetica, Arial, sans-serif`, + fontSize: "15px", + lineHeight: "1.5", + code: { + code: "#87E8C7", + backgroundColor: "#4D4D4E", + }, + }, + menu: { + backgroundColor: "#ffffff", + }, + }, }, }) ); -const controllers = new Controller([new ClientController(client, app)]); +app.get("version", (_, res) => { + res.send(APP_VERSION); +}); + +const controllers = new Controller(app, [new ClientController(client)]); controllers.register(); chat.register(config.TOKEN || ""); -app.listen(config.PORT); +app.listen(config.PORT, () => + console.log(`Legica bot API listening on port ${config.PORT}!`) +); diff --git a/src/controllers/Client.controller.ts b/src/controllers/Client.controller.ts index f6eb70d..06c8e35 100644 --- a/src/controllers/Client.controller.ts +++ b/src/controllers/Client.controller.ts @@ -2,13 +2,15 @@ import { Client, MessageEmbed, TextChannel } from "discord.js"; import * as cron from "cron"; import axios from "axios"; import cheerio from "cheerio"; -import { Express } from "express"; +import { Router } from "express"; import { IController, Legica } from "@models"; -import { APP_VERSION, config } from "@constants"; +import { config } from "@constants"; +import basicAuth from "express-basic-auth"; class ClientController implements IController { private legicaTask: cron.CronJob | null = null; - constructor(private client: Client, private app: Express) {} + public path: string = "task"; + constructor(private client: Client) {} public register = (): void => { this.client.on("ready", (): void => { @@ -20,24 +22,30 @@ class ClientController implements IController { "utc" ); }); + }; - this.app.get("", (_, res) => { + public registerRouter = (): Router => { + const router = Router(); + + router.use( + basicAuth({ + users: { + admin: config.PASSWORD, + }, + }) + ); + router.get("", (_, res) => { res.send(this.legicaTask?.running); }); - this.app.get("/next", (_, res) => { + router.get("next", (_, res) => { if (!this.legicaTask?.running) { res.status(400).send("Task is not running."); } else { res.send(this.legicaTask.nextDate().toISO()); } }); - - this.app.get("/version", (_, res) => { - res.send(APP_VERSION); - }); - - this.app.post("/start", (_, res) => { + router.post("", (_, res) => { if (this.legicaTask?.running) { res.status(400).send("Task already running."); } else { @@ -45,8 +53,7 @@ class ClientController implements IController { res.send("Task started."); } }); - - this.app.post("/stop", (_, res) => { + router.delete("", (_, res) => { if (!this.legicaTask?.running) { res.status(400).send("Task already stopped."); } else { @@ -55,7 +62,7 @@ class ClientController implements IController { } }); - this.app.post("/post-next", async (_, res) => { + router.post("send-latest", async (_, res) => { try { await this.sendNextMessage(); res.send(true); @@ -64,7 +71,7 @@ class ClientController implements IController { } }); - this.app.post("/post", async (req, res) => { + router.post("send", async (req, res) => { try { const url = req.body.url; await this.sendMessage(url); @@ -73,6 +80,7 @@ class ClientController implements IController { res.status(400).send(err); } }); + return router; }; private sendNextMessage = async (): Promise => { diff --git a/src/core/Controller.ts b/src/core/Controller.ts index 6a7e088..b3a7089 100644 --- a/src/core/Controller.ts +++ b/src/core/Controller.ts @@ -1,11 +1,13 @@ import { IController } from "models"; +import { Express } from "express"; class Controller { - constructor(private controllers: IController[]) {} + constructor(private app: Express, private controllers: IController[]) {} public register = (): void => { this.controllers?.forEach((controller) => { controller.register(); + this.app.use(controller.path || "", controller.registerRouter()); }); }; } diff --git a/src/models/Controller.ts b/src/models/Controller.ts index b39f569..3816a37 100644 --- a/src/models/Controller.ts +++ b/src/models/Controller.ts @@ -1,3 +1,7 @@ +import { Router } from "express"; + export interface IController { register(): void; + registerRouter(): Router; + path: string; } diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..e28a439 --- /dev/null +++ b/swagger.json @@ -0,0 +1,154 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Legica Bot API", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "0.7.0" + }, + "tags": [ + { + "name": "api", + "description": "API information" + }, + { + "name": "task", + "description": "Everything about the task" + } + ], + "paths": { + "/version": { + "get": { + "tags": [ + "api" + ], + "summary": "Display current API version.", + "description": "Displays the current API version defined in package.json.", + "responses": { + "200": { + "description": "Successful operation" + } + } + } + }, + "/task": { + "get": { + "tags": [ + "task" + ], + "summary": "Check if task is running.", + "description": "Retrieve the current state of scheduled task.", + "responses": { + "200": { + "description": "Successful operation" + } + } + }, + "post": { + "tags": [ + "task" + ], + "summary": "Start task if it is not running.", + "description": "Starts the task if it is not currently running.", + "responses": { + "200": { + "description": "Task started." + }, + "400": { + "description": "Task already running." + } + } + }, + "delete": { + "tags": [ + "task" + ], + "summary": "Stop task if it is running.", + "description": "Stops the task if it is currently running.", + "responses": { + "200": { + "description": "Task stopped." + }, + "400": { + "description": "Task already stopped." + } + } + } + }, + "/task/next": { + "get": { + "tags": [ + "task" + ], + "summary": "Check when the task is scheduled due next.", + "description": "Retrieve the datetime when task is scheduled to execute.", + "responses": { + "200": { + "description": "Next datetime" + }, + "400": { + "description": "Task is not running." + } + } + } + }, + "/task/send-latest": { + "post": { + "tags": [ + "task" + ], + "summary": "Send latest post of legica dana.", + "description": "Sends latest post of legica dana to all discord channels.", + "responses": { + "200": { + "description": "Confirmation." + } + } + } + }, + "/task/send": { + "post": { + "tags": [ + "task" + ], + "summary": "Send post of legica dana.", + "description": "Sends provided post of legica dana to all discord channels.", + "requestBody": { + "description": "URL", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Legica" + } + } + } + }, + "responses": { + "200": { + "description": "Confirmation." + } + } + } + } + }, + "components": { + "schemas": { + "Legica": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "string", + "example": "https://sib.net.hr/legica-dana/4390659/legica-dana-2992023/" + } + }, + "xml": { + "name": "order" + } + } + } + } + } \ No newline at end of file