From c07e7434806e386d92b0d510da465cec323ba80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Mon, 2 Oct 2023 21:33:22 +0200 Subject: [PATCH] restructure and expressjs endpoints --- app.ts | 15 --- common/getSettings.ts | 33 ------ common/index.ts | 2 - constants/environments.ts | 3 - constants/index.ts | 2 - constants/version.ts | 3 - controllers/Client.controller.ts | 59 ---------- controllers/Pule.controller.ts | 45 ------- core/Controller.ts | 14 --- process-env.d.ts | 12 ++ src/app.ts | 28 +++++ {common => src/common}/chat.ts | 11 +- src/common/index.ts | 1 + src/constants/config.ts | 11 ++ src/constants/index.ts | 2 + src/constants/version.ts | 3 + src/controllers/Client.controller.ts | 137 ++++++++++++++++++++++ {controllers => src/controllers}/index.ts | 1 - src/core/Controller.ts | 13 ++ {core => src/core}/index.ts | 0 src/models/Command.ts | 8 ++ src/models/Controller.ts | 3 + src/models/Legica.ts | 4 + src/models/index.ts | 3 + {modules => src/modules}/Common.module.ts | 0 {modules => src/modules}/index.ts | 0 26 files changed, 232 insertions(+), 181 deletions(-) delete mode 100644 app.ts delete mode 100644 common/getSettings.ts delete mode 100644 common/index.ts delete mode 100644 constants/environments.ts delete mode 100644 constants/index.ts delete mode 100644 constants/version.ts delete mode 100644 controllers/Client.controller.ts delete mode 100644 controllers/Pule.controller.ts delete mode 100644 core/Controller.ts create mode 100644 process-env.d.ts create mode 100644 src/app.ts rename {common => src/common}/chat.ts (74%) create mode 100644 src/common/index.ts create mode 100644 src/constants/config.ts create mode 100644 src/constants/index.ts create mode 100644 src/constants/version.ts create mode 100644 src/controllers/Client.controller.ts rename {controllers => src/controllers}/index.ts (51%) create mode 100644 src/core/Controller.ts rename {core => src/core}/index.ts (100%) create mode 100644 src/models/Command.ts create mode 100644 src/models/Controller.ts create mode 100644 src/models/Legica.ts create mode 100644 src/models/index.ts rename {modules => src/modules}/Common.module.ts (100%) rename {modules => src/modules}/index.ts (100%) diff --git a/app.ts b/app.ts deleted file mode 100644 index 07becd5..0000000 --- a/app.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Client } from "discord.js"; -import Chat from "./common/chat"; -import { config as dotenv } from "dotenv"; -import { Controller } from "./core"; -import { ClientController } from "./controllers"; - -dotenv(); - -const client: Client = new Client(); -const chat: Chat = new Chat(client); - -const controllers = new Controller(new ClientController(client)); - -controllers.register(); -chat.register(process.env.DEV_TOKEN || ""); diff --git a/common/getSettings.ts b/common/getSettings.ts deleted file mode 100644 index 18ae0e6..0000000 --- a/common/getSettings.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { readFileSync } from "fs"; - -export default function getSettings(environment: string) { - let _returnValue = null; - if (environment === "development") { - try { - _returnValue = safelyJsonParse(readFileSync("./.configs/development/config.json", "utf-8")); - } catch (err) { - _returnValue = null; - } - } else if (environment === "testing") { - try { - _returnValue = safelyJsonParse(readFileSync("./.configs/testing/config.json", "utf-8")); - } catch (err) { - _returnValue = null; - } - } else if (environment === "production") { - try { - _returnValue = safelyJsonParse(readFileSync("./.configs/production/config.json", "utf-8")); - } catch (err) { - _returnValue = null; - } - } - return _returnValue; -} - -function safelyJsonParse(data: any) { - try { - return JSON.parse(data); - } catch (err) { - return ""; - } -} diff --git a/common/index.ts b/common/index.ts deleted file mode 100644 index 725ad40..0000000 --- a/common/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Chat } from "./chat"; -export { default as getSettings } from "./getSettings"; diff --git a/constants/environments.ts b/constants/environments.ts deleted file mode 100644 index 28cea62..0000000 --- a/constants/environments.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const ENVIRONMENTS = ["development", "testing", "production"]; -export const ENVIRONMENT = - ENVIRONMENTS.filter((env) => process?.argv?.includes?.(`--${env}`))?.[0] || "development"; diff --git a/constants/index.ts b/constants/index.ts deleted file mode 100644 index 5af8468..0000000 --- a/constants/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./environments"; -export * from "./version"; diff --git a/constants/version.ts b/constants/version.ts deleted file mode 100644 index 0876501..0000000 --- a/constants/version.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { version } from "../package.json"; - -export const APP_VERSION = version; diff --git a/controllers/Client.controller.ts b/controllers/Client.controller.ts deleted file mode 100644 index 92bb776..0000000 --- a/controllers/Client.controller.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Client, Message, MessageEmbed, TextChannel } from "discord.js"; -import * as cron from "node-cron"; -import axios from "axios"; -import cheerio from "cheerio"; - -class ClientController { - constructor(private client: Client) {} - - public register = (): void => { - this.client.on("ready", (): void => { - cron.schedule("0 9 * * *", this.sendMessage); - }); - }; - - private sendMessage = async (): Promise => { - try { - const href = await getFirstHtml(); - const { img, title } = await getImgTitle(href); - - this.client.channels.cache.forEach(async (channel) => { - 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) { - await msg.react(reaction); - } - }); - } catch (err) { - console.error(err); - } - }; -} - -type Legica = { - img: string; - title: string; -}; - -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/controllers/Pule.controller.ts b/controllers/Pule.controller.ts deleted file mode 100644 index 41f927f..0000000 --- a/controllers/Pule.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Client, Message, MessageEmbed } from "discord.js"; -import axios from "axios"; -import * as puppeteer from "puppeteer"; - -class PuleController { - constructor(private client: Client) {} - - public register = (): void => { - this.client.on("ready", (): void => { - this.sendMessage(); - }); - }; - - private sendMessage = async (): Promise => { - const href = await getFirstHtml(); - const user = await this.client.users.fetch("329236932309680128"); - const dm = await user.createDM(); - const embeddedMessage = new MessageEmbed().setTitle("Nibba").setImage(href || ""); - const msg = await dm.send(embeddedMessage); - }; -} - -async function getFirstHtml(): Promise { - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - await page.goto( - "https://duckduckgo.com/?q=black+guy&t=newext&atb=v315-4&iar=images&iax=images&ia=images" - ); - await page.waitForSelector(".tile.tile--img.has-detail", { timeout: 10000 }); - - const body = await page.evaluate(() => { - function randomIntFromInterval(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1) + min); - } - const randNum = randomIntFromInterval(1, 25); - return document.querySelectorAll(".tile.tile--img.has-detail")[randNum].querySelector("img") - ?.src; - }); - - await browser.close(); - - return body; -} - -export default PuleController; diff --git a/core/Controller.ts b/core/Controller.ts deleted file mode 100644 index 5f7cab5..0000000 --- a/core/Controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -class Controller { - private controllers: any[]; - constructor(...args: any[]) { - this.controllers = [...args]; - } - - public register = (): void => { - this?.controllers?.forEach?.((controller) => { - controller.register(); - }); - }; -} - -export default Controller; diff --git a/process-env.d.ts b/process-env.d.ts new file mode 100644 index 0000000..b614818 --- /dev/null +++ b/process-env.d.ts @@ -0,0 +1,12 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + TOKEN: string; + PORT: string; + CRON_LEGICA: string; + PASSWORD: string; + } + } +} + +export {}; diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..008d177 --- /dev/null +++ b/src/app.ts @@ -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); diff --git a/common/chat.ts b/src/common/chat.ts similarity index 74% rename from common/chat.ts rename to src/common/chat.ts index 78945ed..03190b8 100644 --- a/common/chat.ts +++ b/src/common/chat.ts @@ -1,20 +1,23 @@ +import { CommandFunction, ICommand } from "@models"; import type { Client, Message } from "discord.js"; export default class Chat { - private commands: any[] = []; private prefix: string = "!"; - constructor(private client: Client) {} + 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}`) { + } else if ( + message?.content?.split?.(/\s/g)?.[0] == `${this.prefix}${command?.name}` + ) { const args = message?.content ?.replace?.(`${this.prefix}${command?.name}`, "") .trim?.() @@ -33,7 +36,7 @@ export default class Chat { this.client.login(token); }; - public command = (name: string, callback: Function): void => { + public command = (name: string, callback: CommandFunction): void => { this.commands = [ ...this.commands, { diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..24fe1c6 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1 @@ +export { default as Chat } from "./chat"; diff --git a/src/constants/config.ts b/src/constants/config.ts new file mode 100644 index 0000000..c70e8b5 --- /dev/null +++ b/src/constants/config.ts @@ -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 }; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..b56936a --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,2 @@ +export * from "./version"; +export * from "./config"; diff --git a/src/constants/version.ts b/src/constants/version.ts new file mode 100644 index 0000000..a0cbd1f --- /dev/null +++ b/src/constants/version.ts @@ -0,0 +1,3 @@ +import { version } from "../../package.json"; + +export const APP_VERSION = version; diff --git a/src/controllers/Client.controller.ts b/src/controllers/Client.controller.ts new file mode 100644 index 0000000..8147dca --- /dev/null +++ b/src/controllers/Client.controller.ts @@ -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 => { + 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/controllers/index.ts b/src/controllers/index.ts similarity index 51% rename from controllers/index.ts rename to src/controllers/index.ts index 76c080b..1aa324d 100644 --- a/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1 @@ export { default as ClientController } from "./Client.controller"; -export { default as PuleController } from "./Pule.controller"; diff --git a/src/core/Controller.ts b/src/core/Controller.ts new file mode 100644 index 0000000..6a7e088 --- /dev/null +++ b/src/core/Controller.ts @@ -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; diff --git a/core/index.ts b/src/core/index.ts similarity index 100% rename from core/index.ts rename to src/core/index.ts diff --git a/src/models/Command.ts b/src/models/Command.ts new file mode 100644 index 0000000..4d10bf9 --- /dev/null +++ b/src/models/Command.ts @@ -0,0 +1,8 @@ +import { Message } from "discord.js"; + +export type CommandFunction = (message: Message, args?: string[]) => void; + +export interface ICommand { + callback: CommandFunction; + name: string; +} diff --git a/src/models/Controller.ts b/src/models/Controller.ts new file mode 100644 index 0000000..b39f569 --- /dev/null +++ b/src/models/Controller.ts @@ -0,0 +1,3 @@ +export interface IController { + register(): void; +} diff --git a/src/models/Legica.ts b/src/models/Legica.ts new file mode 100644 index 0000000..72d635f --- /dev/null +++ b/src/models/Legica.ts @@ -0,0 +1,4 @@ +export type Legica = { + img: string; + title: string; +}; diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..6ee8930 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,3 @@ +export * from "./Controller"; +export * from "./Command"; +export * from "./Legica"; diff --git a/modules/Common.module.ts b/src/modules/Common.module.ts similarity index 100% rename from modules/Common.module.ts rename to src/modules/Common.module.ts diff --git a/modules/index.ts b/src/modules/index.ts similarity index 100% rename from modules/index.ts rename to src/modules/index.ts