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 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 <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 <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); }