diff --git a/src/salvium_tipbot_commands.php b/src/salvium_tipbot_commands.php index 2699422..dbfe8c5 100644 --- a/src/salvium_tipbot_commands.php +++ b/src/salvium_tipbot_commands.php @@ -73,7 +73,6 @@ class SalviumTipBotCommands { return "Your deposit address: {$user['salvium_subaddress']}"; } - private function cmd_balance(array $args, array $ctx): string { $user = $this->db->getUserByTelegramId($ctx['user_id']); return $user ? "Your balance: {$user['tip_balance']} SAL" : "No account found. Use /deposit first."; @@ -99,100 +98,117 @@ class SalviumTipBotCommands { return "Withdrawal request submitted. Processing soon."; } - -private function cmd_tip(array $args, array $ctx): string { - if (count($args) < 3) { - return "Usage: /tip [user2 ...] "; - } - - $rawAmount = $args[count($args) - 1]; - $amount = (float)$rawAmount; - - if (!is_numeric($rawAmount) || $amount <= 0) { - return "Invalid tip amount."; - } - - if ($amount < $this->config['MIN_TIP_AMOUNT']) { - return "Each tip must be at least {$this->config['MIN_TIP_AMOUNT']} SAL."; - } - - - $usernames = array_filter(array_slice($args, 1, -1), function($u) { - $u = trim($u); - return $u !== '' && $u !== '@'; - }); - - $maxRecipients = $this->config['MAX_MULTI_TIPS'] ?? 1; - - if (count($usernames) > $maxRecipients) { - return "You can tip up to {$maxRecipients} users at once."; - } - - $sender = $this->db->getUserByTelegramId($ctx['user_id']); - $total = $amount * count($usernames); - - if (!$sender || $sender['tip_balance'] < $total) { - return "Insufficient funds. You need at least {$total} SAL to tip these users."; - } - - $successful = []; - - foreach ($usernames as $targetUsername) { - $cleanUsername = trim(ltrim($targetUsername, '@')); - - if ($cleanUsername === '' || !preg_match('/^[a-zA-Z0-9_]{5,32}$/', $cleanUsername)) { - continue; // skip invalid usernames + private function cmd_tip(array $args, array $ctx): string { + if (count($args) < 3) { + return "Usage: /tip [user2 ...] "; } - try { - $recipient = $this->db->ensureUserExists( - 0, - $cleanUsername, - fn() => $this->wallet->getNewSubaddress(), - true - ); + $rawAmount = $args[count($args) - 1]; + $amount = (float)$rawAmount; - if (!$recipient) continue; + if (!is_numeric($rawAmount) || $amount <= 0) { + return "Invalid tip amount."; + } - $this->db->updateUserTipBalance($sender['id'], $amount, 'subtract'); - $this->db->addTip($sender['id'], $recipient['id'], $amount, $ctx['chat_id']); - $successful[] = $cleanUsername; + if ($amount < $this->config['MIN_TIP_AMOUNT']) { + return "Each tip must be at least {$this->config['MIN_TIP_AMOUNT']} SAL."; + } - if (!empty($recipient['telegram_user_id']) && $recipient['telegram_user_id'] > 0 && $recipient['telegram_user_id'] !== (100_000 + (crc32($cleanUsername) % 900_000))) { - sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} SAL! Use /balance to check."); - } else { - sendMessage($ctx['chat_id'], "Hey @$cleanUsername, you just got a tip from @$ctx[username]!"); - sendGif( - $ctx['chat_id'], - 'CgACAgQAAxkBAAOQaDjcu6ftEKHp3ZCCKX8p6hTkqxEAAtYaAAL4YMlR4yZwk_GMuWg2BA', - "DM me and run /claim to receive it." - ); + + $usernames = array_filter(array_slice($args, 1, -1), function($u) { + $u = trim($u); + return $u !== '' && $u !== '@'; + }); + + $maxRecipients = $this->config['MAX_MULTI_TIPS'] ?? 1; + + if (count($usernames) > $maxRecipients) { + return "You can tip up to {$maxRecipients} users at once."; + } + + $sender = $this->db->getUserByTelegramId($ctx['user_id']); + $total = $amount * count($usernames); + + if (!$sender || $sender['tip_balance'] < $total) { + return "Insufficient funds. You need at least {$total} SAL to tip these users."; + } + + $successful = []; + + foreach ($usernames as $targetUsername) { + $cleanUsername = trim(ltrim($targetUsername, '@')); + + if ($cleanUsername === '' || !preg_match('/^[a-zA-Z0-9_]{5,32}$/', $cleanUsername)) { + continue; // skip invalid usernames } - } catch (Throwable $e) { - // Silently skip + try { + $recipient = $this->db->ensureUserExists( + 0, + $cleanUsername, + fn() => $this->wallet->getNewSubaddress(), + true + ); + + if (!$recipient) continue; + + $this->db->updateUserTipBalance($sender['id'], $amount, 'subtract'); + $this->db->addTip($sender['id'], $recipient['id'], $amount, $ctx['chat_id']); + $successful[] = $cleanUsername; + + if (!empty($recipient['telegram_user_id']) && $recipient['telegram_user_id'] > 0 && $recipient['telegram_user_id'] !== (100_000 + (crc32($cleanUsername) % 900_000))) { + sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} SAL! Use /balance to check."); + } else { + sendMessage($ctx['chat_id'], "Hey @$cleanUsername, you just got a tip from @$ctx[username]!"); + sendGif( + $ctx['chat_id'], + 'CgACAgQAAxkBAAOQaDjcu6ftEKHp3ZCCKX8p6hTkqxEAAtYaAAL4YMlR4yZwk_GMuWg2BA', + "DM me and run /claim to receive it." + ); + } + + } catch (Throwable $e) { + // Silently skip + } } + + if (empty($successful)) { + return "Tip failed — no valid recipients."; + } + + return "Tipped " . implode(', ', $successful) . " {$amount} SAL each!"; } - if (empty($successful)) { - return "Tip failed — no valid recipients."; - } - - return "Tipped " . implode(', ', $successful) . " {$amount} SAL each!"; -} - - - private function cmd_claim(array $args, array $ctx): string { - $user = $this->db->getUserByUsername($ctx['username']); - - if (!$user || $user['telegram_user_id'] > 1_000_000) { - return "Nothing to claim or already claimed."; + $username = $ctx['username'] ?? null; + if (!$username) { + return "You must set a Telegram username to claim tips."; } - $this->db->upgradeTelegramUserId($user['telegram_user_id'], $ctx['user_id']); + // Step 1: Check if a placeholder user exists with this username + $placeholder = $this->db->getUserByUsername($username); + $wasPlaceholder = $placeholder && $placeholder['telegram_user_id'] < 1_000_000; - return "Welcome @{$ctx['username']}, your account has been activated. You can now check your balance and receive tips!"; + // Step 2: Run ensure logic (which may upgrade user) + $this->db->ensureUserExists( + $ctx['user_id'], + $username, + fn() => $this->wallet->getNewSubaddress(), + false + ); + + // Step 3: Fetch again to verify update + $user = $this->db->getUserByUsername($username); + if (!$user) { + return "Nothing to claim."; + } + + // Step 4: Check if user was successfully upgraded + if ($wasPlaceholder && $user['telegram_user_id'] >= 1_000_000) { + return "Welcome @$username, your account has been activated. You can now check your balance and receive tips!"; + } + + return "Nothing to claim or already claimed."; } } diff --git a/src/salvium_tipbot_db.php b/src/salvium_tipbot_db.php index b7a9542..5a26b1d 100755 --- a/src/salvium_tipbot_db.php +++ b/src/salvium_tipbot_db.php @@ -4,6 +4,7 @@ namespace Salvium; use PDO; use PDOException; +use RuntimeException; class SalviumTipBotDB { private PDO $pdo; @@ -128,14 +129,24 @@ class SalviumTipBotDB { bool $allowSynthetic = false ): array { - if (!$username || trim($username) === '' || $telegramId === 0 && !$allowSynthetic) { - throw new RuntimeException("Invalid username or telegram ID."); + if ((empty($username) || trim($username) === '') && $telegramId === 0 && !$allowSynthetic) { + throw new RuntimeException("Invalid username or telegram ID. ".$username." x ".$telegramId); } // 1. Try exact match by Telegram ID $user = $this->getUserByTelegramId($telegramId); - // 2. Try upgrade from placeholder if matching username + // 2. Merge users if one user has 2 records - one with valid username and synthetic id and another one with valid telegram id and null username + if ($telegramId >= 1_000_000 && $username) { + $namedUser = $this->getUserByUsername($username); + if ($namedUser && $user && $namedUser['id'] !== $user['id']) { + $this->mergeUsers($fromId = $namedUser['id'], $intoId = $user['id']); + $this->updateUsername($telegramId, $username); + $user = $this->getUserByTelegramId($telegramId); + } + } + + // 3. Try upgrade from placeholder if matching username if (!$user && $username) { $placeholder = $this->getUserByUsername($username); @@ -148,9 +159,7 @@ class SalviumTipBotDB { } } - - - // 3. Still not found? Possibly create new user + // 4. Still not found? Possibly create new user if (!$user) { $idToUse = $telegramId; @@ -177,6 +186,63 @@ class SalviumTipBotDB { return $user; } + public function mergeUsers(int $fromId, int $intoId): void { + $this->pdo->beginTransaction(); + + try { + // 1. Transfer all tips involving fromId to intoId + $this->pdo->prepare("UPDATE tips SET sender_user_id = ? WHERE sender_user_id = ?") + ->execute([$intoId, $fromId]); + $this->pdo->prepare("UPDATE tips SET recipient_user_id = ? WHERE recipient_user_id = ?") + ->execute([$intoId, $fromId]); + + // 2. Transfer deposits + $this->pdo->prepare("UPDATE deposits SET user_id = ? WHERE user_id = ?") + ->execute([$intoId, $fromId]); + + // 3. Transfer withdrawals + $this->pdo->prepare("UPDATE withdrawals SET user_id = ? WHERE user_id = ?") + ->execute([$intoId, $fromId]); + + // 4. Recalculate balance + // Total deposits + $stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM deposits WHERE user_id = ?"); + $stmt->execute([$intoId]); + $totalDeposits = (float) $stmt->fetchColumn(); + + // Total tips received + $stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM tips WHERE recipient_user_id = ?"); + $stmt->execute([$intoId]); + $totalReceivedTips = (float) $stmt->fetchColumn(); + + // Total tips sent + $stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM tips WHERE sender_user_id = ?"); + $stmt->execute([$intoId]); + $totalSentTips = (float) $stmt->fetchColumn(); + + // Total successful withdrawals + $stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM withdrawals WHERE user_id = ? AND status = 'sent'"); + $stmt->execute([$intoId]); + $totalWithdrawals = (float) $stmt->fetchColumn(); + + // Final balance + $finalBalance = $totalDeposits + $totalReceivedTips - $totalSentTips - $totalWithdrawals; + + // 5. Update user record with recalculated balance + $this->pdo->prepare("UPDATE users SET tip_balance = ? WHERE id = ?") + ->execute([$finalBalance, $intoId]); + + // 6. Delete the placeholder user + $this->pdo->prepare("DELETE FROM users WHERE id = ?") + ->execute([$fromId]); + + $this->pdo->commit(); + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + public function updateUserTipBalance(int $userId, float $amount, string $operation = 'add'): bool { $sql = $operation === 'add' ? "UPDATE users SET tip_balance = tip_balance + ? WHERE id = ?" : "UPDATE users SET tip_balance = tip_balance - ? WHERE id = ?"; $stmt = $this->pdo->prepare($sql);