Add merge user functionality for some edge cases for users without username
This commit is contained in:
@@ -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.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user