From 18c28cb746e89e0dc4738fa4f2b2334b00f738ab Mon Sep 17 00:00:00 2001 From: Ali Zemani Date: Fri, 30 Aug 2024 00:06:36 +0330 Subject: [PATCH] better kv user model --- .vscode/settings.json | 3 +- src/bot/actions.ts | 200 ++++++++++++++------------ src/bot/bot.ts | 65 ++------- src/bot/commands.ts | 306 +++++++++++++++------------------------- src/types.ts | 24 +--- src/utils/kv-storage.ts | 62 ++++++++ src/utils/messages.ts | 16 --- tools/clear.js | 7 +- 8 files changed, 308 insertions(+), 375 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e19aeb..66ed8fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ - "Nekonymous" + "Nekonymous", + "nekonymousr" ] } \ No newline at end of file diff --git a/src/bot/actions.ts b/src/bot/actions.ts index 98a8d08..b487b24 100644 --- a/src/bot/actions.ts +++ b/src/bot/actions.ts @@ -1,5 +1,5 @@ import { Context, InlineKeyboard } from "grammy"; -import { BlockList, Conversation, CurrentConversation } from "../types"; +import { Conversation, User } from "../types"; import { KVModel } from "../utils/kv-storage"; import Logger from "../utils/logs"; import { @@ -40,62 +40,66 @@ export const createReplyKeyboard = ( * It verifies the conversation context and initiates a reply by linking it to the correct conversation. * * @param {Context} ctx - The context of the current Telegram update. - * @param {KVModel} currentConversationModel - KVModel instance for managing current conversations. - * @param {KVModel} userBlockListModel - KVModel instance for managing user block lists. + * @param {KVModel} userModel - KVModel instance for managing user data. * @param {KVModel} conversationModel - KVModel instance for managing encrypted conversation data. * @param {Logger} logger - Logger instance for saving logs to R2. * @param {string} APP_SECURE_KEY - The application-specific secure key. */ export const handleReplyAction = async ( ctx: Context, - currentConversationModel: KVModel, - userBlockListModel: KVModel, + userModel: KVModel, conversationModel: KVModel, logger: Logger, APP_SECURE_KEY: string ): Promise => { const ticketId = ctx.match[1]; - const conversationId = getConversationId(ticketId, APP_SECURE_KEY); - const currentUserId = ctx.from?.id!; + + const conversationId = getConversationId(ticketId, APP_SECURE_KEY); const conversationData = await conversationModel.get(conversationId); + + if (!conversationData) { + await ctx.reply(NoConversationFoundMessage); + await logger.saveLog("new_replay_failed", {}); + await ctx.answerCallbackQuery(); + return; + } + const rawConversation = await decryptPayload( ticketId, - conversationData!, + conversationData, APP_SECURE_KEY ); const parentConversation: Conversation = JSON.parse(rawConversation); try { - if (parentConversation) { - const blockList = - (await userBlockListModel.get(parentConversation.from.toString())) || - {}; - if (blockList[currentUserId]) { - await ctx.reply(USER_IS_BLOCKED_MESSAGE); - await ctx.answerCallbackQuery(); - return; - } + const otherUser = await userModel.get(parentConversation.from.toString()); - const conversation = { - to: parentConversation.from, - reply_to_message_id: parentConversation.reply_to_message_id, - }; - await currentConversationModel.save( - currentUserId.toString(), - conversation - ); - await ctx.reply(REPLAY_TO_MESSAGE); - await logger.saveLog("new_replay_success", {}); - } else { - await ctx.reply(NoConversationFoundMessage); - await logger.saveLog("new_replay_failed", {}); + // Check if the other user has blocked the current user + if (otherUser?.blockList.includes(currentUserId.toString())) { + await ctx.reply(USER_IS_BLOCKED_MESSAGE); + await ctx.answerCallbackQuery(); + return; } + + const conversation = { + to: parentConversation.from, + reply_to_message_id: parentConversation.reply_to_message_id, + }; + + await userModel.updateField( + currentUserId.toString(), + "currentConversation", + conversation + ); + await ctx.reply(REPLAY_TO_MESSAGE); + await logger.saveLog("new_replay_success", {}); } catch (error) { - await ctx.reply(JSON.stringify(error)); - await logger.saveLog("new_replay_unknown", error); + await ctx.reply(HuhMessage); + await logger.saveLog("new_replay_unknown", { error }); + } finally { + await ctx.answerCallbackQuery(); } - await ctx.answerCallbackQuery(); }; /** @@ -105,57 +109,63 @@ export const handleReplyAction = async ( * It ensures that further communication from the blocked user is prevented until they are unblocked. * * @param {Context} ctx - The context of the current Telegram update. - * @param {KVModel} userBlockListModel - KVModel instance for managing user block lists. + * @param {KVModel} userModel - KVModel instance for managing user data. * @param {KVModel} conversationModel - KVModel instance for managing encrypted conversation data. * @param {Logger} logger - Logger instance for saving logs to R2. * @param {string} APP_SECURE_KEY - The application-specific secure key. */ export const handleBlockAction = async ( ctx: Context, - userBlockListModel: KVModel, + userModel: KVModel, conversationModel: KVModel, logger: Logger, APP_SECURE_KEY: string ): Promise => { const ticketId = ctx.match[1]; - const conversationId = getConversationId(ticketId, APP_SECURE_KEY); - const currentUserId = ctx.from?.id!; + + const conversationId = getConversationId(ticketId, APP_SECURE_KEY); const conversationData = await conversationModel.get(conversationId); + + if (!conversationData) { + await ctx.reply(HuhMessage); + await logger.saveLog("user_block_failed", {}); + await ctx.answerCallbackQuery(); + return; + } + const rawConversation = await decryptPayload( ticketId, - conversationData!, + conversationData, APP_SECURE_KEY ); const parentConversation: Conversation = JSON.parse(rawConversation); + try { - if (parentConversation) { - let blockList = await userBlockListModel.get(currentUserId.toString()); - if (!blockList) { - blockList = {}; - } + await userModel.updateField( + currentUserId.toString(), + "blockList", + parentConversation.from.toString(), + true + ); - blockList[parentConversation.from?.toString()] = true; - await userBlockListModel.save(currentUserId.toString(), blockList); - await ctx.reply(USER_BLOCKED_MESSAGE); + await ctx.reply(USER_BLOCKED_MESSAGE); - const replyKeyboard = createReplyKeyboard(ticketId, true); - await ctx.api.editMessageReplyMarkup( - ctx.chat?.id!, - ctx.callbackQuery?.message?.message_id!, - { - reply_markup: replyKeyboard, - } - ); - await logger.saveLog("user_block_success", {}); - } else { - await ctx.reply(HuhMessage); - await logger.saveLog("user_block_failed", {}); - } + const replyKeyboard = createReplyKeyboard(ticketId, true); + await ctx.api.editMessageReplyMarkup( + ctx.chat?.id!, + ctx.callbackQuery?.message?.message_id!, + { + reply_markup: replyKeyboard, + } + ); + await logger.saveLog("user_block_success", {}); } catch (error) { - await logger.saveLog("user_block_unknown", error); + await ctx.reply(HuhMessage); + await logger.saveLog("user_block_unknown", { error }); + } finally { + await ctx.answerCallbackQuery(); } - await ctx.answerCallbackQuery(); }; /** @@ -165,57 +175,67 @@ export const handleBlockAction = async ( * allowing communication to resume between the two users. * * @param {Context} ctx - The context of the current Telegram update. - * @param {KVModel} userBlockListModel - KVModel instance for managing user block lists. + * @param {KVModel} userModel - KVModel instance for managing user data. * @param {KVModel} conversationModel - KVModel instance for managing encrypted conversation data. * @param {Logger} logger - Logger instance for saving logs to R2. * @param {string} APP_SECURE_KEY - The application-specific secure key. */ export const handleUnblockAction = async ( ctx: Context, - userBlockListModel: KVModel, + userModel: KVModel, conversationModel: KVModel, logger: Logger, APP_SECURE_KEY: string ): Promise => { const ticketId = ctx.match[1]; - const conversationId = getConversationId(ticketId, APP_SECURE_KEY); - const currentUserId = ctx.from?.id!; + + const conversationId = getConversationId(ticketId, APP_SECURE_KEY); const conversationData = await conversationModel.get(conversationId); + + if (!conversationData) { + await ctx.reply(HuhMessage); + await logger.saveLog("user_unblock_failed2", {}); + await ctx.answerCallbackQuery(); + return; + } + const rawConversation = await decryptPayload( ticketId, - conversationData!, + conversationData, APP_SECURE_KEY ); const parentConversation: Conversation = JSON.parse(rawConversation); + try { - if (parentConversation) { - let blockList = await userBlockListModel.get(currentUserId.toString()); - if (blockList && blockList[parentConversation.from.toString()]) { - delete blockList[parentConversation.from.toString()]; - await userBlockListModel.save(currentUserId.toString(), blockList); - await ctx.reply(USER_UNBLOCKED_MESSAGE); - - const replyKeyboard = createReplyKeyboard(ticketId, false); - await ctx.api.editMessageReplyMarkup( - ctx.chat?.id!, - ctx.callbackQuery?.message?.message_id!, - { - reply_markup: replyKeyboard, - } - ); - await logger.saveLog("user_unblock_success", {}); - } else { - await ctx.reply(HuhMessage); - await logger.saveLog("user_unblock_failed1", {}); - } + const currentUser = await userModel.get(currentUserId.toString()); + + if (currentUser?.blockList.includes(parentConversation.from.toString())) { + await userModel.popItemFromField( + currentUserId.toString(), + "blockList", + parentConversation.from.toString() + ); + + await ctx.reply(USER_UNBLOCKED_MESSAGE); + + const replyKeyboard = createReplyKeyboard(ticketId, false); + await ctx.api.editMessageReplyMarkup( + ctx.chat?.id!, + ctx.callbackQuery?.message?.message_id!, + { + reply_markup: replyKeyboard, + } + ); + await logger.saveLog("user_unblock_success", {}); } else { await ctx.reply(HuhMessage); - await logger.saveLog("user_unblock_failed2", {}); + await logger.saveLog("user_unblock_failed1", {}); } } catch (error) { - await logger.saveLog("user_unblock_unknown", error); + await ctx.reply(HuhMessage); + await logger.saveLog("user_unblock_unknown", { error }); + } finally { + await ctx.answerCallbackQuery(); } - - await ctx.answerCallbackQuery(); }; diff --git a/src/bot/bot.ts b/src/bot/bot.ts index 2d9312c..205ee08 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -1,17 +1,13 @@ import { Bot } from "grammy"; -import { BlockList, CurrentConversation, Environment, User } from "../types"; +import { Environment, User } from "../types"; import { KVModel } from "../utils/kv-storage"; -import Logger from "../utils/logs"; // Import Logger class +import Logger from "../utils/logs"; import { handleBlockAction, handleReplyAction, handleUnblockAction, } from "./actions"; -import { - handleDeleteUserCommand, - handleMessage, - handleStartCommand, -} from "./commands"; +import { handleMessage, handleStartCommand } from "./commands"; /** * Initializes and configures a new instance of the Telegram bot. @@ -38,13 +34,8 @@ export const createBot = (env: Environment) => { // Initialize KV models for different data types const userModel = new KVModel("user", NekonymousKV); - const userBlockListModel = new KVModel("blockList", NekonymousKV); const conversationModel = new KVModel("conversation", NekonymousKV); - const currentConversationModel = new KVModel( - "currentConversation", - NekonymousKV - ); - const userIdToUUID = new KVModel("userIdToUUID", NekonymousKV); + const userUUIDtoId = new KVModel("userUUIDtoId", NekonymousKV); // Initialize Logger const logger = new Logger(nekonymousr2); @@ -57,24 +48,7 @@ export const createBot = (env: Environment) => { * in the KV storage. */ bot.command("start", (ctx) => - handleStartCommand( - ctx, - userModel, - userIdToUUID, - userBlockListModel, - currentConversationModel, - logger - ) - ); - - /** - * Handles the /deleteAccount command. - * - * When a user sends the /deleteAccount command, this handler will delete their user records - * from the KV storage, effectively removing their presence from the system. - */ - bot.command("deleteAccount", (ctx) => - handleDeleteUserCommand(ctx, userModel, userIdToUUID, logger) + handleStartCommand(ctx, userModel, userUUIDtoId, logger) ); /** @@ -85,15 +59,7 @@ export const createBot = (env: Environment) => { * the current context. */ bot.on("message", (ctx) => - handleMessage( - ctx, - userIdToUUID, - userBlockListModel, - currentConversationModel, - conversationModel, - logger, - APP_SECURE_KEY - ) + handleMessage(ctx, userModel, conversationModel, logger, APP_SECURE_KEY) ); /** @@ -104,14 +70,7 @@ export const createBot = (env: Environment) => { * preparing the bot to receive the reply. */ bot.callbackQuery(/^reply_(.+)$/, (ctx) => - handleReplyAction( - ctx, - currentConversationModel, - userBlockListModel, - conversationModel, - logger, - APP_SECURE_KEY - ) + handleReplyAction(ctx, userModel, conversationModel, logger, APP_SECURE_KEY) ); /** @@ -121,13 +80,7 @@ export const createBot = (env: Environment) => { * their block list, preventing further communication until the block is removed. */ bot.callbackQuery(/^block_(.+)$/, (ctx) => - handleBlockAction( - ctx, - userBlockListModel, - conversationModel, - logger, - APP_SECURE_KEY - ) + handleBlockAction(ctx, userModel, conversationModel, logger, APP_SECURE_KEY) ); /** @@ -139,7 +92,7 @@ export const createBot = (env: Environment) => { bot.callbackQuery(/^unblock_(.+)$/, (ctx) => handleUnblockAction( ctx, - userBlockListModel, + userModel, conversationModel, logger, APP_SECURE_KEY diff --git a/src/bot/commands.ts b/src/bot/commands.ts index bfbfab7..0b03d62 100644 --- a/src/bot/commands.ts +++ b/src/bot/commands.ts @@ -1,21 +1,18 @@ import { Context, Keyboard } from "grammy"; import { WebUUID } from "web-uuid"; -import { BlockList, CurrentConversation, User } from "../types"; +import { User } from "../types"; import { KVModel } from "../utils/kv-storage"; -import Logger from "../utils/logs"; // Import Logger class +import Logger from "../utils/logs"; import { ABOUT_PRIVACY_COMMAND_MESSAGE, - DELETE_USER_COMMAND_MESSAGE, HuhMessage, MESSAGE_SENT_MESSAGE, NoUserFoundMessage, - SETTINGS_COMMAND_MESSAGE, StartConversationMessage, UnsupportedMessageTypeMessage, USER_IS_BLOCKED_MESSAGE, USER_LINK_MESSAGE, WelcomeMessage, - SHUFFLE_MODE_COMMAND_MESSAGE, } from "../utils/messages"; import { encryptedPayload, @@ -27,91 +24,83 @@ import { createReplyKeyboard } from "./actions"; // Main menu keyboard used across various commands const mainMenu = new Keyboard() - .text("شافل مود:)") .text("دریافت لینک") - .row() .text("درباره و حریم خصوصی") - .text("تنظیمات") .resized(); /** * Handles the /start command to initiate or continue a user's interaction with the bot. - * - * This function manages the user's entry point into the bot's system. It handles new users by generating - * a unique UUID and storing it in the KV store, or continues the interaction for existing users. + * It generates a unique UUID for new users and continues the interaction for existing users. * * @param {Context} ctx - The context of the current Telegram update. * @param {KVModel} userModel - KVModel instance for managing user data. - * @param {KVModel} userIdToUUID - KVModel instance for mapping user IDs to UUIDs. - * @param {KVModel} userBlockListModel - KVModel instance for managing user block lists. - * @param {KVModel} currentConversationModel - KVModel instance for managing current conversations. + * @param {KVModel} userUUIDtoId - KVModel instance for managing UUID to user ID mapping. + * @param {Logger} logger - Logger instance for saving logs. */ export const handleStartCommand = async ( ctx: Context, userModel: KVModel, - userIdToUUID: KVModel, - userBlockListModel: KVModel, - currentConversationModel: KVModel, + userUUIDtoId: KVModel, logger: Logger ): Promise => { - const currentUserId: number = ctx.from?.id!; - let currentUserUUID = await userIdToUUID.get(currentUserId.toString()); + const currentUserId = ctx.from?.id!; - // New user: Generate UUID and save to KV storage if (!ctx.match) { - if (!currentUserUUID) { - currentUserUUID = new WebUUID().toString(); - await userIdToUUID.save(currentUserId.toString(), currentUserUUID); - await userModel.save(currentUserUUID, { - userId: currentUserId, - userName: ctx.from?.first_name!, - }); - await logger.saveLog("new_user_success", {}); - } + try { + let currentUserUUID = ""; + const currentUser = await userModel.get(currentUserId.toString()); - // Send welcome message with the user's unique bot link - await ctx.reply( - WelcomeMessage.replace( - "UUID_USER_URL", - `https://t.me/nekonymous_bot?start=${currentUserUUID}` - ), - { - reply_markup: mainMenu, + if (!currentUser) { + currentUserUUID = new WebUUID().toString(); + await userUUIDtoId.save(currentUserUUID, currentUserId.toString()); + await userModel.save(currentUserId.toString(), { + userUUID: currentUserUUID, + userName: ctx.from?.first_name, + blockList: [], + currentConversation: {}, + }); + await logger.saveLog("new_user_success", {}); + } else { + currentUserUUID = currentUser.userUUID; } - ); - // Log the new user action + + await ctx.reply( + WelcomeMessage.replace( + "UUID_USER_URL", + `https://t.me/nekonymous_bot?start=${currentUserUUID}` + ), + { + reply_markup: mainMenu, + } + ); + } catch (error) { + await logger.saveLog("new_user_failed", error); + } } else if (typeof ctx.match === "string") { - // User initiated bot with another user's UUID (e.g., from a shared link) const otherUserUUID = ctx.match; - const otherUser = await userModel.get(otherUserUUID); + const otherUserId = await userUUIDtoId.get(otherUserUUID); - if (otherUser) { - const blockList = - (await userBlockListModel.get(otherUser.userId.toString())) || {}; - if (blockList[currentUserId]) { + if (otherUserId) { + const otherUser = await userModel.get(otherUserId); + if (otherUser?.blockList.includes(currentUserId)) { await ctx.reply(USER_IS_BLOCKED_MESSAGE); return; } - // Establish conversation with the other user - const conversation = { - to: otherUser.userId, - }; - await currentConversationModel.save( + await userModel.updateField( currentUserId.toString(), - conversation + "currentConversation", + { to: otherUserId } ); await ctx.reply( StartConversationMessage.replace("USER_NAME", otherUser.userName) ); await logger.saveLog("new_conversation_success", {}); } else { - // No user found with the provided UUID - await logger.saveLog("new_conversation_failed", {}); + await logger.saveLog("new_conversation_failed", { NoUserFoundMessage }); await ctx.reply(NoUserFoundMessage); } } else { - // Handle unexpected cases await ctx.reply(HuhMessage, { reply_markup: mainMenu, }); @@ -120,147 +109,120 @@ export const handleStartCommand = async ( }; /** - * Handles menu-related commands such as "دریافت لینک", "تنظیمات", "درباره", etc. - * - * This function manages the main menu commands available to the user. - * It responds to specific commands by sending the appropriate message or performing the related action. + * Handles menu-related commands such as "دریافت لینک", "درباره", etc. * * @param {Context} ctx - The context of the current Telegram update. - * @param {KVModel} userIdToUUID - KVModel instance for mapping user IDs to UUIDs. + * @param {string} userUUID - The UUID of the current user. * @returns {Promise} - Returns true if a command was successfully handled, otherwise false. */ export const handleMenuCommand = async ( ctx: Context, - userIdToUUID: KVModel + userUUID: string ): Promise => { - const currentUserId = ctx.from?.id!; const msgPayload = ctx.message?.text; - const currentUserUUID = await userIdToUUID.get(currentUserId.toString()); switch (msgPayload) { case "دریافت لینک": await ctx.reply( USER_LINK_MESSAGE.replace( "UUID_USER_URL", - `https://t.me/nekonymous_bot?start=${currentUserUUID}` + `https://t.me/nekonymous_bot?start=${userUUID}` ), { reply_markup: mainMenu, } ); break; - - case "تنظیمات": - await ctx.reply(SETTINGS_COMMAND_MESSAGE, { - reply_markup: mainMenu, - }); - break; - case "شافل مود:)": - await ctx.reply(SHUFFLE_MODE_COMMAND_MESSAGE, { - reply_markup: mainMenu, - }); - break; - case "درباره و حریم خصوصی": await ctx.reply(escapeMarkdownV2(ABOUT_PRIVACY_COMMAND_MESSAGE), { reply_markup: mainMenu, parse_mode: "MarkdownV2", }); break; - default: - return false; // Command not found in the menu + return false; } - return true; // Command was handled + return true; }; /** - * Handles all incoming messages that are not menu commands. - * - * This function processes messages that don't correspond to menu commands, such as normal text messages or media. - * It checks if the user is currently engaged in a conversation and routes the message accordingly. + * Handles all incoming messages that are not menu commands, routing them based on the user's current conversation state. * * @param {Context} ctx - The context of the current Telegram update. - * @param {KVModel} userIdToUUID - KVModel instance for mapping user IDs to UUIDs. - * @param {KVModel} userBlockListModel - KVModel instance for managing user block lists. - * @param {KVModel} currentConversationModel - KVModel instance for managing current conversations. - * @param {KVModel} conversationModel - KVModel instance for managing encrypted conversation data. - * @param {Logger} logger - Logger instance for saving logs to R2. -* @param {string} APP_SECURE_KEY - The application-specific secure key. - -*/ + * @param {KVModel} userModel - KVModel instance for managing user data. + * @param {KVModel} conversationModel - KVModel instance for managing conversation data. + * @param {Logger} logger - Logger instance for saving logs. + * @param {string} APP_SECURE_KEY - The application-specific secure key. + */ export const handleMessage = async ( ctx: Context, - userIdToUUID: KVModel, - userBlockListModel: KVModel, - currentConversationModel: KVModel, + userModel: KVModel, conversationModel: KVModel, logger: Logger, APP_SECURE_KEY: string ): Promise => { const currentUserId = ctx.from?.id!; + const currentUser = await userModel.get(currentUserId.toString()); - // First, check if the message is a menu command - const isMenuCommandHandled = await handleMenuCommand(ctx, userIdToUUID); - if (isMenuCommandHandled) { - return; // If a menu command was handled, return early - } - - const currentConversation = await currentConversationModel.get( - currentUserId.toString() + const isMenuCommandHandled = await handleMenuCommand( + ctx, + currentUser?.userUUID || "" ); + if (isMenuCommandHandled) return; - if (!currentConversation) { - // If no conversation is active, respond with a generic error message + if (!currentUser?.currentConversation?.to) { await ctx.reply(HuhMessage, { reply_markup: mainMenu, }); await logger.saveLog("current_conversation_failed", {}); - return; } try { const ticketId = generateTicketId(APP_SECURE_KEY); - const blockList = - (await userBlockListModel.get(currentConversation.to.toString())) || {}; - const isBlocked = !!blockList[currentUserId]; + const otherUser = await userModel.get( + currentUser.currentConversation.to.toString() + ); + const isBlocked = + otherUser?.blockList.includes(currentUserId.toString()) || false; const replyOptions: any = { reply_markup: createReplyKeyboard(ticketId, isBlocked), }; - // Conditionally add the reply_to_message_id parameter if reply_to_message_id exists - if (currentConversation.reply_to_message_id) { + if (currentUser.currentConversation.reply_to_message_id) { replyOptions.reply_to_message_id = - currentConversation.reply_to_message_id; + currentUser.currentConversation.reply_to_message_id; } - if (ctx.message?.text) { - // Handle text messages with MarkdownV2 spoilers + switch (true) { + case !!ctx.message?.text: await ctx.api.sendMessage( - currentConversation.to, - escapeMarkdownV2(ctx.message.text), + currentUser.currentConversation.to, + escapeMarkdownV2(ctx.message.text!), { parse_mode: "MarkdownV2", ...replyOptions, } ); - } else if (ctx.message?.photo) { - // Handle photo messages with an optional caption - const photo = ctx.message.photo[ctx.message.photo.length - 1]; - await ctx.api.sendPhoto(currentConversation.to, photo.file_id, { - ...replyOptions, - caption: ctx.message.caption - ? escapeMarkdownV2(ctx.message.caption) - : undefined, - parse_mode: "MarkdownV2", - }); - } else if (ctx.message?.video) { - // Handle video messages with an optional caption + break; + case !!ctx.message?.photo: + await ctx.api.sendPhoto( + currentUser.currentConversation.to, + ctx.message.photo[ctx.message.photo.length - 1].file_id, + { + ...replyOptions, + caption: ctx.message.caption + ? escapeMarkdownV2(ctx.message.caption) + : undefined, + parse_mode: "MarkdownV2", + } + ); + break; + case !!ctx.message?.video: await ctx.api.sendVideo( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.video.file_id, { ...replyOptions, @@ -270,9 +232,10 @@ export const handleMessage = async ( parse_mode: "MarkdownV2", } ); - } else if (ctx.message?.animation) { + break; + case !!ctx.message?.animation: await ctx.api.sendAnimation( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.animation.file_id, { ...replyOptions, @@ -282,10 +245,10 @@ export const handleMessage = async ( parse_mode: "MarkdownV2", } ); - } else if (ctx.message?.document) { - // Handle file/document messages with an optional caption + break; + case !!ctx.message?.document: await ctx.api.sendDocument( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.document.file_id, { ...replyOptions, @@ -295,36 +258,36 @@ export const handleMessage = async ( parse_mode: "MarkdownV2", } ); - } else if (ctx.message?.sticker) { - // Handle sticker messages + break; + case !!ctx.message?.sticker: await ctx.api.sendSticker( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.sticker.file_id, replyOptions ); - } else if (ctx.message?.voice) { - // Handle voice messages + break; + case !!ctx.message?.voice: await ctx.api.sendVoice( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.voice.file_id, replyOptions ); - } else if (ctx.message?.video_note) { - // Handle video note messages + break; + case !!ctx.message?.video_note: await ctx.api.sendVideoNote( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.video_note.file_id, replyOptions ); - } else if (ctx.message?.audio) { - // Handle audio messages + break; + case !!ctx.message?.audio: await ctx.api.sendAudio( - currentConversation.to, + currentUser.currentConversation.to, ctx.message.audio.file_id, replyOptions ); - } else { - // If the message type is not recognized, respond with an error message or handle accordingly + break; + default: await ctx.reply(UnsupportedMessageTypeMessage, replyOptions); } @@ -336,13 +299,18 @@ export const handleMessage = async ( ticketId, JSON.stringify({ from: currentUserId, - to: currentConversation.to, + to: currentUser.currentConversation.to, reply_to_message_id: ctx.message?.message_id, }), APP_SECURE_KEY ); + await conversationModel.save(conversationId, conversationData); - await currentConversationModel.delete(currentUserId.toString()); + await userModel.updateField( + currentUserId.toString(), + "currentConversation", + undefined + ); } catch (error) { await ctx.reply(HuhMessage + JSON.stringify(error), { reply_markup: mainMenu, @@ -350,45 +318,3 @@ export const handleMessage = async ( await logger.saveLog("new_conversation_unknown", error); } }; - -/** - * Handles the /deleteAccount command to remove a user's data from the bot's storage. - * - * This function deletes the user's record, UUID mapping, and any other associated data, - * effectively removing them from the bot's system. - * - * @param {Context} ctx - The context of the current Telegram update. - * @param {KVModel} userModel - KVModel instance for managing user data. - * @param {KVModel} userIdToUUID - KVModel instance for mapping user IDs to UUIDs. - * @param {Logger} logger - Logger instance for saving logs to R2. - - */ -export const handleDeleteUserCommand = async ( - ctx: Context, - userModel: KVModel, - userIdToUUID: KVModel, - logger: Logger -): Promise => { - const currentUserId = ctx.from?.id!; - const currentUserUUID = await userIdToUUID.get(currentUserId.toString()); - - try { - if (currentUserUUID) { - await userModel.delete(currentUserUUID); - await userIdToUUID.delete(currentUserId.toString()); - - // Log the delete user action - await logger.saveLog("delete_user_success", {}); - - await ctx.reply(DELETE_USER_COMMAND_MESSAGE, { - reply_markup: mainMenu, - }); - } else { - await logger.saveLog("delete_user_failed", {}); - await ctx.reply(NoUserFoundMessage); - } - } catch (error) { - await ctx.reply(JSON.stringify(error)); - await logger.saveLog("delete_user_unknown", error); - } -}; diff --git a/src/types.ts b/src/types.ts index 754e71b..135a757 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,12 @@ */ export interface User { userName: string; - userId: number; + userUUID: string; + blockList: number[]; + currentConversation?: { + to?: number; + reply_to_message_id?: number; + }; } /** @@ -15,23 +20,6 @@ export interface Conversation { reply_to_message_id: number; } -/** - * Interface representing the CurrentConversation state of a user. - * This tracks the current conversation a user is engaged in, along with the parent message ID. - */ -export interface CurrentConversation { - to: number; - reply_to_message_id?: number; -} - -/** - * Interface representing a BlockList. - * The key is a user ID (string) and the value is a boolean indicating if the user is blocked. - */ -export interface BlockList { - [userId: string]: boolean; -} - /** * Interface representing the Environment variables used in the bot. */ diff --git a/src/utils/kv-storage.ts b/src/utils/kv-storage.ts index 60b48a2..06fee25 100644 --- a/src/utils/kv-storage.ts +++ b/src/utils/kv-storage.ts @@ -83,4 +83,66 @@ export class KVModel> { }); return keys.keys.length; } + + /** + * Updates a specific field in the stored record. If the field is an array, this method + * allows you to push a new item into that array. + * + * @param {string} id - The unique identifier for the record. + * @param {string} field - The field to update within the record. + * @param {any} value - The value to set or push to the field. + * @param {boolean} [push=false] - Whether to push the value to an array or simply update the field. + * @returns {Promise} - A promise that resolves when the operation completes. + */ + async updateField( + id: string, + field: keyof T, + value: any, + push: boolean = false + ): Promise { + const record = await this.get(id); + + if (!record) { + throw new Error(`Record with ID ${id} not found.`); + } + + if (push && Array.isArray(record[field])) { + // Ensure no duplicates before pushing the new value + if (!(record[field] as any[]).includes(value)) { + (record[field] as any[]).push(value); + } + } else { + record[field] = value; + } + + await this.save(id, record); + } + + /** + * Removes an item from an array field in the stored record. + * + * @param {string} id - The unique identifier for the record. + * @param {string} field - The field to update within the record. + * @param {any} value - The value to remove from the array. + * @returns {Promise} - A promise that resolves when the operation completes. + */ + async popItemFromField( + id: string, + field: keyof T, + value: any + ): Promise { + const record = await this.get(id); + + if (!record) { + throw new Error(`Record with ID ${id} not found.`); + } + + if (Array.isArray(record[field])) { + record[field] = (record[field] as any[]).filter((item) => item !== value); + } else { + throw new Error(`Field ${String(field)} is not an array.`); + } + + await this.save(id, record); + } } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 1f2322b..39327f1 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -71,16 +71,6 @@ export const YOU_ARE_UNBLOCKED_MESSAGE = ` محدودیت‌های قبلی برداشته شده است. `; -export const SETTINGS_COMMAND_MESSAGE = ` -تنظیمات: -/updateName: به‌روزرسانی نام کاربری -/pauseAccount: توقف دریافت پیام‌ها -/deleteAccount: حذف حساب کاربری -`; -export const SHUFFLE_MODE_COMMAND_MESSAGE = ` -یک قاعده کاملا رندوم ولی با معنی :) -`; - export const ABOUT_PRIVACY_COMMAND_MESSAGE = ` درباره نِکونیموس: (نسخه آزمایشی!) @@ -107,12 +97,6 @@ export const ABOUT_PRIVACY_COMMAND_MESSAGE = ` لینک بات : https://nekonymous.alizemani.ir/ `; - -export const DELETE_USER_COMMAND_MESSAGE = ` -حساب کاربری شما و تمام پیام‌های مرتبط به طور کامل حذف شد. -اگر قصد بازگشت دارید، همیشه خوش آمدید! -`; - export const UnsupportedMessageTypeMessage = ` فرمت فایل پشتیبانی نمی‌شود! لطفاً فرمت‌های پشتیبانی شده مانند متن، تصویر، ویدئو و ... را ارسال کنید. diff --git a/tools/clear.js b/tools/clear.js index 8852f1b..29ecbbd 100644 --- a/tools/clear.js +++ b/tools/clear.js @@ -1,8 +1,8 @@ const { exec } = require("child_process"); const fs = require("fs"); -const NAMESPACE_ID = "414fb07aae8b4b5d9fac86a0cad1720e"; // replace with your namespace ID -const CONVERSATION_PREFIX = "conversation:"; // Prefix for conversation keys +const NAMESPACE_ID = "de26a1b398614383a2b9702fafaa8824"; // replace with your namespace ID +// const CONVERSATION_PREFIX = "conversation:"; // Prefix for conversation keys // Step 1: List all keys exec( @@ -15,8 +15,7 @@ exec( // Step 2: Filter keys that start with the conversation prefix const keys = JSON.parse(stdout) - .map((keyObj) => keyObj.name) - .filter((key) => key.startsWith(CONVERSATION_PREFIX)); + .map((keyObj) => keyObj.name); if (keys.length === 0) { console.log("No conversation keys found to delete.");