Software
v0.0..1
This commit is contained in:
246
software/v0.0.1/web/api.php
Normal file
246
software/v0.0.1/web/api.php
Normal 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;
|
||||
}
|
||||
1
software/v0.0.1/web/bets.php
Normal file
1
software/v0.0.1/web/bets.php
Normal 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>
|
||||
1
software/v0.0.1/web/commands.php
Normal file
1
software/v0.0.1/web/commands.php
Normal 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>
|
||||
1
software/v0.0.1/web/control.php
Normal file
1
software/v0.0.1/web/control.php
Normal 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');
|
||||
22
software/v0.0.1/web/create_admin.php
Normal file
22
software/v0.0.1/web/create_admin.php
Normal 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)";
|
||||
28
software/v0.0.1/web/db.php
Normal file
28
software/v0.0.1/web/db.php
Normal 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;
|
||||
}
|
||||
35
software/v0.0.1/web/events.php
Normal file
35
software/v0.0.1/web/events.php
Normal 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
|
||||
}
|
||||
82
software/v0.0.1/web/eventsub.php
Normal file
82
software/v0.0.1/web/eventsub.php
Normal 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';
|
||||
54
software/v0.0.1/web/eventsub_setup.php
Normal file
54
software/v0.0.1/web/eventsub_setup.php
Normal 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>
|
||||
70
software/v0.0.1/web/giveaway.php
Normal file
70
software/v0.0.1/web/giveaway.php
Normal 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>
|
||||
20
software/v0.0.1/web/guard.php
Normal file
20
software/v0.0.1/web/guard.php
Normal 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 : [];
|
||||
}
|
||||
88
software/v0.0.1/web/help.php
Normal file
88
software/v0.0.1/web/help.php
Normal 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>1–100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>$random:N</code></td>
|
||||
<td>1–N</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>
|
||||
140
software/v0.0.1/web/index.php
Normal file
140
software/v0.0.1/web/index.php
Normal 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
|
||||
15
software/v0.0.1/web/log.php
Normal file
15
software/v0.0.1/web/log.php
Normal 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.';
|
||||
}
|
||||
76
software/v0.0.1/web/login.php
Normal file
76
software/v0.0.1/web/login.php
Normal 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>
|
||||
6
software/v0.0.1/web/logout.php
Normal file
6
software/v0.0.1/web/logout.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
23
software/v0.0.1/web/migrate_users.php
Normal file
23
software/v0.0.1/web/migrate_users.php
Normal 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.";
|
||||
75
software/v0.0.1/web/permissions.php
Normal file
75
software/v0.0.1/web/permissions.php
Normal 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>
|
||||
42
software/v0.0.1/web/points.php
Normal file
42
software/v0.0.1/web/points.php
Normal 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>
|
||||
63
software/v0.0.1/web/poll.php
Normal file
63
software/v0.0.1/web/poll.php
Normal 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, 15–1800)
|
||||
<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>
|
||||
87
software/v0.0.1/web/raffle.php
Normal file
87
software/v0.0.1/web/raffle.php
Normal 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>
|
||||
42
software/v0.0.1/web/send.php
Normal file
42
software/v0.0.1/web/send.php
Normal 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>
|
||||
395
software/v0.0.1/web/settings.php
Normal file
395
software/v0.0.1/web/settings.php
Normal 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>
|
||||
46
software/v0.0.1/web/slots.php
Normal file
46
software/v0.0.1/web/slots.php
Normal 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>
|
||||
135
software/v0.0.1/web/style.css
Normal file
135
software/v0.0.1/web/style.css
Normal 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;
|
||||
}
|
||||
135
software/v0.0.1/web/timers.php
Normal file
135
software/v0.0.1/web/timers.php
Normal 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>
|
||||
44
software/v0.0.1/web/validate.php
Normal file
44
software/v0.0.1/web/validate.php
Normal 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
|
||||
);
|
||||
Reference in New Issue
Block a user