First working version.

This commit is contained in:
t1amak
2025-05-29 00:34:19 +00:00
parent d6ca3049ae
commit 9f9738591c
10 changed files with 235 additions and 189 deletions

0
.gitignore vendored Normal file → Executable file
View File

4
LICENSE Normal file → Executable file
View File

@@ -1,5 +1,3 @@
// LICENSE
/*
MIT License
Copyright (c) 2025 [Your Name]
@@ -12,4 +10,4 @@ copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/

3
README.md Normal file → Executable file
View File

@@ -1,5 +1,3 @@
// README.md
/*
# Salvium Tip Bot
A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency.
@@ -26,4 +24,3 @@ A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency.
## License
MIT
*/

1
config.sample.php Normal file → Executable file
View File

@@ -12,4 +12,5 @@ return [
'DB_CHARSET' => 'utf8mb4',
'TELEGRAM_BOT_TOKEN' => 'YOUR_TELEGRAM_BOT_TOKEN_HERE',
'IPV4_ONLY' => 1
];

98
salvium_tipbot.php Normal file → Executable file
View File

@@ -7,6 +7,8 @@ use Salvium\SalviumWallet;
$config = require __DIR__ . '/config.php';
require_once __DIR__ . '/src/salvium_tipbot_db.php';
require_once __DIR__ . '/src/salvium_tipbot_wallet.php';
require_once __DIR__ . '/src/salvium_tipbot_common.php';
require_once __DIR__ . '/src/salvium_tipbot_commands.php';
$db = new SalviumTipBotDB($config);
$wallet = new SalviumWallet(
@@ -16,97 +18,21 @@ $wallet = new SalviumWallet(
$config['SALVIUM_RPC_PASSWORD']
);
function sendMessage(int $chatId, string $text, array $options = []): void {
global $config;
$payload = array_merge(['chat_id' => $chatId, 'text' => $text], $options);
file_get_contents("https://api.telegram.org/bot{$config['TELEGRAM_BOT_TOKEN']}/sendMessage?" . http_build_query($payload));
}
$update = json_decode(file_get_contents("php://input"), true);
if (!$update || !isset($update['message'])) exit;
$message = $update['message'];
$chatId = $message['chat']['id'];
$userId = $message['from']['id'];
$username = $message['from']['username'] ?? '';
$text = trim($message['text'] ?? '');
$args = explode(' ', $text);
$args = explode(' ', trim($message['text'] ?? ''));
$command = strtolower($args[0] ?? '');
switch ($command) {
case '/start':
sendMessage($chatId, "Welcome to the Salvium Tip Bot! Use /deposit to get started.");
break;
$context = [
'chat_id' => $message['chat']['id'],
'chat_name' => $message['chat']['title'] ?? $message['chat']['first_name'] ?? '',
'username' => $message['from']['username'] ?? '',
'user_id' => (int) $message['from']['id'],
'raw' => $message['text'] ?? '',
];
case '/deposit':
$user = $db->getUserByTelegramId($userId);
if (!$user) {
$subaddress = $wallet->getNewSubaddress();
if (!$subaddress) {
sendMessage($chatId, "Error generating subaddress. Try again later.");
exit;
}
$db->createUser($userId, $subaddress);
$user = ['salvium_subaddress' => $subaddress];
}
sendMessage($chatId, "Your Salvium deposit address is: {$user['salvium_subaddress']}");
break;
case '/balance':
$user = $db->getUserByTelegramId($userId);
if (!$user) {
sendMessage($chatId, "You don't have an account yet. Use /deposit to create one.");
break;
}
sendMessage($chatId, "Your balance: {$user['tip_balance']} XSL.");
break;
case '/withdraw':
if (count($args) < 3) {
sendMessage($chatId, "Usage: /withdraw <address> <amount>");
break;
}
list(, $address, $amount) = $args;
$amount = (float)$amount;
$user = $db->getUserByTelegramId($userId);
if (!$user || $user['tip_balance'] < $amount) {
sendMessage($chatId, "Insufficient balance or invalid account.");
break;
}
if (!preg_match('/^4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$/', $address)) {
sendMessage($chatId, "Invalid address format.");
break;
}
$db->updateUserTipBalance($user['id'], $amount, 'subtract');
$db->logWithdrawal($user['id'], $address, $amount);
sendMessage($chatId, "Withdrawal request submitted. Processing soon.");
break;
default:
if (str_starts_with($command, '/tip')) {
if (count($args) < 3) {
sendMessage($chatId, "Usage: /tip <username> <amount>");
break;
}
list(, $targetUsername, $amount) = $args;
$amount = (float)$amount;
$sender = $db->getUserByTelegramId($userId);
$recipient = $db->getUserByTelegramId(ltrim($targetUsername, '@'));
if (!$sender || $sender['tip_balance'] < $amount) {
sendMessage($chatId, "Insufficient funds or invalid sender.");
break;
}
if (!$recipient) {
sendMessage($chatId, "Recipient not found. Ask them to run /start first.");
break;
}
$db->updateUserTipBalance($sender['id'], $amount, 'subtract');
$db->addTip($sender['id'], $recipient['id'], $amount, $chatId);
sendMessage($chatId, "Tipped {$targetUsername} {$amount} XSL successfully!");
sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} XSL! Use /balance to check.");
} else {
sendMessage($chatId, "Unknown command.");
}
break;
}
$handler = new SalviumTipBotCommands($db, $wallet);
$handler->handle($command, $args, $context);
?>

