File: /var/www/exnova-telegram-bot-v2/app/Models/User.php
<?php
namespace App\Models;
use Illuminate\Support\Facades\Log as SysLog;
use App\Jobs\InitTradingSession;
use App\Telegram\Messages\BlocksHandler;
use App\Telegram\Messages\ParamsReplacer;
use App\Telegram\Messages\Postman;
use App\Telegram\Messages\Preparer;
use App\Telegram\Utils\Curl;
use App\Twill\Capsules\Bots\Models\Bot;
use App\Twill\Capsules\Bots\Models\BotUser;
use App\Twill\Capsules\ExnovaAccounts\Models\ExnovaAccount;
use App\Twill\Capsules\Logs\Models\Log;
use App\Twill\Capsules\Spins\Models\Balance;
use App\Twill\Capsules\Spins\Models\Spin;
use App\Twill\Capsules\Strategies\Models\Strategy;
use App\Twill\Capsules\TradingSessions\Models\TradingSession;
use App\Twill\Capsules\UserSettings\Models\UserSetting;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'telegram_id',
'telegram_name',
'first_name',
'last_name',
'locale',
'state',
'last_message_sent',
'state_changed_date',
'retention_sent_date',
'referral_params',
'country_code',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function settings()
{
return $this->hasOne(UserSetting::class, 'user_id');
}
public function spins()
{
return $this->hasOne(Spin::class, 'user_id');
}
public function logs()
{
return $this->hasMany(Log::class, 'user_id');
}
public function getTelegramUserById($id)
{
return $this->where('telegram_id', '=', $id)->first();
}
public function getBotUser(int $botId)
{
$botUser = Botuser::query()
->where('users_id', $this->id)
->where('bots_id', $botId);
$botUser = $botUser->first();
return $botUser;
}
public static function createUserFromTelegram($data)
{
$activated = true;
if (empty($data['username']) && !empty($data['id'])) {
$data['username'] = $data['id'];
}
$email = $data['username'] . "@example.com";
$iniPassword = substr(md5(date('YmdHis')), 0, 6);
$password = Hash::make($iniPassword);
$userData = [
'name' => strip_tags(@$data['username']),
'telegram_name' => strip_tags(@$data['username']),
'telegram_id' => strip_tags($data['id']),
'first_name' => strip_tags(@$data['first_name']),
'last_name' => strip_tags(@$data['last_name']),
'email' => $email,
'password' => $password,
'token' => Str::random(64),
'activated' => $activated,
'bot_user_types' => ['user'],
'locale' => $data['language_code'] ?? 'en',
'referral_params' => !empty($data['referral_params']) ? $data['referral_params'] : null,
'country_code' => !empty($data['country_code']) ? $data['country_code'] : null,
];
if (isset($data['type']) && $data['type'] == 'group') {
$userData['name'] = $data['title'] . $data['username'];
$userData['telegram_name'] = $data['title'];
}
if (isset($data['type']) && $data['type'] == 'supergroup') {
$userData['name'] = $data['username'];
$userData['telegram_name'] = $data['title'];
}
$user = User::create($userData);
if (!empty($data['photo_url'])) {
$user->avatar = strip_tags($data['photo_url']);
}
$user->save();
$userSettings = new UserSetting();
$userSettings->user_id = $user->id;
$userSettings->signal_generator_state = 0;
$userSettings->save();
$spins = new Spin();
$spins->user_id = $user->id;
$spins->amount = 100;
$spins->save();
return $user;
}
public function exnovaRegisterUser($email, $password, $country, $affid)
{
$url = 'https://api.trade.exnova.com/v5/users/register';
$data = [
'identifier' => $email,
'password' => $password,
'country_id' => $country,
'aff' => (int)$affid,
'afftrack' => 'tg_backend_reg',
'accepted' => ['terms', 'privacy policy'],
];
$curl = new Curl();
$response = $curl->exnovaRequest($url, $data);
return $response;
}
public function exnovaRefreshSsid()
{
if (!$this->activeExnovaAccount()) {
return false;
}
$activeAccount = $this->activeExnovaAccount();
$accountUpdatedAt = Carbon::parse($activeAccount->ssid_date);
$date = Carbon::now()->subDay(7);
if ($activeAccount->ssid_date && $activeAccount->ssid && $accountUpdatedAt > $date) {
return $activeAccount->ssid;
}
$url = 'https://api.trade.exnova.com/v2/login';
$data = [
'identifier' => $activeAccount->email,
'password' => $activeAccount->password,
];
$curl = new Curl();
$response = $curl->exnovaRequest($url, $data);
if (!empty($response['status']) && $response['status'] == 'error') {
throw new \Exception($response['message']);
}
if (!empty($response['code']) && $response['code'] == 'invalid_credentials') {
$post = [];
$post['bot_id'] = $this->bot_id;
$blocksHandler = new BlocksHandler($this, $post);
$bot = new Bot();
$blocks = $bot->getBlocks('invalidCredentials', $this);
$blocksHandler->handle($blocks);
$errorMessage = 'Error during refreshing SSID. Invalid credentials for user: ' . $this->telegram_name . ' account: ' . $activeAccount->email;
$this->sendMessageToChannel($errorMessage);
} elseif (!empty($response['data']) && !empty($response['data']['ssid'])) {
$activeAccount->ssid = $response['data']['ssid'];
if (!empty( $response['data']['user_id'])) {
$activeAccount->exnova_id = $response['data']['user_id'];
}
$activeAccount->ssid_date = Carbon::now();
$activeAccount->save();
} elseif (!empty($response['ssid'])) {
$activeAccount->ssid = $response['ssid'];
if (!empty( $response['user_id'])) {
$activeAccount->exnova_id = $response['user_id'];
}
$activeAccount->ssid_date = Carbon::now();
$activeAccount->save();
} else {
$errorMessage = 'Error during refreshing SSID. Invalid credentials for user: ' . $this->telegram_name . ' account: ' . $activeAccount->email . "\n" . json_encode($response);
throw new \Exception($errorMessage);
}
return $activeAccount->ssid;
}
public function exnovaAuthorizeAccount($email, $password)
{
$url = 'https://api.trade.exnova.com/v2/login';
$data = [
'identifier' => $email,
'password' => $password,
];
$curl = new Curl();
$response = $curl->exnovaRequest($url, $data);
if (!empty($response['status']) && $response['status'] == 'error') {
throw new \Exception($response['message']);
}
return $response;
}
public function exnovaAccounts()
{
return $this->hasMany(ExnovaAccount::class);
}
public function activeExnovaAccount()
{
return $this->exnovaAccounts()->getResults()->where('is_active', 1)->first();
}
public function tradingSessions()
{
return $this->hasMany(TradingSession::class);
}
public function activeTradingSession()
{
return $this->tradingSessions()->getResults()->where('is_active', 1)->first();
}
public function balances()
{
return $this->hasMany(Balance::class);
}
public function debug($message = '', $force = false)
{
if ($force || $this->telegram_name == 'lifecraft' || $this->telegram_name == 'Mendoza911') {
$postman = new Postman($this, []);
if (empty($message)) {
$message = 'Empty message';
}
if (is_array($message)) {
$message = json_encode($message);
}
$message = Preparer::cleanText($message);
$postman->sendMessage($message);
}
}
public function superDebug($session, $addMessage = '')
{
$message = '';
$date = date('Y-m-d H:i:s');
$message .= $date . " ";
$message .= 'Session #'.$session->id . " " . $addMessage . " ";
$message .= sprintf("CPU Load: %.2f%%", $this->getServerLoad()). " ";
$message .= sprintf("Memory Usage: %.2f MB", $this->getServerMemoryUsage()) . " ";
$totalActiveSessions = TradingSession::query()
->where('is_active', 1)
->count();
$message .= " Total active sessions: " . $totalActiveSessions;
if ($addMessage == 'finished') {
$diff = (strtotime($session->updated_at) - strtotime($session->created_at));
$message .= " Session duration: " . gmdate("H:i:s", $diff);
$tradingDiff = (strtotime($session->updated_at) - strtotime($session->start_trading_at));
$message .= " Trading duration: " . gmdate("H:i:s", $tradingDiff);
$message .= " Trades: " . $session->used_spins;
}
SysLog::channel('trading')->info($message);
// $this->sendMessageToChannel($message);
}
function getServerMemoryUsage() {
$meminfo = file_get_contents("/proc/meminfo");
$data = [];
foreach (explode("\n", $meminfo) as $line) {
if (preg_match('/^(\w+):\s+(\d+)\s+kB$/', $line, $matches)) {
$data[$matches[1]] = (int)$matches[2];
}
}
$total = $data['MemTotal'] ?? 0;
$available = $data['MemAvailable'] ?? 0;
return ($total - $available) / 1024; // Convert to MB
}
public function getServerLoad() {
if (is_readable("/proc/stat")) {
$stat1 = file_get_contents("/proc/stat");
sleep(1);
$stat2 = file_get_contents("/proc/stat");
$cpu1 = explode(" ", preg_replace("/cpu +/", "", explode("\n", $stat1)[0]));
$cpu2 = explode(" ", preg_replace("/cpu +/", "", explode("\n", $stat2)[0]));
$idle1 = $cpu1[3];
$idle2 = $cpu2[3];
$total1 = array_sum($cpu1);
$total2 = array_sum($cpu2);
$idle = $idle2 - $idle1;
$total = $total2 - $total1;
$cpuUsage = 100 * ($total - $idle) / $total;
return round($cpuUsage, 2);
} else {
return 0;
}
}
public function sendMessageToChannel($message = '')
{
$channelId = Settings::get('admin.messages', 'errors_channel');
if (empty($message)) {
$message = 'Empty message';
}
if (is_array($message)) {
$message = json_encode($message);
}
$paramsReplacer = new ParamsReplacer($this, []);
$message = $paramsReplacer->replaceParams($message, true);
$message = Preparer::cleanText($message);
$channel = User::where('telegram_id', $channelId)->first();
$postman = new Postman($channel, []);
$postman->sendMessage($message, []);
$this->debug("<b>DEBUG VISIBLE ONLY FOR DEVELOPERS</b>\n".$message);
}
public function getBalances()
{
if (substr_count($this->email, 'lkashdjhskdjfh') && $this->balance_type == 'real') {
// PROMO account
return ['type' => 'real', 'amount' => 999, 'currency' => 'USD'];
}
if (!$activeAccount = $this->activeExnovaAccount()) {
return [];
}
$this->exnovaRefreshSsid();
$output = [];
$returnStatus = null;
$shellCommand = 'timeout 10s node ' . base_path() . '/ssid-ws.js ' . $this->activeExnovaAccount()->ssid . ' allBalances';
$result = exec($shellCommand, $output, $returnStatus);
if ($returnStatus != 0) {
$this->sendMessageToChannel("@{$this->telegram_name} \n Get balances command failed. Response: " . json_encode($output) . "\n Status:" . $returnStatus);
}
$balances = json_decode($result, true);
if (empty($balances)) {
return [];
}
foreach ($balances as $balance) {
$balanceObject = Balance::where('exnova_id', $balance['id'])->first();
if (is_null($balanceObject)) {
$balanceObject = new Balance();
}
if (!$balanceObject->exnova_id || $balanceObject->amount != $balance['amount'] || $balanceObject->bonusAmount != $balance['bonusAmount']) {
$balanceObject->exnova_id = $balance['id'];
$balanceObject->user_id = $this->id;
$balanceObject->exnova_account_id = $activeAccount->id;
$balanceObject->amount = $balance['amount'];
$balanceObject->bonusAmount = $balance['bonusAmount'];
$balanceObject->totalAmount = $balance['amount'] + $balance['bonusAmount'];
$balanceObject->currency = $balance['currency'];
$balanceObject->type = $balance['type'];
$balanceObject->save();
}
if ($balance['type'] == $activeAccount->mode) {
$activeAccount->currency = $balance['currency'];
$activeAccount->save();
}
}
return $balances;
}
public function realBalance()
{
$balances = $this->getBalances();
foreach ($balances as $balance) {
if ($balance['type'] == 'real') {
$balance['amount'] += $balance['bonusAmount'];
return $balance;
}
}
return null;
}
public function demoBalance()
{
$balances = $this->getBalances();
foreach ($balances as $balance) {
if ($balance['type'] == 'demo') {
$balance['amount'] += $balance['bonusAmount'];
return $balance;
}
}
return null;
}
public function getRequiredSessionBalance($betAmount)
{
$currentStrategy = $this->getCurrentStrategy();
$coef = $currentStrategy['coef'];
$max_steps = $currentStrategy['steps'];
$max_trades = $currentStrategy['trades'];
$sum = 0;
for ($i = 0; $i < $max_steps; $i++) {
$sum += $betAmount * pow($coef, $i);
}
$sum *= $max_trades;
return $sum;
}
public function getRequiredSessionSpins()
{
$currentStrategy = $this->getCurrentStrategy();
$max_steps = $currentStrategy['steps'];
$max_trades = $currentStrategy['trades'];
$requiredSpins = $max_steps * $max_trades;
return $requiredSpins;
}
public function getCurrentStrategy()
{
$strategy = $this->settings()->getResults()->current_strategy_id;
$settings = $this->settings()->getResults();
$allStrategies = Strategy::all()
->keyBy('id')
->toArray();
$manualStrategyId = Strategy::MANUAL_STRATEGY_ID;
$allStrategies[$manualStrategyId] = array_merge($allStrategies[$manualStrategyId], [
'coef' => $settings->manual_strategy_coef,
'steps' => $settings->manual_strategy_steps,
'trades' => $settings->manual_strategy_trades,
]);
return $allStrategies[$strategy];
}
public function startAutotradeSession($botId, $forceOrManual = false, $direction = null, $state = TradingSession::STATE_JUST_STARTED)
{
try {
$currentStrategy = $this->getCurrentStrategy();
$newSession = new TradingSession();
$data = [
'is_active' => 1,
'bot_id' => $botId,
'user_id' => $this->id,
'coef' => $currentStrategy['coef'],
'max_steps' => $currentStrategy['steps'],
'max_trades' => $currentStrategy['trades'],
'expiration_time' => $currentStrategy['expiration_time'],
'start_spins' => $this->spins()->getResults()->amount,
'strategy_id' => $this->settings()->getResults()->current_strategy_id,
'is_real' => $this->activeExnovaAccount()->mode == 'real' ? 1 : 0,
'used_spins' => 0,
'profit' => 0,
'state' => $state,
'currency' => $this->exnovaAccounts()->getResults()->where('is_active')->first()->currency ?? 'USD',
];
if ($direction == 'buy') {
$data['direction'] = 1;
} elseif ($direction == 'sell') {
$data['direction'] = -1;
}
if ($forceOrManual == 2 || $forceOrManual == 4) {
$data['is_manual'] = 1;
}
$newSession->fill($data);
$newSession->save();
return $newSession;
} catch (\Exception $e) {
$errorMessage = "User: {$this->telegram_name} \nAccount: {$this->activeExnovaAccount()->email} \nAutotrading command execution failed. \n" . $e->getMessage();
$this->sendMessageToChannel($errorMessage);
return ['error' => $e->getMessage()];
}
}
public function getLocaleValue($data)
{
if (!empty($data[$this->locale]) && !empty(trim(strip_tags($data[$this->locale])))) {
return $data[$this->locale];
}
return $data['en'];
}
public function getTodayStats()
{
$todayStats = Settings::get('trading.stats', 'today');
$todayStatsDate = Settings::get('trading.stats', 'date');
if (strtotime($todayStatsDate) < strtotime(date('Y-m-d'))) {
$successDeals = rand(75,90);
$todayStats = [
'totalDeals' => rand(311,2883),
'successDeals' => $successDeals . '%',
'failedDeals' => (100 - $successDeals) . '%',
'totalProfit' => rand(8311, 35837) . '$',
];
Settings::save('trading.stats', 'today', $todayStats);
Settings::save('trading.stats', 'date', date('Y-m-d'));
}
return $todayStats;
}
public function formatCurrency($amount, $signed = false)
{
$activeAccount = $this->activeExnovaAccount();
$balance = Balance::query()->where('exnova_account_id', $activeAccount->id)->where('type', $activeAccount->mode)->first();
if (is_object($balance)) {
$currency = $balance->currency;
} else {
$currency = $this->activeExnovaAccount()->currency;
}
$sign = '';
if ($signed) {
if ($amount<0) {
$sign = '-';
$amount = abs($amount);
} else {
$sign = '+';
}
}
if ($currency == 'USD') {
return $sign . '$' . number_format($amount, 2, '.', ' ');
} else {
return $sign . number_format($amount, 2, '.', ' ') . ' ' . $currency;
}
}
public function isTranslatable()
{
return false;
}
}