Files
PHP_Bot-ModInterface/software/v0.0.1/bot.php
Thomas a184c31cca Software
v0.0..1
2025-10-05 14:58:05 +02:00

172 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
$DEBUG=true;
$LOG_FILE=__DIR__.'/data/bot.log';
$OUTBOX_FILE=__DIR__.'/data/outbox.txt';
$STOP_FLAG=__DIR__.'/data/stop.flag';
$ENV_FILE=__DIR__.'/.env';
$SETTINGS_FILE=__DIR__.'/data/settings.json';
$TIMERS_FILE=__DIR__.'/data/timers.json';
$GIVEAWAY_FILE=__DIR__.'/data/giveaway.json';
$COMMANDS_FILE=__DIR__.'/data/commands.json';
$PERM_FILE=__DIR__.'/data/permissions.json';
$DB_FILE=__DIR__.'/data/app.db';
function logmsg($s){ global $LOG_FILE,$DEBUG; $line='['.date('Y-m-d H:i:s').'] '.$s.PHP_EOL; file_put_contents($LOG_FILE,$line,FILE_APPEND); if($DEBUG) echo $line; }
function env_load($path){ $env=[]; if(!file_exists($path)) return $env; foreach(file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line){ if(strpos(ltrim($line),'#')===0) continue; $p=explode('=',$line,2); if(count($p)===2)$env[trim($p[0])]=trim($p[1]); } return $env; }
function load_json($f,$d){ if(!file_exists($f)) return $d; $j=json_decode(file_get_contents($f),true); return is_array($j)?$j:$d; }
function save_json($f,$d){ file_put_contents($f, json_encode($d, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); }
function say($send,$chan,$msg){ $send("PRIVMSG #$chan :$msg"); }
function parseTags($raw){ $o=[]; $raw=ltrim($raw,'@'); foreach(explode(';',$raw) as $kv){ $p=explode('=',$kv,2); $o[$p[0]]=$p[1]??''; } return $o; }
function parseFirstMentionOrArg($msg){ if(preg_match('/@([A-Za-z0-9_]{3,25})/',$msg,$m)) return $m[1]; $parts=preg_split('/\s+/',trim($msg)); return count($parts)>1?ltrim($parts[1],'@'):''; }
function expandRandoms($txt){ $txt=preg_replace_callback('/\$(random)(?::(\d+))?/i',function($m){ $max=isset($m[2])?max(1,(int)$m[2]):100; return (string)random_int(1,$max);},$txt); $txt=preg_replace_callback('/\$pick\[(.+?)\]/i',function($m){ $opts=array_map('trim',explode('|',$m[1])); return $opts? (string)$opts[array_rand($opts)] : '';},$txt); return $txt; }
function renderPlaceholders($tpl,$ctx){ $map=['@$user'=>'@'.$ctx['user'],'@$display'=>'@'.$ctx['display'],'@$target'=>'@'.$ctx['target'],'$user'=>$ctx['user'],'$display'=>$ctx['display'],'$target'=>$ctx['target'],'$channel'=>$ctx['channel'],'$cmd'=>$ctx['cmd'],'$args'=>$ctx['args'],'$arg1'=>$ctx['arg1'],'$arg2'=>$ctx['arg2'],'$arg3'=>$ctx['arg3'],'$arg4'=>$ctx['arg4'],'$arg5'=>$ctx['arg5'],'$points'=>$ctx['points']??'0','$time'=>date('H:i'),'$date'=>date('Y-m-d')]; return expandRandoms(strtr($tpl,$map)); }
function db(){ static $pdo=null; global $DB_FILE; if(!$pdo){ $pdo=new PDO('sqlite:'.$DB_FILE); $pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); } return $pdo; }
function add_points($login,$display,$amt){ $db=db(); $st=$db->prepare('INSERT INTO points(user_login,display_name,points) VALUES(?,?,?) ON CONFLICT(user_login) DO UPDATE SET points=points+excluded.points, display_name=excluded.display_name'); $st->execute([$login,$display,$amt]); }
function get_points($login){ $db=db(); $st=$db->prepare('SELECT points FROM points WHERE user_login=?'); $st->execute([$login]); $r=$st->fetch(PDO::FETCH_ASSOC); return $r?(int)$r['points']:0; }
function add_spin($login,$amt,$r1,$r2,$r3,$payout){ $db=db(); $db->prepare('INSERT INTO slots_spins(user_login,amount,r1,r2,r3,payout,ts) VALUES(?,?,?,?,?,?,?)')->execute([$login,$amt,$r1,$r2,$r3,$payout,gmdate('c')]); }
function role_level($tags,$username,$nick){ $lvl=0; $badges=$tags['badges']??''; if($badges){ foreach(explode(',',$badges) as $b){ if(strpos($b,'broadcaster/')===0) return 5; if(strpos($b,'moderator/')===0) $lvl=max($lvl,4); if(strpos($b,'vip/')===0) $lvl=max($lvl,3); if(strpos($b,'subscriber/')===0) $lvl=max($lvl,2); } } if(($tags['mod']??'0')==='1') $lvl=max($lvl,4); if(strtolower($nick)===strtolower($username)) return 5; if(($tags['subscriber']??'0')==='1') $lvl=max($lvl,2); return $lvl; }
function role_required_level($role){ switch($role){ case 'follower': return 1; case 'sub': return 2; case 'vip': return 3; case 'mod': return 4; case 'streamer': return 5; default: return 0; } }
function connect_and_run($env,$settings){
global $OUTBOX_FILE,$STOP_FLAG,$TIMERS_FILE,$GIVEAWAY_FILE,$COMMANDS_FILE,$PERM_FILE;
$username=strtolower($env['TWITCH_USERNAME']??''); $channel=strtolower($env['TWITCH_CHANNEL']??''); $oauth=$env['TWITCH_OAUTH']??''; $useSSL=(($env['IRC_SSL']??'1')==='1');
if(!$username||!$channel||!$oauth){ logmsg('Manglende .env'); sleep(5); return; }
$uri=($useSSL?'tls':'tcp').'://irc.chat.twitch.tv:'.($useSSL?6697:6667);
$fp=@stream_socket_client($uri,$errno,$errstr,30); if(!$fp){ logmsg("Forbindelse fejlede: $errstr"); sleep(5); return; }
stream_set_blocking($fp,false);
$send=function($raw)use($fp){ $log=(stripos($raw,'PASS ')===0)?'PASS oauth:********':$raw; fwrite($fp,$raw."\r\n"); logmsg('>>> '.$log); };
$send("PASS $oauth"); $send("NICK $username"); $send("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"); $send("JOIN #$channel");
$joined=false; $lastMsg=time();
$watchdogEnabled=(bool)($settings['watchdog']??true); $watchdogTimeout=max(30,(int)($settings['timeout']??120));
$commands=[]; $perms=load_json($PERM_FILE,[]); $timers=load_json($TIMERS_FILE,[]); $timerState=[];
$lastReload=0; $lastPermReload=0; $lastTimerReload=0;
$emotes=['Kappa','PogChamp','LUL','BibleThump','Kreygasm'];
while(true){
if(file_exists($STOP_FLAG)){ logmsg('Stopflag fundet'); @unlink($STOP_FLAG); return; }
if(time()-$lastReload>=5){ $commands=load_json($COMMANDS_FILE,[]); $lastReload=time(); }
if(time()-$lastPermReload>=5){ $perms=load_json($PERM_FILE,[]); $lastPermReload=time(); }
if(time()-$lastTimerReload>=5){ $timers=load_json($TIMERS_FILE,[]); $lastTimerReload=time(); }
$line=fgets($fp);
if($line===''||$line===false){
if($watchdogEnabled && time()-$lastMsg>$watchdogTimeout){ logmsg("Ingen data i {$watchdogTimeout}s reconnect."); return; }
} else {
$lastMsg=time(); $line=rtrim($line,"\r\n"); logmsg('<< '.$line);
if(strpos($line,'PING')===0){ $send('PONG :tmi.twitch.tv'); continue; }
if(!$joined && preg_match('/^:([^\\s!]+)!.*\\sJOIN\\s#([^\\s]+)/',$line,$jm)){ if(strtolower($jm[1])===$username){ $joined=true; logmsg('JOIN bekræftet'); say($send,$channel,'🙋 TechNet Botten Online 🙋'); } }
if(preg_match('/^@([^\\s]+)\\s:([^\\s!]+)!.*\\sPRIVMSG\\s#([^\\s]+)\\s:(.*)$/',$line,$m)){
$tags=parseTags($m[1]); $nick=$m[2]; $chan=strtolower($m[3]); $msg=trim($m[4]);
$display=$tags['display-name']??$nick; $login=strtolower($nick);
// +1 point pr. besked
add_points($login,$display,1);
// basic
if(strcasecmp($msg,'!ping')===0){ say($send,$chan,'Pong! 🏓'); continue; }
if(strcasecmp($msg,'!points')===0){ $pts=get_points($login); say($send,$chan,"@$display har $pts point"); continue; }
if(strcasecmp($msg,'!join')===0){
$st=load_json($GIVEAWAY_FILE,['open'=>false,'entries'=>[],'winner'=>null]);
if(!empty($st['open'])){ $st['entries'][]=$login; save_json($GIVEAWAY_FILE,$st); say($send,$chan,"@$display er med i giveaway 🎟️"); }
continue;
}
// !spin <amount>
if(preg_match('/^!spin\\s+(\\d+)/i',$msg,$mm)){
$amt=max(1,(int)$mm[1]); $bal=get_points($login);
if($bal<$amt){ say($send,$chan,"@$display mangler point (har $bal)"); continue; }
add_points($login,$display,-$amt);
$r1=$emotes[array_rand($emotes)]; $r2=$emotes[array_rand($emotes)]; $r3=$emotes[array_rand($emotes)];
$payout=0; if($r1===$r2 && $r2===$r3){ $payout=$amt*5; } elseif($r1===$r2 || $r1===$r3 || $r2===$r3){ $payout=$amt*2; }
if($payout>0) add_points($login,$display,$payout);
add_spin($login,$amt,$r1,$r2,$r3,$payout);
$delta=$payout-$amt; $sign=$delta>=0?'+':''; say($send,$chan,"@$display 🎰 $r1 | $r2 | $r3{$sign}{$delta} (saldo: ".get_points($login).")");
continue;
}
// Bets: !bet <amount> <1|2>
if(preg_match('/^!bet\\s+(\\d+)\\s+([12])/i',$msg,$mm)){
$amt=(int)$mm[1]; $opt=(int)$mm[2]; $db=db();
$last=$db->query("SELECT * FROM bets WHERE status='open' ORDER BY id DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if(!$last){ say($send,$chan,"@$display der er ikke et åbent bet."); continue; }
$bal=get_points($login); if($bal<$amt){ say($send,$chan,"@$display mangler point (har $bal)"); continue; }
add_points($login,$display,-$amt);
$db->prepare('INSERT INTO bet_entries(bet_id,user_login,amount,option) VALUES(?,?,?,?) ON CONFLICT(bet_id,user_login) DO UPDATE SET amount=excluded.amount, option=excluded.option')->execute([$last['id'],$login,$amt,$opt]);
say($send,$chan,"@$display lagde $amt på option $opt");
continue;
}
// Raffle: !buy X
if(preg_match('/^!buy\\s+(\\d+)/i',$msg,$mm)){
$amt=max(1,(int)$mm[1]); $db=db(); $st=$db->query('SELECT open, ticket_cost FROM raffle_state WHERE id=1')->fetch(PDO::FETCH_ASSOC);
if(!$st || !$st['open']){ say($send,$chan,'Raffle er lukket.'); continue; }
$price=(int)$st['ticket_cost']*$amt; $bal=get_points($login); if($bal<$price){ say($send,$chan,"@$display mangler point (har $bal)"); continue; }
add_points($login,$display,-$price); $ins=$db->prepare('INSERT INTO raffle_entries(user_login) VALUES(?)'); for($i=0;$i<$amt;$i++) $ins->execute([$login]);
say($send,$chan,"@$display købte $amt billet(ter) 🎟️");
continue;
}
// Mod/streamer only: bet control
$isMod = ($tags['mod']??'0')==='1' || (strpos($tags['badges']??'','broadcaster/')!==false);
if($isMod){
if(preg_match('/^!betopen\\s+(.+?)\\s*\\|\\s*(.+)$/i',$msg,$mm)){
$db=db(); $db->prepare('INSERT INTO bets(status,option1,option2,created_ts) VALUES(?,?,?,?)')->execute(['open',$mm[1],$mm[2],gmdate('c')]);
say($send,$chan,"Bet åbent: 1) {$mm[1]} 2) {$mm[2]} — brug !bet <beløb> <1|2>"); continue;
}
if(strcasecmp($msg,'!betclose')===0){ $db=db(); $db->exec("UPDATE bets SET status='closed', close_ts='".gmdate('c')."' WHERE status='open'"); say($send,$chan,'Bet lukket for indskud.'); continue; }
if(preg_match('/^!betresolve\\s+([12])/i',$msg,$mm)){
$win=(int)$mm[1]; $db=db(); $last=$db->query("SELECT * FROM bets WHERE status='closed' ORDER BY id DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if($last){ $st=$db->prepare('SELECT user_login, amount FROM bet_entries WHERE bet_id=? AND option=?'); $st->execute([$last['id'],$win]); foreach($st->fetchAll(PDO::FETCH_ASSOC) as $w){ add_points($w['user_login'],$w['user_login'], (int)$w['amount']*2); } $db->prepare('UPDATE bets SET status="resolved", resolved_option=? WHERE id=?')->execute([$win,$last['id']]); say($send,$chan,"Bet afsluttet. Option $win vandt gevinster udbetalt."); }
continue;
}
}
// Custom commands.json with permissions
if(preg_match('/^!([a-z0-9_]+)(?:\\s+(.*))?$/i',$msg,$cm)){
$cmd=strtolower($cm[1]); $args=$cm[2]??'';
$cmds=load_json($GLOBALS['COMMANDS_FILE'],[]);
if(isset($cmds[$cmd])){
$perms=load_json($GLOBALS['PERM_FILE'],[]); $req=$perms[$cmd]??'chatter';
$lvl_user=role_level($tags, $GLOBALS['ENV_USERNAME']??'', $nick); $lvl_req=role_required_level($req);
if($lvl_user < $lvl_req){ /* no permission */ continue; }
$target=parseFirstMentionOrArg($msg); $argparts=$args?preg_split('/\\s+/',$args):[];
$ctx=['user'=>strtolower($nick),'display'=>$display,'target'=>$target,'channel'=>$chan,'cmd'=>$cmd,'args'=>$args,'arg1'=>$argparts[0]??'','arg2'=>$argparts[1]??'','arg3'=>$argparts[2]??'','arg4'=>$argparts[3]??'','arg5'=>$argparts[4]??'','points'=>get_points($login)];
$reply=renderPlaceholders($cmds[$cmd],$ctx); say($send,$chan,$reply);
}
}
}
}
// Timers
if($joined && $timers){
foreach($timers as $i=>$t){
if(empty($t['enabled'])) continue; $key='t'.$i; $now=time(); $interval=max(1,(int)($t['interval']??15))*60;
if(!isset($timerState[$key])) $timerState[$key]=$now;
if($now-$timerState[$key] >= $interval){ say($send,$channel,$t['text']); $timerState[$key]=$now; }
}
}
// Outbox
if($joined){
$o=@file_get_contents($OUTBOX_FILE);
if($o){ $lines=array_filter(array_map('trim',explode("\n",$o))); file_put_contents($OUTBOX_FILE,""); foreach($lines as $l){ say($send,$channel,$l);} logmsg('Outbox sendt '.count($lines).' linjer'); }
}
usleep(50000);
}
}
logmsg('Starter bot manager');
while(true){
if(file_exists($STOP_FLAG)){ logmsg('Stopflag i manager lukker'); @unlink($STOP_FLAG); break; }
$env=env_load($ENV_FILE); $GLOBALS['ENV_USERNAME']=strtolower($env['TWITCH_USERNAME']??'');
$settings=load_json($SETTINGS_FILE, ['watchdog'=>true,'timeout'=>120]);
connect_and_run($env,$settings);
logmsg('Genstarter om 5 sek'); sleep(5);
}