restructure and expressjs endpoints

This commit is contained in:
Fran Jurmanović
2023-10-02 21:33:22 +02:00
parent 9a9a4f2ced
commit c07e743480
26 changed files with 232 additions and 181 deletions

28
src/app.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Client } from "discord.js";
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 bodyParser from "body-parser";
const client: Client = new Client();
const chat: Chat = new Chat(client);
const app = express();
app.use(bodyParser.json());
app.use(
basicAuth({
users: {
admin: config.PASSWORD,
},
})
);
const controllers = new Controller([new ClientController(client, app)]);
controllers.register();
chat.register(config.TOKEN || "");
app.listen(config.PORT);

48
src/common/chat.ts Normal file
View File

@@ -0,0 +1,48 @@
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,
},
];
};
}

1
src/common/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as Chat } from "./chat";

11
src/constants/config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { config as dotenv } from "dotenv";
dotenv();
const config: NodeJS.ProcessEnv = {
TOKEN: process.env.TOKEN,
PASSWORD: process.env.PASSWORD,
PORT: process.env.PORT || "3000",
CRON_LEGICA: process.env.CRON_LEGICA || "0 9 * * *",
};
export { config };

2
src/constants/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./version";
export * from "./config";

3
src/constants/version.ts Normal file
View File

@@ -0,0 +1,3 @@
import { version } from "../../package.json";
export const APP_VERSION = version;

View File

@@ -0,0 +1,137 @@
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 { IController, Legica } from "@models";
import { config } from "@constants";
class ClientController implements IController {
private legicaTask: cron.CronJob | null = null;
constructor(private client: Client, private app: Express) {}
public register = (): void => {
this.client.on("ready", (): void => {
this.legicaTask = new cron.CronJob(
config.CRON_LEGICA,
this.sendNextMessage,
null,
true,
"utc"
);
});
this.app.get("", (_, res) => {
res.send(this.legicaTask?.running);
});
this.app.post("/start", (_, res) => {
if (this.legicaTask?.running) {
res.status(400).send("Task already running.");
} else {
this.legicaTask?.start();
res.send("Task started.");
}
});
this.app.post("/stop", (_, res) => {
if (!this.legicaTask?.running) {
res.status(400).send("Task already stopped.");
} else {
this.legicaTask.stop();
res.send("Task stopped.");
}
});
this.app.get("/next", (_, res) => {
if (!this.legicaTask?.running) {
res.status(400).send("Task is not running.");
} else {
res.send(this.legicaTask.nextDate().toISO());
}
});
this.app.post("/post-next", async (_, res) => {
try {
await this.sendNextMessage();
res.send(true);
} catch (err) {
res.status(400).send(err);
}
});
this.app.post("/post", async (req, res) => {
try {
const url = req.body.url;
await this.sendMessage(url);
res.send(true);
} catch (err) {
res.status(400).send(err);
}
});
};
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
src/controllers/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as ClientController } from "./Client.controller";

13
src/core/Controller.ts Normal file
View File

@@ -0,0 +1,13 @@
import { IController } from "models";
class Controller {
constructor(private controllers: IController[]) {}
public register = (): void => {
this.controllers?.forEach((controller) => {
controller.register();
});
};
}
export default Controller;

1
src/core/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as Controller } from "./Controller";

8
src/models/Command.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Message } from "discord.js";
export type CommandFunction = (message: Message, args?: string[]) => void;
export interface ICommand {
callback: CommandFunction;
name: string;
}

3
src/models/Controller.ts Normal file
View File

@@ -0,0 +1,3 @@
export interface IController {
register(): void;
}

4
src/models/Legica.ts Normal file
View File

@@ -0,0 +1,4 @@
export type Legica = {
img: string;
title: string;
};

3
src/models/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from "./Controller";
export * from "./Command";
export * from "./Legica";

View File

@@ -0,0 +1,11 @@
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
src/modules/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as CommonModule } from "./Common.module";