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

15
app.ts
View File

@@ -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 || "");

View File

@@ -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 "";
}
}

View File

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

View File

@@ -1,3 +0,0 @@
export const ENVIRONMENTS = ["development", "testing", "production"];
export const ENVIRONMENT =
ENVIRONMENTS.filter((env) => process?.argv?.includes?.(`--${env}`))?.[0] || "development";

View File

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

View File

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

View File

@@ -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<void> => {
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<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;

View File

@@ -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<void> => {
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<string | undefined> {
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;

View File

@@ -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;

12
process-env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
TOKEN: string;
PORT: string;
CRON_LEGICA: string;
PASSWORD: string;
}
}
}
export {};

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);

View File

@@ -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,
{

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;

View File

@@ -1,2 +1 @@
export { default as ClientController } from "./Client.controller";
export { default as PuleController } from "./Pule.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;

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";