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

246
software/v0.0.1/web/api.php Normal file
View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
/**
* Twitch Helix helper med performance:
* - Persistent cURL + HTTP/2 + gzip
* - Fælles auth headers fra .env
* - Retry/backoff på 429/5xx (respekterer Retry-After)
* - Simpel filcache + ETag/Last-Modified
* - Pagineret GET helper
*/
const HELIX_BASE = 'https://api.twitch.tv/helix';
const DEFAULT_TIMEOUT = 5; // sek (kort for hurtigere side-loads)
const DEFAULT_RETRIES = 2; // ekstra forsøg udover første
const DEFAULT_BACKOFF_MS = 250; // start-backoff i ms
const MAX_BACKOFF_MS = 1500; // maks backoff i ms
/* ----------------------- ENV + AUTH ----------------------- */
function env_load(string $file): array {
$env = [];
if (!file_exists($file)) return $env;
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (strpos(ltrim($line), '#') === 0) continue;
[$k, $v] = array_pad(explode('=', $line, 2), 2, null);
if ($k !== null && $v !== null) $env[trim($k)] = trim($v);
}
return $env;
}
/** Returnerer [ok(bool), headers(array), err(array|null)] */
function twitch_auth_headers(): array {
$env = env_load(dirname(__DIR__) . '/.env');
$token = preg_replace('/^oauth:/i', '', $env['TWITCH_OAUTH'] ?? '');
$client = $env['TWITCH_CLIENT_ID'] ?? '';
if ($token === '' || $client === '') {
return [false, [], ['http' => 0, 'data' => ['error' => 'missing env: TWITCH_OAUTH / TWITCH_CLIENT_ID']]];
}
return [true, [
'Client-ID: ' . $client,
'Authorization: Bearer ' . $token,
'Accept: application/json',
], null];
}
/* ----------------------- CACHE ----------------------- */
function cache_dir(): string {
$dir = dirname(__DIR__) . '/data/cache';
if (!is_dir($dir)) @mkdir($dir, 0777, true);
return $dir;
}
function cache_path(string $key): string {
return cache_dir() . '/' . preg_replace('/[^a-z0-9_.-]/i', '_', $key) . '.json';
}
function cache_meta_path(string $key): string {
return cache_dir() . '/' . preg_replace('/[^a-z0-9_.-]/i', '_', $key) . '.meta.json';
}
/* ----------------------- CORE REQUEST ----------------------- */
function _curl_handle() {
static $ch = null;
if ($ch === null) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '', // gzip/deflate/br
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS,
CURLOPT_TIMEOUT => DEFAULT_TIMEOUT,
CURLOPT_HEADER => true, // vi splitter headers/body selv
]);
}
return $ch;
}
/**
* Helix request med retry/backoff + ETag.
* @return array ['http'=>int, 'data'=>array, 'raw'=>string, 'headers'=>array]
*/
function helix_request(string $method, string $path, array $params = [], $payload = null, array $opts = []): array {
[$ok, $authHeaders, $err] = twitch_auth_headers();
if (!$ok) return $err;
$ch = _curl_handle();
$url = HELIX_BASE . $path . ($params ? '?' . http_build_query($params) : '');
$ttl = (int)($opts['ttl'] ?? 0);
$ckey = $opts['cache_key'] ?? ($path . '?' . http_build_query($params));
$useCache = (bool)($opts['use_cache'] ?? false);
// ETag/Last-Modified support
$metaFile = cache_meta_path('helix_meta_' . md5($ckey));
$meta = file_exists($metaFile) ? (json_decode((string)file_get_contents($metaFile), true) ?: []) : [];
$headers = $authHeaders;
if (!empty($meta['etag'])) $headers[] = 'If-None-Match: ' . $meta['etag'];
if (!empty($meta['last_modified'])) $headers[] = 'If-Modified-Since: ' . $meta['last_modified'];
// Sæt request
$set = [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_POSTFIELDS => null,
];
if (strtoupper($method) === 'POST') {
$set[CURLOPT_HTTPHEADER] = array_merge($headers, ['Content-Type: application/json']);
$set[CURLOPT_POSTFIELDS] = json_encode($payload ?? [], JSON_UNESCAPED_UNICODE);
}
curl_setopt_array($ch, $set);
$attempts = 1 + DEFAULT_RETRIES;
$backoff = DEFAULT_BACKOFF_MS;
do {
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($response === false) {
if (--$attempts > 0) { usleep(min($backoff, MAX_BACKOFF_MS) * 1000); $backoff *= 2; continue; }
return ['http' => 0, 'data' => ['error' => 'curl_error', 'code' => $errno, 'message' => $error]];
}
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($response, 0, $headerSize);
$rawBody = substr($response, $headerSize);
$hdrLines = preg_split('/\r\n/', trim($rawHeaders));
$hdrAssoc = [];
foreach ($hdrLines as $h) {
$p = strpos($h, ':');
if ($p !== false) {
$k = strtolower(trim(substr($h, 0, $p)));
$v = trim(substr($h, $p + 1));
$hdrAssoc[$k] = $v;
}
}
// Retry på 429/5xx
if ($status === 429 || ($status >= 500 && $status <= 599)) {
$retryAfter = (int)($hdrAssoc['retry-after'] ?? 0);
$sleepMs = max($retryAfter * 1000, min($backoff, MAX_BACKOFF_MS));
if (--$attempts > 0) { usleep($sleepMs * 1000); $backoff *= 2; continue; }
}
// 304 = brug cache hvis mulig
if ($status === 304 && $useCache) {
$cacheFile = cache_path('helix_' . md5($ckey) . '_ttl' . $ttl);
if (file_exists($cacheFile)) {
$cached = json_decode((string)file_get_contents($cacheFile), true);
if (is_array($cached)) {
$cached['headers'] = $hdrAssoc;
$cached['http'] = 200; // lever data som 200 fra cache
return $cached;
}
}
return ['http' => 304, 'data' => [], 'raw' => '', 'headers' => $hdrAssoc];
}
$data = json_decode($rawBody, true);
if (!is_array($data)) $data = [];
// Gem ETag/LM til senere
$etag = $hdrAssoc['etag'] ?? null;
$lm = $hdrAssoc['last-modified'] ?? null;
if ($etag || $lm) {
@file_put_contents($metaFile, json_encode(['etag' => $etag, 'last_modified' => $lm]));
}
$out = ['http' => $status, 'data' => $data, 'raw' => $rawBody, 'headers' => $hdrAssoc];
// Cache ved success
if ($useCache && $ttl > 0 && $status === 200) {
@file_put_contents(cache_path('helix_' . md5($ckey) . '_ttl' . $ttl), json_encode($out, JSON_UNESCAPED_UNICODE));
}
return $out;
} while ($attempts > 0);
}
/* ----------------------- PUBLIC WRAPPERS ----------------------- */
function helix_get(string $path, array $params = []): array {
return helix_request('GET', $path, $params, null, ['use_cache' => false]);
}
function helix_post(string $path, array $payload): array {
return helix_request('POST', $path, [], $payload, ['use_cache' => false]);
}
/** Cachet GET samme signatur som før. */
function helix_get_cached(string $path, array $params = [], int $ttl = 30): array {
$ckey = $path . '?' . http_build_query($params);
$file = cache_path('helix_' . md5($ckey) . '_ttl' . $ttl);
if (file_exists($file) && (time() - filemtime($file) < $ttl)) {
$cached = json_decode((string)file_get_contents($file), true);
if (is_array($cached)) return $cached;
}
return helix_request('GET', $path, $params, null, [
'ttl' => $ttl,
'cache_key' => $ckey,
'use_cache' => true,
]);
}
/** Pagineret helper samler alle sider i ét array. */
function helix_get_paginated(string $path, array $params = [], int $maxPages = 10, int $ttl = 0): array {
$all = [];
$cursor = null;
$pages = 0;
$http = 200;
do {
$pages++;
$p = $params;
if ($cursor) $p['after'] = $cursor;
$res = $ttl > 0
? helix_get_cached($path, $p, $ttl)
: helix_get($path, $p);
$http = $res['http'] ?? 0;
if ($http !== 200) break;
$chunk = $res['data']['data'] ?? [];
if (is_array($chunk)) $all = array_merge($all, $chunk);
$cursor = $res['data']['pagination']['cursor'] ?? null;
} while ($cursor && $pages < $maxPages);
return ['http' => $http, 'data' => $all, 'pages' => $pages];
}
/** Slår login op → Twitch user_id (cachet i 5 min) */
function get_user_id(string $login): ?string {
$r = helix_get_cached('/users', ['login' => $login], 300);
if (($r['http'] ?? 0) === 200 && !empty($r['data']['data'][0]['id'])) {
return (string)$r['data']['data'][0]['id'];
}
return null;
}

