From 82acbf93fce7cc7c9e1f2c873b50a13ac4195d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Wed, 4 Oct 2023 20:19:38 +0200 Subject: [PATCH] use elysia instead of express --- bun.lockb | Bin 96504 -> 89135 bytes package.json | 17 ++- src/app.ts | 166 +++++++++++++++++++-------- src/common/chat.ts | 48 -------- src/common/getFirstHtml.ts | 10 ++ src/common/getImgTitle.ts | 14 +++ src/common/index.ts | 4 +- src/common/sendDiscordMessage.ts | 48 ++++++++ src/constants/config.ts | 12 +- src/constants/index.ts | 1 - src/constants/version.ts | 3 - src/controllers/Client.controller.ts | 151 ------------------------ src/controllers/index.ts | 1 - src/core/Controller.ts | 15 --- src/core/basicAuth.ts | 65 +++++++++++ src/core/index.ts | 2 +- src/models/Controller.ts | 7 -- src/models/index.ts | 1 - src/modules/Common.module.ts | 11 -- src/modules/index.ts | 1 - swagger.json | 154 ------------------------- 21 files changed, 276 insertions(+), 455 deletions(-) delete mode 100644 src/common/chat.ts create mode 100644 src/common/getFirstHtml.ts create mode 100644 src/common/getImgTitle.ts create mode 100644 src/common/sendDiscordMessage.ts delete mode 100644 src/constants/version.ts delete mode 100644 src/controllers/Client.controller.ts delete mode 100644 src/controllers/index.ts delete mode 100644 src/core/Controller.ts create mode 100644 src/core/basicAuth.ts delete mode 100644 src/models/Controller.ts delete mode 100644 src/modules/Common.module.ts delete mode 100644 src/modules/index.ts delete mode 100644 swagger.json diff --git a/bun.lockb b/bun.lockb index 840c710910cd8ce30130eb06ceeae49a6c2d8b6c..a98a5541139335cf431cc4ceefa631617a28faed 100644 GIT binary patch delta 26285 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

