v0.0..1
This commit is contained in:
Thomas
2025-10-05 14:58:05 +02:00
parent df30542248
commit a184c31cca
41 changed files with 3439 additions and 0 deletions

171
software/v0.0.1/bot.php Normal file
View File

@@ -0,0 +1,171 @@
<?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);
}