Initial commit

This commit is contained in:
SoftUniq
2024-11-13 22:31:18 +00:00
commit abfa7b43af
21 changed files with 8368 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import db from '../config/database.js';
import config from '../config/config.js';
export default class AdminHandler {
constructor(bot) {
this.bot = bot;
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleAdminCommand(msg) {
const chatId = msg.chat.id;
if (!this.isAdmin(msg.from.id)) {
await this.bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
const keyboard = {
reply_markup: {
keyboard: [
['👥 Manage Users', '📦 Manage Products'],
['💰 Manage Wallets', '📍 Manage Locations'],
['💾 Database Backup']
],
resize_keyboard: true
}
};
await this.bot.sendMessage(chatId, 'Admin Panel:', keyboard);
}
}

View File

@@ -0,0 +1,281 @@
import db from '../config/database.js';
import Validators from '../utils/validators.js';
import config from '../config/config.js';
export default class AdminLocationHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleAddLocation(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
this.userStates.set(chatId, { action: 'add_location' });
await this.bot.editMessageText(
'Please enter the location in the following format:\nCountry|City|District',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[{ text: '« Back', callback_data: 'view_locations' }]]
}
}
);
}
async handleLocationInput(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || state.action !== 'add_location') return false;
const parts = msg.text.split('|').map(s => s.trim());
if (parts.length !== 3) {
await this.bot.sendMessage(
chatId,
'Invalid format. Please use: Country|City|District'
);
return true;
}
const [country, city, district] = parts;
if (!Validators.isValidLocation(country, city, district)) {
await this.bot.sendMessage(
chatId,
'Invalid location data. All fields are required.'
);
return true;
}
try {
await db.runAsync('BEGIN TRANSACTION');
const result = await db.runAsync(
'INSERT INTO locations (country, city, district) VALUES (?, ?, ?)',
[country, city, district]
);
await db.runAsync('COMMIT');
if (result.changes > 0) {
await this.bot.sendMessage(
chatId,
`✅ Location added successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
reply_markup: {
inline_keyboard: [
[{ text: ' Add Another Location', callback_data: 'add_location' }],
[{ text: '« Back to Locations', callback_data: 'view_locations' }]
]
}
}
);
} else {
throw new Error('Failed to insert location');
}
this.userStates.delete(chatId);
} catch (error) {
await db.runAsync('ROLLBACK');
if (error.code === 'SQLITE_CONSTRAINT') {
await this.bot.sendMessage(
chatId,
'❌ This location already exists.',
{
reply_markup: {
inline_keyboard: [[
{ text: 'Try Again', callback_data: 'add_location' }
]]
}
}
);
} else {
console.error('Error adding location:', error);
await this.bot.sendMessage(
chatId,
'❌ Error adding location. Please try again.',
{
reply_markup: {
inline_keyboard: [[
{ text: 'Try Again', callback_data: 'add_location' }
]]
}
}
);
}
}
return true;
}
async handleViewLocations(msg) {
const chatId = msg.chat?.id || msg.message?.chat.id;
const messageId = msg.message?.message_id;
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
await this.bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
try {
const locations = await db.allAsync(`
SELECT l.*,
COUNT(DISTINCT p.id) as product_count,
COUNT(DISTINCT c.id) as category_count
FROM locations l
LEFT JOIN products p ON l.id = p.location_id
LEFT JOIN categories c ON l.id = c.location_id
GROUP BY l.id
ORDER BY l.country, l.city, l.district
`);
if (locations.length === 0) {
const message = 'No locations found.';
const keyboard = {
inline_keyboard: [[{ text: ' Add Location', callback_data: 'add_location' }]]
};
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
} else {
await this.bot.sendMessage(chatId, message, { reply_markup: keyboard });
}
return;
}
let currentCountry = '';
let message = '📍 *Locations List:*\n\n';
for (const loc of locations) {
if (loc.country !== currentCountry) {
currentCountry = loc.country;
message += `\n🌍 *${currentCountry}*\n`;
}
message += ` └ 🏙 ${loc.city} > ${loc.district}\n`;
message += ` ├ Products: ${loc.product_count}\n`;
message += ` └ Categories: ${loc.category_count}\n`;
}
const keyboard = {
inline_keyboard: [
[{ text: ' Add Location', callback_data: 'add_location' }],
[{ text: '❌ Delete Location', callback_data: 'delete_location' }],
[{ text: '« Back to Admin Menu', callback_data: 'admin_menu' }]
]
};
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard,
parse_mode: 'Markdown'
});
} else {
await this.bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'Markdown'
});
}
} catch (error) {
console.error('Error viewing locations:', error);
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
}
}
async handleDeleteLocation(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
try {
const locations = await db.allAsync(`
SELECT l.*,
COUNT(DISTINCT p.id) as product_count,
COUNT(DISTINCT c.id) as category_count
FROM locations l
LEFT JOIN products p ON l.id = p.location_id
LEFT JOIN categories c ON l.id = c.location_id
GROUP BY l.id
ORDER BY l.country, l.city, l.district
`);
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}`
}])
};
keyboard.inline_keyboard.push([{ text: '« Back', callback_data: 'view_locations' }]);
await this.bot.editMessageText(
'❌ Select location to delete:\n\n*Note:* Deleting a location will also remove all associated products and categories!',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'Markdown'
}
);
} catch (error) {
console.error('Error in handleDeleteLocation:', error);
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
}
}
async handleConfirmDelete(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const [country, city, district] = callbackQuery.data
.replace('confirm_delete_', '')
.split('_');
try {
await db.runAsync('BEGIN TRANSACTION');
const result = await db.runAsync(
'DELETE FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
await db.runAsync('COMMIT');
if (result.changes > 0) {
await this.bot.editMessageText(
`✅ Location deleted successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[{ text: '« Back to Locations', callback_data: 'view_locations' }]]
}
}
);
} else {
throw new Error('Location not found');
}
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error deleting location:', error);
await this.bot.sendMessage(
chatId,
'❌ Error deleting location. Please try again.',
{
reply_markup: {
inline_keyboard: [[{ text: '« Back to Locations', callback_data: 'view_locations' }]]
}
}
);
}
}
}

View File

@@ -0,0 +1,580 @@
import db from '../config/database.js';
import config from '../config/config.js';
import fs from 'fs/promises';
export default class AdminProductHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleProductManagement(msg) {
const chatId = msg.chat?.id || msg.message?.chat.id;
if (!this.isAdmin(msg.from?.id || msg.message?.from.id)) {
await this.bot.sendMessage(chatId, 'Unauthorized access.');
return;
}
try {
const countries = await db.allAsync(
'SELECT DISTINCT country FROM locations ORDER BY country'
);
if (countries.length === 0) {
await this.bot.sendMessage(
chatId,
'No locations available. Please add locations first.',
{
reply_markup: {
inline_keyboard: [[
{ text: '📍 Manage Locations', callback_data: 'view_locations' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: countries.map(loc => [{
text: loc.country,
callback_data: `prod_country_${loc.country}`
}])
};
await this.bot.sendMessage(
chatId,
'🌍 Select country to manage products:',
{ reply_markup: keyboard }
);
} catch (error) {
console.error('Error in handleProductManagement:', error);
await this.bot.sendMessage(chatId, 'Error loading locations. Please try again.');
}
}
async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('prod_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `prod_city_${country}_${loc.city}`
}]),
[{ text: '« Back', callback_data: 'manage_products' }]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCountrySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('prod_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `prod_district_${country}_${city}_${loc.district}`
}]),
[{ text: '« Back', callback_data: `prod_country_${country}` }]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCitySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_');
try {
const location = await db.getAsync(
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
if (!location) {
throw new Error('Location not found');
}
const categories = await db.allAsync(
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
[location.id]
);
const keyboard = {
inline_keyboard: [
...categories.map(cat => [{
text: cat.name,
callback_data: `prod_category_${location.id}_${cat.id}`
}]),
[{ text: ' Add Category', callback_data: `add_category_${location.id}` }],
[{ text: '« Back', callback_data: `prod_city_${country}_${city}` }]
]
};
await this.bot.editMessageText(
'📦 Select or add category:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleDistrictSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.');
}
}
async handleCategoryInput(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || !state.action?.startsWith('add_category_')) return false;
try {
const locationId = state.action.replace('add_category_', '');
await db.runAsync(
'INSERT INTO categories (location_id, name) VALUES (?, ?)',
[locationId, msg.text]
);
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
await this.bot.sendMessage(
chatId,
`✅ Category "${msg.text}" added successfully!`,
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Categories', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }
]]
}
}
);
this.userStates.delete(chatId);
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT') {
await this.bot.sendMessage(chatId, 'This category already exists in this location.');
} else {
console.error('Error adding category:', error);
await this.bot.sendMessage(chatId, 'Error adding category. Please try again.');
}
}
return true;
}
async handleAddCategory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const locationId = callbackQuery.data.replace('add_category_', '');
this.userStates.set(chatId, { action: `add_category_${locationId}` });
await this.bot.editMessageText(
'Please enter the name for the new category:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_district_${locationId}` }
]]
}
}
);
}
async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('prod_category_', '').split('_');
try {
const subcategories = await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
[categoryId]
);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const location = await db.getAsync('SELECT country, city, district FROM locations WHERE id = ?', [locationId]);
const keyboard = {
inline_keyboard: [
...subcategories.map(sub => [{
text: sub.name,
callback_data: `prod_subcategory_${locationId}_${categoryId}_${sub.id}`
}]),
[{ text: ' Add Subcategory', callback_data: `add_subcategory_${locationId}_${categoryId}` }],
[{ text: '✏️ Edit Category', callback_data: `edit_category_${locationId}_${categoryId}` }],
[{ text: '« Back', callback_data: `prod_district_${location.country}_${location.city}_${location.district}` }]
]
};
await this.bot.editMessageText(
`📦 Category: ${category.name}\nSelect or add subcategory:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
}
}
async handleSubcategoryInput(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || !state.action?.startsWith('add_subcategory_')) return false;
try {
const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_');
await db.runAsync(
'INSERT INTO subcategories (category_id, name) VALUES (?, ?)',
[categoryId, msg.text]
);
await this.bot.sendMessage(
chatId,
`✅ Subcategory "${msg.text}" added successfully!`,
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Subcategories', callback_data: `prod_category_${locationId}_${categoryId}` }
]]
}
}
);
this.userStates.delete(chatId);
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT') {
await this.bot.sendMessage(chatId, 'This subcategory already exists in this category.');
} else {
console.error('Error adding subcategory:', error);
await this.bot.sendMessage(chatId, 'Error adding subcategory. Please try again.');
}
}
return true;
}
async handleAddSubcategory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const [locationId, categoryId] = callbackQuery.data.replace('add_subcategory_', '').split('_');
this.userStates.set(chatId, { action: `add_subcategory_${locationId}_${categoryId}` });
await this.bot.editMessageText(
'Please enter the name for the new subcategory:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_category_${locationId}_${categoryId}` }
]]
}
}
);
}
async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('prod_subcategory_', '').split('_');
try {
const products = await db.allAsync(
`SELECT id, name, price, quantity_in_stock
FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
ORDER BY name`,
[locationId, categoryId, subcategoryId]
);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
text: `${prod.name} - $${prod.price} (${prod.quantity_in_stock} left)`,
callback_data: `view_product_${prod.id}`
}]),
[{ text: '📥 Import Products', callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}` }],
[{ text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}` }]
]
};
await this.bot.editMessageText(
`📦 ${category.name} > ${subcategory.name}\nSelect product or import new ones:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleAddProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId] = callbackQuery.data.replace('add_product_', '').split('_');
try {
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
const sampleProducts = [
{
name: "Sample Product 1",
price: 100,
description: "Product description",
private_data: "Hidden details about the product",
quantity_in_stock: 10,
photo_url: "https://example.com/photo.jpg",
hidden_photo_url: "https://example.com/hidden.jpg",
hidden_coordinates: "40.7128,-74.0060",
hidden_description: "Secret location details"
}
];
const jsonExample = JSON.stringify(sampleProducts, null, 2);
const message = `To import products, send a JSON file with an array of products in the following format:\n\n<pre>${jsonExample}</pre>\n\nEach product must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
this.userStates.set(chatId, {
action: 'import_products',
locationId,
categoryId,
subcategoryId
});
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
parse_mode: 'HTML',
reply_markup: {
inline_keyboard: [[
{ text: '❌ Cancel', callback_data: `prod_subcategory_${locationId}_${categoryId}_${subcategoryId}` }
]]
}
});
} catch (error) {
console.error('Error in handleAddProduct:', error);
await this.bot.sendMessage(chatId, 'Error preparing product import. Please try again.');
}
}
async handleProductImport(msg) {
const chatId = msg.chat.id;
const state = this.userStates.get(chatId);
if (!state || state.action !== 'import_products') return false;
try {
let products;
let jsonContent;
// Handle file upload
if (msg.document) {
if (!msg.document.file_name.endsWith('.json')) {
await this.bot.sendMessage(chatId, 'Please upload a .json file.');
return true;
}
const file = await this.bot.getFile(msg.document.file_id);
const fileContent = await this.bot.downloadFile(file.file_id, '/tmp');
jsonContent = await fs.readFile(fileContent, 'utf8');
} else if (msg.text) {
jsonContent = msg.text;
} else {
await this.bot.sendMessage(chatId, 'Please send either a JSON file or JSON text.');
return true;
}
try {
products = JSON.parse(jsonContent);
if (!Array.isArray(products)) {
throw new Error('Input must be an array of products');
}
} catch (e) {
await this.bot.sendMessage(chatId, 'Invalid JSON format. Please check the format and try again.');
return true;
}
await db.runAsync('BEGIN TRANSACTION');
for (const product of products) {
await db.runAsync(
`INSERT INTO products (
location_id, category_id, subcategory_id,
name, price, description, private_data,
quantity_in_stock, photo_url, hidden_photo_url,
hidden_coordinates, hidden_description
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
state.locationId, state.categoryId, state.subcategoryId,
product.name, product.price, product.description, product.private_data,
product.quantity_in_stock, product.photo_url, product.hidden_photo_url,
product.hidden_coordinates, product.hidden_description
]
);
}
await db.runAsync('COMMIT');
await this.bot.sendMessage(
chatId,
`✅ Successfully imported ${products.length} products!`,
{
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Products', callback_data: `prod_subcategory_${state.locationId}_${state.categoryId}_${state.subcategoryId}` }
]]
}
}
);
this.userStates.delete(chatId);
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error importing products:', error);
await this.bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
}
return true;
}
async handleViewProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('view_product_', '');
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name,
l.country, l.city, l.district
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
JOIN locations l ON p.location_id = l.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const message = `
📦 Product Details:
Name: ${product.name}
Price: $${product.price}
Description: ${product.description}
Stock: ${product.quantity_in_stock}
Location: ${product.country}, ${product.city}, ${product.district}
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
🔒 Private Information:
${product.private_data}
Hidden Location: ${product.hidden_description}
Coordinates: ${product.hidden_coordinates}
`;
const keyboard = {
inline_keyboard: [
[
{ text: '✏️ Edit', callback_data: `edit_product_${productId}` },
{ text: '❌ Delete', callback_data: `delete_product_${productId}` }
],
[{ text: '« Back', callback_data: `prod_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}` }]
]
};
// Send product photos
if (product.photo_url) {
await this.bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
}
if (product.hidden_photo_url) {
await this.bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
}
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleViewProduct:', error);
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
}

View File

@@ -0,0 +1,271 @@
import User from '../models/User.js';
import config from '../config/config.js';
import db from '../config/database.js';
export default class AdminUserHandler {
constructor(bot) {
this.bot = bot;
}
isAdmin(userId) {
return config.ADMIN_IDS.includes(userId.toString());
}
async handleUserList(msg) {
if (!this.isAdmin(msg.from.id)) {
await this.bot.sendMessage(msg.chat.id, 'Unauthorized access.');
return;
}
try {
const users = await db.allAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as total_purchases,
COUNT(DISTINCT cw.id) as total_wallets,
COALESCE(SUM(t.amount), 0) as total_balance
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
LEFT JOIN transactions t ON u.id = t.user_id
GROUP BY u.id
ORDER BY u.created_at DESC
`);
if (users.length === 0) {
await this.bot.sendMessage(msg.chat.id, 'No users registered yet.');
return;
}
// Calculate general statistics
const totalUsers = users.length;
const activeUsers = users.filter(u => u.total_purchases > 0).length;
const totalBalance = users.reduce((sum, u) => sum + (u.total_balance || 0), 0);
const totalPurchases = users.reduce((sum, u) => sum + (u.total_purchases || 0), 0);
// Create statistics message
let message = `📊 System Statistics\n\n`;
message += `👥 Total Users: ${totalUsers}\n`;
message += `✅ Active Users: ${activeUsers}\n`;
message += `💰 Total Balance: $${totalBalance.toFixed(2)}\n`;
message += `🛍 Total Purchases: ${totalPurchases}\n\n`;
message += `Select a user from the list below:`;
// Create inline keyboard with user list
const keyboard = {
inline_keyboard: users.map(user => [{
text: `ID: ${user.telegram_id} | Balance: $${user.total_balance || 0}`,
callback_data: `view_user_${user.telegram_id}`
}])
};
await this.bot.sendMessage(msg.chat.id, message, {
parse_mode: 'HTML',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleUserList:', error);
await this.bot.sendMessage(msg.chat.id, 'Error loading user list. Please try again.');
}
}
async handleViewUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('view_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const userStats = await db.getAsync(`
SELECT
u.*,
COUNT(DISTINCT p.id) as purchase_count,
COALESCE(SUM(p.total_price), 0) as total_spent,
COUNT(DISTINCT cw.id) as wallet_count,
COALESCE(SUM(t.amount), 0) as total_balance
FROM users u
LEFT JOIN purchases p ON u.id = p.user_id
LEFT JOIN crypto_wallets cw ON u.id = cw.user_id
LEFT JOIN transactions t ON u.id = t.user_id
WHERE u.telegram_id = ?
GROUP BY u.id
`, [userId]);
if (!userStats) {
await this.bot.sendMessage(chatId, 'User not found.');
return;
}
// Get recent transactions
const transactions = await db.allAsync(`
SELECT t.amount, t.created_at, t.wallet_type, t.tx_hash
FROM transactions t
JOIN users u ON t.user_id = u.id
WHERE u.telegram_id = ?
ORDER BY t.created_at DESC
LIMIT 5
`, [userId]);
// Get recent purchases
const purchases = await db.allAsync(`
SELECT p.quantity, p.total_price, p.purchase_date,
pr.name as product_name
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN users u ON p.user_id = u.id
WHERE u.telegram_id = ?
ORDER BY p.purchase_date DESC
LIMIT 5
`, [userId]);
const message = `
👤 User Profile:
ID: ${userId}
📍 Location: ${userStats.country || 'Not set'}, ${userStats.city || 'Not set'}, ${userStats.district || 'Not set'}
📊 Activity:
- Total Purchases: ${userStats.purchase_count}
- Total Spent: $${userStats.total_spent || 0}
- Active Wallets: ${userStats.wallet_count}
- Total Balance: $${userStats.total_balance || 0}
💰 Recent Transactions:
${transactions.map(t => `${t.amount} ${t.wallet_type} (${t.tx_hash})`).join('\n')}
🛍 Recent Purchases:
${purchases.map(p => `${p.product_name} x${p.quantity} - $${p.total_price}`).join('\n')}
📅 Registered: ${new Date(userStats.created_at).toLocaleString()}
`;
const keyboard = {
inline_keyboard: [
[
{ text: '💰 Edit Balance', callback_data: `edit_user_balance_${userId}` },
{ text: '📍 Edit Location', callback_data: `edit_user_location_${userId}` }
],
[
{ text: '🚫 Block User', callback_data: `block_user_${userId}` },
{ text: '❌ Delete User', callback_data: `delete_user_${userId}` }
],
[{ text: '« Back to User List', callback_data: 'admin_users' }]
]
};
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
});
} catch (error) {
console.error('Error in handleViewUser:', error);
await this.bot.sendMessage(chatId, 'Error loading user details. Please try again.');
}
}
async handleDeleteUser(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('delete_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
const keyboard = {
inline_keyboard: [
[
{ text: '✅ Confirm Delete', callback_data: `confirm_delete_user_${userId}` },
{ text: '❌ Cancel', callback_data: `view_user_${userId}` }
]
]
};
await this.bot.editMessageText(
`⚠️ Are you sure you want to delete user ${userId}?\n\nThis action will:\n- Delete all user data\n- Remove all wallets\n- Erase purchase history\n\nThis action cannot be undone!`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard,
parse_mode: 'HTML'
}
);
} catch (error) {
console.error('Error in handleDeleteUser:', error);
await this.bot.sendMessage(chatId, 'Error processing delete request. Please try again.');
}
}
async handleConfirmDelete(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('confirm_delete_user_', '');
const chatId = callbackQuery.message.chat.id;
try {
await User.delete(userId);
const keyboard = {
inline_keyboard: [
[{ text: '« Back to User List', callback_data: 'admin_users' }]
]
};
await this.bot.editMessageText(
`✅ User ${userId} has been successfully deleted.`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleConfirmDelete:', error);
await this.bot.sendMessage(chatId, 'Error deleting user. Please try again.');
}
}
async handleEditUserBalance(callbackQuery) {
if (!this.isAdmin(callbackQuery.from.id)) return;
const userId = callbackQuery.data.replace('edit_user_balance_', '');
const chatId = callbackQuery.message.chat.id;
try {
const user = await User.getById(userId);
if (!user) {
await this.bot.sendMessage(chatId, 'User not found.');
return;
}
const wallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = (SELECT id FROM users WHERE telegram_id = ?)
ORDER BY wallet_type
`, [userId]);
const keyboard = {
inline_keyboard: [
...wallets.map(wallet => [
{ text: `${wallet.wallet_type}: ${wallet.address}`, callback_data: `edit_wallet_${wallet.wallet_type}` }
]),
[{ text: '« Back', callback_data: `view_user_${userId}` }]
]
};
await this.bot.editMessageText(
`Select wallet to edit for user ${userId}:`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleEditUserBalance:', error);
await this.bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
}
}
}