J<{F??`g^dX0Ilk}r?{Tt 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 delta 30851 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 z z^YJY|Zb04_ncDLg!cEx!AH#2xh3~|DcMjU1*J#S}$Gl|DYQEDb$C|U!*BX2I+<_^t z7R}x6U@!{hydeU;JI95GN*z5%ORs!=9dvv839Hmpafx6;9uO)Ayc%cmKP zKiaLdcJ0Q`1vdy`5oF79KUy=lj}FQRTlT8d+g?5~VOqE2!OK(HUPIXc7>p1S%4%4z zGGFCT4RimXk;x|^>`htqKWR;>CYCpXBPu3nSkVVx<#9W9?1T5eNsd$smAsX$s1Om>jZW{`K!7MzKPv_OTtUw!7zE-O|mO`jqMflf|XYA-&X z-N?I?wXWSUV23w z7R#B}D|@~ab(K~Qyp0jpMp?&!j{~)rPaBvtZ+(}t;Xif}S`%bNIPfQoP@}yAf7b|} z=+QfsqyJBCb7DM5}GhCO`o8q+q_gNR#BmntWqp=xgf8cQwX5EFWXBHGIwwTE)(nLMz@hSiBwi zg(%@KpM;UU?PY*baH|z4LC+!>^EeAbcp}|!R6n&@{7M(8F4{-ksw%7lUoO$unGZ(^ zNpB5l-TOYJ5_^=&Uo^{)!>>Y-h3XYy?idkw{b=5c3wP+Ifz7JQdU&+;h&&} zy?la2h-1s>AyZpxArrw$E-CWm>zG2XKMZI=h}7kLz9~0Kxz>-bX7;7xLj3Ra<9nH* zN%?Gx70%t}zZ?6pj?kpwr2hW=6e=N~dhwvDlU zZrOC86sJ(m~N0&YiKfaq1^9m&~a;bx38}l_`pZ&7N;<{$o&Aao- zO3M$S1g)Yd@xC$N(F!u;qd8Vz-to{mvr%=Fz#Kz?CYvvstFiyX85sX);)7VxtM>9~ z9feyC97wF?g-3qjKWKzcX((@I`^nEVJEp^o_K}sRWxlX_Pl&_gz(RmI3}bV`EZF(&#JwVLtc@OpOA9! zoY@~a=E#u`st}YDrX%^p6*{^?PiI#kO9~+QEEM@r2_+|c;rj^TGwxjZI0~T-KH2f{ zxrZEjc9RdR_*6$enngb2;!}=%M$0n&2I07iexI`B<5}c$EAW#wc%Vs{#_2r{d=Mt4Jt49toR|H8syV!ay1-2@u>KTaT8Wmey9WY z74O*?pGrwGD@ccF_-K!0#$;r~mBQ+y4S(g!y zL;6rMBQY%t38^VKb1x+^GeH+8oOYF!>XxC4hpK5Aso6PhF>!IwECqFh|6_4{UdAAu z8=dTz7@MifLZPfw(kZo{a9Wp}AYX8cnm)&Ytwh|sc#p zvaetOGAr5x&GtHQwfPhuZV-R{YpyE45p7q45-3QzK(@ain(yv`a`_%yJfG~zC722y z%b<`z-8}hu?p$qC`VdQ}g(CkFr2HH>%AsORlQtDX9MS}Zes<%0`6{&$Od}(>T70M4 zV6%O>aJN4gQV{V6snFSvUl#($CcANtrowvuE~I|O^QJzWi;MJeI%UQRCo@w2xd&38r0C*Pvl3%47KLF`;}eh4pBU(cIHV3g zTAS0G#q>?h$P%o|NKL{R-_o)Uw_eGY*5nrOGwX9zc%yopJ8x81uJWY_!G)Ff);UrHuP;bkg3v!H-L z-F#_?)~;P%ni{ECn|SmyK7Hp48*+}0bQ-C&Hb^OQ+0SNZ9L}hd^3yY=n~DYaryV)x=AX^#XtrVr*nn>ld@3|8t!uQb zNfL#{gCtcG=59z3rf?sjql9?lyLoVq{MeRK_*L^rr!Y;=bo0jtzE~~fFzr)r@@G1b z85P{|Z+UKlgM5U^R??~BYEy1_N}OP@IV}8ay$ayH8e@o4FPVL2Liu!0S+zb}!k|yZ zHsB|3Okw4g@JVi*gQZu{XRgD9mcsXG#MKm diff --git a/package.json b/package.json index 7c40542..f1aac22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "legica-dana", - "version": "0.8.0", + "version": "2.0.0", "main": "src/app.ts", "scripts": { "start": "bun src/app.ts" @@ -8,25 +8,24 @@ "author": "Fran Jurmanović ", "license": "MIT", "dependencies": { - "@types/node": "^14.14.31", + "@elysiajs/static": "^0.7.1", + "@elysiajs/swagger": "^0.7.3", "axios": "^0.26.0", "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.10", "cron": "^3.0.0", "discord.js": "^12.5.1", "dotenv": "^8.2.0", - "express": "^4.18.2", - "express-basic-auth": "^1.2.1", - "redoc-express": "^2.1.0", + "elysia": "^0.7.15", + "minimatch": "^9.0.3", + "pino": "^8.15.4", "typescript": "^4.1.5" }, "devDependencies": { - "@types/express": "^4.17.18", - "@types/node-cron": "^3.0.1", - "@types/pg": "^7.14.10", - "@types/ws": "^7.4.0", + "@types/node": "^20.8.2", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", + "bun-types": "^1.0.4-canary.20231004T140131", "eslint": "^8.50.0", "prettier": "^2.2.1" } diff --git a/src/app.ts b/src/app.ts index 59befa7..ef9948b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,60 +1,126 @@ import { Client } from "discord.js"; -import { Chat } from "@common"; -import { Controller } from "@core"; -import { ClientController } from "@controllers"; -import express from "express"; -import { APP_VERSION, config } from "@constants"; -import bodyParser from "body-parser"; -import redoc from "redoc-express"; -import path from "path"; +import { config } from "@constants"; +import { CronJob } from "cron"; +import { sendDiscordMessage, sendNextMessage } from "@common"; +import { Elysia, t } from "elysia"; +import { swagger } from "@elysiajs/swagger"; +import { basicAuth } from "@core"; +import pino from "pino"; +import staticPlugin from "@elysiajs/static"; const client: Client = new Client(); -const chat: Chat = new Chat(client); -const app = express(); -app.use(bodyParser.json()); +const fileTransport = pino.transport({ + target: "pino/file", -app.get("/docs/swagger.json", (req, res) => { - res.sendFile("swagger.json", { root: path.join(__dirname, "..") }); + options: { destination: `app.log` }, }); -app.get( - "/docs", - redoc({ - title: "API Docs", - specUrl: "/docs/swagger.json", - nonce: "", - redocOptions: { - theme: { - colors: { - primary: { - main: "#6EC5AB", - }, +const logger = pino( + { + level: "error", + }, + fileTransport +); + +const taskPlugin = new Elysia({ prefix: "/job" }) + .state("job", null as CronJob | null) + .onStart(({ store }) => { + client.on("ready", (): void => { + if (store.job) { + store.job.stop(); + } + store.job = new CronJob( + config.CRON_LEGICA, + () => sendNextMessage(client), + null, + true, + "utc" + ); + }); + }) + .onBeforeHandle(({ store: { job }, set }) => { + if (!job) { + set.status = 400; + return "Job is not running."; + } + }) + .use( + basicAuth({ + users: [ + { + username: "admin", + password: config.PASSWORD, }, - typography: { - fontFamily: `"museo-sans", 'Helvetica Neue', Helvetica, Arial, sans-serif`, - fontSize: "15px", - lineHeight: "1.5", - code: { - code: "#87E8C7", - backgroundColor: "#4D4D4E", - }, - }, - menu: { - backgroundColor: "#ffffff", + ], + errorMessage: "Unauthorized", + }) + ) + .get("/", ({ store: { job } }) => ({ + running: job?.running ?? false, + next: job?.nextDate().toISO(), + })) + .post("/", ({ store: { job }, set }) => { + if (job?.running) { + set.status = 400; + return "Task already running"; + } + job?.start(); + return "Task started"; + }) + .delete("/", ({ store: { job }, set }) => { + if (!job?.running) { + set.status = 400; + return "Task already stopped"; + } + job?.stop(); + return "Task stopped"; + }) + .post( + "/send", + async ({ set, body }) => { + try { + const url = body.url; + if (url) { + await sendDiscordMessage(client, url); + } else { + await sendNextMessage(client); + } + return true; + } catch (err) { + set.status = 400; + return err; + } + }, + { + body: t.Object({ + url: t.String(), + }), + } + ) + .get("/log", () => Bun.file("app.log")); + +client.login(config.TOKEN); + +const app = new Elysia() + .onError(({ error }) => { + logger.error(error); + return new Response(error.toString()); + }) + .get("/", () => config.APP_VERSION) + .use( + swagger({ + documentation: { + info: { + title: "Legica Bot", + version: config.APP_VERSION, }, }, - }, - }) -); - -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, () => - console.log(`Legica bot API listening on port ${config.PORT}!`) + }) + ) + .use(staticPlugin()) + .use(taskPlugin) + .listen(config.PORT); + +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ); diff --git a/src/common/chat.ts b/src/common/chat.ts deleted file mode 100644 index 03190b8..0000000 --- a/src/common/chat.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CommandFunction, ICommand } from "@models"; -import type { Client, Message } from "discord.js"; - -export default class Chat { - private prefix: string = "!"; - constructor(private client: Client, private commands: ICommand[] = []) {} - - public registerPrefix = (prefix: string): void => { - this.prefix = prefix; - }; - - public register = (token: string): void => { - if (!this.commands) return; - this.client.on("message", (message: Message): void => { - this.commands.forEach((command) => { - if (message?.content === `${this.prefix}${command?.name}`) { - command?.callback?.(message); - } else if ( - message?.content?.split?.(/\s/g)?.[0] == `${this.prefix}${command?.name}` - ) { - const args = message?.content - ?.replace?.(`${this.prefix}${command?.name}`, "") - .trim?.() - ?.split?.(/\s(?=(?:[^'"`]*(['"`])[^'"`]*\1)*[^'"`]*$)/g) - .map((d) => { - if (d?.[0] == '"' && d?.[d?.length - 1] == '"') { - return d?.substr?.(1)?.slice?.(0, -1); - } - return d; - }) - .filter((d) => d); - command?.callback?.(message, args); - } - }); - }); - this.client.login(token); - }; - - public command = (name: string, callback: CommandFunction): void => { - this.commands = [ - ...this.commands, - { - name, - callback, - }, - ]; - }; -} diff --git a/src/common/getFirstHtml.ts b/src/common/getFirstHtml.ts new file mode 100644 index 0000000..764b150 --- /dev/null +++ b/src/common/getFirstHtml.ts @@ -0,0 +1,10 @@ +import axios from "axios"; +import cheerio from "cheerio"; + +export async function getFirstHtml(): Promise { + const response = await axios.get("https://sib.net.hr/legica-dana"); + const html = response.data; + const $ = cheerio.load(html); + const { href } = $(".News-link.c-def")?.attr() || {}; + return href; +} diff --git a/src/common/getImgTitle.ts b/src/common/getImgTitle.ts new file mode 100644 index 0000000..8166e2f --- /dev/null +++ b/src/common/getImgTitle.ts @@ -0,0 +1,14 @@ +import { Legica } from "@models"; +import axios from "axios"; +import cheerio from "cheerio"; + +export async function getImgTitle(href: string): Promise { + const response = await axios.get(href); + const html = response.data; + const $ = cheerio.load(html); + + const title = $(".Article-inner > h1").text(); + const { src: img } = $(".Article-media > img").attr() || {}; + + return { title, img }; +} diff --git a/src/common/index.ts b/src/common/index.ts index 24fe1c6..1139ec3 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1 +1,3 @@ -export { default as Chat } from "./chat"; +export { getFirstHtml } from "./getFirstHtml"; +export { getImgTitle } from "./getImgTitle"; +export { sendDiscordMessage, sendNextMessage } from "./sendDiscordMessage"; diff --git a/src/common/sendDiscordMessage.ts b/src/common/sendDiscordMessage.ts new file mode 100644 index 0000000..64ab73a --- /dev/null +++ b/src/common/sendDiscordMessage.ts @@ -0,0 +1,48 @@ +import { getFirstHtml, getImgTitle } from "@common"; +import { Client, MessageEmbed, TextChannel } from "discord.js"; + +export async function sendDiscordMessage( + client: Client, + url: string +): Promise { + if (!url) return; + const { img, title } = await getImgTitle(url); + + client.channels.cache.forEach(async (channel) => { + try { + if (channel.type !== "text") return null; + const embeddedMessage = new MessageEmbed().setTitle(title).setImage(img); + const msg = await (channel as TextChannel).send(embeddedMessage); + const reactions = [ + "1️⃣", + "2️⃣", + "3️⃣", + "4️⃣", + "5️⃣", + "6️⃣", + "7️⃣", + "8️⃣", + "9️⃣", + "🔟", + ]; + for (const reaction of reactions) { + try { + await msg.react(reaction); + } catch { + console.error(`Reaction ${reaction} to channel ${channel.id} failed.`); + } + } + } catch { + console.error(`Message to channel ${channel.id} failed.`); + } + }); +} + +export async function sendNextMessage(client: Client): Promise { + try { + const href = await getFirstHtml(); + await sendDiscordMessage(client, href); + } catch (err) { + console.error(err); + } +} diff --git a/src/constants/config.ts b/src/constants/config.ts index c70e8b5..93130aa 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -1,11 +1,21 @@ import { config as dotenv } from "dotenv"; +import { version } from "../../package.json"; dotenv(); -const config: NodeJS.ProcessEnv = { +type Config = { + APP_VERSION: string; + LEGICA_URL: string; +}; + +export type ProjectConfig = Config & NodeJS.ProcessEnv; + +const config: ProjectConfig = { TOKEN: process.env.TOKEN, PASSWORD: process.env.PASSWORD, PORT: process.env.PORT || "3000", CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *", + APP_VERSION: version, + LEGICA_URL: "https://sib.net.hr/legica-dana", }; export { config }; diff --git a/src/constants/index.ts b/src/constants/index.ts index b56936a..5c62e04 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,2 +1 @@ -export * from "./version"; export * from "./config"; diff --git a/src/constants/version.ts b/src/constants/version.ts deleted file mode 100644 index a0cbd1f..0000000 --- a/src/constants/version.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { version } from "../../package.json"; - -export const APP_VERSION = version; diff --git a/src/controllers/Client.controller.ts b/src/controllers/Client.controller.ts deleted file mode 100644 index 7b9e6c3..0000000 --- a/src/controllers/Client.controller.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Client, MessageEmbed, TextChannel } from "discord.js"; -import * as cron from "cron"; -import axios from "axios"; -import cheerio from "cheerio"; -import { Router } from "express"; -import { IController, Legica } from "@models"; -import { config } from "@constants"; -import basicAuth from "express-basic-auth"; - -class ClientController implements IController { - private legicaTask: cron.CronJob | null = null; - public path: string = "/task"; - constructor(private client: Client) {} - - public register = (): void => { - this.client.on("ready", (): void => { - this.legicaTask = new cron.CronJob( - config.CRON_LEGICA, - this.sendNextMessage, - null, - true, - "utc" - ); - }); - }; - - public registerRouter = (): Router => { - const router = Router(); - - router.use( - basicAuth({ - users: { - admin: config.PASSWORD, - }, - }) - ); - router.get("/", (_, res) => { - res.send(this.legicaTask?.running); - }); - - router.post("/", (_, res) => { - if (this.legicaTask?.running) { - res.status(400).send("Task already running."); - } else { - this.legicaTask?.start(); - res.send("Task started."); - } - }); - - router.delete("/", (_, res) => { - if (!this.legicaTask?.running) { - res.status(400).send("Task already stopped."); - } else { - this.legicaTask.stop(); - res.send("Task stopped."); - } - }); - - router.get("/next", (_, res) => { - if (!this.legicaTask?.running) { - res.status(400).send("Task is not running."); - } else { - res.send(this.legicaTask.nextDate().toISO()); - } - }); - - router.post("/send-latest", async (_, res) => { - try { - await this.sendNextMessage(); - res.send(true); - } catch (err) { - res.status(400).send(err); - } - }); - - router.post("/send", async (req, res) => { - try { - const url = req.body.url; - await this.sendMessage(url); - res.send(true); - } catch (err) { - res.status(400).send(err); - } - }); - return router; - }; - - private sendNextMessage = async (): Promise => { - try { - const href = await getFirstHtml(); - await this.sendMessage(href); - } catch (err) { - console.error(err); - } - }; - - private sendMessage = async (url: string): Promise => { - if (!url) return; - const { img, title } = await getImgTitle(url); - - this.client.channels.cache.forEach(async (channel) => { - try { - if (channel.type !== "text") return null; - const embeddedMessage = new MessageEmbed().setTitle(title).setImage(img); - const msg = await (channel as TextChannel).send(embeddedMessage); - const reactions = [ - "1️⃣", - "2️⃣", - "3️⃣", - "4️⃣", - "5️⃣", - "6️⃣", - "7️⃣", - "8️⃣", - "9️⃣", - "🔟", - ]; - for (const reaction of reactions) { - try { - await msg.react(reaction); - } catch { - console.error(`Reaction ${reaction} to channel ${channel.id} failed.`); - } - } - } catch { - console.error(`Message to channel ${channel.id} failed.`); - } - }); - }; -} - -async function getImgTitle(href: string): Promise { - const response = await axios.get(href); - const html = response.data; - const $ = cheerio.load(html); - - const title = $(".Article-inner > h1").text(); - const { src: img } = $(".Article-media > img").attr() || {}; - - return { title, img }; -} - -async function getFirstHtml(): Promise { - const response = await axios.get("https://sib.net.hr/legica-dana"); - const html = response.data; - const $ = cheerio.load(html); - const { href } = $(".News-link.c-def")?.attr() || {}; - return href; -} - -export default ClientController; diff --git a/src/controllers/index.ts b/src/controllers/index.ts deleted file mode 100644 index 1aa324d..0000000 --- a/src/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ClientController } from "./Client.controller"; diff --git a/src/core/Controller.ts b/src/core/Controller.ts deleted file mode 100644 index b3a7089..0000000 --- a/src/core/Controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IController } from "models"; -import { Express } from "express"; - -class Controller { - constructor(private app: Express, private controllers: IController[]) {} - - public register = (): void => { - this.controllers?.forEach((controller) => { - controller.register(); - this.app.use(controller.path || "", controller.registerRouter()); - }); - }; -} - -export default Controller; diff --git a/src/core/basicAuth.ts b/src/core/basicAuth.ts new file mode 100644 index 0000000..3e34a63 --- /dev/null +++ b/src/core/basicAuth.ts @@ -0,0 +1,65 @@ +import Elysia from "elysia"; +import { minimatch } from "minimatch"; + +export class BasicAuthError extends Error { + constructor(public message: string) { + super(message); + } +} + +export interface BasicAuthUser { + username: string; + password: string; +} + +export interface BasicAuthConfig { + users: BasicAuthUser[]; + realm?: string; + errorMessage?: string; + exclude?: string[]; + noErrorThrown?: boolean; +} + +export const basicAuth = (config: BasicAuthConfig) => + new Elysia({ name: "basic-auth", seed: config }) + .error({ BASIC_AUTH_ERROR: BasicAuthError }) + .derive((ctx) => { + const authorization = ctx.headers?.authorization; + if (!authorization) return { basicAuth: { isAuthed: false, username: "" } }; + const [username, password] = atob(authorization.split(" ")[1]).split(":"); + const user = config.users.find( + (user) => user.username === username && user.password === password + ); + if (!user) return { basicAuth: { isAuthed: false, username: "" } }; + return { basicAuth: { isAuthed: true, username: user.username } }; + }) + .onTransform((ctx) => { + if ( + !ctx.basicAuth.isAuthed && + !config.noErrorThrown && + !isPathExcluded(ctx.path, config.exclude) && + ctx.request && + ctx.request.method !== "OPTIONS" + ) + throw new BasicAuthError(config.errorMessage ?? "Unauthorized"); + }) + .onError((ctx) => { + if (ctx.code === "BASIC_AUTH_ERROR") { + return new Response(ctx.error.message, { + status: 401, + headers: { + "WWW-Authenticate": `Basic${ + config.realm ? ` realm="${config.realm}"` : "" + }`, + }, + }); + } + }); + +export const isPathExcluded = (path: string, excludedPatterns?: string[]) => { + if (!excludedPatterns) return false; + for (const pattern of excludedPatterns) { + if (minimatch(path, pattern)) return true; + } + return false; +}; diff --git a/src/core/index.ts b/src/core/index.ts index 97e1555..06d786c 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1 +1 @@ -export { default as Controller } from "./Controller"; +export { basicAuth } from "./basicAuth"; diff --git a/src/models/Controller.ts b/src/models/Controller.ts deleted file mode 100644 index 3816a37..0000000 --- a/src/models/Controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Router } from "express"; - -export interface IController { - register(): void; - registerRouter(): Router; - path: string; -} diff --git a/src/models/index.ts b/src/models/index.ts index 6ee8930..78755f2 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,2 @@ -export * from "./Controller"; export * from "./Command"; export * from "./Legica"; diff --git a/src/modules/Common.module.ts b/src/modules/Common.module.ts deleted file mode 100644 index 9e7b1e2..0000000 --- a/src/modules/Common.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Message } from "discord.js"; -import { APP_VERSION } from "../constants"; - -class CommonModule { - constructor() {} - public showVersion = (message: Message): void => { - message?.channel?.send?.(`Current version of the Monke BOT is ${APP_VERSION}.`); - }; -} - -export default CommonModule; diff --git a/src/modules/index.ts b/src/modules/index.ts deleted file mode 100644 index 74083ce..0000000 --- a/src/modules/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CommonModule } from "./Common.module"; diff --git a/swagger.json b/swagger.json deleted file mode 100644 index 9514b2e..0000000 --- a/swagger.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "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.8.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