140
salvium_tipbot_monitor.php Normal file → Executable file
View File

@@ -1,12 +1,12 @@
<?php
// salvium_tipbot.php
// salvium_tipbot_monitor.php
use Salvium\SalviumTipBotDB;
use Salvium\SalviumWallet;
$config = require __DIR__ . '/config.php';
require_once __DIR__ . '/src/salvium_tipbot_db.php';
require_once __DIR__ . '/src/salvium_tipbot_wallet.php';
require_once __DIR__ . '/src/salvium_tipbot_common.php';
$db = new SalviumTipBotDB($config);
$wallet = new SalviumWallet(
@@ -16,97 +16,55 @@ $wallet = new SalviumWallet(
$config['SALVIUM_RPC_PASSWORD']
);
function sendMessage(int $chatId, string $text, array $options = []): void {
global $config;
$payload = array_merge(['chat_id' => $chatId, 'text' => $text], $options);
file_get_contents("https://api.telegram.org/bot{$config['TELEGRAM_BOT_TOKEN']}/sendMessage?" . http_build_query($payload));
// Handle incoming transfers
$incoming = $wallet->getTransfers('in');
if ($incoming) {
foreach ($incoming as $tx) {
if ($db->isTxidLogged($tx['txid'])) continue;
$subaddress = $tx['address'];
$user = $db->getUserBySubaddress($subaddress);
if (!$user) continue;
$amount = $tx['amount'] / 1e8; // Convert atomic to major units (1 SAL = 1e8 atomic)
$db->logDeposit($user['id'], $tx['txid'], $amount, $tx['height']);
$db->updateUserTipBalance($user['id'], $amount, 'add');
sendMessage($user['telegram_user_id'], "Deposit received: {$amount} SAL added to your balance.");
}
}
$update = json_decode(file_get_contents("php://input"), true);
if (!$update || !isset($update['message'])) exit;
// Handle pending withdrawals
$withdrawals = $db->getPendingWithdrawals();
foreach ($withdrawals as $withdrawal) {
$result = $wallet->transfer([
[
'address' => $withdrawal['address'],
'amount' => (int)($withdrawal['amount'] * 1e8) // major to atomic
]
]);
$message = $update['message'];
$chatId = $message['chat']['id'];
$userId = $message['from']['id'];
$username = $message['from']['username'] ?? '';
$text = trim($message['text'] ?? '');
$args = explode(' ', $text);
$command = strtolower($args[0] ?? '');
switch ($command) {
case '/start':
sendMessage($chatId, "Welcome to the Salvium Tip Bot! Use /deposit to get started.");
break;
case '/deposit':
$user = $db->getUserByTelegramId($userId);
if (!$user) {
$subaddress = $wallet->getNewSubaddress();
if (!$subaddress) {
sendMessage($chatId, "Error generating subaddress. Try again later.");
exit;
}
$db->createUser($userId, $subaddress);
$user = ['salvium_subaddress' => $subaddress];
}
sendMessage($chatId, "Your Salvium deposit address is: {$user['salvium_subaddress']}");
break;
case '/balance':
$user = $db->getUserByTelegramId($userId);
if (!$user) {
sendMessage($chatId, "You don't have an account yet. Use /deposit to create one.");
break;
}
sendMessage($chatId, "Your balance: {$user['tip_balance']} XSL.");
break;
case '/withdraw':
if (count($args) < 3) {
sendMessage($chatId, "Usage: /withdraw <address> <amount>");
break;
}
list(, $address, $amount) = $args;
$amount = (float)$amount;
$user = $db->getUserByTelegramId($userId);
if (!$user || $user['tip_balance'] < $amount) {
sendMessage($chatId, "Insufficient balance or invalid account.");
break;
}
if (!preg_match('/^4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$/', $address)) {
sendMessage($chatId, "Invalid address format.");
break;
}
$db->updateUserTipBalance($user['id'], $amount, 'subtract');
$db->logWithdrawal($user['id'], $address, $amount);
sendMessage($chatId, "Withdrawal request submitted. Processing soon.");
break;
default:
if (str_starts_with($command, '/tip')) {
if (count($args) < 3) {
sendMessage($chatId, "Usage: /tip <username> <amount>");
break;
}
list(, $targetUsername, $amount) = $args;
$amount = (float)$amount;
$sender = $db->getUserByTelegramId($userId);
$recipient = $db->getUserByTelegramId(ltrim($targetUsername, '@'));
if (!$sender || $sender['tip_balance'] < $amount) {
sendMessage($chatId, "Insufficient funds or invalid sender.");
break;
}
if (!$recipient) {
sendMessage($chatId, "Recipient not found. Ask them to run /start first.");
break;
}
$db->updateUserTipBalance($sender['id'], $amount, 'subtract');
$db->addTip($sender['id'], $recipient['id'], $amount, $chatId);
sendMessage($chatId, "Tipped {$targetUsername} {$amount} XSL successfully!");
sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} XSL! Use /balance to check.");
} else {
sendMessage($chatId, "Unknown command.");
}
break;
if ($result && isset($result['tx_hash'])) {
$db->updateWithdrawalTxid($withdrawal['id'], $result['tx_hash']);
$db->updateWithdrawalStatus($withdrawal['id'], 'sent');
sendMessage($withdrawal['user_id'], "Withdrawal of {$withdrawal['amount']} SAL sent. TxID: {$result['tx_hash']}");
} else {
$db->updateWithdrawalStatus($withdrawal['id'], 'failed');
sendMessage($withdrawal['user_id'], "Withdrawal failed. Please try again later or contact support.");
}
}
// Handle pending tips
$users = [];
$tips = $db->getAllPendingTips();
foreach ($tips as $tip) {
$recipientId = $tip['recipient_user_id'];
if (!isset($users[$recipientId])) {
$users[$recipientId] = $db->getUserByTelegramId($recipientId);
}
$db->updateUserTipBalance($recipientId, $tip['amount'], 'add');
$db->markTipsAsCredited([$tip['id']]);
sendMessage($recipientId, "You received a credited tip of {$tip['amount']} SAL. Use /balance to check.");
}
?>

View File

@@ -0,0 +1,87 @@
<?php
use Salvium\SalviumTipBotDB;
use Salvium\SalviumWallet;
class SalviumTipBotCommands {
private SalviumTipBotDB $db;
private SalviumWallet $wallet;
public function __construct(SalviumTipBotDB $db, SalviumWallet $wallet) {
$this->db = $db;
$this->wallet = $wallet;
}
public function handle(string $command, array $args, array $context): void {
$method = 'cmd_' . ltrim($command, '/');
if (method_exists($this, $method)) {
$response = $this->$method($args, $context);
} else {
//$response = "Unknown command.";
$response = "";
}
sendMessage($context['chat_id'], $response);
$this->db->logMessage($context['chat_id'], $context['chat_name'], $context['username'], $context['raw'], $response);
}
private function cmd_start(array $args, array $ctx): string {
if (!empty($ctx['username'])) {
$this->db->updateUsername($ctx['user_id'], $ctx['username']);
}
return "Welcome to the Salvium Tip Bot! Use /deposit to get started.";
}
private function cmd_deposit(array $args, array $ctx): string {
$user = $this->db->getUserByTelegramId($ctx['user_id']);
if (!$user) {
$sub = $this->wallet->getNewSubaddress();
if (!$sub) return "Error generating subaddress.";
$this->db->createUser($ctx['user_id'], $sub);
if (!empty($ctx['username'])) {
$this->db->updateUsername($ctx['user_id'], $ctx['username']);
}
return "Your deposit address: $sub";
}
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.";
}
private function cmd_withdraw(array $args, array $ctx): string {
if (count($args) < 3) return "Usage: /withdraw <address> <amount>";
list(, $address, $amount) = $args;
$amount = (float) $amount;
$user = $this->db->getUserByTelegramId($ctx['user_id']);
if (!$user || $user['tip_balance'] < $amount) return "Insufficient balance or invalid account.";
if (!isValidSalviumAddress($address)) return "Invalid SAL address format.";
$this->db->updateUserTipBalance($user['id'], $amount, 'subtract');
$this->db->logWithdrawal($user['id'], $address, $amount);
return "Withdrawal request submitted. Processing soon.";
}
private function cmd_tip(array $args, array $ctx): string {
if (count($args) < 3) return "Usage: /tip <username> <amount>";
list(, $targetUsername, $amount) = $args;
$amount = (float)$amount;
$sender = $this->db->getUserByTelegramId($ctx['user_id']);
$recipient = $this->db->getUserByUsername(ltrim($targetUsername, '@'));
if (!$sender || $sender['tip_balance'] < $amount) return "Insufficient funds or invalid sender.";
if (!$recipient) return "Recipient not found. Ask them to run /start first.";
$this->db->updateUserTipBalance($sender['id'], $amount, 'subtract');
$this->db->addTip($sender['id'], $recipient['id'], $amount, $ctx['chat_id']);
sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} SAL! Use /balance to check.");
return "Tipped {$targetUsername} {$amount} SAL successfully!";
}
}
?>

View File

@@ -0,0 +1,24 @@
<?php
function sendMessage(int $chatId, string $text, array $options = []): void {
global $config;
$url = "https://api.telegram.org/bot{$config['TELEGRAM_BOT_TOKEN']}/sendMessage";
$payload = array_merge(['chat_id' => $chatId, 'text' => $text], $options);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
if (!empty($config['IPV4_ONLY'])) {
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
}
curl_exec($ch);
curl_close($ch);
}
function isValidSalviumAddress(string $address): bool {
// Accepts standard and subaddress prefixes for Salvium (e.g. SaLvd, SaLvs)
return preg_match('/^SaLv[a-zA-Z0-9]{95}$/', $address) === 1;
}
?>

44
src/salvium_tipbot_db.php Normal file → Executable file
View File

@@ -63,6 +63,17 @@ class SalviumTipBotDB {
FOREIGN KEY (sender_user_id) REFERENCES users(id),
FOREIGN KEY (recipient_user_id) REFERENCES users(id)
);
CREATE TABLE bot_log (
id INT AUTO_INCREMENT PRIMARY KEY,
chat_id BIGINT NOT NULL,
chat_name VARCHAR(255),
username VARCHAR(255),
message TEXT,
response TEXT,
logged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*/
public function getUserByTelegramId(int $telegramUserId): array|false {
@@ -71,6 +82,17 @@ class SalviumTipBotDB {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function getUserByUsername(string $username): array|false {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function updateUsername(int $telegramUserId, string $username): void {
$stmt = $this->pdo->prepare("UPDATE users SET username = ? WHERE telegram_user_id = ?");
$stmt->execute([$username, $telegramUserId]);
}
public function getUserBySubaddress(string $subaddress): array|false {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE salvium_subaddress = ?");
$stmt->execute([$subaddress]);
@@ -127,6 +149,11 @@ class SalviumTipBotDB {
return $stmt->execute([$senderUserId, $recipientUserId, $amount, $channelId]);
}
public function getAllPendingTips(): array {
$stmt = $this->pdo->query("SELECT * FROM tips WHERE status = 'pending'");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getPendingTipsForUser(int $recipientUserId): array {
$stmt = $this->pdo->prepare("SELECT * FROM tips WHERE recipient_user_id = ? AND status = 'pending'");
$stmt->execute([$recipientUserId]);
@@ -143,5 +170,22 @@ class SalviumTipBotDB {
$stmt = $this->pdo->query("SELECT * FROM withdrawals WHERE status = 'pending'");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function logMessage(int $chatId, string $chatName, string $username, string $message, string $response): void {
try {
$sql = "INSERT INTO bot_log (chat_id, chat_name, username, message, response) VALUES (:chat_id, :chat_name, :username, :message, :response)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':chat_id' => $chatId,
':chat_name' => $chatName,
':username' => $username,
':message' => $message,
':response' => $response,
]);
} catch (PDOException $e) {
error_log("Error logging message: " . $e->getMessage());
}
}
}
?>

23
src/salvium_tipbot_wallet.php Normal file → Executable file
View File

@@ -68,14 +68,25 @@ class SalviumWallet {
return $result['addresses'] ?? false;
}
public function transfer(array $destinations, int $mixin = 11, int $unlockTime = 0, bool $getTxKey = false, bool $doNotRelay = false): array|false {
public function transfer(array $destinations, int $accountIndex = 0, array $subaddrIndices = [0], int $priority = 0, int $ringSize = 16, bool $getTxKey = true): array|false {
$params = [
'destinations' => $destinations,
'mixin' => $mixin,
'unlock_time' => $unlockTime,
'get_tx_key' => $getTxKey,
'do_not_relay' => $doNotRelay
'destinations' => array_map(function ($dest) {
return [
'address' => $dest['address'],
'amount' => (int)($dest['amount']), // already in atomic units
'asset_type' => 'SAL1'
];
}, $destinations),
'source_asset' => 'SAL1',
'dest_asset' => 'SAL1',
'tx_type' => 3,
'account_index' => $accountIndex,
// 'subaddr_indices' => $subaddrIndices,
'priority' => $priority,
'ring_size' => $ringSize,
'get_tx_key' => $getTxKey
];
return $this->_callRpc('transfer', $params);
}