View File

@@ -0,0 +1,93 @@
import db from '../config/database.js';
import User from '../models/User.js';
export default class UserHandler {
constructor(bot) {
this.bot = bot;
}
async showProfile(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
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}\`
📍 Location: ${locationText}
📊 Statistics:
├ Total Purchases: ${userStats.purchase_count || 0}
├ Total Spent: $${userStats.total_spent || 0}
├ Active Wallets: ${userStats.crypto_wallet_count || 0}
└ Total Balance: $${userStats.total_balance || 0}
📅 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' }]
]
};
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;
try {
// Create user profile
await User.create(userId);
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);
}
}

View File

@@ -0,0 +1,160 @@
import db from '../config/database.js';
import Validators from '../utils/validators.js';
export default class UserLocationHandler {
constructor(bot) {
this.bot = bot;
}
async handleSetLocation(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
const countries = await db.allAsync('SELECT DISTINCT country FROM locations ORDER BY country');
if (countries.length === 0) {
await this.bot.editMessageText(
'No locations available yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Profile', callback_data: 'back_to_profile' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...countries.map(loc => [{
text: loc.country,
callback_data: `set_country_${loc.country}`
}]),
[{ text: '« Back to Profile', callback_data: 'back_to_profile' }]
]
};
await this.bot.editMessageText(
'🌍 Select your country:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetLocation:', error);
await this.bot.sendMessage(chatId, 'Error loading countries. Please try again.');
}
}
async handleSetCountry(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('set_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `set_city_${country}_${loc.city}`
}]),
[{ text: '« Back to Countries', callback_data: 'set_location' }]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCountry:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleSetCity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('set_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `set_district_${country}_${city}_${loc.district}`
}]),
[{ text: '« Back to Cities', callback_data: `set_country_${country}` }]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSetCity:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleSetDistrict(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const userId = callbackQuery.from.id;
const [country, city, district] = callbackQuery.data.replace('set_district_', '').split('_');
try {
await db.runAsync('BEGIN TRANSACTION');
await db.runAsync(
'UPDATE users SET country = ?, city = ?, district = ? WHERE telegram_id = ?',
[country, city, district, userId.toString()]
);
await db.runAsync('COMMIT');
await this.bot.editMessageText(
`✅ Location updated successfully!\n\nCountry: ${country}\nCity: ${city}\nDistrict: ${district}`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Profile', callback_data: 'back_to_profile' }
]]
}
}
);
} catch (error) {
await db.runAsync('ROLLBACK');
console.error('Error in handleSetDistrict:', error);
await this.bot.sendMessage(chatId, 'Error updating location. Please try again.');
}
}
}

View File

@@ -0,0 +1,656 @@
import db from '../config/database.js';
import User from '../models/User.js';
export default class UserProductHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
async showProducts(msg) {
const chatId = msg.chat.id;
const messageId = msg?.message_id;
try {
const countries = await db.allAsync(
'SELECT DISTINCT country FROM locations ORDER BY country'
);
if (countries.length === 0) {
const message = 'No products available at the moment.';
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId
});
} else {
await this.bot.sendMessage(chatId, message);
}
return;
}
const keyboard = {
inline_keyboard: countries.map(loc => [{
text: loc.country,
callback_data: `shop_country_${loc.country}`
}])
};
const message = '🌍 Select your country:';
try {
if (messageId) {
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
});
}
} catch (error) {
await this.bot.sendMessage(chatId, message, { reply_markup: keyboard });
}
} catch (error) {
console.error('Error in showProducts:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleCountrySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const country = callbackQuery.data.replace('shop_country_', '');
try {
const cities = await db.allAsync(
'SELECT DISTINCT city FROM locations WHERE country = ? ORDER BY city',
[country]
);
const keyboard = {
inline_keyboard: [
...cities.map(loc => [{
text: loc.city,
callback_data: `shop_city_${country}_${loc.city}`
}]),
[{ text: '« Back to Countries', callback_data: 'shop_start' }]
]
};
await this.bot.editMessageText(
`🏙 Select city in ${country}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCountrySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading cities. Please try again.');
}
}
async handleCitySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city] = callbackQuery.data.replace('shop_city_', '').split('_');
try {
const districts = await db.allAsync(
'SELECT district FROM locations WHERE country = ? AND city = ? ORDER BY district',
[country, city]
);
const keyboard = {
inline_keyboard: [
...districts.map(loc => [{
text: loc.district,
callback_data: `shop_district_${country}_${city}_${loc.district}`
}]),
[{ text: '« Back to Cities', callback_data: `shop_country_${country}` }]
]
};
await this.bot.editMessageText(
`📍 Select district in ${city}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCitySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading districts. Please try again.');
}
}
async handleDistrictSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [country, city, district] = callbackQuery.data.replace('shop_district_', '').split('_');
try {
const location = await db.getAsync(
'SELECT id FROM locations WHERE country = ? AND city = ? AND district = ?',
[country, city, district]
);
if (!location) {
throw new Error('Location not found');
}
const categories = await db.allAsync(
'SELECT id, name FROM categories WHERE location_id = ? ORDER BY name',
[location.id]
);
if (categories.length === 0) {
await this.bot.editMessageText(
'No products available in this location yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Districts', callback_data: `shop_city_${country}_${city}` }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...categories.map(cat => [{
text: cat.name,
callback_data: `shop_category_${location.id}_${cat.id}`
}]),
[{ text: '« Back to Districts', callback_data: `shop_city_${country}_${city}` }]
]
};
await this.bot.editMessageText(
'📦 Select category:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleDistrictSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading categories. Please try again.');
}
}
async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId] = callbackQuery.data.replace('shop_category_', '').split('_');
try {
const subcategories = await db.allAsync(
'SELECT id, name FROM subcategories WHERE category_id = ? ORDER BY name',
[categoryId]
);
const location = await db.getAsync(
'SELECT country, city, district FROM locations WHERE id = ?',
[locationId]
);
if (subcategories.length === 0) {
await this.bot.editMessageText(
'No products available in this category yet.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...subcategories.map(sub => [{
text: sub.name,
callback_data: `shop_subcategory_${locationId}_${categoryId}_${sub.id}`
}]),
[{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` }]
]
};
await this.bot.editMessageText(
'📦 Select subcategory:',
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleCategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading subcategories. Please try again.');
}
}
async handleSubcategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const [locationId, categoryId, subcategoryId, photoMessageId] = callbackQuery.data.replace('shop_subcategory_', '').split('_');
try {
// Delete the photo message if it exists
if (photoMessageId) {
try {
await this.bot.deleteMessage(chatId, photoMessageId);
} catch (error) {
console.error('Error deleting photo message:', error);
}
}
const products = await db.allAsync(
`SELECT id, name, price, description, quantity_in_stock, photo_url
FROM products
WHERE location_id = ? AND category_id = ? AND subcategory_id = ?
AND quantity_in_stock > 0
ORDER BY name`,
[locationId, categoryId, subcategoryId]
);
const location = await db.getAsync('SELECT * FROM locations WHERE id = ?', [locationId]);
const category = await db.getAsync('SELECT name FROM categories WHERE id = ?', [categoryId]);
const subcategory = await db.getAsync('SELECT name FROM subcategories WHERE id = ?', [subcategoryId]);
if (products.length === 0) {
await this.bot.editMessageText(
'No products available in this subcategory.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...products.map(prod => [{
text: `${prod.name} - $${prod.price}`,
callback_data: `shop_product_${prod.id}`
}]),
[{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` }]
]
};
await this.bot.editMessageText(
`📦 Products in ${subcategory.name}:`,
{
chat_id: chatId,
message_id: messageId,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleSubcategorySelection:', error);
await this.bot.sendMessage(chatId, 'Error loading products. Please try again.');
}
}
async handleProductSelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('shop_product_', '');
try {
const product = await db.getAsync(
`SELECT p.*, c.name as category_name, s.name as subcategory_name
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN subcategories s ON p.subcategory_id = s.id
WHERE p.id = ?`,
[productId]
);
if (!product) {
throw new Error('Product not found');
}
// Delete the previous message
await this.bot.deleteMessage(chatId, messageId);
const message = `
📦 ${product.name}
💰 Price: $${product.price}
📝 Description: ${product.description}
📦 Available: ${product.quantity_in_stock} pcs
Category: ${product.category_name}
Subcategory: ${product.subcategory_name}
`;
let photoMessageId = null;
// First send the photo if it exists
if (product.photo_url) {
const photoMessage = await this.bot.sendPhoto(chatId, product.photo_url);
photoMessageId = photoMessage.message_id;
}
const keyboard = {
inline_keyboard: [
[{ text: '🛒 Buy Now', callback_data: `buy_product_${productId}` }],
[
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: {} // Initially disabled as quantity starts at 1
},
{ text: '1', callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: product.quantity_in_stock <= 1 ? {} : null // Disabled if stock is 1 or less
}
],
[{ text: `« Back to ${product.subcategory_name}`, callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}` }]
]
};
// Then send the message with controls
await this.bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'HTML'
});
// Store the current quantity and photo message ID in user state
this.userStates.set(chatId, {
action: 'buying_product',
productId,
quantity: 1,
photoMessageId
});
} catch (error) {
console.error('Error in handleProductSelection:', error);
await this.bot.sendMessage(chatId, 'Error loading product details. Please try again.');
}
}
async handleIncreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('increase_quantity_', '');
const state = this.userStates.get(chatId);
try {
const product = await db.getAsync(
'SELECT quantity_in_stock FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at max stock, silently ignore
if (currentQuantity >= product.quantity_in_stock) {
await this.bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
// Update state
this.userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{ text: newQuantity.toString(), callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await this.bot.editMessageReplyMarkup(
{ inline_keyboard: keyboard },
{
chat_id: chatId,
message_id: messageId
}
);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleIncreaseQuantity:', error);
await this.bot.answerCallbackQuery(callbackQuery.id);
}
}
async handleDecreaseQuantity(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
const productId = callbackQuery.data.replace('decrease_quantity_', '');
const state = this.userStates.get(chatId);
try {
const product = await db.getAsync(
'SELECT quantity_in_stock FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const currentQuantity = state?.quantity || 1;
// If already at minimum, silently ignore
if (currentQuantity <= 1) {
await this.bot.answerCallbackQuery(callbackQuery.id);
return;
}
const newQuantity = Math.max(currentQuantity - 1, 1);
// Update state
this.userStates.set(chatId, {
...state,
quantity: newQuantity
});
// Update quantity display in keyboard
const keyboard = callbackQuery.message.reply_markup.inline_keyboard;
keyboard[1] = [
{
text: '',
callback_data: `decrease_quantity_${productId}`,
callback_game: newQuantity <= 1 ? {} : null
},
{ text: newQuantity.toString(), callback_data: 'current_quantity' },
{
text: '',
callback_data: `increase_quantity_${productId}`,
callback_game: newQuantity >= product.quantity_in_stock ? {} : null
}
];
await this.bot.editMessageReplyMarkup(
{ inline_keyboard: keyboard },
{
chat_id: chatId,
message_id: messageId
}
);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error in handleDecreaseQuantity:', error);
await this.bot.answerCallbackQuery(callbackQuery.id);
}
}
async handleBuyProduct(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const productId = callbackQuery.data.replace('buy_product_', '');
const state = this.userStates.get(chatId);
try {
const user = await User.getById(userId);
if (!user) {
throw new Error('User not found');
}
const product = await db.getAsync(
'SELECT * FROM products WHERE id = ?',
[productId]
);
if (!product) {
throw new Error('Product not found');
}
const quantity = state?.quantity || 1;
const totalPrice = product.price * quantity;
// Get user's crypto wallets with balances
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await this.bot.sendMessage(
chatId,
'You need to add a crypto wallet first to make purchases.',
{
reply_markup: {
inline_keyboard: [[
{ text: ' Add Wallet', callback_data: 'add_wallet' }
]]
}
}
);
return;
}
const keyboard = {
inline_keyboard: [
...cryptoWallets.map(wallet => [{
text: `Pay with ${wallet.wallet_type}`,
callback_data: `pay_with_${wallet.wallet_type}_${productId}_${quantity}`
}]),
[{ text: '« Cancel', callback_data: `shop_product_${productId}` }]
]
};
await this.bot.editMessageText(
`🛒 Purchase Summary:\n\n` +
`Product: ${product.name}\n` +
`Quantity: ${quantity}\n` +
`Total: $${totalPrice}\n\n` +
`Select payment method:`,
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error in handleBuyProduct:', error);
await this.bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
async showPurchases(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const user = await User.getById(userId);
if (!user) {
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
const purchases = await db.allAsync(`
SELECT p.*, pr.name as product_name, pr.description,
l.country, l.city, l.district
FROM purchases p
JOIN products pr ON p.product_id = pr.id
JOIN locations l ON pr.location_id = l.id
WHERE p.user_id = ?
ORDER BY p.purchase_date DESC
LIMIT 10
`, [user.id]);
if (purchases.length === 0) {
await this.bot.sendMessage(
chatId,
'You haven\'t made any purchases yet.',
{
reply_markup: {
inline_keyboard: [[
{ text: '🛍 Browse Products', callback_data: 'shop_start' }
]]
}
}
);
return;
}
let message = '🛍 *Your Recent Purchases:*\n\n';
for (const purchase of purchases) {
const date = new Date(purchase.purchase_date).toLocaleString();
message += `📦 *${purchase.product_name}*\n`;
message += `├ Quantity: ${purchase.quantity}\n`;
message += `├ Total: $${purchase.total_price}\n`;
message += `├ Location: ${purchase.country}, ${purchase.city}\n`;
message += `├ Payment: ${purchase.wallet_type}\n`;
message += `├ TX: \`${purchase.tx_hash}\`\n`;
message += `└ Date: ${date}\n\n`;
}
await this.bot.sendMessage(chatId, message, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '🛍 Browse Products', callback_data: 'shop_start' }
]]
}
});
} catch (error) {
console.error('Error in showPurchases:', error);
await this.bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
}
}
}

