diff --git a/.gitignore b/.gitignore index 7648d29..d18582d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ package-lock.json !log/.gitkeep log/* node_modules +bun.lockb diff --git a/sql/update_2023-11-18_01.sql b/sql/update_2023-11-18_01.sql new file mode 100644 index 0000000..3fc2de4 --- /dev/null +++ b/sql/update_2023-11-18_01.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `macros` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `chat_id` int(10) unsigned NOT NULL, + `macro` varchar(50) NOT NULL, + `content` text NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_macro_id_chat_id` (`chat_id`), + CONSTRAINT `fk_macro_id_chat_id` FOREIGN KEY (`chat_id`) REFERENCES `chats` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/sql/update_2024-04-22_01.sql b/sql/update_2024-04-22_01.sql new file mode 100644 index 0000000..85187ef --- /dev/null +++ b/sql/update_2024-04-22_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `warns` ADD `status` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 AFTER `reason`; +RENAME TABLE `warns` TO `warnings`; diff --git a/src/App.ts b/src/App.ts index a8eedb2..b134b23 100644 --- a/src/App.ts +++ b/src/App.ts @@ -32,7 +32,7 @@ export default class App { * * @var {number} */ - private port: number; + private readonly port: number; /** * The constructor. diff --git a/src/action/Greetings.ts b/src/action/Greetings.ts index ed8a32e..8c91472 100644 --- a/src/action/Greetings.ts +++ b/src/action/Greetings.ts @@ -147,8 +147,8 @@ export default class Greetings extends Action { const captchaButton: InlineKeyboardButton = { text: Lang.get("captchaButton"), callbackData: JSON.stringify({ - callback: "captchaconfirmation", - data: { + c: "captchaconfirmation", + d: { userId: this.context.newChatMember!.getId() } }) diff --git a/src/action/SaveMessage.ts b/src/action/SaveMessage.ts index 57c7810..1be073e 100644 --- a/src/action/SaveMessage.ts +++ b/src/action/SaveMessage.ts @@ -15,7 +15,7 @@ import UserHelper from "../helper/User.js"; import ChatHelper from "../helper/Chat.js"; import Messages from "../model/Messages.js"; -export default class saveUserAndChat extends Action { +export default class SaveMessage extends Action { /** * The constructor. @@ -45,6 +45,10 @@ export default class saveUserAndChat extends Action { const user = await UserHelper.getByTelegramId(contextUser.getId()); const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); + if (!user || !chat) { + return Promise.resolve(); + } + switch (this.context.type) { case "editedMessage": @@ -73,6 +77,7 @@ export default class saveUserAndChat extends Action { const message = new Messages(); const insert = message.insert(); + insert .set("type", this.context.type) .set("user_id", user.id) diff --git a/src/action/SaveUserAndChat.ts b/src/action/SaveUserAndChat.ts index f257ceb..4968710 100644 --- a/src/action/SaveUserAndChat.ts +++ b/src/action/SaveUserAndChat.ts @@ -15,7 +15,7 @@ import UserHelper from "../helper/User.js"; import ChatHelper from "../helper/Chat.js"; import RelUsersChats from "../model/RelUsersChats.js"; -export default class saveUserAndChat extends Action { +export default class SaveUserAndChat extends Action { /** * The constructor. @@ -39,10 +39,10 @@ export default class saveUserAndChat extends Action { const contextUser = this.context.newChatMember || this.context.leftChatMember || this.context.user; const user = await UserHelper.getByTelegramId(contextUser.getId()); - const userId = user === null ? await UserHelper.createUser(contextUser) : user.id; + const userId = user?.id ?? await UserHelper.createUser(contextUser); const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); - const chatId = chat === null ? await ChatHelper.createChat(this.context.chat) : chat!.id; + const chatId = chat?.id ?? await ChatHelper.createChat(this.context.chat); UserHelper.updateUser(contextUser); ChatHelper.updateChat(this.context.chat); diff --git a/src/callback/Callback.ts b/src/callback/Callback.ts index 47bea8e..d66c28f 100644 --- a/src/callback/Callback.ts +++ b/src/callback/Callback.ts @@ -66,11 +66,11 @@ export default abstract class Callback { */ public isCalled(): boolean { - if (!this.context.callbackQuery?.callbackData || !this.context.callbackQuery?.callbackData.callback) { + if (!this.context.callbackQuery?.callbackData || !this.context.callbackQuery?.callbackData.c) { return false; } - return this.callbacks.includes(this.context.callbackQuery?.callbackData.callback); + return this.callbacks.includes(this.context.callbackQuery?.callbackData.c); } /** diff --git a/src/callback/CaptchaConfirmation.ts b/src/callback/CaptchaConfirmation.ts index 990814f..4c36821 100644 --- a/src/callback/CaptchaConfirmation.ts +++ b/src/callback/CaptchaConfirmation.ts @@ -40,7 +40,7 @@ export default class CaptchaConfirmation extends Callback { */ public async run(): Promise { - if (this.context.callbackQuery?.callbackData.data.userId !== this.context.user.getId()) { + if (this.context.callbackQuery?.callbackData.d.userId !== this.context.user.getId()) { return; } diff --git a/src/callback/Warning.ts b/src/callback/Warning.ts new file mode 100644 index 0000000..155b5f6 --- /dev/null +++ b/src/callback/Warning.ts @@ -0,0 +1,103 @@ +/** + * Ada Lovelace Telegram Bot + * + * This file is part of Ada Lovelace Telegram Bot. + * You are free to modify and share this project or its files. + * + * @package mslovelace_bot + * @author Marcos Leandro + * @license GPLv3 + */ + +import Callback from "./Callback.js"; +import Warnings from "../model/Warnings.js"; +import Context from "../library/telegram/context/Context.js"; +import UserHelper from "../helper/User.js"; +import ChatHelper from "../helper/Chat.js"; +import Lang from "../helper/Lang.js"; +import { parse } from "dotenv"; + +export default class CaptchaConfirmation extends Callback { + + /** + * The constructor. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param context + */ + public constructor(context: Context) { + super(context); + this.setCallbacks(["warning"]); + } + + /** + * Command main route. + * + * @author Marcos Leandro + * @since 2024-04-22 + */ + public async run(): Promise { + + const user = await UserHelper.getByTelegramId(this.context.user.getId()); + const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); + + if (!user || !chat) { + this.context.callbackQuery?.answer(Lang.get("adminOnlyAction")); + return; + } + + Lang.set(chat.language || "us"); + + if (!await this.context.user.isAdmin()) { + this.context.callbackQuery?.answer(Lang.get("adminOnlyAction")); + } + + const [userId, chatId, warningId] = this.context.callbackQuery?.callbackData?.d?.split(","); + this.context.callbackQuery?.answer("OK"); + this.context.message.delete(); + + const contextUser = await UserHelper.getByTelegramId(userId); + await this.remove(userId, chatId, warningId); + + let message = Lang.get(typeof warningId === "undefined" ? "warningAdminRemovedAll" : "warningAdminRemovedLast") + .replace("{userid}", contextUser.user_id) + .replace("{username}", contextUser.first_name || user.username) + .replace("{adminId}", this.context.user.getId()) + .replace("{adminUsername}", this.context.user.getFirstName() || this.context.user.getUsername()); + + this.context.chat.sendMessage(message, { parseMode: "HTML" }); + } + + /** + * Removes one or all the warnings. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param userId + * @param chatId + * @param warningId + */ + private async remove(userId: number, chatId: number, warningId: number|undefined = undefined): Promise { + + const user = await UserHelper.getByTelegramId(userId); + const chat = await ChatHelper.getByTelegramId(chatId); + + const warnings = new Warnings(); + const update = warnings.update(); + + update + .set("status", 0) + .where("user_id").equal(user!.id) + .and("chat_id").equal(chat!.id); + + if (typeof warningId !== "undefined") { + update.and("id").equal(warningId); + } + + await warnings.execute(); + } + +} diff --git a/src/callback/Yarn.ts b/src/callback/Yarn.ts index 7d5c464..eec5bce 100644 --- a/src/callback/Yarn.ts +++ b/src/callback/Yarn.ts @@ -37,12 +37,12 @@ export default class Yarn extends Callback { */ public async run(): Promise { - if (!this.context.callbackQuery?.callbackData.data.package) { + if (!this.context.callbackQuery?.callbackData.d.package) { return; } const yarnCommand = new YarnCommand(this.context); - await yarnCommand.getPackage(this.context.callbackQuery?.callbackData.data.package); - this.context.callbackQuery.answer(this.context.callbackQuery?.callbackData.data.package.toUpperCase()); + await yarnCommand.getPackage(this.context.callbackQuery?.callbackData.d.package); + this.context.callbackQuery.answer(this.context.callbackQuery?.callbackData.d.package.toUpperCase()); } } diff --git a/src/command/Ban.ts b/src/command/Ban.ts index c88021c..aa9235a 100644 --- a/src/command/Ban.ts +++ b/src/command/Ban.ts @@ -93,9 +93,13 @@ export default class Ban extends Command { * * @returns void */ - private async banByReply(replyToMessage: Message, reason: string): Promise> { - this.saveBan(replyToMessage.getUser(), reason); - return replyToMessage.getUser().ban(); + private async banByReply(replyToMessage: Message, reason: string): Promise { + + if (await replyToMessage.getUser().ban()) { + this.saveBan(replyToMessage.getUser(), reason); + } + + return Promise.resolve(); } /** @@ -106,9 +110,13 @@ export default class Ban extends Command { * * @returns void */ - private async banByMention(mention: User, reason: string): Promise> { - this.saveBan(mention, reason); - return mention.ban(); + private async banByMention(mention: User, reason: string): Promise { + + if (await mention.ban()) { + this.saveBan(mention, reason); + } + + return Promise.resolve(); } /** @@ -120,7 +128,7 @@ export default class Ban extends Command { * @param userId * @param reason */ - private async banByUserId(userId: number, reason: string): Promise|undefined> { + private async banByUserId(userId: number, reason: string): Promise { const user = await UserHelper.getByTelegramId(userId); @@ -133,8 +141,11 @@ export default class Ban extends Command { }; const contextUser = new UserContext(userType, this.context.chat); - this.saveBan(contextUser, reason); - return contextUser.ban(); + if (await contextUser.ban()) { + this.saveBan(contextUser, reason); + } + + return Promise.resolve(); } /** diff --git a/src/command/Macro.ts b/src/command/Macro.ts new file mode 100644 index 0000000..e46ad25 --- /dev/null +++ b/src/command/Macro.ts @@ -0,0 +1,247 @@ +/** + * Ada Lovelace Telegram Bot + * + * This file is part of Ada Lovelace Telegram Bot. + * You are free to modify and share this project or its files. + * + * @package mslovelace_bot + * @author Marcos Leandro + * @license GPLv3 + */ + +import Command from "./Command.js"; +import Context from "../library/telegram/context/Context.js"; +import CommandContext from "../library/telegram/context/Command.js"; +import ChatHelper from "../helper/Chat.js"; +import Chats from "../model/Chats.js"; +import Macros from "../model/Macros.js"; +import Lang from "../helper/Lang.js"; + +export default class Macro extends Command { + + /** + * Current loaded chat. + * + * @author Marcos Leandro + * @since 2023-11-18 + */ + private chat: Record = {}; + + /** + * The constructor. + * + * @author Marcos Leandro + * @since 2023-11-18 + * + * @param context + */ + public constructor(context: Context) { + super(context); + this.setCommands(["macro", "madd", "mlist", "mremove"]); + } + + /** + * Executes the command. + * + * @author Marcos Leandro + * @since 2023-11-18 + * + * @param command + */ + public async run(command: CommandContext): Promise { + + this.context.message.delete(); + + const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); + if (!chat) { + return; + } + + Lang.set(chat.language || "us"); + + this.chat = chat; + + let action = "index"; + if (Macro.prototype.hasOwnProperty(command.getCommand())) { + action = command.getCommand(); + } + + const method = action as keyof typeof Macro.prototype; + await this[method](command as never); + } + + /** + * Shows a macro. + * + * @author Marcos Leandro + * @since 2023-11-18 + * + * @param command + */ + private index(command: CommandContext): void { + + const params = command.getParams(); + if (!Array.isArray(params) || params.length < 1) { + return; + } + + const macro = params.shift()?.trim(); + if (!macro || !macro.length) { + return; + } + + const macros = new Macros(); + macros + .select() + .where("chat_id").equal(this.chat.id) + .and("macro").equal(macro); + + macros.execute().then((result: Record) => { + + if (!result.length) { + return; + } + + const content = result[0].content; + const replyToMessage = this.context.message.getReplyToMessage(); + + if (replyToMessage) { + replyToMessage.reply(content, { parseMode : "HTML" }); + return; + } + + this.context.chat.sendMessage(content, { parseMode : "HTML" }); + }); + } + + /** + * Adds a macro. + * + * @author Marcos Leandro + * @since 2023-11-18 + * + * @param command + */ + private async madd(command: CommandContext): Promise { + + if (!await this.context.user.isAdmin()) { + return; + } + + let params = command.getParams(); + if (!Array.isArray(params) || params.length < 2) { + this.context.chat.sendMessage(Lang.get("macroMalformedCommandError"), { parseMode : "HTML" }); + return; + } + + const macro = params.shift()?.trim(); + const content = params.join(" ").trim(); + + if (!macro || !macro.length || !content.length) { + this.context.chat.sendMessage(Lang.get("macroMalformedCommandError"), { parseMode : "HTML" }); + return; + } + + const macros = new Macros(); + macros + .select() + .where("chat_id").equal(this.chat.id) + .and("macro").equal(macro.toLowerCase()); + + const result = await macros.execute(); + + if (result.length) { + const alreadyExistLang = Lang.get("macroAlreadyExists").replace("{macro}", macro); + this.context.chat.sendMessage(alreadyExistLang, { parseMode : "HTML" }); + return; + } + + macros + .insert() + .set("chat_id", this.chat.id) + .set("macro", macro.toLowerCase()) + .set("content", content); + + if (await macros.execute()) { + this.mlist(command); + return; + } + + this.context.chat.sendMessage(Lang.get("macroAddError"), { parseMode : "HTML" }); + } + + /** + * Lists the macros. + * + * @author Marcos Leandro + * @since 2023-11-18 + * + * @param command + */ + private async mlist(command: CommandContext): Promise { + + const macros = new Macros(); + macros + .select() + .where("chat_id").equal(this.chat.id) + .orderBy("macro", "asc"); + + const result = await macros.execute(); + if (!result.length) { + this.context.chat.sendMessage(Lang.get("macroNoMacroFound"), { parseMode : "HTML" }); + return; + } + + let message = Lang.get("macroList"); + for (const row of result) { + message += ` • ${row.macro}\n`; + } + + this.context.chat.sendMessage(message, { parseMode : "HTML" }); + } + + /** + * Removes a macro. + * + * @author Marcos Leandro + * @since 2023-11-18 + * + * @param command + */ + private async mremove(command: CommandContext): Promise { + + if (!await this.context.user.isAdmin()) { + return; + } + + const params = command.getParams(); + if (!Array.isArray(params) || params.length < 1) { + return; + } + + const macro = params.shift()?.trim(); + if (!macro || !macro.length) { + return; + } + + const macros = new Macros(); + macros + .select() + .where("chat_id").equal(this.chat.id) + .and("macro").equal(macro); + + const result = await macros.execute(); + + if (result.length) { + + macros + .delete() + .where("chat_id").equal(this.chat.id) + .and("macro").equal(macro.toLowerCase()); + + macros.execute(); + } + + this.mlist(command); + } +} diff --git a/src/command/Npm.ts b/src/command/Npm.ts index 7153ff5..8c47366 100644 --- a/src/command/Npm.ts +++ b/src/command/Npm.ts @@ -64,7 +64,7 @@ export default class Npm extends Command { }); } catch (err: any) { - Log.save(err.toString()); + Log.save(err.toString(), err.stack); } } @@ -81,7 +81,7 @@ export default class Npm extends Command { private async processResponse(error: any, stdout: string, stderr: string): Promise { if (error) { - Log.save(error.message); + Log.save(error.message, error.skack || undefined); return; } diff --git a/src/command/Warn.ts b/src/command/Warn.ts deleted file mode 100644 index 9d121e8..0000000 --- a/src/command/Warn.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Ada Lovelace Telegram Bot - * - * This file is part of Ada Lovelace Telegram Bot. - * You are free to modify and share this project or its files. - * - * @package mslovelace_bot - * @author Marcos Leandro - * @license GPLv3 - */ - -import Command from "./Command.js"; -import Context from "../library/telegram/context/Context.js"; -import CommandContext from "../library/telegram/context/Command.js"; -import User from "../library/telegram/context/User.js"; -import Message from "../library/telegram/context/Message.js"; -import ChatConfigs from "../model/ChatConfigs.js"; -import Warns from "../model/Warns.js"; -import UserHelper from "../helper/User.js"; -import ChatHelper from "../helper/Chat.js"; -import Lang from "../helper/Lang.js"; -import Log from "../helper/Log.js"; - -export default class Warn extends Command { - - /** - * Command context. - * - * @author Marcos Leandro - * @since 2023-06-14 - * - * @var {CommandContext} - */ - private command?: CommandContext; - - /** - * The constructor. - * - * @author Marcos Leandro - * @since 2022-09-12 - * - * @param app App instance. - */ - public constructor(context: Context) { - super(context); - this.setCommands(["warn"]); - } - - /** - * Executes the command. - * - * @author Marcos Leandro - * @since 2023-06-07 - * - * @param command - * - * @returns - */ - public async run(command: CommandContext): Promise { - - if (!await this.context.user.isAdmin()) { - return; - } - - if (this.context.chat.getType() === "private") { - return; - } - - this.command = command; - - const replyToMessage = this.context.message.getReplyToMessage(); - if (replyToMessage) { - this.warnByReply(replyToMessage); - return; - } - - const mentions = await this.context.message.getMentions(); - if (!mentions.length) { - return; - } - - for (const mention of mentions) { - this.warnByMention(mention); - } - } - - /** - * Warns an user by message reply. - * - * @author Marcos Leandro - * @since 2023-06-07 - * - * @returns void - */ - private async warnByReply(replyToMessage: Message): Promise { - - const params = this.command!.getParams(); - if (!params || !params.length) { - return; - } - - this.warn(replyToMessage.getUser(), params.join(" ")); - } - - /** - * Warns an user by mention reply. - * - * @author Marcos Leandro - * @since 2023-06-07 - * - * @returns void - */ - private async warnByMention(mention: User): Promise { - - const params = this.command!.getParams(); - if (!params || !params.length) { - return; - } - - params.shift(); - this.warn(mention, params.join(" ")); - } - - /** - * Saves the user warning. - * - * @author Marcos Leandro - * @since 2023-06-14 - * - * @param {User} contextUser - * @param {string} reason - */ - private async warn(contextUser: User, reason: string): Promise { - - const user = await UserHelper.getByTelegramId(contextUser.getId()); - const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); - - if (!user || !chat) { - return; - } - - Lang.set(chat.language || "us"); - - if (contextUser.getId() === parseInt(process.env.TELEGRAM_USER_ID!)) { - this.context.message.reply(Lang.get("selfWarnMessage")); - return; - } - - if (await contextUser.isAdmin()) { - this.context.message.reply(Lang.get("adminWarnMessage")); - return; - } - - this.context.message.delete(); - - const warn = new Warns(); - warn - .insert() - .set("user_id", user.id) - .set("chat_id", chat.id) - .set("date", Math.ceil(Date.now() / 1000)) - .set("reason", reason); - - try { - await warn.execute(); - this.reportWarnAndBan(contextUser, user, chat); - - } catch (error) { - Log.save(error as string); - } - } - - /** - * Reports the warning and bans the user if necessary. - * - * @author Marcos Leandro - * @since 2023-06-14 - * - * @param {User} contextUser - * @param {Record} user - * @param {Record} chat - */ - private async reportWarnAndBan(contextUser: User, user: Record, chat: Record): Promise { - - const warn = new Warns(); - warn - .select() - .where("user_id").equal(user.id) - .and("chat_id").equal(chat.id) - .orderBy("date", "ASC"); - - const warns = await warn.execute(); - - const chatConfigs = new ChatConfigs(); - chatConfigs - .select() - .where("chat_id").equal(chat.id); - - const chatConfig = await chatConfigs.execute(); - const warnLimit = chatConfig[0].warn_limit || 3; - - const username = contextUser.getFirstName() || contextUser.getUsername(); - const langIndex = warns.length === 1 ? "warningSigleMessage" : "warningPluralMessage"; - - let message = Lang.get(langIndex) - .replace("{userid}", contextUser.getId()) - .replace("{username}", username) - .replace("{warns}", warns.length.toString() + "/" + warnLimit.toString()); - - if (warns.length >= warnLimit) { - contextUser.ban(); - message = Lang.get("warningBanMessage") - .replace("{userid}", contextUser.getId()) - .replace("{username}", username) - .replace("{warns}", warns.length.toString() + "/" + warnLimit.toString()); - } - - for (let i = 0, length = warns.length; i < length; i++) { - message += ` • ${warns[i].reason}\n`; - } - - this.context.chat.sendMessage(message, { parseMode: "HTML" }); - } -} diff --git a/src/command/Warning/Base.ts b/src/command/Warning/Base.ts new file mode 100644 index 0000000..ca767cf --- /dev/null +++ b/src/command/Warning/Base.ts @@ -0,0 +1,204 @@ +/** + * Ada Lovelace Telegram Bot + * + * This file is part of Ada Lovelace Telegram Bot. + * You are free to modify and share this project or its files. + * + * @package mslovelace_bot + * @author Marcos Leandro + * @license GPLv3 + */ + +import Command from "../Command.js"; +import Context from "../../library/telegram/context/Context.js"; +import User from "../../library/telegram/context/User.js"; +import { InlineKeyboardButton } from "../../library/telegram/type/InlineKeyboardButton.js"; +import { InlineKeyboardMarkup } from "../../library/telegram/type/InlineKeyboardMarkup.js"; +import ChatConfigs from "../../model/ChatConfigs.js"; +import WarningsModel from "../../model/Warnings.js"; +import UserHelper from "../../helper/User.js"; +import Lang from "../../helper/Lang.js"; + +export default class Base extends Command { + + /** + * The constructor. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param context + */ + public constructor(context: Context) { + super(context); + } + + /** + * Returns the group's warning limits. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param chat + * + * @return Warning limit. + */ + protected async getWarningLimit(chat: Record): Promise { + + const chatConfigs = new ChatConfigs(); + chatConfigs + .select() + .where("chat_id").equal(chat.id); + + const chatConfig = await chatConfigs.execute(); + const warningLimit = chatConfig[0].warn_limit; + + return warningLimit ? parseInt(warningLimit) : 3; + } + + /** + * Returns the user's warns. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param user + * @param chat + * + * @return Warnings length. + */ + protected async getWarnings(contextUser: User, chat: Record): Promise[]> { + + const user = await UserHelper.getByTelegramId(contextUser.getId()); + + if (!user || !chat) { + return Promise.resolve([]); + } + + const warnings = new WarningsModel(); + warnings + .select() + .where("user_id").equal(user.id) + .and("chat_id").equal(chat.id) + .and("status").equal(1) + .orderBy("date", "ASC"); + + return await warnings.execute(); + } + + /** + * Returns the warning message. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param contextUser + * @param warnings + * @param warningsLimit + * + * @return Warning message. + */ + protected async getWarningMessage(contextUser: User, warnings: Record[], warningsLimit: number): Promise { + + const username = contextUser.getFirstName() || contextUser.getUsername(); + + let langIndex = warnings.length === 1 ? "warningSigleMessage" : "warningPluralMessage"; + langIndex = warnings.length >= warningsLimit ? "warningBanMessage" : langIndex; + langIndex = warnings.length === 0 ? "warningNoneMessage" : langIndex; + + let message = Lang.get(langIndex) + .replace("{userid}", contextUser.getId()) + .replace("{username}", username) + .replace("{warns}", warnings.length.toString() + "/" + warningsLimit.toString()); + + for (let i = 0, length = warnings.length; i < length; i++) { + message += ` • ${warnings[i].reason}\n`; + } + + return Promise.resolve(message); + } + + /** + * Sends the warning messages. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param users + * @param chat + * + * @return + */ + protected async sendWarningMessages(users: User[], chat: Record): Promise { + + Lang.set(chat.language || "us"); + + const warnings = await this.getWarnings(users[0], chat); + const warningLimit = await this.getWarningLimit(chat); + const messages = []; + + for (let i = 0, length = users.length; i < length; i++) { + const contextUser = users[i]; + messages.push(await this.getWarningMessage(contextUser, warnings, warningLimit)); + } + + if (!messages.length) { + return Promise.resolve(); + } + + const message = messages.join("\n-----\n"); + const options = this.getMessageOptions(users, warnings); + + await this.context.chat.sendMessage(message, options); + } + + /** + * Returns the message options. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param users + * @param chat + * + * @returns + */ + private getMessageOptions(users: User[], warnings: Record[]): Record { + + const options: Record = { + parseMode: "HTML" + }; + + if (users.length !== 1) { + return options; + } + + if (!warnings.length) { + return options; + } + + const lastWarning: Record = warnings.at(-1)!; + const lastWarningRemowalButton: InlineKeyboardButton = { + text: Lang.get("lastWarningRemovalButton"), + callbackData: JSON.stringify({ + c: "warning", + d: `${users[0].getId()},${this.context.chat.getId()},${lastWarning.id}` + }) + }; + + const allWarningsRemowalButton: InlineKeyboardButton = { + text: Lang.get("warningsRemovalButton"), + callbackData: JSON.stringify({ + c: "warning", + d: `${users[0].getId()},${this.context.chat.getId()}` + }) + }; + + const markup: InlineKeyboardMarkup = { + inlineKeyboard: [[lastWarningRemowalButton], [allWarningsRemowalButton]] + }; + + options.replyMarkup = markup; + return options; + } +} diff --git a/src/command/Warning/Warn.ts b/src/command/Warning/Warn.ts new file mode 100644 index 0000000..e8877a0 --- /dev/null +++ b/src/command/Warning/Warn.ts @@ -0,0 +1,186 @@ +/** + * Ada Lovelace Telegram Bot + * + * This file is part of Ada Lovelace Telegram Bot. + * You are free to modify and share this project or its files. + * + * @package mslovelace_bot + * @author Marcos Leandro + * @license GPLv3 + */ + +import Context from "../../library/telegram/context/Context.js"; +import CommandContext from "../../library/telegram/context/Command.js"; +import User from "../../library/telegram/context/User.js"; +import Message from "../../library/telegram/context/Message.js"; +import ChatConfigs from "../../model/ChatConfigs.js"; +import WarningsModel from "../../model/Warnings.js"; +import WarningsBase from "./Base.js"; +import UserHelper from "../../helper/User.js"; +import ChatHelper from "../../helper/Chat.js"; +import Lang from "../../helper/Lang.js"; +import Log from "../../helper/Log.js"; + +export default class Warn extends WarningsBase { + + /** + * Command context. + * + * @author Marcos Leandro + * @since 2023-06-14 + * + * @var {CommandContext} + */ + private command?: CommandContext; + + /** + * The constructor. + * + * @author Marcos Leandro + * @since 2022-09-12 + * + * @param app App instance. + */ + public constructor(context: Context) { + super(context); + this.setCommands(["warn"]); + } + + /** + * Executes the command. + * + * @author Marcos Leandro + * @since 2023-06-07 + * + * @param command + * + * @returns + */ + public async run(command: CommandContext): Promise { + + if (!await this.context.user.isAdmin()) { + return; + } + + if (this.context.chat.getType() === "private") { + return; + } + + this.command = command; + + const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); + if (!chat) { + return; + } + + const params = this.command!.getParams(); + if (!params || !params.length) { + return; + } + + Lang.set(chat.language || "us"); + + const users = []; + const warningLimit = await this.getWarningLimit(chat); + const replyToMessage = this.context.message.getReplyToMessage(); + + if (replyToMessage) { + users.push(replyToMessage.getUser()); + } + + const mentions = await this.context.message.getMentions() || []; + for (const mention of mentions) { + users.push(mention); + params.shift(); + } + + if (!users.length) { + return; + } + + for (let i = 0, length = users.length; i < length; i++) { + const contextUser = users[i]; + await this.warn(contextUser, chat, warningLimit, params.join(" ")); + } + + this.sendWarningMessages(users, chat); + } + + /** + * Saves the user warning. + * + * @author Marcos Leandro + * @since 2023-06-14 + * + * @param contextUser + * @param chat + * @param warningLimit + * @param reason + */ + private async warn(contextUser: User, chat: Record, warningLimit: number, reason: string): Promise { + + if (contextUser.getId() === parseInt(process.env.TELEGRAM_USER_ID!)) { + this.context.message.reply(Lang.get("selfWarnMessage")); + return; + } + + if (await contextUser.isAdmin()) { + this.context.message.reply(Lang.get("adminWarnMessage")); + return; + } + + const user = await UserHelper.getByTelegramId(contextUser.getId()); + if (!user) { + return; + } + + this.context.message.delete(); + + const warn = new WarningsModel(); + warn + .insert() + .set("user_id", user.id) + .set("chat_id", chat.id) + .set("date", Math.ceil(Date.now() / 1000)) + .set("reason", reason); + + try { + + await warn.execute(); + this.checkBan(contextUser, user, chat, warningLimit); + + } catch (error: any) { + Log.save(error.message, error.stack); + } + } + + /** + * Bans the user if necessary. + * + * @author Marcos Leandro + * @since 2023-06-14 + * + * @param contextUser + * @param user + * @param chat + * @param warningLimit + */ + private async checkBan(contextUser: User, user: Record, chat: Record, warningLimit: number): Promise { + + const warnings = new WarningsModel(); + warnings + .select() + .where("user_id").equal(user.id) + .and("chat_id").equal(chat.id) + .and("status").equal(1) + .orderBy("date", "ASC"); + + const results = await warnings.execute(); + + if (results.length >= warningLimit) { + contextUser.ban(); + } + + return Promise.resolve(); + } +} diff --git a/src/command/Warning/Warnings.ts b/src/command/Warning/Warnings.ts new file mode 100644 index 0000000..ff26dfe --- /dev/null +++ b/src/command/Warning/Warnings.ts @@ -0,0 +1,82 @@ +/** + * Ada Lovelace Telegram Bot + * + * This file is part of Ada Lovelace Telegram Bot. + * You are free to modify and share this project or its files. + * + * @package mslovelace_bot + * @author Marcos Leandro + * @license GPLv3 + */ + +import Context from "../../library/telegram/context/Context.js"; +import CommandContext from "../../library/telegram/context/Command.js"; +import User from "../../library/telegram/context/User.js"; +import WarningsBase from "./Base.js"; +import ChatHelper from "../../helper/Chat.js"; +import Lang from "../../helper/Lang.js"; + +export default class Warnings extends WarningsBase { + + /** + * Command context. + * + * @author Marcos Leandro + * @since 2023-06-14 + * + * @var {CommandContext} + */ + private command?: CommandContext; + + /** + * The constructor. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param app App instance. + */ + public constructor(context: Context) { + super(context); + this.setCommands(["warnings", "warns"]); + } + + /** + * Executes the command. + * + * @author Marcos Leandro + * @since 2023-06-07 + * + * @param command + * + * @returns + */ + public async run(command: CommandContext): Promise { + + if (!await this.context.user.isAdmin()) { + return; + } + + if (this.context.chat.getType() === "private") { + return; + } + + const chat = await ChatHelper.getByTelegramId(this.context.chat.getId()); + if (!chat) { + return; + } + + const users = []; + const replyToMessage = this.context.message.getReplyToMessage(); + if (replyToMessage) { + users.push(replyToMessage.getUser()); + } + + const mentions = await this.context.message.getMentions() || []; + for (const mention of mentions) { + users.push(mention); + } + + this.sendWarningMessages(users, chat); + } +} diff --git a/src/command/Yarn.ts b/src/command/Yarn.ts index e67cbdb..73c734f 100644 --- a/src/command/Yarn.ts +++ b/src/command/Yarn.ts @@ -76,7 +76,7 @@ export default class Yarn extends Command { }); } catch (err: any) { - Log.save(err.toString()); + Log.save(err.message, err.stack); } } @@ -93,7 +93,7 @@ export default class Yarn extends Command { private async processResponse(error: any, stdout: string, stderr: string): Promise { if (error) { - Log.save(error.message); + Log.save(error.message, error.stack); return; } diff --git a/src/config/callbacks.ts b/src/config/callbacks.ts index f936af2..a04f500 100644 --- a/src/config/callbacks.ts +++ b/src/config/callbacks.ts @@ -10,9 +10,11 @@ */ import CaptchaConfirmationCallback from "../callback/CaptchaConfirmation.js"; +import Warning from "../callback/Warning.js"; import YarnCallback from "../callback/Yarn.js"; export const callbacks = [ CaptchaConfirmationCallback, + Warning, YarnCallback ]; diff --git a/src/config/commands.ts b/src/config/commands.ts index 2ca601c..bdb7215 100644 --- a/src/config/commands.ts +++ b/src/config/commands.ts @@ -18,13 +18,15 @@ import FederationManage from "../command/federation/Manage.js"; import FederationUser from "../command/federation/User.js"; import Greetings from "../command/Greetings.js"; import Kick from "../command/Kick.js"; +import Macro from "../command/Macro.js"; import Npm from "../command/Npm.js"; import Report from "../command/Report.js"; import Restrict from "../command/Restrict.js"; import Send from "../command/Send.js"; import Start from "../command/Start.js"; import Unban from "../command/Unban.js"; -import Warn from "../command/Warn.js"; +import Warn from "../command/Warning/Warn.js"; +import Warnings from "../command/Warning/Warnings.js"; import Yarn from "../command/Yarn.js"; export const commands = [ @@ -37,6 +39,7 @@ export const commands = [ FederationUser, Greetings, Kick, + Macro, Npm, Report, Restrict, @@ -44,5 +47,6 @@ export const commands = [ Start, Unban, Warn, + Warnings, Yarn ]; diff --git a/src/controller/Controller.ts b/src/controller/Controller.ts index 078c7a8..1f9e216 100644 --- a/src/controller/Controller.ts +++ b/src/controller/Controller.ts @@ -111,10 +111,17 @@ export default class Controller { * @param {Record} payload */ protected async handle(payload: Record): Promise { - const context = new Context(payload); - this.handleActions(context); - this.handleCommands(context); - this.handleCallbacks(context); + + try { + + const context = new Context(payload); + this.handleActions(context); + this.handleCommands(context); + this.handleCallbacks(context); + + } catch (error: any) { + Log.save(error.message, error.stack, true, "error"); + } } /** @@ -178,7 +185,7 @@ export default class Controller { return (action.isSync()) ? await action.run() : action.run(); } catch (error: any) { - Log.save(error.toString()); + Log.save(error.message, error.stack); } } @@ -190,15 +197,20 @@ export default class Controller { * * @param command */ - private executeCommand(command: Command): void { + private async executeCommand(command: Command): Promise { const commandContext = command.isCalled(); + if (!commandContext) { + return Promise.resolve(); + } + try { - !commandContext || command.run(commandContext); + + await command.run(commandContext); } catch (error: any) { - Log.save(error.toString()); + Log.save(error.message, error.stack); } } @@ -213,10 +225,11 @@ export default class Controller { private executeCallback(callback: Callback): void { try { + !callback.isCalled() || callback.run(); } catch (error: any) { - Log.save(error.toString()); + Log.save(error.message, error.stack); } } } diff --git a/src/helper/Chat.ts b/src/helper/Chat.ts index 164b2eb..a5af9b7 100644 --- a/src/helper/Chat.ts +++ b/src/helper/Chat.ts @@ -11,6 +11,7 @@ import Chats from "../model/Chats.js"; import ChatConfigs from "../model/ChatConfigs.js"; +import Log from "./Log.js"; export default class ChatHelper { @@ -111,15 +112,23 @@ export default class ChatHelper { .set("type", chat.getType()) .set("joined", 1); - const result = await newChat.execute(); - const chatId = result.insertId; + try { - const newChatConfig = new ChatConfigs(); - newChatConfig - .insert() - .set("chat_id", chatId); + const result = await newChat.execute(); + const chatId = result.insertId; + + const newChatConfig = new ChatConfigs(); + newChatConfig + .insert() + .set("chat_id", chatId); - newChatConfig.execute(); + await newChatConfig.execute(); + return chatId; + + } catch (err) { + Log.error(err); + return null; + } } /** diff --git a/src/helper/Log.ts b/src/helper/Log.ts index b6174b6..47d7b31 100644 --- a/src/helper/Log.ts +++ b/src/helper/Log.ts @@ -304,7 +304,7 @@ export default class Log { * @param {string} content * @param {boolean} print */ - public static save(content: string, print?: boolean, level?: string): void { + public static save(content: string, stack?: string, print?: boolean, level?: string): void { const date = new Date(); @@ -318,7 +318,7 @@ export default class Log { const directory = path.resolve(); const filename = `${year}-${month}-${day}.log`; - fs.appendFileSync(`${directory}/log/${filename}`, `${hours}:${minutes}:${seconds} :: ${content}\n`); + fs.appendFileSync(`${directory}/log/${filename}`, `${hours}:${minutes}:${seconds} :: ${content}\n${stack}\n`); type LogLevel = "assert" | "clear" | "count" | "countReset" | "debug" | "dir" | "dirxml" | "error" | "group" | "groupCollapsed" | "groupEnd" | "info" | "log" | "table" | "time" | "timeEnd" | "timeLog" | "trace" | "warn"; const method: LogLevel = level?.toLowerCase() as LogLevel || "log"; diff --git a/src/helper/YarnPackage.ts b/src/helper/YarnPackage.ts index f033a25..fcd53bc 100644 --- a/src/helper/YarnPackage.ts +++ b/src/helper/YarnPackage.ts @@ -64,8 +64,8 @@ export default class YarnPackage extends JsPackage { const button: InlineKeyboardButton = { text : `${key} ${this.package.data.dependencies[key]}`, callbackData : JSON.stringify({ - callback : "yarn", - data : { + c : "yarn", + d : { package : key } }) diff --git a/src/lang/br.ts b/src/lang/br.ts index 8378578..09c589c 100644 --- a/src/lang/br.ts +++ b/src/lang/br.ts @@ -55,9 +55,12 @@ export default { adminReportMessage: "Por que eu reportaria um administrador?", selfWarnMessage: "Por que eu me daria advertência?", adminWarnMessage: "Por que eu daria advertência em um administrador?", + warningNoneMessage: "✔ {username} não tem advertências.", warningSigleMessage: "⚠️ {username} tem {warns} advertências.\n\nMotivo:\n", warningPluralMessage: "⚠️ {username} tem {warns} advertências.\n\nMotivos:\n", warningBanMessage: "❌ {username} levou ban por ter {warns} advertências.\n\nMotivos:\n", + warningAdminRemovedLast: "Última advertência de {username} removida por {adminUsername}.", + warningAdminRemovedAll: "Todas as advertências de {username} foram removidas por {adminUsername}.", reportMessage: "Reportado aos administradores.", federationCreateOnlyPrivate: "Me chame no privado pra criar uma federação.", federationCreateSuccess: "Federação {name} criada com sucesso!\nVocê já pode adicionar grupos usando o comando /fjoin {hash}", @@ -85,4 +88,13 @@ export default { fedBannedMessage: "{username} banido na federação.\nMotivo: {reason}", fedBanOnlyAdminError: "Somente administradores podem banir usuários da federação.", fedBanAdminError: "Você não pode banir administradores da federação.", + macroNoMacroFound: "A lista de macros está vazia.\nPara adicionar uma macro, use o comando /madd .", + macroMalformedCommandError: "Para adicionar uma nova macro, utilize o seguinte comando:\n/madd {macro} {conteúdo}", + macroList: "As seguintes macros estão disponíveis:\n\n", + macroAlreadyExists: "A macro {macro} já existe.", + macroAddError: "Ocorreu um erro ao adicionar a macro. Por favor, tente novamente mais tarde.", + macroRemoveError: "Ocorreu um erro ao remover a macro. Por favor, tente novamente mais tarde.", + lastWarningRemovalButton: "Remover Advertência (somente admins)", + warningsRemovalButton: "Remover todas as advertências (somente admins)", + adminOnlyAction: "Esta ação só pode ser executada por administradores." }; diff --git a/src/lang/us.ts b/src/lang/us.ts index 1547a55..59f1948 100644 --- a/src/lang/us.ts +++ b/src/lang/us.ts @@ -55,9 +55,12 @@ export default { adminReportMessage: "Why would I report an admin?", selfWarnMessage: "Why would I warn myself?", adminWarnMessage: "Why would I warn an admin?", + warningNoneMessage: "✔ {username} has no warnings.", warningSigleMessage: "⚠️ {username} has {warns} warnings.\n\nReason:\n", warningPluralMessage: "⚠️ {username} has {warns} warnings.\n\nReasons:\n", warningBanMessage: "❌ {username} has {warns} warnings and has been banned.\n\nReasons:\n", + warningAdminRemovedLast: "Last warning of {username} removed by {adminUsername}.", + warningAdminRemovedAll: "All warnings of {username} removed by {adminUsername}.", reportMessage: "Reported to the admins.", federationCreateOnlyPrivate: "PM me if you want to create a federation.", federationCreateSuccess: "Federation {name} successfully created.\nYou can now add groups to your federation using the command /fjoin {hash}.", @@ -85,4 +88,13 @@ export default { fedBannedMessage: "{username} banned in federation.\nReason: {reason}", fedBanOnlyAdminError: "Only admins can ban users in a federation.", fedBanAdminError: "You can't ban admins in a federation.", + macroNoMacroFound: "The list of macros is empty.\nTo add a macro, use the command /madd .", + macroMalformedCommandError: "Malformed command. Please use the following syntax:\n/madd {name} {content}", + macroList: "The following macros are available:\n\n", + macroAlreadyExists: "The macro {macro} already exists.", + macroAddError: "An error occurred while adding the macro. Please try again later.", + macroRemoveError: "An error occurred while removing the macro. Please try again later.", + lastWarningRemovalButton: "Remove warning (admin only)", + warningsRemovalButton: "Remove all warnings (admin only)", + adminOnlyAction: "This action can only be performed by admins." }; diff --git a/src/library/telegram/context/CallbackQuery.ts b/src/library/telegram/context/CallbackQuery.ts index 0554bf1..056e16a 100644 --- a/src/library/telegram/context/CallbackQuery.ts +++ b/src/library/telegram/context/CallbackQuery.ts @@ -69,7 +69,7 @@ export default class CallbackQuery { const answer = new AnswerCallbackQuery(); answer .setCallbackQueryId(this.payload.callbackQuery.id) - .setText(this.callbackData.data.package.toUpperCase()); + .setText(content); return answer .post() diff --git a/src/library/telegram/context/Chat.ts b/src/library/telegram/context/Chat.ts index 6304dbc..26c6e77 100644 --- a/src/library/telegram/context/Chat.ts +++ b/src/library/telegram/context/Chat.ts @@ -16,6 +16,7 @@ import { ChatLocation } from "../type/ChatLocation.js"; import { ChatPermissions } from "../type/ChatPermissions.js"; import { ChatPhoto } from "../type/ChatPhoto.js"; import { Message as MessageType } from "../type/Message.js"; +import Log from "../../../helper/Log.js"; export default class Chat { @@ -421,13 +422,17 @@ export default class Chat { .setChatId(this.context.chat.id) .setText(text); + if (typeof this.context.messageThreadId !== "undefined") { + sendMessage.setThreadId(this.context.messageThreadId); + } + if (options) { sendMessage.setOptions(options); } - return sendMessage - .post() + return sendMessage.post() .then((response) => response.json()) - .then((json) => new Message(json.result)); + .then((json) => new Message(json.result)) + .catch((error) => console.error(error)); } } diff --git a/src/library/telegram/context/Message.ts b/src/library/telegram/context/Message.ts index 3ab229e..5ff8a8a 100644 --- a/src/library/telegram/context/Message.ts +++ b/src/library/telegram/context/Message.ts @@ -443,6 +443,10 @@ export default class Message { return; } + if (this.context.replyToMessage.messageId === this.context.messageThreadId) { + return; + } + this.replyToMessage = new Message(this.context.replyToMessage); } @@ -599,6 +603,7 @@ export default class Message { * @return {Record} */ private validateJsonResponse(response: Record): Record { + if (!response.result) { throw new Error(JSON.stringify(response)); } diff --git a/src/library/telegram/context/User.ts b/src/library/telegram/context/User.ts index fc43db7..114e3df 100644 --- a/src/library/telegram/context/User.ts +++ b/src/library/telegram/context/User.ts @@ -207,7 +207,7 @@ export default class User { * * @param untilDate */ - public async ban(untilDate?: number): Promise> { + public async ban(untilDate?: number): Promise { const ban = new BanChatMember(); ban @@ -218,7 +218,16 @@ export default class User { ban.setUntilDate(untilDate); } - return ban.post().then((response) => response.json()); + try { + + const response = await ban.post(); + const json = await response.json(); + + return Promise.resolve(json?.ok || false); + + } catch (error) { + return Promise.resolve(false); + } } /** @@ -229,7 +238,7 @@ export default class User { * * @param onlyIfBanned */ - public async unban(onlyIfBanned?: boolean): Promise> { + public async unban(onlyIfBanned?: boolean): Promise { const unban = new UnbanChatMember(); unban @@ -240,7 +249,16 @@ export default class User { unban.setOnlyIfBanned(false); } - return unban.post().then((response) => response.json()); + try { + + const response = await unban.post(); + const json = await response.json(); + + return Promise.resolve(json?.ok || false); + + } catch (error) { + return Promise.resolve(false); + } } /** @@ -249,7 +267,7 @@ export default class User { * @author Marcos Leandro * @since 2023-06-02 */ - public async kick(): Promise> { + public async kick(): Promise { return this.unban(false); } diff --git a/src/library/telegram/resource/SendMessage.ts b/src/library/telegram/resource/SendMessage.ts index 51ed83a..4701f0c 100644 --- a/src/library/telegram/resource/SendMessage.ts +++ b/src/library/telegram/resource/SendMessage.ts @@ -66,6 +66,21 @@ export default class SendMessage extends TelegramBotApi { return this; } + /** + * Sets the thread id. + * + * @author Marcos Leandro + * @since 2024-04-22 + * + * @param threadId + * + * @return + */ + public setThreadId(threadId: number): SendMessage { + this.payload.messageThreadId = threadId; + return this; + } + /** * Sets the message content. * diff --git a/src/model/Macros.ts b/src/model/Macros.ts new file mode 100644 index 0000000..43d081e --- /dev/null +++ b/src/model/Macros.ts @@ -0,0 +1,25 @@ +/** + * Ada Lovelace Telegram Bot + * + * This file is part of Ada Lovelace Telegram Bot. + * You are free to modify and share this project or its files. + * + * @package mslovelace_bot + * @author Marcos Leandro + * @license GPLv3 + */ + +import DefaultModel from "./Model.js"; + +export default class Macros extends DefaultModel { + + /** + * The constructor. + * + * @author Marcos Leandro + * @since 2023-11-18 + */ + public constructor() { + super("macros"); + } +} diff --git a/src/model/Warns.ts b/src/model/Warnings.ts similarity index 86% rename from src/model/Warns.ts rename to src/model/Warnings.ts index 98d6332..0a1219a 100644 --- a/src/model/Warns.ts +++ b/src/model/Warnings.ts @@ -11,7 +11,7 @@ import DefaultModel from "./Model.js"; -export default class Warns extends DefaultModel { +export default class Warnings extends DefaultModel { /** * The constructor. @@ -20,6 +20,6 @@ export default class Warns extends DefaultModel { * @since 2023-06-14 */ public constructor() { - super("warns"); + super("warnings"); } }