Add merge user functionality for some edge cases for users without username

This commit is contained in:
t1amak
2025-06-02 12:39:00 +00:00
parent b844ce9b46
commit 87fdc40b26
2 changed files with 169 additions and 87 deletions

View File

@@ -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 <user1> [user2 ...] <amount>";
}
$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 <user1> [user2 ...] <amount>";
}
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.";
}
}

View File

@@ -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);