View File

@@ -0,0 +1,567 @@
import db from '../config/database.js';
import User from '../models/User.js';
import WalletGenerator from '../utils/walletGenerator.js';
import WalletService from '../utils/walletService.js';
export default class UserWalletsHandler {
constructor(bot) {
this.bot = bot;
this.userStates = new Map();
}
async showBalance(msg) {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
if (!user) {
await this.bot.sendMessage(chatId, 'Profile not found. Please use /start to create one.');
return;
}
// Get active crypto wallets only
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
let message = '💰 *Your Active Wallets:*\n\n';
if (cryptoWallets.length > 0) {
const walletService = new WalletService(
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'TRON')?.address,
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
let totalUsdValue = 0;
// Show active wallets
for (const [type, balance] of Object.entries(balances)) {
const baseType = this.getBaseWalletType(type);
const wallet = cryptoWallets.find(w =>
w.wallet_type === baseType ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
if (wallet) {
message += `🔐 *${type}*\n`;
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
message += `└ Address: \`${wallet.address}\`\n\n`;
totalUsdValue += balance.usdValue;
}
}
message += `📊 *Total Balance:* $${totalUsdValue.toFixed(2)}\n`;
} else {
message = 'You don\'t have any active wallets yet.';
}
// Check if user has archived wallets
const archivedCount = await db.getAsync(`
SELECT COUNT(*) as count
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%_%'
`, [user.id]);
const keyboard = {
inline_keyboard: [
[
{ text: ' Add Crypto Wallet', callback_data: 'add_wallet' },
{ text: '💸 Top Up', callback_data: 'top_up_wallet' }
],
[{ text: '🔄 Refresh Balance', callback_data: 'refresh_balance' }],
[{ text: '📊 Transaction History', callback_data: 'wallet_history' }]
]
};
// Add archived wallets button if any exist
if (archivedCount.count > 0) {
keyboard.inline_keyboard.splice(2, 0, [
{ text: `📁 Archived Wallets (${archivedCount.count})`, callback_data: 'view_archived_wallets' }
]);
}
await this.bot.sendMessage(chatId, message, {
reply_markup: keyboard,
parse_mode: 'Markdown'
});
} catch (error) {
console.error('Error in showBalance:', error);
await this.bot.sendMessage(chatId, 'Error loading balance. Please try again.');
}
}
async handleAddWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const cryptoOptions = [
['BTC', 'ETH', 'LTC'],
['USDT TRC-20', 'USDD TRC-20'],
['USDT ERC-20', 'USDC ERC-20']
];
const keyboard = {
inline_keyboard: [
...cryptoOptions.map(row =>
row.map(coin => ({
text: coin,
callback_data: `generate_wallet_${coin.replace(' ', '_')}`
}))
),
[{ text: '« Back', callback_data: 'back_to_balance' }]
]
};
await this.bot.editMessageText(
'🔐 Select cryptocurrency to generate wallet:',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: keyboard
}
);
}
async handleGenerateWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
try {
const user = await User.getById(userId);
if (!user) {
throw new Error('User not found');
}
await db.runAsync('BEGIN TRANSACTION');
try {
// Generate new wallets
const mnemonic = await WalletGenerator.generateMnemonic();
const wallets = await WalletGenerator.generateWallets(mnemonic);
const encryptedMnemonic = await WalletGenerator.encryptMnemonic(mnemonic, userId);
// Get the base wallet type (ETH for ERC-20, TRON for TRC-20)
const baseType = this.getBaseWalletType(walletType);
// Get existing wallet of this type
const existingWallet = await db.getAsync(
'SELECT id, address FROM crypto_wallets WHERE user_id = ? AND wallet_type = ?',
[user.id, baseType]
);
if (existingWallet) {
// Archive the old wallet by adding a suffix to its type
const timestamp = Date.now();
await db.runAsync(
'UPDATE crypto_wallets SET wallet_type = ? WHERE id = ?',
[`${baseType}_${timestamp}`, existingWallet.id]
);
}
// Store the new wallet
await db.runAsync(
`INSERT INTO crypto_wallets (
user_id, wallet_type, address, derivation_path, encrypted_mnemonic
) VALUES (?, ?, ?, ?, ?)`,
[
user.id,
baseType,
wallets[baseType].address,
wallets[baseType].path,
encryptedMnemonic
]
);
// Get the appropriate address for the requested wallet type
const displayAddress = this.getWalletAddress(wallets, walletType);
const network = this.getNetworkName(walletType);
let message = `✅ New wallet generated successfully!\n\n`;
message += `Type: ${walletType}\n`;
message += `Network: ${network}\n`;
message += `Address: \`${displayAddress}\`\n\n`;
if (existingWallet) {
message += ` Your previous wallet has been archived and will remain accessible for existing funds.\n`;
}
message += `\n⚠️ Important: Your recovery phrase has been securely stored. Keep your wallet address safe!`;
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
]]
}
});
await db.runAsync('COMMIT');
} catch (error) {
await db.runAsync('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error generating wallet:', error);
await this.bot.editMessageText(
'❌ Error generating wallet. Please try again.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back to Balance', callback_data: 'back_to_balance' }
]]
}
}
);
}
}
async handleTopUpWallet(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
// Get crypto wallets
const cryptoWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ?
ORDER BY wallet_type
`, [user.id]);
if (cryptoWallets.length === 0) {
await this.bot.editMessageText(
'You don\'t have any wallets yet.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: ' Add Wallet', callback_data: 'add_wallet' }
]]
}
}
);
return;
}
let message = '💰 *Available Wallets:*\n\n';
const walletService = new WalletService(
cryptoWallets.find(w => w.wallet_type === 'BTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'LTC')?.address,
cryptoWallets.find(w => w.wallet_type === 'TRON')?.address,
cryptoWallets.find(w => w.wallet_type === 'ETH')?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
for (const [type, balance] of Object.entries(balances)) {
if (cryptoWallets.some(w => w.wallet_type === type.split(' ')[0] ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH'))) {
const wallet = cryptoWallets.find(w =>
w.wallet_type === type.split(' ')[0] ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
message += `🔐 *${type}*\n`;
message += `├ Balance: ${balance.amount.toFixed(8)} ${type.split(' ')[0]}\n`;
message += `├ Value: $${balance.usdValue.toFixed(2)}\n`;
message += `└ Address: \`${wallet.address}\`\n\n`;
}
}
const keyboard = {
inline_keyboard: [
[{ text: '« Back', callback_data: 'back_to_balance' }]
]
};
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleTopUpWallet:', error);
await this.bot.sendMessage(chatId, 'Error loading wallets. Please try again.');
}
}
async handleWalletHistory(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
const transactions = await db.allAsync(`
SELECT type, amount, tx_hash, created_at, wallet_type
FROM transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
`, [user.id]);
if (transactions.length === 0) {
await this.bot.editMessageText(
'No transactions found.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
return;
}
let message = '📊 *Recent Transactions:*\n\n';
transactions.forEach(tx => {
const date = new Date(tx.created_at).toLocaleString();
const symbol = tx.type === 'deposit' ? '' : '';
message += `${symbol} ${tx.amount} ${tx.wallet_type}\n`;
message += `🔗 TX: \`${tx.tx_hash}\`\n`;
message += `🕒 ${date}\n\n`;
});
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
} catch (error) {
console.error('Error in handleWalletHistory:', error);
await this.bot.sendMessage(chatId, 'Error loading transaction history. Please try again.');
}
}
async handleViewArchivedWallets(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
try {
const user = await db.getAsync('SELECT id FROM users WHERE telegram_id = ?', [userId.toString()]);
// Get archived wallets and validate timestamps
const archivedWallets = await db.allAsync(`
SELECT wallet_type, address
FROM crypto_wallets
WHERE user_id = ? AND wallet_type LIKE '%_%'
ORDER BY wallet_type
`, [user.id]);
// Filter out wallets with invalid timestamps
const validArchivedWallets = archivedWallets.filter(wallet => {
const [, timestamp] = wallet.wallet_type.split('_');
const date = new Date(parseInt(timestamp));
return !isNaN(date.getTime()); // Check if date is valid
});
if (validArchivedWallets.length === 0) {
await this.bot.editMessageText(
'No archived wallets found.',
{
chat_id: chatId,
message_id: callbackQuery.message.message_id,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
return;
}
// Group wallets by base type
const groupedWallets = {};
let totalUsdValue = 0;
for (const wallet of validArchivedWallets) {
const [baseType, timestamp] = wallet.wallet_type.split('_');
if (!groupedWallets[baseType]) {
groupedWallets[baseType] = [];
}
groupedWallets[baseType].push({
address: wallet.address,
timestamp: parseInt(timestamp)
});
}
// Create wallet service instance
const walletService = new WalletService(
groupedWallets['BTC']?.[0]?.address,
groupedWallets['LTC']?.[0]?.address,
groupedWallets['TRON']?.[0]?.address,
groupedWallets['ETH']?.[0]?.address,
user.id,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
// Get all balances
const balances = await walletService.getAllBalances();
let message = '📁 *Archived Wallets:*\n\n';
// Process each cryptocurrency type
for (const baseType of Object.keys(groupedWallets).sort()) {
let typeTotal = 0;
let typeUsdTotal = 0;
message += `🔒 *${baseType}*\n`;
// Sort wallets by timestamp (newest first)
const sortedWallets = groupedWallets[baseType].sort((a, b) => b.timestamp - a.timestamp);
for (const wallet of sortedWallets) {
const date = new Date(wallet.timestamp);
let balance = 0;
let usdValue = 0;
// Get balance based on wallet type
switch (baseType) {
case 'BTC':
balance = balances.BTC.amount;
usdValue = balances.BTC.usdValue;
break;
case 'LTC':
balance = balances.LTC.amount;
usdValue = balances.LTC.usdValue;
break;
case 'ETH':
balance = balances.ETH.amount;
usdValue = balances.ETH.usdValue;
break;
case 'TRON':
balance = balances['USDT TRC-20'].amount + balances['USDD TRC-20'].amount;
usdValue = balances['USDT TRC-20'].usdValue + balances['USDD TRC-20'].usdValue;
break;
}
typeTotal += balance;
typeUsdTotal += usdValue;
message += `├ Balance: ${balance.toFixed(8)} ${baseType}\n`;
message += `├ Value: $${usdValue.toFixed(2)}\n`;
message += `├ Address: \`${wallet.address}\`\n`;
message += `└ Archived: ${date.toLocaleDateString()}\n\n`;
}
message += `📊 *Total ${baseType}*:\n`;
message += `├ Amount: ${typeTotal.toFixed(8)} ${baseType}\n`;
message += `└ Value: $${typeUsdTotal.toFixed(2)}\n\n`;
totalUsdValue += typeUsdTotal;
}
message += `💰 *Total Value of Archived Wallets:* $${totalUsdValue.toFixed(2)}`;
await this.bot.editMessageText(message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
});
} catch (error) {
console.error('Error in handleViewArchivedWallets:', error);
await this.bot.sendMessage(chatId, 'Error loading archived wallets. Please try again.');
}
}
async handleRefreshBalance(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
try {
await this.bot.editMessageText(
'🔄 Refreshing balances...',
{
chat_id: chatId,
message_id: messageId
}
);
// Re-fetch and display updated balances
await this.showBalance({
chat: { id: chatId },
from: { id: callbackQuery.from.id }
});
// Delete the "refreshing" message
await this.bot.deleteMessage(chatId, messageId);
} catch (error) {
console.error('Error in handleRefreshBalance:', error);
await this.bot.editMessageText(
'❌ Error refreshing balances. Please try again.',
{
chat_id: chatId,
message_id: messageId,
reply_markup: {
inline_keyboard: [[
{ text: '« Back', callback_data: 'back_to_balance' }
]]
}
}
);
}
}
async handleBackToBalance(callbackQuery) {
await this.showBalance({
chat: { id: callbackQuery.message.chat.id },
from: { id: callbackQuery.from.id }
});
await this.bot.deleteMessage(callbackQuery.message.chat.id, callbackQuery.message.message_id);
}
// Helper methods
getBaseWalletType(walletType) {
if (walletType.includes('TRC-20')) return 'TRON';
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
getWalletAddress(wallets, walletType) {
if (walletType.includes('TRC-20')) return wallets.TRON.address;
if (walletType.includes('ERC-20')) return wallets.ETH.address;
if (walletType === 'BTC') return wallets.BTC.address;
if (walletType === 'LTC') return wallets.LTC.address;
if (walletType === 'ETH') return wallets.ETH.address;
throw new Error('Invalid wallet type');
}
getNetworkName(walletType) {
if (walletType.includes('TRC-20')) return 'Tron Network (TRC-20)';
if (walletType.includes('ERC-20')) return 'Ethereum Network (ERC-20)';
if (walletType === 'BTC') return 'Bitcoin Network';
if (walletType === 'LTC') return 'Litecoin Network';
if (walletType === 'ETH') return 'Ethereum Network';
return 'Unknown Network';
}
}