<?php declare(strict_types=1);
$BOT_TOKEN=getenv('VIOLET_BOT_TOKEN')?:'8790833427:AAGNzxqLfdxWek84eCDs_rYV_w1Z2KfhkoI';
$DB_HOST=getenv('VIOLET_DB_HOST')?:'localhost';
$DB_PORT=getenv('VIOLET_DB_PORT')?:'3306';
$DB_NAME=getenv('VIOLET_DB_NAME')?:'odarair1_violet';
$DB_USER=getenv('VIOLET_DB_USER')?:'odarair1_nothing';
$DB_PASS=getenv('VIOLET_DB_PASS')?:'mohsenM2230570@';
$WEBHOOK_URL=getenv('VIOLET_WEBHOOK_URL')?:'https://bots.odara.ir/violet/bot.php';
$CONTROL_SECRET=getenv('VIOLET_CONTROL_SECRET')?:'violetsecretkey2256';
$OWNER_ID=8298437636;
const MAX_SCORE=600000.0;
const MESSAGE_SCORE=4.5;
const SPAM_FAST_SECONDS=2;
const SPAM_BLOCK_SECONDS=1;
const RANK_CHECK_SECONDS=30;
const CRON_BATCH_SIZE=500;
const CRON_MAX_GROUPS=1000;
const DEFAULT_LEADERBOARD_HOUR=20;
$DEFAULT_RANKS=[
1=>['name'=>'𝐈 𝐄𝐦𝐛𝐞𝐫','score'=>0],
2=>['name'=>'𝐈𝐈 𝐒𝐩𝐚𝐫𝐤','score'=>1500],
3=>['name'=>'𝐈𝐈𝐈 𝐆𝐥𝐨𝐰','score'=>3250],
4=>['name'=>'𝐈𝐕 𝐒𝐡𝐢𝐧𝐞','score'=>5500],
5=>['name'=>'𝐕 𝐑𝐚𝐝𝐢𝐚𝐧𝐭','score'=>8500],
6=>['name'=>'𝐕𝐈 𝐋𝐮𝐦𝐢𝐧','score'=>12500],
7=>['name'=>'𝐕𝐈𝐈 𝐁𝐫𝐢𝐥𝐥','score'=>18000],
8=>['name'=>'𝐕𝐈𝐈𝐈 𝐆𝐥𝐞𝐚𝐦','score'=>25000],
9=>['name'=>'𝐈𝐗 𝐆𝐥𝐢𝐦𝐦𝐞𝐫','score'=>34000],
10=>['name'=>'𝐗 𝐃𝐚𝐳𝐳𝐥𝐞','score'=>45000],
11=>['name'=>'𝐗𝐈 𝐈𝐥𝐥𝐮𝐦','score'=>59000],
12=>['name'=>'𝐗𝐈𝐈 𝐋𝐮𝐦𝐢𝐧𝐚𝐫𝐲','score'=>76000],
13=>['name'=>'𝐗𝐈𝐈𝐈 𝐁𝐞𝐚𝐜𝐨𝐧','score'=>97000],
14=>['name'=>'𝐗𝐈𝐕 𝐓𝐨𝐫𝐜𝐡','score'=>123000],
15=>['name'=>'𝐗𝐕 𝐋𝐚𝐧𝐭𝐞𝐫𝐧','score'=>155000],
16=>['name'=>'𝐗𝐕𝐈 𝐒𝐭𝐚𝐫','score'=>193000],
17=>['name'=>'𝐗𝐕𝐈𝐈 𝐍𝐨𝐯𝐚','score'=>238000],
18=>['name'=>'𝐗𝐕𝐈𝐈𝐈 𝐄𝐜𝐥𝐢𝐩𝐬','score'=>290000],
19=>['name'=>'𝐗𝐈𝐗 𝐀𝐮𝐫𝐨𝐫𝐚','score'=>345000],
20=>['name'=>'𝐗𝐗 𝐕𝐢𝐨𝐥𝐞𝐭','score'=>405000],
21=>['name'=>'𝐗𝐗𝐈 𝐀𝐫𝐜𝐚𝐧𝐞','score'=>465000],
22=>['name'=>'𝐗𝐗𝐈𝐈 𝐌𝐲𝐬𝐭𝐢𝐜','score'=>515000],
23=>['name'=>'𝐗𝐗𝐈𝐈𝐈 𝐙𝐞𝐧𝐢𝐭𝐡','score'=>555000],
24=>['name'=>'𝐗𝐗𝐈𝐕 𝐀𝐩𝐞𝐱','score'=>585000],
25=>['name'=>'𝐗𝐗𝐕 𝐕𝐢𝐨𝐥𝐞𝐭','score'=>MAX_SCORE]
];
function e(string $s):string{return htmlspecialchars($s,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');}
function now():int{return time();}
function fmt($n):string{return number_format((float)$n,1);}
function tgName(array $user):string{$name=trim((string)($user['first_name']??'').' '.(string)($user['last_name']??''));if($name==='')$name='کاربر';return $name;}
class Database{private static ?PDO $pdo=null;
public static function get():PDO{if(self::$pdo instanceof PDO)return self::$pdo;global $DB_HOST,$DB_PORT,$DB_NAME,$DB_USER,$DB_PASS;self::$pdo=new PDO("mysql:host={$DB_HOST};port={$DB_PORT};dbname={$DB_NAME};charset=utf8mb4",$DB_USER,$DB_PASS,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES=>false,PDO::ATTR_PERSISTENT=>false]);self::$pdo->exec("SET SESSION innodb_lock_wait_timeout=3");self::$pdo->exec("SET SESSION time_zone='+00:00'");return self::$pdo;}
public static function install():void{$pdo=self::get();$pdo->exec("CREATE TABLE IF NOT EXISTS violet_users (chat_id BIGINT NOT NULL,user_id BIGINT NOT NULL,username VARCHAR(255) NULL,first_name VARCHAR(255) NULL,joined_at INT UNSIGNED NOT NULL,total_messages BIGINT UNSIGNED NOT NULL DEFAULT 0,current_rank TINYINT UNSIGNED NOT NULL DEFAULT 1,current_score DECIMAL(14,3) NOT NULL DEFAULT 0,bonus_score DECIMAL(14,3) NOT NULL DEFAULT 0,last_tag VARCHAR(64) NOT NULL DEFAULT '',is_admin TINYINT(1) NOT NULL DEFAULT 0,member_status VARCHAR(32) NOT NULL DEFAULT 'member',last_message_at INT UNSIGNED NOT NULL DEFAULT 0,last_rank_check INT UNSIGNED NOT NULL DEFAULT 0,rank_updated_at INT UNSIGNED NOT NULL DEFAULT 0,demotion_warning_at INT UNSIGNED NULL DEFAULT NULL,PRIMARY KEY(chat_id,user_id),INDEX idx_chat_status(chat_id,member_status,is_admin),INDEX idx_activity(chat_id,last_message_at),INDEX idx_rank(chat_id,current_rank,current_score),INDEX idx_rank_scan(is_admin,member_status,last_rank_check,last_message_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");$pdo->exec("CREATE TABLE IF NOT EXISTS violet_group_settings (chat_id BIGINT PRIMARY KEY,is_active TINYINT(1) NOT NULL DEFAULT 0,active_until INT UNSIGNED NOT NULL DEFAULT 0,ranks JSON NOT NULL,leaderboard_hour TINYINT UNSIGNED NOT NULL DEFAULT 20,leaderboard_last_sent INT UNSIGNED NOT NULL DEFAULT 0,group_owner_id BIGINT NOT NULL DEFAULT 0,updated_by BIGINT NOT NULL DEFAULT 0,updated_at INT UNSIGNED NOT NULL,welcome_sent TINYINT(1) NOT NULL DEFAULT 0,INDEX idx_active(is_active,active_until,leaderboard_hour,leaderboard_last_sent)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");}}
class TelegramAPI{private string $token;private ?int $botId=null;private ?string $botUsername=null;public function __construct(string $token){$this->token=$token;}public function call(string $method,array $data=[]):array{$url="https://api.telegram.org/bot".$this->token."/".$method;$ch=curl_init($url);curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>$data,CURLOPT_CONNECTTIMEOUT=>2,CURLOPT_TIMEOUT=>8,CURLOPT_HTTPHEADER=>['Expect:'],CURLOPT_ENCODING=>'']);$res=curl_exec($ch);if($res===false){error_log('TELEGRAM CURL ERROR: '.curl_error($ch));curl_close($ch);return [];}curl_close($ch);$json=json_decode($res,true);if(!is_array($json))return[];if(($json['ok']??false)===false)error_log('TELEGRAM API ERROR ['.$method.']: '.($json['description']??'Unknown'));return $json;}public function getBotInfo():?array{if($this->botId!==null)return['id'=>$this->botId,'username'=>$this->botUsername];$r=$this->call('getMe');if(empty($r['ok'])||empty($r['result']['id']))return null;$this->botId=(int)$r['result']['id'];$this->botUsername=$r['result']['username']??null;return['id'=>$this->botId,'username'=>$this->botUsername];}public function sendMessage(int $chatId,string $text,array $extra=[]):?array{return $this->call('sendMessage',array_merge(['chat_id'=>$chatId,'text'=>$text,'parse_mode'=>'HTML','disable_web_page_preview'=>true],$extra));}public function sendPhoto(int $chatId,string $photo,string $caption='',?int $replyTo=null):?array{$data=['chat_id'=>$chatId,'photo'=>$photo,'caption'=>$caption,'parse_mode'=>'HTML'];if($replyTo)$data['reply_parameters']=json_encode(['message_id'=>$replyTo],JSON_UNESCAPED_UNICODE);return $this->call('sendPhoto',$data);}public function getChatMember(int $chatId,int $userId):?array{$r=$this->call('getChatMember',['chat_id'=>$chatId,'user_id'=>$userId]);return !empty($r['ok'])?($r['result']??null):null;}public function getChatAdministrators(int $chatId):array{$r=$this->call('getChatAdministrators',['chat_id'=>$chatId]);return !empty($r['ok'])?($r['result']??[]):[];}public function setMemberTag(int $chatId,int $userId,string $tag):bool{$r=$this->call('setChatMemberTag',['chat_id'=>$chatId,'user_id'=>$userId,'tag'=>$tag]);return !empty($r['ok']);}public function getUserProfilePhoto(int $userId):?string{$r=$this->call('getUserProfilePhotos',['user_id'=>$userId,'limit'=>1]);if(!empty($r['ok'])&&!empty($r['result']['photos'][0][0]['file_id']))return $r['result']['photos'][0][0]['file_id'];return null;}}
class RankManager{private TelegramAPI $tg;private array $defaultRanks;private array $settingsCache=[];private array $userCache=[];public function __construct(TelegramAPI $tg,array $defaultRanks){$this->tg=$tg;$this->defaultRanks=$defaultRanks;}private function getGroupSettings(int $chatId):array{if(isset($this->settingsCache[$chatId]))return $this->settingsCache[$chatId];$pdo=Database::get();$stmt=$pdo->prepare("SELECT * FROM violet_group_settings WHERE chat_id=? LIMIT 1");$stmt->execute([$chatId]);$row=$stmt->fetch();if(!$row){$time=now();$stmt=$pdo->prepare("INSERT INTO violet_group_settings (chat_id,is_active,active_until,ranks,leaderboard_hour,leaderboard_last_sent,group_owner_id,updated_by,updated_at,welcome_sent) VALUES (?,0,0,?,20,0,0,0,?,0)");$stmt->execute([$chatId,json_encode($this->defaultRanks,JSON_UNESCAPED_UNICODE),$time]);$row=['chat_id'=>$chatId,'is_active'=>0,'active_until'=>0,'ranks'=>$this->defaultRanks,'leaderboard_hour'=>DEFAULT_LEADERBOARD_HOUR,'leaderboard_last_sent'=>0,'group_owner_id'=>0,'updated_by'=>0,'updated_at'=>$time,'welcome_sent'=>0];}else{$decoded=json_decode((string)$row['ranks'],true);if(is_array($decoded)){$newRanks=[];foreach($decoded as $level=>$rank){$newRanks[(int)$level]=['name'=>(string)($rank['name']??"Rank {$level}"),'score'=>(float)($rank['score']??0)];}if(!empty($newRanks))$decoded=$newRanks;}$row['ranks']=is_array($decoded)?$decoded:$this->defaultRanks;}$this->settingsCache[$chatId]=$row;return $row;}private function updateGroupSettings(int $chatId,array $data,int $updatedBy):void{if(!$data)return;$pdo=Database::get();$allowed=['is_active','active_until','ranks','leaderboard_hour','leaderboard_last_sent','group_owner_id','welcome_sent'];$fields=[];$params=[];foreach($data as $key=>$value){if(!in_array($key,$allowed,true))continue;if($key==='ranks')$value=json_encode($value,JSON_UNESCAPED_UNICODE);$fields[]="`{$key}`=?";$params[]=$value;}if(!$fields)return;$fields[]='updated_by=?';$params[]=$updatedBy;$fields[]='updated_at=?';$params[]=now();$params[]=$chatId;$stmt=$pdo->prepare("UPDATE violet_group_settings SET ".implode(', ',$fields)." WHERE chat_id=?");$stmt->execute($params);unset($this->settingsCache[$chatId]);}private function getUser(int $chatId,int $userId):?array{$key=$chatId.':'.$userId;if(array_key_exists($key,$this->userCache))return $this->userCache[$key];$stmt=Database::get()->prepare("SELECT * FROM violet_users WHERE chat_id=? AND user_id=? LIMIT 1");$stmt->execute([$chatId,$userId]);$user=$stmt->fetch()?:null;$this->userCache[$key]=$user;return $user;}private function clearUserCache(int $chatId,int $userId):void{unset($this->userCache[$chatId.':'.$userId]);}private function upsertUser(int $chatId,array $user,int $joinedAt,string $status='member',bool $isAdmin=false):void{$stmt=Database::get()->prepare("INSERT INTO violet_users (chat_id,user_id,username,first_name,joined_at,member_status,is_admin) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE username=VALUES(username),first_name=VALUES(first_name),member_status=VALUES(member_status),is_admin=VALUES(is_admin)");$stmt->execute([$chatId,(int)$user['id'],$user['username']??null,$user['first_name']??'',max(1,$joinedAt),$status,$isAdmin?1:0]);$this->clearUserCache($chatId,(int)$user['id']);}private function addMessageScore(int $chatId,int $userId):float{$pdo=Database::get();$time=now();$user=$this->getUser($chatId,$userId);if(!$user)return 0.0;$lastMessage=(int)($user['last_message_at']??0);$elapsed=$lastMessage>0?$time-$lastMessage:PHP_INT_MAX;$points=MESSAGE_SCORE;if($elapsed<SPAM_BLOCK_SECONDS)$points=0.0;elseif($elapsed<SPAM_FAST_SECONDS)$points=1.0;$stmt=$pdo->prepare("UPDATE violet_users SET total_messages=total_messages+1,current_score=LEAST(?,current_score+?),last_message_at=?,last_rank_check=CASE WHEN last_rank_check=0 THEN 0 ELSE last_rank_check END WHERE chat_id=? AND user_id=?");$stmt->execute([MAX_SCORE,$points,$time,$chatId,$userId]);$this->clearUserCache($chatId,$userId);return $points;}public function isGroupActive(int $chatId):bool{$settings=$this->getGroupSettings($chatId);if((int)$settings['is_active']!==1)return false;$until=(int)$settings['active_until'];if($until>0&&$until<=now()){$this->updateGroupSettings($chatId,['is_active'=>0,'active_until'=>0],0);return false;}return true;}public function applyRank(int $chatId,int $userId,bool $force=false):void{$user=$this->getUser($chatId,$userId);if(!$user)return;if((int)$user['is_admin']===1)return;$settings=$this->getGroupSettings($chatId);if((int)$settings['is_active']!==1)return;$time=now();if((int)$settings['active_until']>0&&(int)$settings['active_until']<=$time)return;if(!$force){$lastCheck=(int)($user['last_rank_check']??0);if($lastCheck>0&&($time-$lastCheck)<RANK_CHECK_SECONDS)return;}$pdo=Database::get();$stmt=$pdo->prepare("UPDATE violet_users SET last_rank_check=? WHERE chat_id=? AND user_id=?");$stmt->execute([$time,$chatId,$userId]);$this->clearUserCache($chatId,$userId);$user=$this->getUser($chatId,$userId);if(!$user)return;$score=(float)$user['current_score'];$currentRank=max(1,(int)$user['current_rank']);$qualifiedRank=1;foreach($settings['ranks'] as $level=>$rank){if($score>=(float)$rank['score'])$qualifiedRank=(int)$level;else break;}if($qualifiedRank>$currentRank){$this->changeRank($chatId,$userId,$qualifiedRank,$settings,'promotion');return;}if($qualifiedRank===$currentRank&&empty($user['last_tag'])){$this->applyTag($chatId,$userId,$settings,true);}$this->clearUserCache($chatId,$userId);}private function changeRank(int $chatId,int $userId,int $newRank,array $settings,string $type):void{$user=$this->getUser($chatId,$userId);if(!$user)return;$oldRank=(int)$user['current_rank'];if($oldRank===$newRank)return;$score=(float)$user['current_score'];$rankName=$settings['ranks'][$newRank]['name']??"Rank {$newRank}";$oldRankName=$settings['ranks'][$oldRank]['name']??"Rank {$oldRank}";$pdo=Database::get();$stmt=$pdo->prepare("UPDATE violet_users SET current_rank=?,current_score=?,rank_updated_at=?,demotion_warning_at=NULL WHERE chat_id=? AND user_id=?");$stmt->execute([$newRank,$score,now(),$chatId,$userId]);$this->clearUserCache($chatId,$userId);try{if($this->tg->setMemberTag($chatId,$userId,$rankName)){$stmt=$pdo->prepare("UPDATE violet_users SET last_tag=?,rank_updated_at=? WHERE chat_id=? AND user_id=?");$stmt->execute([$rankName,now(),$chatId,$userId]);}}catch(Throwable $e){error_log('TAG ERROR: '.$e->getMessage());}$userName=!empty($user['username'])?'@'.$user['username']:($user['first_name']?:'کاربر');if($type==='promotion'){$message=$this->promotionMessage($userName,$oldRankName,$rankName,$score);$this->tg->sendMessage($chatId,$message);}}private function applyTag(int $chatId,int $userId,array $settings,bool $force=false):bool{$user=$this->getUser($chatId,$userId);if(!$user)return false;if((int)$user['is_admin']===1)return false;$rank=max(1,(int)$user['current_rank']);$tag=$settings['ranks'][$rank]['name']??'𝐈 𝐄𝐦𝐛𝐞𝐫';if(!$force&&!empty($user['last_tag'])&&$user['last_tag']===$tag)return true;if(!$this->tg->setMemberTag($chatId,$userId,$tag))return false;$stmt=Database::get()->prepare("UPDATE violet_users SET last_tag=?,rank_updated_at=? WHERE chat_id=? AND user_id=?");$stmt->execute([$tag,now(),$chatId,$userId]);$this->clearUserCache($chatId,$userId);return true;}private function promotionMessage(string $user,string $old,string $new,float $score):string{$msgs=["✦ <b>VIOLET RANK UP</b> ✦\n\n👤 <b>".e($user)."</b>\n\n⌁ <b>".e($old)."</b>  →  <b>".e($new)."</b>\n💠 امتیاز فعلی: <b>".number_format($score,1)."</b>\n\nفعالیتت دیده شد؛ مسیر صعود ادامه دارد. ✨","🌟 <b>یک مرحله بالاتر!</b>\n\n<b>".e($user)."</b> به <b>".e($new)."</b> رسید.\n\n💠 امتیاز: <b>".number_format($score,1)."</b>\n\nقله هنوز تمام نشده. ⚡","🏆 <b>RANK UP</b>\n\nتبریک به <b>".e($user)."</b>!\n\n<b>".e($old)."</b>  →  <b>".e($new)."</b>\nامتیاز: <b>".number_format($score,1)."</b>\n\nادامه بده؛ رنک بعدی سخت‌تره. 🔥"];return $msgs[array_rand($msgs)];}public function handleMyChatMember(array $update):void{if(empty($update['chat']['id'])||empty($update['new_chat_member']['user']['id']))return;$bot=$this->tg->getBotInfo();if(!$bot)return;$botId=(int)$bot['id'];$userId=(int)$update['new_chat_member']['user']['id'];if($userId!==$botId)return;$new=(string)($update['new_chat_member']['status']??'');$old=(string)($update['old_chat_member']['status']??'');if(!in_array($new,['member','administrator'],true))return;if(!in_array($old,['','left','kicked'],true))return;$chatId=(int)$update['chat']['id'];$settings=$this->getGroupSettings($chatId);if((int)$settings['welcome_sent']===1)return;$this->updateGroupSettings($chatId,['welcome_sent'=>1],$botId);$this->tg->sendMessage($chatId,"🌸 <b>ویولت وارد شد.</b>\n\nسیستم رنکینگ هوشمند گروه فعال شد؛ جایی که فعالیت شما جایگاهتان را تعیین می‌کند.\n\n✦ <b>سیستم امتیازدهی</b>\n• هر چت = 4.5 امتیاز\n• چت‌های پشت‌سرهم و اسپمی امتیاز کمتری می‌گیرند\n• رنک فقط بر اساس امتیاز تعیین می‌شود\n\n✦ <b>دستورات</b>\n• <code>/rank</code> — پروفایل\n• <code>/top</code> — ۱۰ نفر برتر\n\n⛔ <b>وضعیت:</b> ربات در حال حاضر غیرفعال است.\n\n🔐 فعال‌سازی فقط توسط مالک ربات انجام می‌شود.");}public function handleChatMember(array $update):void{if(empty($update['chat']['id'])||empty($update['new_chat_member']['user']['id']))return;$chatId=(int)$update['chat']['id'];$member=$update['new_chat_member'];$user=$member['user'];$userId=(int)$user['id'];$status=(string)($member['status']??'member');$isAdmin=in_array($status,['creator','administrator'],true);$existing=$this->getUser($chatId,$userId);if(!$existing){$this->upsertUser($chatId,$user,(int)($update['date']??now()),$status,$isAdmin);if(!$isAdmin&&$this->isGroupActive($chatId)){$this->applyRank($chatId,$userId,true);$settings=$this->getGroupSettings($chatId);$this->applyTag($chatId,$userId,$settings,true);}return;}$wasAdmin=(int)$existing['is_admin']===1;$stmt=Database::get()->prepare("UPDATE violet_users SET username=?,first_name=?,member_status=?,is_admin=? WHERE chat_id=? AND user_id=?");$stmt->execute([$user['username']??null,$user['first_name']??'',$status,$isAdmin?1:0,$chatId,$userId]);$this->clearUserCache($chatId,$userId);if($wasAdmin&&!$isAdmin&&$this->isGroupActive($chatId)){$this->applyRank($chatId,$userId,true);$settings=$this->getGroupSettings($chatId);$this->applyTag($chatId,$userId,$settings,true);}if(!$wasAdmin&&$isAdmin){$stmt=Database::get()->prepare("UPDATE violet_users SET last_tag='' WHERE chat_id=? AND user_id=?");$stmt->execute([$chatId,$userId]);$this->clearUserCache($chatId,$userId);}}public function handleMessage(array $message):void{if(empty($message['chat']['id'])||empty($message['from']['id']))return;$chatId=(int)$message['chat']['id'];$user=$message['from'];$userId=(int)$user['id'];$chatType=(string)($message['chat']['type']??'');if(!in_array($chatType,['group','supergroup'],true))return;if(!empty($user['is_bot']))return;$text=isset($message['text'])?trim((string)$message['text']):'';if(preg_match('~^/rank(?:@\w+)?$~iu',$text)){$this->sendRank($chatId,$userId,isset($message['message_id'])?(int)$message['message_id']:null);return;}if(preg_match('~^/top(?:@\w+)?$~iu',$text)){$this->sendTop($chatId);return;}if(preg_match('~^فعال\s*سازی\s*ویولت\s+(\d+)$~iu',$text,$m)){$this->handleActivate($chatId,$userId,(int)$m[1]);return;}if(preg_match('~^غیرفعال\s*سازی\s*ویولت$~iu',$text)){$this->handleDeactivate($chatId,$userId);return;}if(preg_match('~^تنظیم\s*مالک\s*گروه$~iu',$text)){$this->setGroupOwner($chatId,$userId);return;}if(preg_match('~^مهلت\s*اشتراک\s*ویولت$~iu',$text)){$this->subscriptionStatus($chatId,$userId);return;}if(preg_match('~^تمدید\s+(\d+)$~iu',$text,$m)){$this->extendSubscription($chatId,$userId,(int)$m[1]);return;}if(preg_match('~^بازیابی\s*تگ$~iu',$text)){$this->recoverTags($chatId,$userId);return;}if(preg_match('~^/addscore(?:@\w+)?\s+(\d+(?:\.\d+)?)$~iu',$text,$m)){$target=$this->getReplyUserId($message)??$userId;$this->addBonusScore($chatId,$userId,$target,(float)$m[1]);return;}if(preg_match('~^/addscore(?:@\w+)?\s+(\d+)\s+(\d+(?:\.\d+)?)$~iu',$text,$m)){$this->addBonusScore($chatId,$userId,(int)$m[1],(float)$m[2]);return;}if(!$this->isGroupActive($chatId))return;$existing=$this->getUser($chatId,$userId);if(!$existing){$member=$this->tg->getChatMember($chatId,$userId);if(!$member)return;$status=(string)($member['status']??'member');$isAdmin=in_array($status,['creator','administrator'],true);$this->upsertUser($chatId,$user,now(),$status,$isAdmin);if($isAdmin)return;$existing=$this->getUser($chatId,$userId);}else{if((int)$existing['is_admin']===1)return;$newUsername=$user['username']??null;$newFirstName=(string)($user['first_name']??'');if(($existing['username']??null)!==$newUsername||(string)($existing['first_name']??'')!==$newFirstName){$stmt=Database::get()->prepare("UPDATE violet_users SET username=?,first_name=? WHERE chat_id=? AND user_id=?");$stmt->execute([$newUsername,$newFirstName,$chatId,$userId]);$this->clearUserCache($chatId,$userId);}}$this->addMessageScore($chatId,$userId);$cached=$this->getUser($chatId,$userId);if($cached){$lastCheck=(int)($cached['last_rank_check']??0);if($lastCheck===0||now()-$lastCheck>=RANK_CHECK_SECONDS){$this->applyRank($chatId,$userId,false);}}}private function getReplyUserId(array $message):?int{if(!empty($message['reply_to_message']['from']['id']))return (int)$message['reply_to_message']['from']['id'];return null;}private function addBonusScore(int $chatId,int $executor,int $target,float $amount):void{global $OWNER_ID;if($executor!==$OWNER_ID){$this->tg->sendMessage($chatId,"⛔ فقط مالک ربات");return;}if($amount<=0||$amount>MAX_SCORE){$this->tg->sendMessage($chatId,"⚠️ مقدار نامعتبر است.");return;}if(!$this->getUser($chatId,$target)){$this->tg->sendMessage($chatId,"⚠️ کاربر ثبت نشده.");return;}$pdo=Database::get();$stmt=$pdo->prepare("UPDATE violet_users SET bonus_score=LEAST(?,bonus_score+?),current_score=LEAST(?,current_score+?) WHERE chat_id=? AND user_id=?");$stmt->execute([MAX_SCORE,$amount,MAX_SCORE,$amount,$chatId,$target]);$this->clearUserCache($chatId,$target);$this->applyRank($chatId,$target,true);$user=$this->getUser($chatId,$target);if(!$user)return;$settings=$this->getGroupSettings($chatId);$rankName=$settings['ranks'][(int)$user['current_rank']]['name']??'نامشخص';$this->tg->sendMessage($chatId,"✨ <b>امتیاز ویژه ثبت شد.</b>\n\n➕ مقدار: <b>".fmt($amount)."</b>\n🎖 رنک: <b>".e($rankName)."</b>\n📊 امتیاز کل: <b>".fmt($user['current_score'])."</b>");}private function setGroupOwner(int $chatId,int $userId):void{global $OWNER_ID;if($userId!==$OWNER_ID){$this->tg->sendMessage($chatId,"⛔ فقط مالک ربات");return;}foreach($this->tg->getChatAdministrators($chatId) as $admin){if(($admin['status']??'')==='creator'){$owner=(int)$admin['user']['id'];$this->updateGroupSettings($chatId,['group_owner_id'=>$owner],$userId);$this->tg->sendMessage($chatId,"👑 <b>مالک گروه ثبت شد.</b>");return;}}$this->tg->sendMessage($chatId,"⚠️ مالک گروه شناسایی نشد.");}private function handleActivate(int $chatId,int $userId,int $minutes):void{global $OWNER_ID;if($userId!==$OWNER_ID){$this->tg->sendMessage($chatId,"⛔ فقط مالک ربات");return;}if($minutes<1||$minutes>525600){$this->tg->sendMessage($chatId,"⚠️ مدت نامعتبر.");return;}$settings=$this->getGroupSettings($chatId);$base=max(now(),(int)$settings['active_until']);$until=$base+($minutes*60);$this->updateGroupSettings($chatId,['is_active'=>1,'active_until'=>$until],$userId);$this->tg->sendMessage($chatId,"💜 <b>ویولت فعال شد.</b>\n\n⏱ مدت: <b>{$minutes}</b> دقیقه\n📅 پایان: <code>".gmdate('Y-m-d H:i:s',$until)."</code>\n\n✨ سیستم رنکینگ فعال است.");}private function handleDeactivate(int $chatId,int $userId):void{global $OWNER_ID;if($userId!==$OWNER_ID){$this->tg->sendMessage($chatId,"⛔ فقط مالک ربات");return;}$this->updateGroupSettings($chatId,['is_active'=>0,'active_until'=>0],$userId);$this->tg->sendMessage($chatId,"⛔ <b>ویولت غیرفعال شد.</b>");}private function subscriptionStatus(int $chatId,int $userId):void{$settings=$this->getGroupSettings($chatId);$owner=(int)($settings['group_owner_id']??0);if($userId!==$owner){$this->tg->sendMessage($chatId,"⛔ فقط مالک گروه");return;}$until=(int)$settings['active_until'];if((int)$settings['is_active']!==1||$until<=now()){$this->tg->sendMessage($chatId,"🔴 <b>اشتراک فعال نیست.</b>");return;}$rem=$until-now();$days=intdiv($rem,86400);$hours=intdiv($rem%86400,3600);$mins=intdiv($rem%3600,60);$this->tg->sendMessage($chatId,"💜 <b>وضعیت اشتراک</b>\n━━━━━━━━━━━━━━━━━━\n\n🟢 وضعیت: <b>فعال</b>\n\n⏳ زمان باقی:\n🗓 <b>{$days}</b> روز\n🕐 <b>{$hours}</b> ساعت\n⏱ <b>{$mins}</b> دقیقه\n\n📅 پایان:\n<code>".gmdate('Y-m-d H:i:s',$until)."</code>\n━━━━━━━━━━━━━━━━━━");}private function extendSubscription(int $chatId,int $userId,int $days):void{global $OWNER_ID;$settings=$this->getGroupSettings($chatId);$owner=(int)($settings['group_owner_id']??0);if($userId!==$OWNER_ID&&$userId!==$owner){$this->tg->sendMessage($chatId,"⛔ فقط مالک ربات یا مالک گروه");return;}if($days<1||$days>3650){$this->tg->sendMessage($chatId,"⚠️ ۱ تا ۳۶۵۰ روز.");return;}$until=(int)$settings['active_until'];$newUntil=max(now(),$until)+($days*86400);$this->updateGroupSettings($chatId,['is_active'=>1,'active_until'=>$newUntil],$userId);$this->tg->sendMessage($chatId,"💎 <b>اشتراک تمدید شد.</b>\n\n➕ <b>{$days}</b> روز\n📅 پایان جدید:\n<code>".gmdate('Y-m-d H:i:s',$newUntil)."</code>");}private function recoverTags(int $chatId,int $userId):void{global $OWNER_ID;$settings=$this->getGroupSettings($chatId);$owner=(int)($settings['group_owner_id']??0);if($userId!==$OWNER_ID&&$userId!==$owner){$this->tg->sendMessage($chatId,"⛔ فقط مالک ربات یا مالک گروه");return;}$stmt=Database::get()->prepare("SELECT user_id FROM violet_users WHERE chat_id=? AND is_admin=0 AND member_status NOT IN ('left','kicked') ORDER BY CASE WHEN last_tag='' THEN 0 ELSE 1 END,last_tag ASC LIMIT 500");$stmt->execute([$chatId]);$users=$stmt->fetchAll();if(!$users){$this->tg->sendMessage($chatId,"ℹ️ کاربری یافت نشد.");return;}$this->tg->sendMessage($chatId,"🔄 <b>بازیابی تگ‌ها آغاز شد.</b>\n\nتعداد: <b>".count($users)."</b>");$success=0;$failed=0;foreach($users as $row){$target=(int)$row['user_id'];try{if($this->applyTag($chatId,$target,$settings,true))$success++;else $failed++;}catch(Throwable $e){$failed++;error_log('TAG RECOVERY ERROR: '.$e->getMessage());}usleep(70000);}$this->tg->sendMessage($chatId,"✅ <b>بازیابی تگ‌ها انجام شد.</b>\n\n🟢 موفق: <b>{$success}</b>\n🔴 ناموفق: <b>{$failed}</b>");}public function sendRank(int $chatId,int $userId,?int $replyTo=null):void{$user=$this->getUser($chatId,$userId);if(!$user){$this->tg->sendMessage($chatId,"🥀 اطلاعاتی ثبت نشده.",$replyTo?['reply_to_message_id'=>$replyTo]:[]);return;}$score=(float)$user['current_score'];$settings=$this->getGroupSettings($chatId);$rank=max(1,(int)$user['current_rank']);$rankName=$settings['ranks'][$rank]['name']??'نامشخص';if($rank>=25)$nextText="🏆 <b>MAXIMUM RANK</b>\n💠 <b>600,000</b> امتیاز";else{$next=$settings['ranks'][$rank+1]??null;if(!$next)$nextText="نامشخص";else{$needed=max(0,(float)$next['score']-$score);$nextText="<b>".e($next['name'])."</b>\n• امتیاز باقی‌مانده: <b>".number_format($needed,1)."</b>";}}$member=$this->tg->getChatMember($chatId,$userId);$tgUser=$member['user']??[];$fullName=trim(($tgUser['first_name']??$user['first_name']??'کاربر').' '.($tgUser['last_name']??''));$username=$tgUser['username']??$user['username']??'';$totalMessages=(int)$user['total_messages'];$photo=$this->tg->getUserProfilePhoto($userId);$caption="╭━━━━━━━━━━━━━━━━━━╮\n      💜 <b>VIOLET PROFILE</b>\n╰━━━━━━━━━━━━━━━━━━╯\n\n👤 <b>".e($fullName)."</b>\n🔗 ".($username?'@'.e($username):'ندارد')."\n🆔 <code>{$userId}</code>\n\n🎖 <b>رنک فعلی</b>\n".e($rankName)."\n\n💠 <b>امتیاز</b>\n<code>".number_format($score,1)."</code>\n\n💬 <b>تعداد چت</b>\n<b>".number_format($totalMessages)."</b>\n\n🚀 <b>رنک بعدی</b>\n".$nextText;if($photo)$this->tg->sendPhoto($chatId,$photo,$caption,$replyTo);else $this->tg->sendMessage($chatId,$caption,$replyTo?['reply_to_message_id'=>$replyTo]:[]);}public function sendTop(int $chatId):void{$settings=$this->getGroupSettings($chatId);$stmt=Database::get()->prepare("SELECT user_id,first_name,username,current_rank,current_score,total_messages FROM violet_users WHERE chat_id=? AND is_admin=0 AND member_status NOT IN ('left','kicked') ORDER BY current_score DESC,current_rank DESC,total_messages DESC LIMIT 10");$stmt->execute([$chatId]);$rows=$stmt->fetchAll();if(!$rows){$this->tg->sendMessage($chatId,"🥀 اطلاعات کافی نیست.");return;}$text="╭━━━━━━━━━━━━━━╮\n        🏆 <b>VIOLET TOP</b>\n╰━━━━━━━━━━━━━━╯\n\n";$pos=1;foreach($rows as $row){$icon=$pos===1?'🥇':($pos===2?'🥈':($pos===3?'🥉':'◈'));$name=trim((string)($row['first_name']??''));if($name==='')$name=!empty($row['username'])?'@'.$row['username']:'کاربر ناشناس';$rankName=$settings['ranks'][(int)$row['current_rank']]['name']??'نامشخص';$score=number_format((float)$row['current_score'],1);$text.="{$icon} <b>#{$pos}</b> ".e($name)."\n   🎖 ".e($rankName)."  ·  💠 <b>{$score}</b>\n\n";$pos++;}$text.="━━━━━━━━━━━━━━━━━━━━\n✨ <i>جایگاهت را با فعالیتت بساز.</i>";$this->tg->sendMessage($chatId,$text);}public function runCron(int $batchSize=CRON_BATCH_SIZE):void{$pdo=Database::get();$time=now();$pdo->prepare("UPDATE violet_group_settings SET is_active=0, active_until=0 WHERE is_active=1 AND active_until>0 AND active_until<=?")->execute([$time]);$cutoff=$time-RANK_CHECK_SECONDS;$limit=max(1,min(CRON_BATCH_SIZE,$batchSize));$sql="SELECT chat_id,user_id FROM violet_users WHERE is_admin=0 AND member_status NOT IN ('left','kicked') AND last_message_at>0 AND (last_rank_check=0 OR last_rank_check<=?) ORDER BY last_rank_check ASC,last_message_at DESC LIMIT {$limit}";$stmt=$pdo->prepare($sql);$stmt->execute([$cutoff]);$rows=$stmt->fetchAll();foreach($rows as $row){try{$chatId=(int)$row['chat_id'];$userId=(int)$row['user_id'];$this->clearUserCache($chatId,$userId);$this->applyRank($chatId,$userId,true);}catch(Throwable $e){error_log('CRON RANK ERROR: '.$e->getMessage());}}$this->sendScheduledLeaderboards();echo "VIOLET CRON OK\n";}private function sendScheduledLeaderboards():void{$pdo=Database::get();$hour=(int)gmdate('H');$todayStart=gmmktime(0,0,0);$stmt=$pdo->prepare("SELECT chat_id FROM violet_group_settings WHERE is_active=1 AND leaderboard_hour=? AND (leaderboard_last_sent<? OR leaderboard_last_sent=0) LIMIT ".CRON_MAX_GROUPS);$stmt->execute([$hour,$todayStart]);$groups=$stmt->fetchAll();foreach($groups as $group){$chatId=(int)$group['chat_id'];try{$this->sendTop($chatId);$stmt=$pdo->prepare("UPDATE violet_group_settings SET leaderboard_last_sent=? WHERE chat_id=?");$stmt->execute([now(),$chatId]);unset($this->settingsCache[$chatId]);}catch(Throwable $e){error_log('LEADERBOARD ERROR: '.$e->getMessage());}}}}
try{Database::install();}catch(Throwable $e){http_response_code(500);error_log('DB INSTALL ERROR: '.$e->getMessage());echo 'DB Error';exit;}
$tg=new TelegramAPI($BOT_TOKEN);$rankManager=new RankManager($tg,$DEFAULT_RANKS);
if(isset($_GET['setup'],$_GET['key'])&&$_GET['setup']==='1'&&hash_equals($CONTROL_SECRET,(string)$_GET['key'])){header('Content-Type: application/json; charset=utf-8');$result=$tg->call('setWebhook',['url'=>$WEBHOOK_URL,'allowed_updates'=>json_encode(['message','chat_member','my_chat_member'],JSON_UNESCAPED_UNICODE),'drop_pending_updates'=>false,'max_connections'=>100]);echo json_encode($result,JSON_UNESCAPED_UNICODE);exit;}
if(isset($_GET['cron'],$_GET['key'])&&$_GET['cron']==='1'&&hash_equals($CONTROL_SECRET,(string)$_GET['key'])){$rankManager->runCron();exit;}
if(PHP_SAPI==='cli'){if(isset($argv[1])&&$argv[1]==='cron')$rankManager->runCron();else echo "VIOLET BOT READY\n";exit;}
$raw=file_get_contents('php://input');if($raw===false||trim($raw)===''){echo 'VIOLET BOT ONLINE';exit;}
$update=json_decode($raw,true);if(!is_array($update)){http_response_code(200);exit;}
http_response_code(200);if(function_exists('fastcgi_finish_request'))fastcgi_finish_request();
try{if(isset($update['message'])&&is_array($update['message']))$rankManager->handleMessage($update['message']);if(isset($update['chat_member'])&&is_array($update['chat_member']))$rankManager->handleChatMember($update['chat_member']);if(isset($update['my_chat_member'])&&is_array($update['my_chat_member']))$rankManager->handleMyChatMember($update['my_chat_member']);}catch(Throwable $e){error_log('VIOLET BOT ERROR: '.$e->getMessage());}