use elysia instead of express
This commit is contained in:
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "legica-dana",
|
"name": "legica-dana",
|
||||||
"version": "0.8.0",
|
"version": "2.0.0",
|
||||||
"main": "src/app.ts",
|
"main": "src/app.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun src/app.ts"
|
"start": "bun src/app.ts"
|
||||||
@@ -8,25 +8,24 @@
|
|||||||
"author": "Fran Jurmanović <fjurma12@outlook.com>",
|
"author": "Fran Jurmanović <fjurma12@outlook.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^14.14.31",
|
"@elysiajs/static": "^0.7.1",
|
||||||
|
"@elysiajs/swagger": "^0.7.3",
|
||||||
"axios": "^0.26.0",
|
"axios": "^0.26.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"cheerio": "^1.0.0-rc.10",
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"cron": "^3.0.0",
|
"cron": "^3.0.0",
|
||||||
"discord.js": "^12.5.1",
|
"discord.js": "^12.5.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.18.2",
|
"elysia": "^0.7.15",
|
||||||
"express-basic-auth": "^1.2.1",
|
"minimatch": "^9.0.3",
|
||||||
"redoc-express": "^2.1.0",
|
"pino": "^8.15.4",
|
||||||
"typescript": "^4.1.5"
|
"typescript": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.18",
|
"@types/node": "^20.8.2",
|
||||||
"@types/node-cron": "^3.0.1",
|
|
||||||
"@types/pg": "^7.14.10",
|
|
||||||
"@types/ws": "^7.4.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
|
"bun-types": "^1.0.4-canary.20231004T140131",
|
||||||
"eslint": "^8.50.0",
|
"eslint": "^8.50.0",
|
||||||
"prettier": "^2.2.1"
|
"prettier": "^2.2.1"
|
||||||
}
|
}
|
||||||
|
|||||||
160
src/app.ts
160
src/app.ts
@@ -1,60 +1,126 @@
|
|||||||
import { Client } from "discord.js";
|
import { Client } from "discord.js";
|
||||||
import { Chat } from "@common";
|
import { config } from "@constants";
|
||||||
import { Controller } from "@core";
|
import { CronJob } from "cron";
|
||||||
import { ClientController } from "@controllers";
|
import { sendDiscordMessage, sendNextMessage } from "@common";
|
||||||
import express from "express";
|
import { Elysia, t } from "elysia";
|
||||||
import { APP_VERSION, config } from "@constants";
|
import { swagger } from "@elysiajs/swagger";
|
||||||
import bodyParser from "body-parser";
|
import { basicAuth } from "@core";
|
||||||
import redoc from "redoc-express";
|
import pino from "pino";
|
||||||
import path from "path";
|
import staticPlugin from "@elysiajs/static";
|
||||||
|
|
||||||
const client: Client = new Client();
|
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) => {
|
options: { destination: `app.log` },
|
||||||
res.sendFile("swagger.json", { root: path.join(__dirname, "..") });
|
|
||||||
});
|
});
|
||||||
app.get(
|
const logger = pino(
|
||||||
"/docs",
|
{
|
||||||
redoc({
|
level: "error",
|
||||||
title: "API Docs",
|
|
||||||
specUrl: "/docs/swagger.json",
|
|
||||||
nonce: "",
|
|
||||||
redocOptions: {
|
|
||||||
theme: {
|
|
||||||
colors: {
|
|
||||||
primary: {
|
|
||||||
main: "#6EC5AB",
|
|
||||||
},
|
},
|
||||||
|
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`,
|
errorMessage: "Unauthorized",
|
||||||
fontSize: "15px",
|
})
|
||||||
lineHeight: "1.5",
|
)
|
||||||
code: {
|
.get("/", ({ store: { job } }) => ({
|
||||||
code: "#87E8C7",
|
running: job?.running ?? false,
|
||||||
backgroundColor: "#4D4D4E",
|
next: job?.nextDate().toISO(),
|
||||||
},
|
}))
|
||||||
},
|
.post("/", ({ store: { job }, set }) => {
|
||||||
menu: {
|
if (job?.running) {
|
||||||
backgroundColor: "#ffffff",
|
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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
.use(staticPlugin())
|
||||||
app.get("/version", (_, res) => {
|
.use(taskPlugin)
|
||||||
res.send(APP_VERSION);
|
.listen(config.PORT);
|
||||||
});
|
|
||||||
|
console.log(
|
||||||
const controllers = new Controller(app, [new ClientController(client)]);
|
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||||
|
|
||||||
controllers.register();
|
|
||||||
chat.register(config.TOKEN || "");
|
|
||||||
app.listen(config.PORT, () =>
|
|
||||||
console.log(`Legica bot API listening on port ${config.PORT}!`)
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
10
src/common/getFirstHtml.ts
Normal file
10
src/common/getFirstHtml.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import cheerio from "cheerio";
|
||||||
|
|
||||||
|
export async function getFirstHtml(): Promise<string> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
14
src/common/getImgTitle.ts
Normal file
14
src/common/getImgTitle.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Legica } from "@models";
|
||||||
|
import axios from "axios";
|
||||||
|
import cheerio from "cheerio";
|
||||||
|
|
||||||
|
export async function getImgTitle(href: string): Promise<Legica> {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
export { default as Chat } from "./chat";
|
export { getFirstHtml } from "./getFirstHtml";
|
||||||
|
export { getImgTitle } from "./getImgTitle";
|
||||||
|
export { sendDiscordMessage, sendNextMessage } from "./sendDiscordMessage";
|
||||||
|
|||||||
48
src/common/sendDiscordMessage.ts
Normal file
48
src/common/sendDiscordMessage.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
try {
|
||||||
|
const href = await getFirstHtml();
|
||||||
|
await sendDiscordMessage(client, href);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
import { config as dotenv } from "dotenv";
|
import { config as dotenv } from "dotenv";
|
||||||
|
import { version } from "../../package.json";
|
||||||
dotenv();
|
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,
|
TOKEN: process.env.TOKEN,
|
||||||
PASSWORD: process.env.PASSWORD,
|
PASSWORD: process.env.PASSWORD,
|
||||||
PORT: process.env.PORT || "3000",
|
PORT: process.env.PORT || "3000",
|
||||||
CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *",
|
CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *",
|
||||||
|
APP_VERSION: version,
|
||||||
|
LEGICA_URL: "https://sib.net.hr/legica-dana",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { config };
|
export { config };
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export * from "./version";
|
|
||||||
export * from "./config";
|
export * from "./config";
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { version } from "../../package.json";
|
|
||||||
|
|
||||||
export const APP_VERSION = version;
|
|
||||||
@@ -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<void> => {
|
|
||||||
try {
|
|
||||||
const href = await getFirstHtml();
|
|
||||||
await this.sendMessage(href);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private sendMessage = async (url: string): Promise<void> => {
|
|
||||||
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<Legica> {
|
|
||||||
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<string> {
|
|
||||||
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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as ClientController } from "./Client.controller";
|
|
||||||
@@ -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;
|
|
||||||
65
src/core/basicAuth.ts
Normal file
65
src/core/basicAuth.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -1 +1 @@
|
|||||||
export { default as Controller } from "./Controller";
|
export { basicAuth } from "./basicAuth";
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
|
|
||||||
export interface IController {
|
|
||||||
register(): void;
|
|
||||||
registerRouter(): Router;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from "./Controller";
|
|
||||||
export * from "./Command";
|
export * from "./Command";
|
||||||
export * from "./Legica";
|
export * from "./Legica";
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as CommonModule } from "./Common.module";
|
|
||||||
154
swagger.json
154
swagger.json
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user