From 4251f1a0bd7e14997358258a5b60e7cde16e8675 Mon Sep 17 00:00:00 2001 From: Artyom Ashirov <1323ED5@gmail.com> Date: Fri, 15 Nov 2024 02:26:13 +0300 Subject: [PATCH] user deletion/blocking --- src/config/database.js | 1 + src/handlers/adminLocationHandler.js | 4 +- src/handlers/adminUserHandler.js | 74 ++++++++++++- src/handlers/userHandler.js | 158 +++++++++++++++------------ src/index.js | 32 +++++- src/models/User.js | 120 +++++++++++++------- 6 files changed, 278 insertions(+), 111 deletions(-) diff --git a/src/config/database.js b/src/config/database.js index e7a2e22..33c0ff6 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -94,6 +94,7 @@ const initDb = async () => { country TEXT, city TEXT, district TEXT, + status INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); diff --git a/src/handlers/adminLocationHandler.js b/src/handlers/adminLocationHandler.js index e23233b..7c05143 100644 --- a/src/handlers/adminLocationHandler.js +++ b/src/handlers/adminLocationHandler.js @@ -215,7 +215,7 @@ export default class AdminLocationHandler { const keyboard = { inline_keyboard: locations.map(loc => [{ text: `${loc.country} > ${loc.city} > ${loc.district} (P:${loc.product_count} C:${loc.category_count})`, - callback_data: `confirm_delete_${loc.country}_${loc.city}_${loc.district}` + callback_data: `confirm_delete_location_${loc.country}_${loc.city}_${loc.district}` }]) }; @@ -239,7 +239,7 @@ export default class AdminLocationHandler { async handleConfirmDelete(callbackQuery) { const chatId = callbackQuery.message.chat.id; const [country, city, district] = callbackQuery.data - .replace('confirm_delete_', '') + .replace('confirm_delete_location_', '') .split('_'); try { diff --git a/src/handlers/adminUserHandler.js b/src/handlers/adminUserHandler.js index 25717ef..7ee340d 100644 --- a/src/handlers/adminUserHandler.js +++ b/src/handlers/adminUserHandler.js @@ -265,7 +265,7 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price} const chatId = callbackQuery.message.chat.id; try { - await User.delete(userId); + await User.updateUserStatus(userId, 1); const keyboard = { inline_keyboard: [ @@ -273,6 +273,12 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price} ] }; + try { + await this.bot.sendMessage(userId, '⚠️Your account has been deleted by administrator'); + } catch (e) { + // ignore if we can't notify user + } + await this.bot.editMessageText( `✅ User ${userId} has been successfully deleted.`, { @@ -287,6 +293,72 @@ ${purchases.map(p => ` • ${p.product_name} x${p.quantity} - $${p.total_price} } } + async handleBlockUser(callbackQuery) { + if (!this.isAdmin(callbackQuery.from.id)) return; + + const userId = callbackQuery.data.replace('block_user_', ''); + const chatId = callbackQuery.message.chat.id; + + try { + const keyboard = { + inline_keyboard: [ + [ + {text: '✅ Confirm Block', callback_data: `confirm_block_user_${userId}`}, + {text: '❌ Cancel', callback_data: `view_user_${userId}`} + ] + ] + }; + + await this.bot.editMessageText( + `⚠️ Are you sure you want to block user ${userId}?`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: keyboard, + parse_mode: 'HTML' + } + ); + } catch (error) { + console.error('Error in handleBlockUser:', error); + await this.bot.sendMessage(chatId, 'Error processing block request. Please try again.'); + } + } + + async handleConfirmBlock(callbackQuery) { + if (!this.isAdmin(callbackQuery.from.id)) return; + + const userId = callbackQuery.data.replace('confirm_block_user_', ''); + const chatId = callbackQuery.message.chat.id; + + try { + await User.updateUserStatus(userId, 2); + + const keyboard = { + inline_keyboard: [ + [{text: '« Back to User List', callback_data: 'admin_users'}] + ] + }; + + try { + await this.bot.sendMessage(userId, '⚠️Your account has been blocked by administrator'); + } catch (e) { + // ignore if we can't notify user + } + + await this.bot.editMessageText( + `✅ User ${userId} has been successfully blocked.`, + { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: keyboard + } + ); + } catch (error) { + console.error('Error in handleConfirmBlock:', error); + await this.bot.sendMessage(chatId, 'Error blocking user. Please try again.'); + } + } + async handleEditUserBalance(callbackQuery) { if (!this.isAdmin(callbackQuery.from.id)) return; diff --git a/src/handlers/userHandler.js b/src/handlers/userHandler.js index 6aa4b49..3dcac62 100644 --- a/src/handlers/userHandler.js +++ b/src/handlers/userHandler.js @@ -1,28 +1,52 @@ -import db from '../config/database.js'; import User from '../models/User.js'; +import config from "../config/config.js"; export default class UserHandler { - constructor(bot) { - this.bot = bot; - } + constructor(bot) { + this.bot = bot; + } - async showProfile(msg) { - const chatId = msg.chat.id; - const userId = msg.from.id; + async canUseBot(msg) { + const userId = msg.from.id; + const user = await User.getById(userId); - try { - const userStats = await User.getUserStats(userId); + const keyboard = { + inline_keyboard: [ + [{text: "Contact support", url: config.SUPPORT_LINK}] + ] + }; - if (!userStats) { - await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); - return; - } + switch (user?.status) { + case 0: + return true; + case 1: + await this.bot.sendMessage(userId, '⚠️Your account has been deleted by administrator', {reply_markup: keyboard}); + return false; + case 2: + await this.bot.sendMessage(userId, '⚠️Your account has been blocked by administrator', {reply_markup: keyboard}); + return false; + default: + return true + } + } - const locationText = userStats.country && userStats.city && userStats.district - ? `${userStats.country}, ${userStats.city}, ${userStats.district}` - : 'Not set'; + async showProfile(msg) { + const chatId = msg.chat.id; + const userId = msg.from.id; - const text = ` + try { + const userStats = await User.getUserStats(userId); + + if (!userStats) { + await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.'); + return; + } + + const locationText = userStats.country && userStats.city && userStats.district + ? `${userStats.country}, ${userStats.city}, ${userStats.district}` + : 'Not set'; + + const text = ` 👤 *Your Profile* 📱 Telegram ID: \`${userId}\` @@ -37,58 +61,58 @@ export default class UserHandler { 📅 Member since: ${new Date(userStats.created_at).toLocaleDateString()} `; - const keyboard = { - inline_keyboard: [ - [{ text: '📍 Set Location', callback_data: 'set_location' }], - [{ text: '❌ Delete Account', callback_data: 'delete_account' }] - ] - }; + const keyboard = { + inline_keyboard: [ + [{text: '📍 Set Location', callback_data: 'set_location'}], + [{text: '❌ Delete Account', callback_data: 'delete_account'}] + ] + }; - await this.bot.sendMessage(chatId, text, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); - } catch (error) { - console.error('Error in showProfile:', error); - await this.bot.sendMessage(chatId, 'Error loading profile. Please try again.'); - } - } - - async handleStart(msg) { - const chatId = msg.chat.id; - const userId = msg.from.id; - const username = msg.chat.username; - - try { - // Create user profile - await User.create(userId, username); - - const keyboard = { - reply_markup: { - keyboard: [ - ['📦 Products', '👤 Profile'], - ['🛍 Purchases', '💰 Wallets'] - ], - resize_keyboard: true + await this.bot.sendMessage(chatId, text, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + } catch (error) { + console.error('Error in showProfile:', error); + await this.bot.sendMessage(chatId, 'Error loading profile. Please try again.'); } - }; - - await this.bot.sendMessage( - chatId, - 'Welcome to the shop! Choose an option:', - keyboard - ); - } catch (error) { - console.error('Error in handleStart:', error); - await this.bot.sendMessage(chatId, 'Error creating user profile. Please try again.'); } - } - async handleBackToProfile(callbackQuery) { - await this.showProfile({ - chat: { id: callbackQuery.message.chat.id }, - from: { id: callbackQuery.from.id } - }); - await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id); - } + async handleStart(msg) { + const chatId = msg.chat.id; + const userId = msg.from.id; + const username = msg.chat.username; + + try { + // Create user profile + await User.create(userId, username); + + const keyboard = { + reply_markup: { + keyboard: [ + ['📦 Products', '👤 Profile'], + ['🛍 Purchases', '💰 Wallets'] + ], + resize_keyboard: true + } + }; + + await this.bot.sendMessage( + chatId, + 'Welcome to the shop! Choose an option:', + keyboard + ); + } catch (error) { + console.error('Error in handleStart:', error); + await this.bot.sendMessage(chatId, 'Error creating user profile. Please try again.'); + } + } + + async handleBackToProfile(callbackQuery) { + await this.showProfile({ + chat: {id: callbackQuery.message.chat.id}, + from: {id: callbackQuery.from.id} + }); + await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id); + } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index fde65c1..1f832eb 100644 --- a/src/index.js +++ b/src/index.js @@ -43,6 +43,13 @@ const adminProductHandler = new AdminProductHandler(bot); // Start command - Create user profile bot.onText(/\/start/, async (msg) => { logDebug('/start', 'handleStart'); + + const canUse = await userHandler.canUseBot(msg); + + if (!canUse) { + return; + } + try { await userHandler.handleStart(msg); } catch (error) { @@ -64,6 +71,16 @@ bot.onText(/\/admin/, async (msg) => { bot.on('message', async (msg) => { if (!msg.text) return; + if (msg.text.toLowerCase() === '/start') { + return; + } + + const canUse = await userHandler.canUseBot(msg); + + if (!canUse) { + return; + } + try { // Check for admin location input if (await adminLocationHandler.handleLocationInput(msg)) { @@ -126,6 +143,13 @@ bot.on('callback_query', async (callbackQuery) => { const action = callbackQuery.data; const msg = callbackQuery.message; + const canUse = await userHandler.canUseBot(callbackQuery); + + if (!canUse) { + await bot.answerCallbackQuery(callbackQuery.id); + return; + } + try { // Profile and location management if (action === 'set_location') { @@ -211,7 +235,7 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action === 'delete_location') { logDebug(action, 'handleDeleteLocation'); await adminLocationHandler.handleDeleteLocation(callbackQuery); - } else if (action.startsWith('confirm_delete_')) { + } else if (action.startsWith('confirm_delete_location_')) { logDebug(action, 'handleConfirmDelete'); await adminLocationHandler.handleConfirmDelete(callbackQuery); } else if (action === 'admin_menu') { @@ -264,9 +288,15 @@ bot.on('callback_query', async (callbackQuery) => { } else if (action.startsWith('delete_user_')) { logDebug(action, 'handleDeleteUser'); await adminUserHandler.handleDeleteUser(callbackQuery); + } else if (action.startsWith('block_user_')) { + logDebug(action, 'handleDeleteUser'); + await adminUserHandler.handleBlockUser(callbackQuery); } else if (action.startsWith('confirm_delete_user_')) { logDebug(action, 'handleConfirmDelete'); await adminUserHandler.handleConfirmDelete(callbackQuery); + } else if (action.startsWith('confirm_block_user_')) { + logDebug(action, 'handleConfirmBlock'); + await adminUserHandler.handleConfirmBlock(callbackQuery); } else if (action.startsWith('edit_user_balance_')) { logDebug(action, 'handleEditUserBalance'); await adminUserHandler.handleEditUserBalance(callbackQuery); diff --git a/src/models/User.js b/src/models/User.js index 9e1de17..75e3bf7 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -1,50 +1,50 @@ import db from '../config/database.js'; export default class User { - static async create(telegramId, username) { - try { - // First check if user exists - const existingUser = await this.getById(telegramId); - if (existingUser) { - return existingUser.id; - } + static async create(telegramId, username) { + try { + // First check if user exists + const existingUser = await this.getById(telegramId); + if (existingUser) { + return existingUser.id; + } - // Begin transaction - await db.runAsync('BEGIN TRANSACTION'); + // Begin transaction + await db.runAsync('BEGIN TRANSACTION'); - // Create new user - const result = await db.runAsync( - 'INSERT INTO users (telegram_id, username) VALUES (?, ?)', - [telegramId.toString(), username] - ); + // Create new user + const result = await db.runAsync( + 'INSERT INTO users (telegram_id, username) VALUES (?, ?)', + [telegramId.toString(), username] + ); - // Commit transaction - await db.runAsync('COMMIT'); + // Commit transaction + await db.runAsync('COMMIT'); - return result.lastID; - } catch (error) { - // Rollback on error - await db.runAsync('ROLLBACK'); - console.error('Error creating user:', error); - throw error; + return result.lastID; + } catch (error) { + // Rollback on error + await db.runAsync('ROLLBACK'); + console.error('Error creating user:', error); + throw error; + } } - } - static async getById(telegramId) { - try { - return await db.getAsync( - 'SELECT * FROM users WHERE telegram_id = ?', - [telegramId.toString()] - ); - } catch (error) { - console.error('Error getting user:', error); - throw error; + static async getById(telegramId) { + try { + return await db.getAsync( + 'SELECT * FROM users WHERE telegram_id = ?', + [telegramId.toString()] + ); + } catch (error) { + console.error('Error getting user:', error); + throw error; + } } - } - static async getUserStats(telegramId) { - try { - return await db.getAsync(` + static async getUserStats(telegramId) { + try { + return await db.getAsync(` SELECT u.*, COUNT(DISTINCT p.id) as purchase_count, @@ -58,9 +58,49 @@ export default class User { WHERE u.telegram_id = ? GROUP BY u.id `, [telegramId.toString()]); - } catch (error) { - console.error('Error getting user stats:', error); - throw error; + } catch (error) { + console.error('Error getting user stats:', error); + throw error; + } + } + + static async updateUserStatus(telegramId, status) { + // statuses + // 0 - active + // 1 - deleted + // 2 - blocked + + try { + await db.runAsync('BEGIN TRANSACTION'); + + // Update user status + await db.runAsync('UPDATE users SET status = ? WHERE telegram_id = ?', [status, telegramId.toString()]); + + // Commit transaction + await db.runAsync('COMMIT'); + } catch (e) { + await db.runAsync("ROLLBACK"); + console.error('Error deleting user:', error); + throw error; + } + } + + static async delete(telegramId) { + try { + await db.runAsync('BEGIN TRANSACTION'); + + // Delete user and his data + await db.runAsync('DELETE FROM users WHERE telegram_id = ?', [telegramId.toString()]); + await db.runAsync('DELETE FROM transactions WHERE user_id = ?', [telegramId.toString()]); + await db.runAsync('DELETE FROM purchases WHERE user_id = ?', [telegramId.toString()]); + await db.runAsync('DELETE FROM crypto_wallets WHERE user_id = ?', [telegramId.toString()]); + + // Commit transaction + await db.runAsync('COMMIT'); + } catch (e) { + await db.runAsync("ROLLBACK"); + console.error('Error deleting user:', error); + throw error; + } } - } } \ No newline at end of file