Merge pull request 'main' (#27) from main into feature/admin-section

Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
1323ed5
2024-11-19 17:57:35 +00:00
6 changed files with 812 additions and 562 deletions

View File

@@ -95,6 +95,8 @@ const initDb = async () => {
city TEXT, city TEXT,
district TEXT, district TEXT,
status INTEGER DEFAULT 0, status INTEGER DEFAULT 0,
total_balance REAL DEFAULT 0,
bonus_balance REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) )
`); `);

View File

@@ -370,6 +370,10 @@ export default class AdminProductHandler {
text: 'No products for this location', text: 'No products for this location',
markup: { markup: {
inline_keyboard: [ inline_keyboard: [
[{
text: '📥 Import Products',
callback_data: `add_product_${locationId}_${categoryId}_${subcategoryId}`
}],
[{text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}`}] [{text: '« Back', callback_data: `prod_category_${locationId}_${categoryId}`}]
] ]
} }

View File

@@ -1,5 +1,7 @@
import db from '../config/database.js'; import db from '../config/database.js';
import User from '../models/User.js'; import User from '../models/User.js';
import WalletService from "../utils/walletService.js";
import config from "../config/config.js";
export default class UserProductHandler { export default class UserProductHandler {
constructor(bot) { constructor(bot) {
@@ -209,7 +211,10 @@ export default class UserProductHandler {
message_id: messageId, message_id: messageId,
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` } {
text: '« Back to Categories',
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
}
]] ]]
} }
} }
@@ -223,7 +228,10 @@ export default class UserProductHandler {
text: sub.name, text: sub.name,
callback_data: `shop_subcategory_${locationId}_${categoryId}_${sub.id}` callback_data: `shop_subcategory_${locationId}_${categoryId}_${sub.id}`
}]), }]),
[{ text: '« Back to Categories', callback_data: `shop_district_${location.country}_${location.city}_${location.district}` }] [{
text: '« Back to Categories',
callback_data: `shop_district_${location.country}_${location.city}_${location.district}`
}]
] ]
}; };
@@ -277,7 +285,10 @@ export default class UserProductHandler {
message_id: messageId, message_id: messageId,
reply_markup: { reply_markup: {
inline_keyboard: [[ inline_keyboard: [[
{ text: '« Back to Subcategories', callback_data: `shop_category_${locationId}_${categoryId}` } {
text: '« Back to Subcategories',
callback_data: `shop_category_${locationId}_${categoryId}`
}
]] ]]
} }
} }
@@ -345,9 +356,13 @@ Subcategory: ${product.subcategory_name}
let photoMessageId = null; let photoMessageId = null;
// First send the photo if it exists // First send the photo if it exists
let photoMessage;
if (product.photo_url) { if (product.photo_url) {
const photoMessage = await this.bot.sendPhoto(chatId, product.photo_url); try {
photoMessageId = photoMessage.message_id; photoMessage = await this.bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
} catch (e) {
photoMessage = await this.bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
}
} }
const keyboard = { const keyboard = {
@@ -366,7 +381,10 @@ Subcategory: ${product.subcategory_name}
callback_game: product.quantity_in_stock <= 1 ? {} : null // Disabled if stock is 1 or less 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}` }] [{
text: `« Back to ${product.subcategory_name}`,
callback_data: `shop_subcategory_${product.location_id}_${product.category_id}_${product.subcategory_id}_${photoMessageId}`
}]
] ]
}; };
@@ -590,6 +608,87 @@ Subcategory: ${product.subcategory_name}
} }
} }
async handlePay(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const userId = callbackQuery.from.id;
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
const state = this.userStates.get(chatId);
try {
await User.recalculateBalance(userId);
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 totalPrice = product.price * quantity;
const balance = user.total_balance + user.bonus_balance;
if (totalPrice > balance) {
this.userStates.delete(chatId);
await this.bot.editMessageText(`Not enough money`, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
});
return;
}
await db.runAsync(
'INSERT INTO purchases (user_id, product_id, wallet_type, tx_hash, quantity, total_price) VALUES (?, ?, ?, ?, ?, ?)',
[user.id, product.id, walletType, "null", quantity, totalPrice]
);
let hiddenPhotoMessage;
if (product.hidden_photo_url) {
try {
hiddenPhotoMessage = await this.bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
} catch (e) {
hiddenPhotoMessage = await this.bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
}
}
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: "I've got it!", callback_data: "Asdasdasd"}],
[{text: "Contact support", url: config.SUPPORT_LINK}]
]
};
await this.bot.sendMessage(chatId, message, {reply_markup: keyboard});
await this.bot.deleteMessage(chatId, callbackQuery.message.message_id);
} catch (error) {
console.error('Error in handleBuyProduct:', error);
await this.bot.sendMessage(chatId, 'Error processing purchase. Please try again.');
}
}
async showPurchases(msg) { async showPurchases(msg) {
const chatId = msg.chat.id; const chatId = msg.chat.id;
const userId = msg.from.id; const userId = msg.from.id;

View File

@@ -239,6 +239,9 @@ bot.on('callback_query', async (callbackQuery) => {
} else if (action.startsWith('buy_product_')) { } else if (action.startsWith('buy_product_')) {
logDebug(action, 'handleBuyProduct'); logDebug(action, 'handleBuyProduct');
await userProductHandler.handleBuyProduct(callbackQuery); await userProductHandler.handleBuyProduct(callbackQuery);
} else if (action.startsWith('pay_with_')) {
logDebug(action, 'handlePay');
await userProductHandler.handlePay(callbackQuery);
} }
// Admin location management // Admin location management
else if (action === 'add_location') { else if (action === 'add_location') {
@@ -287,7 +290,7 @@ bot.on('callback_query', async (callbackQuery) => {
logDebug(action, 'handleSubcategorySelection'); logDebug(action, 'handleSubcategorySelection');
await adminProductHandler.handleSubcategorySelection(callbackQuery); await adminProductHandler.handleSubcategorySelection(callbackQuery);
} else if (action.startsWith('list_products_')) { } else if (action.startsWith('list_products_')) {
logDebug(action, 'handleSubcategorySelection'); logDebug(action, 'handleProductListPage');
await adminProductHandler.handleProductListPage(callbackQuery); await adminProductHandler.handleProductListPage(callbackQuery);
} else if (action.startsWith('add_product_')) { } else if (action.startsWith('add_product_')) {
logDebug(action, 'handleAddProduct'); logDebug(action, 'handleAddProduct');
@@ -299,7 +302,7 @@ bot.on('callback_query', async (callbackQuery) => {
logDebug(action, 'handleProductEdit'); logDebug(action, 'handleProductEdit');
await adminProductHandler.handleProductEdit(callbackQuery) await adminProductHandler.handleProductEdit(callbackQuery)
} else if (action.startsWith('delete_product_')) { } else if (action.startsWith('delete_product_')) {
logDebug(action, 'handleViewProduct'); logDebug(action, 'handleProductDelete');
await adminProductHandler.handleProductDelete(callbackQuery); await adminProductHandler.handleProductDelete(callbackQuery);
} else if (action.startsWith('confirm_delete_product_')) { } else if (action.startsWith('confirm_delete_product_')) {
logDebug(action, 'handleConfirmDelete'); logDebug(action, 'handleConfirmDelete');
@@ -310,13 +313,13 @@ bot.on('callback_query', async (callbackQuery) => {
logDebug(action, 'handleViewUser'); logDebug(action, 'handleViewUser');
await adminUserHandler.handleViewUser(callbackQuery); await adminUserHandler.handleViewUser(callbackQuery);
} else if (action.startsWith('list_users_')) { } else if (action.startsWith('list_users_')) {
logDebug(action, 'handleViewUser'); logDebug(action, 'handleUserListPage');
await adminUserHandler.handleUserListPage(callbackQuery); await adminUserHandler.handleUserListPage(callbackQuery);
} else if (action.startsWith('delete_user_')) { } else if (action.startsWith('delete_user_')) {
logDebug(action, 'handleDeleteUser'); logDebug(action, 'handleDeleteUser');
await adminUserHandler.handleDeleteUser(callbackQuery); await adminUserHandler.handleDeleteUser(callbackQuery);
} else if (action.startsWith('block_user_')) { } else if (action.startsWith('block_user_')) {
logDebug(action, 'handleDeleteUser'); logDebug(action, 'handleBlockUser');
await adminUserHandler.handleBlockUser(callbackQuery); await adminUserHandler.handleBlockUser(callbackQuery);
} else if (action.startsWith('confirm_delete_user_')) { } else if (action.startsWith('confirm_delete_user_')) {
logDebug(action, 'handleConfirmDelete'); logDebug(action, 'handleConfirmDelete');

View File

@@ -1,4 +1,5 @@
import db from '../config/database.js'; import db from '../config/database.js';
import Wallet from "./Wallet.js";
export default class User { export default class User {
static async create(telegramId, username) { static async create(telegramId, username) {
@@ -103,4 +104,24 @@ export default class User {
throw error; throw error;
} }
} }
static async recalculateBalance(telegramId) {
const user = await User.getById(telegramId);
if (!user) {
return;
}
const archivedBalance = await Wallet.getArchivedWalletsBalance(user.id);
const activeBalance = await Wallet.getActiveWalletsBalance(user.id);
const purchases = await db.getAsync(
`SELECT SUM(total_price) as total_sum FROM purchases WHERE user_id = ?`,
[user.id]
);
const userTotalBalance = (activeBalance + archivedBalance) - (purchases?.total_sum || 0);
await db.runAsync(`UPDATE users SET total_balance = ? WHERE id = ?`, [userTotalBalance, user.id]);
}
} }

121
src/models/Wallet.js Normal file
View File

@@ -0,0 +1,121 @@
import db from "../config/database.js";
import WalletService from "../utils/walletService.js";
export default class Wallet {
static getBaseWalletType(walletType) {
if (walletType.includes('TRC-20')) return 'TRON';
if (walletType.includes('ERC-20')) return 'ETH';
return walletType;
}
static async getArchivedWallets(userId) {
const archivedWallets = await db.allAsync(`
SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type LIKE '%_%'
`, [userId]);
const btcAddress = archivedWallets.find(w => w.wallet_type.startsWith('BTC'))?.address;
const ltcAddress = archivedWallets.find(w => w.wallet_type.startsWith('LTC'))?.address;
const tronAddress = archivedWallets.find(w => w.wallet_type.startsWith('TRON'))?.address;
const ethAddress = archivedWallets.find(w => w.wallet_type.startsWith('ETH'))?.address;
return {
btc: btcAddress,
ltc: ltcAddress,
tron: tronAddress,
eth: ethAddress,
wallets: archivedWallets
}
}
static async getActiveWallets(userId) {
const activeWallets = await db.allAsync(
`SELECT wallet_type, address FROM crypto_wallets WHERE user_id = ? ORDER BY wallet_type`,
[userId]
)
const btcAddress = activeWallets.find(w => w.wallet_type === 'BTC')?.address;
const ltcAddress = activeWallets.find(w => w.wallet_type === 'LTC')?.address;
const tronAddress = activeWallets.find(w => w.wallet_type === 'TRON')?.address;
const ethAddress = activeWallets.find(w => w.wallet_type === 'ETH')?.address;
return {
btc: btcAddress,
ltc: ltcAddress,
tron: tronAddress,
eth: ethAddress,
wallets: activeWallets
}
}
static async getActiveWalletsBalance(userId) {
const activeWallets = await this.getActiveWallets(userId);
const walletService = new WalletService(
activeWallets.btc,
activeWallets.ltc,
activeWallets.tron,
activeWallets.eth,
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
let totalUsdBalance = 0;
for (const [type, balance] of Object.entries(balances)) {
const baseType = this.getBaseWalletType(type);
const wallet = activeWallets.wallets.find(w =>
w.wallet_type === baseType ||
(type.includes('TRC-20') && w.wallet_type === 'TRON') ||
(type.includes('ERC-20') && w.wallet_type === 'ETH')
);
if (!wallet) {
continue;
}
if (wallet) {
totalUsdBalance += balance.usdValue;
}
}
return totalUsdBalance;
}
static async getArchivedWalletsBalance(userId) {
const archiveWallets = await this.getArchivedWallets(userId);
const walletService = new WalletService(
archiveWallets.btc,
archiveWallets.ltc,
archiveWallets.tron,
archiveWallets.eth,
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000
);
const balances = await walletService.getAllBalances();
let totalUsdBalance = 0;
for (const [type, balance] of Object.entries(balances)) {
const baseType = this.getBaseWalletType(type);
const wallet = archiveWallets.wallets.find(w =>
w.wallet_type === baseType ||
(type.includes('TRC-20') && w.wallet_type.startsWith('TRON')) ||
(type.includes('ERC-20') && w.wallet_type.startsWith('ETH'))
);
if (!wallet) {
continue;
}
if (wallet) {
totalUsdBalance += balance.usdValue;
}
}
return totalUsdBalance;
}
}