View File

@@ -0,0 +1 @@
<?php require_once __DIR__.'/guard.php'; require_login(); $db=new PDO('sqlite:'.dirname(__DIR__).'/data/app.db'); $msg=''; if($_SERVER['REQUEST_METHOD']==='POST'){ if(isset($_POST['open'])){ $o1=trim($_POST['opt1']??''); $o2=trim($_POST['opt2']??''); if($o1&&$o2){ $st=$db->prepare('INSERT INTO bets(status,option1,option2,created_ts) VALUES(?,?,?,?)'); $st->execute(['open',$o1,$o2,gmdate('c')]); $msg='Bet åbnet.'; } } if(isset($_POST['close'])){ $db->exec("UPDATE bets SET status='closed', close_ts='".gmdate('c')."' WHERE status='open'"); $msg='Bet lukket.'; } if(isset($_POST['resolve'])){ $win=(int)($_POST['winner']??1); $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]); $wins=$st->fetchAll(PDO::FETCH_ASSOC); foreach($wins as $w){ $db->prepare('INSERT INTO points(user_login,display_name,points) VALUES(?,?,?) ON CONFLICT(user_login) DO UPDATE SET points=points+excluded.points')->execute([$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']]); $msg='Bet resolved, vinder: option '.$win; } } } $last=$db->query('SELECT * FROM bets ORDER BY id DESC LIMIT 1')->fetch(PDO::FETCH_ASSOC); $entries=$last?$db->query('SELECT * FROM bet_entries WHERE bet_id='.(int)$last['id'])->fetchAll(PDO::FETCH_ASSOC):[]; ?><!DOCTYPE html><html><head><meta charset="utf-8"><title>Bets</title><link rel="stylesheet" href="style.css"></head><body><div class="wrap"><div class="card"><h2>💸 Bets</h2><p><a class="btn" href="index.php">← Tilbage</a></p><?php if($msg): ?><p class="notice"><?php echo htmlspecialchars($msg); ?></p><?php endif; ?><form method="post" style="margin-bottom:1rem"><label>Option 1<input name="opt1" placeholder="Team A"></label><br><label>Option 2<input name="opt2" placeholder="Team B"></label><br><button class="btn" name="open" value="1">Åbn nyt bet</button> <button class="btn" name="close" value="1">Luk indskud</button> <label>Vinder (1/2)<input name="winner" value="1"></label> <button class="btn" name="resolve" value="1">Afslut & udbetal</button></form><?php if($last): ?><p>Status: <span class="badge"><?php echo htmlspecialchars($last['status']); ?></span> #<?php echo (int)$last['id']; ?> (<?php echo htmlspecialchars($last['option1'].' vs '.$last['option2']); ?>)</p><h3>Indskud</h3><table><tr><th>Bruger</th><th>Beløb</th><th>Option</th></tr><?php foreach($entries as $e){ echo '<tr><td>@'.htmlspecialchars($e['user_login']).'</td><td>'.(int)$e['amount'].'</td><td>'.(int)$e['option'].'</td></tr>'; } ?></table><?php endif; ?></div></div></body></html>

View File

@@ -0,0 +1 @@
<?php require_once __DIR__.'/guard.php'; require_login(); $file=__DIR__.'/../data/commands.json'; $out=__DIR__.'/../data/outbox.txt'; $cmds=file_exists($file)?json_decode(file_get_contents($file),true):[]; $editing=''; $editText=''; if($_SERVER['REQUEST_METHOD']==='POST'){ $a=$_POST['action']??''; if($a==='save'){ $name=strtolower(trim($_POST['name']??'')); $text=trim($_POST['text']??''); if($name&&$text){ $cmds[$name]=$text; file_put_contents($file,json_encode($cmds,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); } header('Location: commands.php?saved=1'); exit; } if($a==='delete'){ $name=strtolower(trim($_POST['name']??'')); if(isset($cmds[$name])){ unset($cmds[$name]); file_put_contents($file,json_encode($cmds,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); } header('Location: commands.php?deleted=1'); exit; } if($a==='edit'){ $editing=strtolower(trim($_POST['name']??'')); $editText=$cmds[$editing]??''; } if($a==='send'){ $name=strtolower(trim($_POST['name']??'')); if(isset($cmds[$name])) file_put_contents($out,$cmds[$name].PHP_EOL,FILE_APPEND); header('Location: commands.php?sent=1'); exit; } } ?><!DOCTYPE html><html><head><meta charset="utf-8"><title>Kommandoer</title><link rel="stylesheet" href="style.css"><script>function confirmDelete(n){return confirm('Slet !'+n+'?')}</script></head><body><div class="wrap"><div class="card"><h2>Bot-kommandoer</h2><p><a class="btn" href="index.php">← Tilbage</a><a class="btn" href="help.php" target="_blank">📖 Hjælp</a></p><form method="post" style="margin-bottom:1rem"><input type="hidden" name="action" value="save"><label>!kommando (uden !) <input name="name" required value="<?php echo htmlspecialchars($editing); ?>"></label><br><label>Svar <textarea name="text" rows="3" required><?php echo htmlspecialchars($editText); ?></textarea></label><br><button class="btn"><?php echo $editing?'Opdater':'Gem'; ?></button></form><table><tr><th>!kommando</th><th>Svar</th><th>Handling</th></tr><?php foreach($cmds as $k=>$v): ?><tr><td><?php echo '!'.htmlspecialchars($k); ?></td><td><?php echo htmlspecialchars($v); ?></td><td><form method="post" style="display:inline"><input type="hidden" name="action" value="edit"><input type="hidden" name="name" value="<?php echo htmlspecialchars($k); ?>"><button class="btn">Redigér</button></form> <form method="post" style="display:inline" onsubmit="return confirmDelete('<?php echo htmlspecialchars($k); ?>')"><input type="hidden" name="action" value="delete"><input type="hidden" name="name" value="<?php echo htmlspecialchars($k); ?>"><button class="btn">Slet</button></form> <form method="post" style="display:inline"><input type="hidden" name="action" value="send"><input type="hidden" name="name" value="<?php echo htmlspecialchars($k); ?>"><button class="btn">Send nu</button></form></td></tr><?php endforeach; ?></table></div></div></body></html>

View File

@@ -0,0 +1 @@
<?php $base=dirname(__DIR__); $stop=$base.'/data/stop.flag'; $bat=$base.'/start_bot.bat'; $a=$_GET['action']??''; if($a==='start'){ $cmd='cmd /c start "" /min "'.$bat.'"'; if(function_exists('popen'))@pclose(@popen($cmd,'r')); else @shell_exec($cmd.' >NUL 2>&1'); header('Location: index.php'); exit; } if($a==='stop'){ file_put_contents($stop,'1'); header('Location: index.php'); exit; } if($a==='restart'){ file_put_contents($stop,'1'); echo '<meta http-equiv="refresh" content="1; url=control.php?action=start">Genstarter...'; exit; } header('Location: index.php');

View File

@@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/db.php';
$db = db();
// Skift selv disse:
$username = 'admin';
$password = 'demo';
$role = 'admin';
// Tjek om brugeren findes
$st = $db->prepare('SELECT id FROM users WHERE username = ?');
$st->execute([$username]);
if ($st->fetch()) {
echo "❗Brugeren '$username' findes allerede.";
exit;
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$st = $db->prepare('INSERT INTO users (username, password, role) VALUES (?, ?, ?)');
$st->execute([$username, $hash, $role]);
echo "✅ Bruger oprettet: $username / $password (rolle: $role)";

View File

@@ -0,0 +1,28 @@
<?php
// web/db.php
function db(): PDO {
static $pdo = null;
if ($pdo) return $pdo;
$pdo = new PDO('sqlite:' . dirname(__DIR__) . '/data/app.db', null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// Bedre concurrency når bot + web arbejder samtidig
$pdo->exec('PRAGMA journal_mode=WAL');
$pdo->exec('PRAGMA synchronous=NORMAL');
$pdo->exec('PRAGMA busy_timeout=3000'); // 3s
// Sikr users-tabellen findes (kompatibel med både 'password' og 'password_hash')
$pdo->exec("
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT, -- foretrukket kolonne (password_hash())
password_hash TEXT, -- ældre kolonne (legacy)
role TEXT DEFAULT 'user'
)
");
return $pdo;
}

View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/guard.php';
require_login();
// Frigiv session-låsen INDEN vi starter det langkørende SSE-script
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
ignore_user_abort(true);
set_time_limit(0);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // undgå proxy-buffering hvis muligt
$log = dirname(__DIR__) . '/data/bot.log';
$last = '';
while (true) {
$tail = '';
if (file_exists($log)) {
$data = @file($log);
if ($data) {
// vis de sidste ~80 linjer
$tail = implode("", array_slice($data, -80));
}
}
if ($tail !== $last) {
echo "event: log\n";
echo "data: " . str_replace("\n", "\\n", $tail) . "\n\n";
@ob_flush();
@flush();
$last = $tail;
}
usleep(500000); // 0,5s
}

View File

@@ -0,0 +1,82 @@
<?php
require_once __DIR__ . '/api.php';
// Load environment variables
$env = env_load(dirname(__DIR__) . '/.env');
$secret = $env['EVENTSUB_SECRET'] ?? 'change_this_secret';
// Get raw POST body
$raw = file_get_contents('php://input');
// Get Twitch headers
$t = $_SERVER['HTTP_TWITCH_EVENTSUB_MESSAGE_TYPE'] ?? '';
$id = $_SERVER['HTTP_TWITCH_EVENTSUB_MESSAGE_ID'] ?? '';
$ts = $_SERVER['HTTP_TWITCH_EVENTSUB_MESSAGE_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_TWITCH_EVENTSUB_MESSAGE_SIGNATURE'] ?? '';
// Calculate signature
$calc = 'sha256=' . hash_hmac('sha256', $id . $ts . $raw, $secret);
// Verify signature
if (!hash_equals($calc, $sig)) {
http_response_code(403);
echo 'bad sig';
exit;
}
// Decode JSON
$data = json_decode($raw, true);
// Handle webhook verification
if ($t === 'webhook_callback_verification') {
header('Content-Type: text/plain');
echo $data['challenge'] ?? '';
exit;
}
// Handle notification
if ($t === 'notification') {
$type = $data['subscription']['type'] ?? '';
$ev = $data['event'] ?? [];
try {
$db = new PDO('sqlite:' . dirname(__DIR__) . '/data/app.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($type === 'channel.cheer') {
$u = strtolower($ev['user_login'] ?? '');
$n = $ev['user_name'] ?? $u;
$bits = (int)($ev['bits'] ?? 0);
$st = $db->prepare('INSERT INTO events(type,user_login,user_name,value,ts) VALUES(?,?,?,?,?)');
$st->execute(['cheer', $u, $n, $bits, gmdate('c')]);
}
if (
$type === 'channel.subscribe' ||
$type === 'channel.subscription.message' ||
$type === 'channel.subscription.gift'
) {
$u = strtolower($ev['user_login'] ?? $ev['gifter_login'] ?? '');
$n = $ev['user_name'] ?? $ev['gifter_name'] ?? $u;
$val = (int)($ev['tier'] ?? $ev['total'] ?? 1);
$st = $db->prepare('INSERT INTO events(type,user_login,user_name,value,ts) VALUES(?,?,?,?,?)');
$st->execute([
($type === 'channel.subscription.gift' ? 'subgift' : 'sub'),
$u,
$n,
$val,
gmdate('c')
]);
}
} catch (PDOException $e) {
error_log('Database error: ' . $e->getMessage());
http_response_code(500);
echo 'db error';
exit;
}
echo 'ok';
exit;
}
echo 'ok';

View File

@@ -0,0 +1,54 @@
<?php
require_once __DIR__.'/api.php';
$env = env_load(dirname(__DIR__).'/.env');
$login = strtolower($env['TWITCH_CHANNEL'] ?? '');
$uid = $login ? get_user_id($login) : null;
$cb = $env['EVENTSUB_CALLBACK'] ?? '';
$msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uid && $cb) {
foreach ([
'channel.cheer',
'channel.subscribe',
'channel.subscription.message',
'channel.subscription.gift'
] as $t) {
$r = helix_post('/eventsub/subscriptions', [
'type' => $t,
'version' => '1',
'condition' => ['broadcaster_user_id' => $uid],
'transport' => [
'method' => 'webhook',
'callback' => $cb,
'secret' => $env['EVENTSUB_SECRET'] ?? 'change_this_secret'
]
]);
$msg .= $t . ': HTTP ' . $r['http'] . ' ' . substr($r['raw'] ?? '', 0, 120) . "\n";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>EventSub Setup</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>⚡ EventSub Setup</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<?php if ($msg): ?>
<pre><?php echo htmlspecialchars($msg); ?></pre>
<?php endif; ?>
<form method="post">
<p>Callback: <code><?php echo htmlspecialchars($cb ?: 'mangler EVENTSUB_CALLBACK'); ?></code></p>
<button class="btn" <?php echo (!$uid || !$cb) ? 'disabled' : ''; ?>>Opret abonnementer</button>
</form>
<p class="small">Brug en offentlig URL (fx ngrok) der peger på <code>/bot/web/eventsub.php</code>.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<?php
require_once __DIR__.'/guard.php';
require_login();
$file = __DIR__.'/../data/giveaway.json';
$st = file_exists($file) ? json_decode(file_get_contents($file), true) : ['open' => false, 'entries' => [], 'winner' => null];
$msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['open'])) {
$st = ['open' => true, 'entries' => [], 'winner' => null];
$msg = 'Giveaway åbnet.';
}
if (isset($_POST['close'])) {
$st['open'] = false;
$msg = 'Giveaway lukket.';
}
if (isset($_POST['draw'])) {
$e = array_values(array_unique($st['entries']));
if ($e) {
$w = $e[array_rand($e)];
$st['winner'] = $w;
$msg = 'Vinder: @' . $w;
} else {
$msg = 'Ingen entries.';
}
}
if (isset($_POST['clear'])) {
$st = ['open' => false, 'entries' => [], 'winner' => null];
$msg = 'Nulstillet.';
}
file_put_contents($file, json_encode($st, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header('Location: giveaway.php');
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Giveaway</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>🎁 Giveaway</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<?php if ($msg): ?>
<p class="notice"><?php echo htmlspecialchars($msg); ?></p>
<?php endif; ?>
<form method="post">
<button class="btn" name="open" value="1">Åbn</button>
<button class="btn" name="close" value="1">Luk</button>
<button class="btn" name="draw" value="1">Træk vinder</button>
<button class="btn" name="clear" value="1" onclick="return confirm('Nulstil?')">Nulstil</button>
</form>
<p>Status: <?php echo $st['open'] ? '<span class="badge">Åben</span>' : '<span class="badge">Lukket</span>'; ?>
— Vinder: <?php echo $st['winner'] ? '@' . htmlspecialchars($st['winner']) : '-'; ?></p>
<h3>Deltagere (<?php echo count(array_unique($st['entries'])); ?>)</h3>
<ul>
<?php foreach (array_unique($st['entries']) as $e) {
echo '<li>@' . htmlspecialchars($e) . '</li>';
} ?>
</ul>
<p class="small">Chat: <code>!join</code> deltager når giveaway er åben.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<?php
// web/guard.php
session_start();
/**
* Kræv login og FRIGIV session-låsen, så andre requests ikke blokerer.
* Returnerer brugerinfo fra session (id, username, role) hvis du vil vise det.
*/
function require_login(): array {
if (!isset($_SESSION['user'])) {
header('Location: /bot/web/login.php');
exit;
}
$user = $_SESSION['user']; // kopiér det du skal bruge
if (session_status() === PHP_SESSION_ACTIVE) {
// VIGTIGT: frigiv låsen straks (ellers blokerer SSE alle andre sider)
session_write_close();
}
return is_array($user) ? $user : [];
}

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Kommando-hjælp</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>🔧 Placeholders</h2>
<table>
<tr>
<th>Placeholder</th>
<th>Beskrivelse</th>
</tr>
<tr>
<td><code>@$user</code></td>
<td>Afsenderens brugernavn (med @)</td>
</tr>
<tr>
<td><code>@$display</code></td>
<td>Display-navn (med @)</td>
</tr>
<tr>
<td><code>@$target</code></td>
<td>Første @mention eller første argument</td>
</tr>
<tr>
<td><code>$user</code></td>
<td>Brugernavn</td>
</tr>
<tr>
<td><code>$display</code></td>
<td>Display-navn</td>
</tr>
<tr>
<td><code>$target</code></td>
<td>Som ovenfor, uden @</td>
</tr>
<tr>
<td><code>$channel</code></td>
<td>Kanal</td>
</tr>
<tr>
<td><code>$cmd</code></td>
<td>Kommando</td>
</tr>
<tr>
<td><code>$args</code></td>
<td>Hele argumentstrengen</td>
</tr>
<tr>
<td><code>$arg1..$arg5</code></td>
<td>Argumenter</td>
</tr>
<tr>
<td><code>$time</code></td>
<td>Klokkeslæt</td>
</tr>
<tr>
<td><code>$date</code></td>
<td>Dags dato</td>
</tr>
<tr>
<td><code>$points</code></td>
<td>Brugerens point</td>
</tr>
<tr>
<td><code>$random</code></td>
<td>1100</td>
</tr>
<tr>
<td><code>$random:N</code></td>
<td>1N</td>
</tr>
<tr>
<td><code>$pick[a|b|c]</code></td>
<td>Tilfældig fra liste</td>
</tr>
</table>
<p class="small">
Eksempel: <code>Hej @$display, klokken er $time jeg vælger: $pick[pizza|sushi]</code>
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,140 @@
<?php
require_once __DIR__ . '/guard.php';
require_login();
require_once __DIR__ . '/api.php';
require_once __DIR__ . '/db.php';
$env = env_load(dirname(__DIR__) . '/.env');
$login = strtolower($env['TWITCH_CHANNEL'] ?? '');
$uid = $login ? get_user_id($login) : null;
$uptime = 'Offline';
$started_at = null;
// Cachet Helix-kald (20s) for hurtigere side-loads
if ($uid) {
$s = helix_get_cached('/streams', ['user_id' => $uid], 20);
if (($s['http'] ?? 0) === 200 && !empty($s['data']['data'][0])) {
$started_at = $s['data']['data'][0]['started_at'];
$t1 = new DateTime($started_at);
$t2 = new DateTime('now', new DateTimeZone('UTC'));
$diff = $t2->getTimestamp() - $t1->getTimestamp();
$uptime = sprintf('%02d:%02d', floor($diff / 3600), floor(($diff % 3600) / 60));
}
}
$db = db(); // bruger web/db.php (WAL + busy_timeout)
$bits_total = 0;
$cheerers = [];
$subs = [];
// Summér bits og list subs KUN hvis streamen kører
if ($started_at) {
$st = $db->prepare(
'SELECT user_name, SUM(value) AS bits
FROM events
WHERE type = "cheer" AND ts >= ?
GROUP BY user_name
ORDER BY bits DESC'
);
$st->execute([$started_at]);
$cheerers = $st->fetchAll(PDO::FETCH_ASSOC);
foreach ($cheerers as $c) {
$bits_total += (int)($c['bits'] ?? 0);
}
$st = $db->prepare(
'SELECT user_name
FROM events
WHERE (type = "sub" OR type = "subgift") AND ts >= ?
ORDER BY id DESC'
);
$st->execute([$started_at]);
$subs = $st->fetchAll(PDO::FETCH_ASSOC);
}
?>
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="utf-8">
<title>Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<div class="right"><a class="btn" href="logout.php">Log ud</a></div>
<h1>✨ Twitch PHP Bot Dashboard</h1>
<p>Uptime: <span class="badge"><?php echo htmlspecialchars($uptime); ?></span></p>
<p>
<a class="btn" href="timers.php">⏱️ Timers</a>
<a class="btn" href="poll.php">📊 Poll</a>
<a class="btn" href="giveaway.php">🎁 Giveaway</a>
<a class="btn" href="commands.php">⚙️ Kommandoer</a>
<a class="btn" href="permissions.php">🔐 Rettigheder</a>
<a class="btn" href="points.php" target="_blank">🏆 Loyalty Points</a>
<a class="btn" href="bets.php">💸 Bets</a>
<a class="btn" href="raffle.php">🎟️ Raffle</a>
<a class="btn" href="slots.php">🎰 Slots</a>
<a class="btn" href="eventsub_setup.php">⚡ EventSub</a>
<a class="btn" href="control.php?action=start">Start</a>
<a class="btn" href="control.php?action=stop">Stop</a>
<a class="btn" href="control.php?action=restart">Restart</a>
<a class="btn" href="send.php">Send test</a>
<a class="btn" href="validate.php" target="_blank">Validate token</a>
<a class="btn" href="log.php" target="_blank">Åbn fuld log</a>
<a class="btn" href="settings.php">⚙️ Indstillinger</a>
</p>
</div>
<div class="grid">
<div class="card">
<h3>Total bits (denne stream)</h3>
<p class="badge"><?php echo (int)$bits_total; ?></p>
<h4>Cheerers</h4>
<ul>
<?php foreach ($cheerers as $c): ?>
<li><?php echo htmlspecialchars($c['user_name']); ?> — <?php echo (int)$c['bits']; ?> bits</li>
<?php endforeach; ?>
</ul>
</div>
<div class="card">
<h3>Subs (denne stream)</h3>
<ul>
<?php foreach ($subs as $s): ?>
<li><?php echo htmlspecialchars($s['user_name']); ?></li>
<?php endforeach; ?>
</ul>
</div>
<div class="card">
<h3>Live log</h3>
<pre id="logpre">Indlæser...</pre>
<p class="small">SSE først; fallback til WebSocket (kræver <code>node ws_server.js</code>).</p>
</div>
</div>
<script>
(function(){
const pre = document.getElementById('logpre');
function setLog(t){ pre.textContent = t; }
if (!!window.EventSource) {
try {
const sse = new EventSource('events.php');
let used = false;
sse.addEventListener('log', (e) => {
used = true;
setLog(e.data.replaceAll('\\n', '\n'));
});
setTimeout(() => {
if (!used) { sse.close(); tryWS(); }
}, 2000);
return;
} catch (e) {}
}
tryWS();
funct

View File

@@ -0,0 +1,15 @@
<?php
header('Content-Type: text/plain; charset=utf-8');
$log = dirname(__DIR__) . '/data/bot.log';
if (file_exists($log)) {
$content = file_get_contents($log);
if ($content === false) {
echo 'Kunne ikke læse logfilen.';
} else {
echo $content;
}
} else {
echo 'Ingen log endnu.';
}

View File

@@ -0,0 +1,76 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if ($username !== '' && $password !== '') {
$db = db();
$st = $db->prepare('SELECT * FROM users WHERE username = ? LIMIT 1');
$st->execute([$username]);
$user = $st->fetch(PDO::FETCH_ASSOC);
$ok = false;
if ($user) {
// Foretræk moderne kolonne 'password' (password_hash())
if (!empty($user['password']) && password_verify($password, $user['password'])) {
$ok = true;
}
// Legacy fallback: 'password_hash' (egen hash fra gammel version)
if (!$ok && !empty($user['password_hash'])) {
// Forventet format: enten salt$sha256(salt+pw) eller ren sha256
$ph = $user['password_hash'];
if (strpos($ph, '$') !== false) {
[$salt, $hash] = explode('$', $ph, 2);
$ok = (hash('sha256', $salt . $password) === $hash);
} else {
$ok = (hash('sha256', $password) === $ph);
}
}
}
if ($ok) {
$_SESSION['user'] = [
'id' => $user['id'],
'username' => $user['username'],
'role' => $user['role'] ?: 'user',
];
header('Location: index.php');
exit;
} else {
$error = 'Forkert brugernavn eller adgangskode.';
}
} else {
$error = 'Udfyld begge felter.';
}
}
?>
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="utf-8">
<title>Login Twitch PHP Bot</title>
<link rel="stylesheet" href="style.css">
<style>.login-box{max-width:400px;margin:100px auto}</style>
</head>
<body>
<div class="wrap">
<div class="card login-box">
<h2>🔐 Login</h2>
<?php if ($error): ?><p class="notice" style="color:#ff8080"><?php echo htmlspecialchars($error); ?></p><?php endif; ?>
<form method="post">
<label>Brugernavn</label>
<input type="text" name="username" required>
<label>Adgangskode</label>
<input type="password" name="password" required>
<br>
<button class="btn" type="submit">Log ind</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,6 @@
<?php
session_start();
session_unset();
session_destroy();
header('Location: login.php');
exit;

View File

@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/db.php';
$db = db();
// Hent nuværende kolonner
$cols = $db->query("PRAGMA table_info(users)")->fetchAll(PDO::FETCH_ASSOC);
$names = array_column($cols, 'name');
// Tilføj kolonner hvis de mangler
if (!in_array('password', $names)) {
$db->exec("ALTER TABLE users ADD COLUMN password TEXT");
echo "✅ Tilføjede kolonnen 'password'<br>";
}
if (!in_array('password_hash', $names)) {
$db->exec("ALTER TABLE users ADD COLUMN password_hash TEXT");
echo "✅ Tilføjede kolonnen 'password_hash'<br>";
}
if (!in_array('role', $names)) {
$db->exec("ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'");
echo "✅ Tilføjede kolonnen 'role'<br>";
}
echo "<br>🎉 Migration fuldført. Du kan nu køre create_admin.php.";

View File

@@ -0,0 +1,75 @@
<?php
require_once __DIR__.'/guard.php';
require_login();
$file_cmd = __DIR__.'/../data/commands.json';
$file_perm = __DIR__.'/../data/permissions.json';
$cmds = [];
if (file_exists($file_cmd)) {
$cmds = json_decode(file_get_contents($file_cmd), true);
if (!is_array($cmds)) $cmds = [];
}
$perms = [];
if (file_exists($file_perm)) {
$perms = json_decode(file_get_contents($file_perm), true);
if (!is_array($perms)) $perms = [];
}
$roles = ['chatter','follower','sub','vip','mod','streamer'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$new = [];
foreach ($cmds as $n => $_) {
$val = $_POST['role_'.$n] ?? 'chatter';
if (!in_array($val, $roles, true)) $val = 'chatter';
$new[$n] = $val;
}
file_put_contents($file_perm, json_encode($new, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header('Location: permissions.php?saved=1');
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rettigheder</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>🔐 Rettigheder pr. kommando</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<form method="post">
<table>
<tr>
<th>!kommando</th>
<th>Krævet rolle</th>
</tr>
<?php foreach ($cmds as $k => $v):
$r = $perms[$k] ?? 'chatter'; ?>
<tr>
<td><?php echo '!'.htmlspecialchars($k); ?></td>
<td>
<select name="<?php echo 'role_'.$k; ?>">
<?php foreach ($roles as $role): ?>
<option value="<?php echo $role; ?>" <?php echo $r === $role ? 'selected' : ''; ?>>
<?php echo ucfirst($role); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<?php endforeach; ?>
</table>
<br>
<button class="btn">Gem</button>
</form>
<p class="small">Follower behandles pt. som chatter (kan udvides med Helix-follow-tjek).</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/guard.php';
require_login();
try {
$db = new PDO('sqlite:' . dirname(__DIR__) . '/data/app.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('CREATE TABLE IF NOT EXISTS points (user_login TEXT PRIMARY KEY, display_name TEXT, points INTEGER DEFAULT 0)');
$rows = $db->query('SELECT user_login, display_name, points FROM points ORDER BY points DESC LIMIT 200')->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
die('Database error: ' . htmlspecialchars($e->getMessage()));
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Loyalty Points</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>🏆 Loyalty Points Top</h2>
<table>
<tr>
<th>#</th>
<th>Bruger</th>
<th>Point</th>
</tr>
<?php foreach ($rows as $i => $r): ?>
<tr>
<td><?= $i + 1 ?></td>
<td>@<?= htmlspecialchars($r['display_name'] ?: $r['user_login']) ?></td>
<td><?= (int)$r['points'] ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<?php
require_once __DIR__.'/guard.php';
require_login();
require_once __DIR__.'/api.php';
$env = env_load(dirname(__DIR__).'/.env');
$login = strtolower($env['TWITCH_CHANNEL'] ?? '');
$uid = $login ? get_user_id($login) : null;
$msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $uid) {
$title = trim($_POST['title'] ?? '');
$opt1 = trim($_POST['opt1'] ?? '');
$opt2 = trim($_POST['opt2'] ?? '');
$dur = max(15, min(1800, (int)($_POST['duration'] ?? 60)));
if ($title && $opt1 && $opt2) {
$r = helix_post('/polls', [
'broadcaster_id' => $uid,
'title' => $title,
'choices' => [
['title' => $opt1],
['title' => $opt2]
],
'duration' => $dur
]);
$msg = ($r['http'] === 200)
? 'Poll oprettet ✔️'
: 'Fejl ('.$r['http'].'): '.($r['raw'] ?? '');
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Poll</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>📊 Poll</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<?php if ($msg): ?>
<p class="<?php echo strpos($msg, '✔️') !== false ? 'notice' : 'error'; ?>">
<?php echo htmlspecialchars($msg); ?>
</p>
<?php endif; ?>
<form method="post">
<label>Titel<input name="title" required></label><br>
<label>Valgmulighed 1<input name="opt1" required></label><br>
<label>Valgmulighed 2<input name="opt2" required></label><br>
<label>Varighed (sek, 151800)
<input type="number" name="duration" min="15" max="1800" value="60">
</label><br>
<button class="btn">Opret poll</button>
</form>
<p class="small">Kræver scope: <code>channel:manage:polls</code>.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,87 @@
<?php
require_once __DIR__ . '/guard.php';
require_login();
$db = new PDO('sqlite:' . dirname(__DIR__) . '/data/app.db');
$msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['open'])) {
$db->exec('UPDATE raffle_state SET open = 1');
$msg = 'Raffle åben.';
}
if (isset($_POST['close'])) {
$db->exec('UPDATE raffle_state SET open = 0');
$msg = 'Raffle lukket.';
}
if (isset($_POST['cost'])) {
$cost = max(1, (int)$_POST['cost']);
$stmt = $db->prepare('UPDATE raffle_state SET ticket_cost = :cost');
$stmt->execute([':cost' => $cost]);
$msg = 'Billetpris opdateret.';
}
if (isset($_POST['clear'])) {
$db->exec('DELETE FROM raffle_entries');
$msg = 'Entries nulstillet.';
}
if (isset($_POST['draw'])) {
$rows = $db->query('SELECT user_login FROM raffle_entries')->fetchAll(PDO::FETCH_COLUMN);
if ($rows) {
$winner = $rows[array_rand($rows)];
$msg = 'Vinder: @' . htmlspecialchars($winner);
} else {
$msg = 'Ingen entries.';
}
}
}
$state = $db->query('SELECT * FROM raffle_state WHERE id = 1')->fetch(PDO::FETCH_ASSOC);
$count = $db->query('SELECT COUNT(*) FROM raffle_entries')->fetchColumn();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Raffle</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>🎟️ Raffle</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<?php if ($msg): ?>
<p class="notice"><?php echo htmlspecialchars($msg); ?></p>
<?php endif; ?>
<form method="post">
<p>
Status:
<?php echo $state['open']
? '<span class="badge">Åben</span>'
: '<span class="badge">Lukket</span>'; ?>
Billetpris: <strong><?php echo (int)$state['ticket_cost']; ?></strong>
Entries: <strong><?php echo (int)$count; ?></strong>
</p>
<label>
Ny billetpris
<input name="cost" type="number" min="1" value="<?php echo (int)$state['ticket_cost']; ?>">
</label>
<br>
<button class="btn" name="open" value="1">Åbn</button>
<button class="btn" name="close" value="1">Luk</button>
<button class="btn" name="clear" value="1">Nulstil</button>
<button class="btn" name="draw" value="1">Træk vinder</button>
</form>
<p class="small">Chat: <code>!buy X</code> køber billetter for points.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<?php
$out = __DIR__ . '/../data/outbox.txt';
$ok = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$msg = trim($_POST['msg'] ?? '');
if ($msg !== '') {
file_put_contents($out, $msg . PHP_EOL, FILE_APPEND);
$ok = true;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Send test</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>Send test-besked</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<?php if ($ok): ?>
<p class="notice">Sendt til outbox.</p>
<?php endif; ?>
<form method="post">
<label>
Besked
<input name="msg" placeholder="Hej chat!">
</label>
<br>
<button class="btn">Send</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,395 @@
<?php
require_once __DIR__ . '/guard.php';
$me = require_login(); // frigiver sessionlåsen
require_once __DIR__ . '/db.php';
function is_admin(array $me): bool { return isset($me['role']) && $me['role'] === 'admin'; }
function is_mod(array $me): bool { return isset($me['role']) && $me['role'] === 'mod'; }
$settingsFile = dirname(__DIR__) . '/data/settings.json';
$settings = file_exists($settingsFile) ? (json_decode(file_get_contents($settingsFile), true) ?: []) : ['watchdog'=>true,'timeout'=>120];
$db = db();
$msg_settings = '';
$msg_self = '';
$msg_admin = '';
$err_self = '';
$err_admin = '';
/** Helper: hent bruger efter id */
function fetch_user(PDO $db, int $id): ?array {
$st = $db->prepare('SELECT * FROM users WHERE id = ? LIMIT 1');
$st->execute([$id]);
$u = $st->fetch(PDO::FETCH_ASSOC);
return $u ?: null;
}
/** Helper: antal admins (bruges til at undgå at fjerne sidste admin) */
function count_admins(PDO $db): int {
$st = $db->query("SELECT COUNT(*) FROM users WHERE role = 'admin'");
return (int)$st->fetchColumn();
}
/* ============================================================
1) System-indstillinger (watchdog / timeout)
============================================================ */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['__action'] ?? '') === 'save_settings') {
$settings['watchdog'] = isset($_POST['watchdog']);
$settings['timeout'] = max(30, (int)($_POST['timeout'] ?? 120));
file_put_contents($settingsFile, json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$msg_settings = 'Indstillinger gemt.';
}
/* ============================================================
2) Egen profil alle roller (user/mod/admin)
- Skift brugernavn
- Skift password (kræver nuværende)
============================================================ */
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['__action'] ?? '') === 'self_update') {
$newUser = trim($_POST['self_username'] ?? '');
$curPass = $_POST['self_current'] ?? '';
$new1 = $_POST['self_new1'] ?? '';
$new2 = $_POST['self_new2'] ?? '';
// Valider brugernavn hvis ændres
if ($newUser === '') {
$err_self = 'Brugernavn må ikke være tomt.';
} elseif (!preg_match('/^[A-Za-z0-9_][A-Za-z0-9_.-]{2,31}$/', $newUser)) {
$err_self = 'Ugyldigt brugernavn (tilladte tegn: A-Z, 0-9, _, ., - og min. længde 3).';
} else {
// Hent nuværende bruger
$user = fetch_user($db, (int)$me['id']);
if (!$user) {
$err_self = 'Bruger ikke fundet.';
} else {
// Ændr brugernavn hvis ændret
if ($user['username'] !== $newUser) {
try {
$upd = $db->prepare('UPDATE users SET username = ? WHERE id = ?');
$upd->execute([$newUser, (int)$me['id']]);
// Opdater sessionens brugernavn
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
if (isset($_SESSION['user'])) {
$_SESSION['user']['username'] = $newUser;
}
session_write_close();
$msg_self .= ' Brugernavn opdateret.';
} catch (Throwable $e) {
if (strpos($e->getMessage(), 'UNIQUE') !== false) {
$err_self = 'Brugernavn findes allerede.';
} else {
$err_self = 'Fejl (brugernavn): ' . $e->getMessage();
}
}
}
// Skift password hvis felterne er udfyldt
$wantsPasswordChange = ($curPass !== '' || $new1 !== '' || $new2 !== '');
if (!$err_self && $wantsPasswordChange) {
if ($curPass === '' || $new1 === '' || $new2 === '') {
$err_self = 'Udfyld alle password-felter.';
} elseif ($new1 !== $new2) {
$err_self = 'De nye adgangskoder matcher ikke.';
} elseif (strlen($new1) < 6) {
$err_self = 'Vælg en adgangskode på mindst 6 tegn.';
} else {
// Verificer nuværende password (understøt både 'password' og legacy 'password_hash')
$validCurrent = false;
if (!empty($user['password']) && password_verify($curPass, $user['password'])) {
$validCurrent = true;
} elseif (!empty($user['password_hash'])) {
$ph = $user['password_hash'];
if (strpos($ph, '$') !== false) {
[$salt, $hash] = explode('$', $ph, 2);
$validCurrent = (hash('sha256', $salt . $curPass) === $hash);
} else {
$validCurrent = (hash('sha256', $curPass) === $ph);
}
}
if (!$validCurrent) {
$err_self = 'Nuværende adgangskode er forkert.';
} else {
$newHash = password_hash($new1, PASSWORD_DEFAULT);
$upd = $db->prepare('UPDATE users SET password = ?, password_hash = NULL WHERE id = ?');
$upd->execute([$newHash, (int)$me['id']]);
$msg_self .= ' Adgangskode opdateret.';
}
}
}
}
}
}
/* ============================================================
3) Admin-only: Opret, Rediger, Slet bruger
- Opret: username + role + password
- Rediger: username, role, (valgfrit) reset password
- Slet: ikke sig selv, og efterlad altid mindst 1 admin
============================================================ */
if (is_admin($me)) {
$adminAction = $_POST['__action'] ?? '';
// Opret bruger
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $adminAction === 'create_user') {
$u = trim($_POST['new_username'] ?? '');
$p1 = $_POST['new_password1'] ?? '';
$p2 = $_POST['new_password2'] ?? '';
$role = $_POST['new_role'] ?? 'user';
if (!in_array($role, ['admin', 'mod', 'user'], true)) $role = 'user';
if ($u === '' || $p1 === '' || $p2 === '') {
$err_admin = 'Udfyld alle felter.';
} elseif (!preg_match('/^[A-Za-z0-9_][A-Za-z0-9_.-]{2,31}$/', $u)) {
$err_admin = 'Ugyldigt brugernavn.';
} elseif ($p1 !== $p2) {
$err_admin = 'Adgangskoder matcher ikke.';
} elseif (strlen($p1) < 6) {
$err_admin = 'Adgangskode skal være mindst 6 tegn.';
} else {
try {
$hash = password_hash($p1, PASSWORD_DEFAULT);
$st = $db->prepare('INSERT INTO users(username, password, role) VALUES(?,?,?)');
$st->execute([$u, $hash, $role]);
$msg_admin = "Bruger @$u oprettet.";
} catch (Throwable $e) {
if (strpos($e->getMessage(), 'UNIQUE') !== false) {
$err_admin = 'Brugernavn findes allerede.';
} else {
$err_admin = 'Fejl: ' . $e->getMessage();
}
}
}
}
// Redigér bruger
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $adminAction === 'update_user') {
$id = (int)($_POST['edit_id'] ?? 0);
$u = trim($_POST['edit_username'] ?? '');
$role = $_POST['edit_role'] ?? 'user';
$p1 = $_POST['edit_password1'] ?? '';
$p2 = $_POST['edit_password2'] ?? '';
if ($id <= 0) {
$err_admin = 'Ugyldigt bruger-ID.';
} else {
$user = fetch_user($db, $id);
if (!$user) {
$err_admin = 'Bruger findes ikke.';
} else {
if (!in_array($role, ['admin', 'mod', 'user'], true)) $role = 'user';
// Forhindre at man fjerner sidste admin-rolle
$isChangingRole = ($user['role'] !== $role);
if ($isChangingRole && $user['role'] === 'admin') {
if (count_admins($db) <= 1) {
$err_admin = 'Du kan ikke fjerne rollen fra den sidste admin.';
}
}
if (!$err_admin) {
// Opdatér brugernavn + rolle
if ($u === '' || !preg_match('/^[A-Za-z0-9_][A-Za-z0-9_.-]{2,31}$/', $u)) {
$err_admin = 'Ugyldigt brugernavn.';
} else {
try {
$upd = $db->prepare('UPDATE users SET username = ?, role = ? WHERE id = ?');
$upd->execute([$u, $role, $id]);
// Opdatér password hvis udfyldt
if ($p1 !== '' || $p2 !== '') {
if ($p1 !== $p2) {
$err_admin = 'Nye adgangskoder matcher ikke.';
} elseif (strlen($p1) < 6) {
$err_admin = 'Adgangskode skal være mindst 6 tegn.';
} else {
$hash = password_hash($p1, PASSWORD_DEFAULT);
$db->prepare('UPDATE users SET password = ?, password_hash = NULL WHERE id = ?')
->execute([$hash, $id]);
$msg_admin .= ' Password opdateret for @' . htmlspecialchars($u) . '.';
}
}
if (!$err_admin) {
$msg_admin = ($msg_admin ?: 'Bruger opdateret.');
}
} catch (Throwable $e) {
if (strpos($e->getMessage(), 'UNIQUE') !== false) {
$err_admin = 'Brugernavn findes allerede.';
} else {
$err_admin = 'Fejl: ' . $e->getMessage();
}
}
}
}
}
}
}
// Slet bruger
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $adminAction === 'delete_user') {
$id = (int)($_POST['del_id'] ?? 0);
if ($id <= 0) {
$err_admin = 'Ugyldigt bruger-ID.';
} elseif ($id === (int)$me['id']) {
$err_admin = 'Du kan ikke slette dig selv.';
} else {
$user = fetch_user($db, $id);
if (!$user) {
$err_admin = 'Bruger findes ikke.';
} else {
// Undgå at slette sidste admin
if ($user['role'] === 'admin' && count_admins($db) <= 1) {
$err_admin = 'Du kan ikke slette den sidste admin.';
} else {
$db->prepare('DELETE FROM users WHERE id = ?')->execute([$id]);
$msg_admin = 'Bruger @' . htmlspecialchars($user['username']) . ' slettet.';
}
}
}
}
}
// Hent liste over brugere (til visning for admin)
$users = [];
if (is_admin($me)) {
$q = $db->query('SELECT id, username, role FROM users ORDER BY username COLLATE NOCASE ASC');
$users = $q->fetchAll(PDO::FETCH_ASSOC);
}
?>
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="utf-8">
<title>Indstillinger</title>
<link rel="stylesheet" href="style.css">
<script>
function confirmDelete(u){
return confirm("Slet bruger @" + u + "? Dette kan ikke fortrydes.");
}
</script>
</head>
<body>
<div class="wrap">
<!-- Systemindstillinger -->
<div class="card">
<h2>⚙️ Indstillinger</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<?php if ($msg_settings): ?><p class="notice"><?php echo htmlspecialchars($msg_settings); ?></p><?php endif; ?>
<form method="post" class="small" style="margin-bottom:1rem">
<input type="hidden" name="__action" value="save_settings">
<label>
<input type="checkbox" name="watchdog" <?php echo !empty($settings['watchdog']) ? 'checked' : ''; ?>>
Aktivér watchdog (genopretter forbindelse ved inaktivitet)
</label>
<br>
<label>Timeout (sekunder)
<input type="number" name="timeout" min="30" value="<?php echo (int)($settings['timeout'] ?? 120); ?>">
</label>
<br><br>
<button class="btn" type="submit">Gem indstillinger</button>
</form>
</div>
<!-- Egen profil -->
<div class="card">
<h3>👤 Min profil</h3>
<?php if ($msg_self): ?><p class="notice"><?php echo htmlspecialchars($msg_self); ?></p><?php endif; ?>
<?php if ($err_self): ?><p class="error"><?php echo htmlspecialchars($err_self); ?></p><?php endif; ?>
<form method="post">
<input type="hidden" name="__action" value="self_update">
<label>Brugernavn
<input type="text" name="self_username" value="<?php echo htmlspecialchars($me['username']); ?>" required>
</label><br>
<label>Nuværende adgangskode
<input type="password" name="self_current" placeholder="Udfyld for at skifte password">
</label><br>
<label>Ny adgangskode
<input type="password" name="self_new1" placeholder="Min. 6 tegn">
</label><br>
<label>Gentag ny adgangskode
<input type="password" name="self_new2">
</label><br>
<button class="btn" type="submit">Opdater min profil</button>
</form>
<p class="small">Rolle: <strong><?php echo htmlspecialchars($me['role'] ?? 'user'); ?></strong></p>
</div>
<?php if (is_admin($me)): ?>
<!-- Admin: Opret / Redigér / Slet -->
<div class="card">
<h3>🛠️ Brugerstyring (Admin)</h3>
<?php if ($msg_admin): ?><p class="notice"><?php echo $msg_admin; ?></p><?php endif; ?>
<?php if ($err_admin): ?><p class="error"><?php echo $err_admin; ?></p><?php endif; ?>
<!-- Opret ny bruger -->
<form method="post" style="margin-bottom:1rem">
<input type="hidden" name="__action" value="create_user">
<h4>Opret ny bruger</h4>
<label>Brugernavn
<input type="text" name="new_username" placeholder="fx mod_jens" required>
</label><br>
<label>Adgangskode
<input type="password" name="new_password1" required>
</label><br>
<label>Gentag adgangskode
<input type="password" name="new_password2" required>
</label><br>
<label>Rolle
<select name="new_role">
<option value="user">User</option>
<option value="mod">Mod</option>
<option value="admin">Admin</option>
</select>
</label><br>
<button class="btn" type="submit">Opret bruger</button>
</form>
<!-- Liste og redigér/slet eksisterende brugere -->
<h4>Eksisterende brugere</h4>
<table>
<tr><th>ID</th><th>Brugernavn</th><th>Rolle</th><th>Nyt password (valgfrit)</th><th>Handling</th></tr>
<?php foreach ($users as $u): ?>
<tr>
<form method="post">
<input type="hidden" name="__action" value="update_user">
<input type="hidden" name="edit_id" value="<?php echo (int)$u['id']; ?>">
<td><?php echo (int)$u['id']; ?></td>
<td><input name="edit_username" value="<?php echo htmlspecialchars($u['username']); ?>" required></td>
<td>
<select name="edit_role">
<option value="user" <?php echo ($u['role']==='user'?'selected':''); ?>>User</option>
<option value="mod" <?php echo ($u['role']==='mod'?'selected':''); ?>>Mod</option>
<option value="admin" <?php echo ($u['role']==='admin'?'selected':''); ?>>Admin</option>
</select>
</td>
<td>
<input type="password" name="edit_password1" placeholder="nyt password">
<br>
<input type="password" name="edit_password2" placeholder="gentag">
</td>
<td>
<button class="btn" type="submit">Gem</button>
</form>
<form method="post" style="display:inline" onsubmit="return confirmDelete('<?php echo htmlspecialchars($u['username']); ?>')">
<input type="hidden" name="__action" value="delete_user">
<input type="hidden" name="del_id" value="<?php echo (int)$u['id']; ?>">
<button class="btn" type="submit">Slet</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
<p class="small">Begrænsninger: Du kan ikke slette dig selv, og systemet forhindrer at stå uden en eneste admin.</p>
</div>
<?php endif; ?>
</div>
</body>
</html>

View File

@@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/guard.php';
require_login();
$db = new PDO('sqlite:' . dirname(__DIR__) . '/data/app.db');
// Hent de 50 seneste spins
$rows = $db->query('SELECT * FROM slots_spins ORDER BY id DESC LIMIT 50')->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Emote Slots</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>🎰 Emote Slot Seneste spins</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<table>
<tr>
<th>Tid</th>
<th>Bruger</th>
<th>Indsats</th>
<th>Resultat</th>
<th>Udbetaling</th>
</tr>
<?php foreach ($rows as $r): ?>
<?php $res = $r['r1'] . ' | ' . $r['r2'] . ' | ' . $r['r3']; ?>
<tr>
<td><?php echo htmlspecialchars($r['ts']); ?></td>
<td>@<?php echo htmlspecialchars($r['user_login']); ?></td>
<td><?php echo (int)$r['amount']; ?></td>
<td><?php echo htmlspecialchars($res); ?></td>
<td><?php echo (int)$r['payout']; ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,135 @@
:root {
--bg: #0b0e14;
--card: #121726;
--border: #1e2538;
--text: #e6e6e6;
--btn-bg: #1a2033;
--btn-hover: #232b46;
--btn-border: #334;
--badge-bg: #25304d;
}
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
}
a {
color: var(--text);
text-decoration: none;
}
.wrap {
max-width: 1100px;
margin: 40px auto;
padding: 0 16px;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
margin-bottom: 16px;
}
h1,
h2,
h3 {
margin: 0 0 10px;
}
.btn {
display: inline-block;
padding: 10px 14px;
border-radius: 12px;
border: 1px solid var(--btn-border);
padding-inline: 16px;
color: var(--text);
background: var(--btn-bg);
margin-right: 8px;
cursor: pointer;
transition: background 0.2s;
}
.btn:hover {
background: var(--btn-hover);
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
background: var(--badge-bg);
border: 1px solid var(--btn-border);
}
pre {
white-space: pre-wrap;
background: #0f1320;
padding: 12px;
border-radius: 12px;
border: 1px solid var(--border);
max-height: 260px;
overflow: auto;
}
label,
input,
textarea,
select {
display: block;
width: 100%;
}
input,
textarea,
select {
background: #0f1320;
color: var(--text);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border-bottom: 1px solid var(--border);
padding: 8px 6px;
vertical-align: top;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.small {
opacity: 0.8;
font-size: 0.9rem;
}
.notice {
color: #9be39e;
}
.error {
color: #ff8e8e;
}
.right {
float: right;
}

View File

@@ -0,0 +1,135 @@
<?php
require_once __DIR__ . '/guard.php';
require_login();
$file = __DIR__ . '/../data/timers.json';
$timers = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Tilføj ny timer
if (isset($_POST['add'])) {
$text = trim($_POST['text'] ?? '');
$interval = max(1, (int)($_POST['interval'] ?? 15));
$enabled = isset($_POST['enabled']);
if ($text) {
$timers[] = [
'text' => $text,
'interval' => $interval,
'enabled' => $enabled,
];
}
}
// Gem eksisterende timers (bulk update)
if (isset($_POST['save'])) {
$new = [];
$c = (int)($_POST['count'] ?? 0);
for ($i = 0; $i < $c; $i++) {
$text = $_POST['text_' . $i] ?? '';
$interval = max(1, (int)($_POST['interval_' . $i] ?? 15));
$enabled = isset($_POST['enabled_' . $i]);
if ($text) {
$new[] = [
'text' => $text,
'interval' => $interval,
'enabled' => $enabled,
];
}
}
$timers = $new;
}
// Slet alle
if (isset($_POST['clear'])) {
$timers = [];
}
file_put_contents($file, json_encode($timers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header('Location: timers.php');
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Timers</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrap">
<div class="card">
<h2>⏱️ Timers</h2>
<p><a class="btn" href="index.php">← Tilbage</a></p>
<!-- Tilføj ny timer -->
<form method="post" style="margin-bottom:1rem">
<label>
Besked
<input name="text" placeholder="Husk at følge kanalen 💜">
</label>
<br>
<label>
Interval (minutter)
<input type="number" name="interval" min="1" value="15">
</label>
<br>
<label>
<input type="checkbox" name="enabled" checked>
Aktiveret
</label>
<br>
<button class="btn" name="add" value="1">Tilføj</button>
</form>
<!-- Redigér eksisterende -->
<form method="post">
<table>
<tr>
<th>#</th>
<th>Besked</th>
<th>Interval</th>
<th>Aktiv</th>
</tr>
<?php foreach ($timers as $i => $t): ?>
<tr>
<td><?php echo $i + 1; ?></td>
<td>
<input
name="text_<?php echo $i; ?>"
value="<?php echo htmlspecialchars($t['text']); ?>"
>
</td>
<td>
<input
type="number"
min="1"
name="interval_<?php echo $i; ?>"
value="<?php echo (int)$t['interval']; ?>"
>
</td>
<td>
<input
type="checkbox"
name="enabled_<?php echo $i; ?>"
<?php echo !empty($t['enabled']) ? 'checked' : ''; ?>
>
</td>
</tr>
<?php endforeach; ?>
</table>
<input type="hidden" name="count" value="<?php echo count($timers); ?>">
<br>
<button class="btn" name="save" value="1">Gem</button>
<button class="btn" name="clear" value="1" onclick="return confirm('Slet alle?')">Slet alle</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,44 @@
<?php
$env = [];
// Indlæs .env-fil
$envFile = dirname(__DIR__) . '/.env';
foreach (file($envFile, 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]);
}
}
// Hent token fra .env
$token = isset($env['TWITCH_OAUTH'])
? preg_replace('/^oauth:/i', '', $env['TWITCH_OAUTH'])
: '';
if (!$token) {
echo 'Mangler TWITCH_OAUTH';
exit;
}
// Kald Twitch validate endpoint
$ch = curl_init('https://id.twitch.tv/oauth2/validate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: OAuth ' . $token],
CURLOPT_TIMEOUT => 10,
]);
$res = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Returnér resultat som JSON
header('Content-Type: application/json');
echo json_encode(
[
'http' => $http,
'data' => json_decode($res, true),
],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);