diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..9c3f8b0 --- /dev/null +++ b/admin.php @@ -0,0 +1,127 @@ +prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'head_ads'"); + $stmt->execute([$head_ads]); + + $stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'body_ads'"); + $stmt->execute([$body_ads]); + + $stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'openai_api_key'"); + $stmt->execute([$openai_api_key]); + + $message = "Settings updated successfully!"; +} + +// Fetch current settings +$stmt = $pdo->query("SELECT setting_key, setting_value FROM site_settings"); +$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); +$head_ads = $settings['head_ads'] ?? ''; +$body_ads = $settings['body_ads'] ?? ''; +$openai_api_key = $settings['openai_api_key'] ?? ''; +?> + + + + + + Admin Panel - TikTok Live AI + + + + + + + + + +
+
+
+
+

Site Settings

+ + +
+ + +
+
+ + +
If left empty, the application will use the Flatlogic AI Proxy.
+
+ +
+ +
+ + +
These scripts will be placed inside the <head> tag.
+
+ +
+ + +
These scripts will be placed at the bottom of the <body> tag.
+
+ +
+ +
+
+
+
+
+
+ + + diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php index d428248..0c97839 100644 --- a/ai/LocalAIApi.php +++ b/ai/LocalAIApi.php @@ -8,20 +8,6 @@ // ['role' => 'user', 'content' => 'Tell me a bedtime story.'], // ], // ]); -// if (!empty($response['success'])) { -// // response['data'] contains full payload, e.g.: -// // { -// // "id": "resp_xxx", -// // "status": "completed", -// // "output": [ -// // {"type": "reasoning", "summary": []}, -// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} -// // ] -// // } -// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...) -// } -// Poll settings override: -// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]); class LocalAIApi { @@ -55,8 +41,14 @@ class LocalAIApi ]; } + // Use custom model if provided, else use default from config if (!isset($payload['model']) || $payload['model'] === '') { - $payload['model'] = $cfg['default_model']; + $payload['model'] = !empty($cfg['openai_api_key']) ? 'gpt-4o-mini' : $cfg['default_model']; + } + + // If we have a custom OpenAI API key, we call OpenAI directly (synchronously) + if (!empty($cfg['openai_api_key'])) { + return self::directOpenAIRequest($payload, $options, $cfg['openai_api_key']); } $initial = self::request($options['path'] ?? null, $payload, $options); @@ -167,6 +159,40 @@ class LocalAIApi return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls); } + /** + * Direct request to OpenAI API. + */ + private static function directOpenAIRequest(array $payload, array $options, string $apiKey): array + { + // Translate Flatlogic 'input' to OpenAI 'messages' + if (isset($payload['input'])) { + $payload['messages'] = $payload['input']; + unset($payload['input']); + } + + $url = 'https://api.openai.com/v1/chat/completions'; + $headers = [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $apiKey + ]; + + $timeout = $options['timeout'] ?? 30; + $verifyTls = $options['verify_tls'] ?? true; + + $body = json_encode($payload); + $resp = self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls); + + if ($resp['success']) { + return [ + 'success' => true, + 'status' => 200, + 'data' => $resp['data'] + ]; + } + + return $resp; + } + /** * Poll AI request status until ready or timeout. * @@ -359,6 +385,22 @@ class LocalAIApi if (!is_array($cfg)) { throw new RuntimeException('Invalid AI config format: expected array'); } + + // Try to load custom API key from DB if possible + $dbConfig = __DIR__ . '/../db/config.php'; + if (file_exists($dbConfig)) { + try { + require_once $dbConfig; + $pdo = db(); + $stmt = $pdo->query("SELECT setting_value FROM site_settings WHERE setting_key = 'openai_api_key' LIMIT 1"); + $val = $stmt->fetchColumn(); + $cfg['openai_api_key'] = $val ?: null; + } catch (Exception $e) { + // Fail silently, fallback to proxy + $cfg['openai_api_key'] = null; + } + } + self::$configCache = $cfg; } @@ -490,4 +532,4 @@ class LocalAIApi // Legacy alias for backward compatibility with the previous class name. if (!class_exists('OpenAIService')) { class_alias(LocalAIApi::class, 'OpenAIService'); -} +} \ No newline at end of file diff --git a/api/bridge_control.php b/api/bridge_control.php new file mode 100644 index 0000000..4a3e05b --- /dev/null +++ b/api/bridge_control.php @@ -0,0 +1,40 @@ + false, 'error' => 'Username required']); + exit; +} + +if ($action === 'start') { + // 1. Kill any existing bridge for this user + shell_exec("pkill -f \"node tiktok_bridge.js $username\""); + + // 2. Start new bridge + $cmd = sprintf( + "node %s %s %s %s %s %s > /dev/null 2>&1 &", + escapeshellarg(__DIR__ . '/../tiktok_bridge.js'), + escapeshellarg($username), + escapeshellarg(DB_HOST), + escapeshellarg(DB_USER), + escapeshellarg(DB_PASS), + escapeshellarg(DB_NAME) + ); + + shell_exec($cmd); + + echo json_encode(['success' => true, 'message' => "Bridge started for @$username"]); +} elseif ($action === 'stop') { + shell_exec("pkill -f \"node tiktok_bridge.js $username\""); + echo json_encode(['success' => true, 'message' => "Bridge stopped for @$username"]); +} elseif ($action === 'status') { + $output = shell_exec("pgrep -f \"node tiktok_bridge.js $username\""); + echo json_encode(['success' => true, 'running' => !empty($output)]); +} else { + echo json_encode(['success' => false, 'error' => 'Invalid action']); +} + diff --git a/api/get_updates.php b/api/get_updates.php new file mode 100644 index 0000000..4f1127c --- /dev/null +++ b/api/get_updates.php @@ -0,0 +1,71 @@ + []]); + exit; +} + +try { + // 1. Fetch pending comments (those without AI replies) + $stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND ai_reply IS NULL ORDER BY created_at ASC LIMIT 5"); + $stmt->execute([$username]); + $pending = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $results = []; + + foreach ($pending as $row) { + $comment_author = $row['comment_author']; + $comment_text = $row['comment_text']; + + // 2. Generate AI Reply + $prompt = "You are a helpful and funny AI assistant for a TikTok live stream. + The streamer is '$username'. + The user '$comment_author' just said: '$comment_text'. + Respond to them in a short, engaging, and friendly way (max 20 words). + Keep it suitable for a live stream audience."; + + $resp = LocalAIApi::createResponse([ + 'input' => [ + ['role' => 'system', 'content' => 'TikTok Live AI Assistant'], + ['role' => 'user', 'content' => $prompt], + ], + ]); + + $ai_reply = ''; + if (!empty($resp['success'])) { + $ai_reply = LocalAIApi::extractText($resp); + } else { + $ai_reply = "Thanks for your comment, $comment_author!"; + } + + // 3. Update DB + $update = db()->prepare("UPDATE tiktok_history SET ai_reply = ? WHERE id = ?"); + $update->execute([$ai_reply, $row['id']]); + + $results[] = [ + 'id' => $row['id'], + 'author' => $comment_author, + 'comment' => $comment_text, + 'reply' => $ai_reply, + 'timestamp' => $row['created_at'] + ]; + } + + // 4. Also fetch recently processed comments (last 10 seconds) + // To ensure the frontend shows them even if they were processed by another call + $since = date('Y-m-d H:i:s', time() - 5); + $stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND ai_reply IS NOT NULL AND created_at >= ? ORDER BY created_at DESC LIMIT 10"); + $stmt->execute([$username, $since]); + $processed = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Merge if needed, but for polling, we just return the newly processed ones usually + echo json_encode(['events' => $results]); + +} catch (Exception $e) { + error_log("Update Error: " . $e->getMessage()); + echo json_encode(['error' => $e->getMessage(), 'events' => []]); +} diff --git a/api/process_comment.php b/api/process_comment.php new file mode 100644 index 0000000..0c3177c --- /dev/null +++ b/api/process_comment.php @@ -0,0 +1,51 @@ + 'Missing required data']); + exit; +} + +$username = $data['username']; +$comment_author = $data['author'] ?? 'Anonymous'; +$comment_text = $data['comment']; + +// Prompt for AI +$prompt = "You are a helpful and funny AI assistant for a TikTok live stream. +The streamer is '$username'. +The user '$comment_author' just said: '$comment_text'. +Respond to them in a short, engaging, and friendly way (max 20 words). +Keep it suitable for a live stream audience."; + +$resp = LocalAIApi::createResponse([ + 'input' => [ + ['role' => 'system', 'content' => 'TikTok Live AI Assistant'], + ['role' => 'user', 'content' => $prompt], + ], +]); + +$ai_reply = ''; +if (!empty($resp['success'])) { + $ai_reply = LocalAIApi::extractText($resp); +} else { + $ai_reply = "Thanks for your comment, $comment_author!"; +} + +// Persist to DB +try { + $stmt = db()->prepare("INSERT INTO tiktok_history (username, comment_author, comment_text, ai_reply) VALUES (?, ?, ?, ?)"); + $stmt->execute([$username, $comment_author, $comment_text, $ai_reply]); +} catch (Exception $e) { + // Log error but continue +} + +echo json_encode([ + 'success' => true, + 'reply' => $ai_reply, + 'author' => $comment_author, + 'comment' => $comment_text +]); \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 50e0502..9f0f0c3 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,302 +1,240 @@ +:root { + --tiktok-cyan: #00f2ea; + --tiktok-red: #ff0050; + --bg-dark: #121212; + --bg-surface: #1e1e1e; + --border-secondary: rgba(255, 255, 255, 0.1); + --text-light: #f8f9fa; + --text-secondary: #adb5bd; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + background-color: var(--bg-dark); + color: var(--text-light); + line-height: 1.6; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.bg-surface { + background-color: var(--bg-surface); } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.text-tiktok-cyan { + color: var(--tiktok-cyan) !important; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.text-tiktok-red { + color: var(--tiktok-red) !important; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.btn-tiktok-cyan { + background-color: var(--tiktok-cyan); + color: #000; + border: none; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.btn-tiktok-cyan:hover { + background-color: #00d8d1; + color: #000; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.btn-outline-tiktok-cyan { + border-color: var(--tiktok-cyan); + color: var(--tiktok-cyan); } -::-webkit-scrollbar-track { - background: transparent; +.btn-outline-tiktok-cyan:hover { + background-color: var(--tiktok-cyan); + color: #000; } -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; +.status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; } -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); +.bg-secondary { + background-color: var(--text-secondary); } -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +.bg-success { + background-color: #28a745 !important; +} + +.comment-feed { + scrollbar-width: thin; + scrollbar-color: var(--border-secondary) transparent; +} + +.comment-feed::-webkit-scrollbar { + width: 6px; +} + +.comment-feed::-webkit-scrollbar-thumb { + background-color: var(--border-secondary); + border-radius: 10px; +} + +.comment-item { + border-bottom: 1px solid var(--border-secondary); + padding: 10px 0; + animation: fadeIn 0.3s ease-out; +} + +.comment-author { + font-weight: 600; + color: var(--tiktok-cyan); + margin-right: 8px; +} + +.comment-text { + color: var(--text-light); +} + +.ai-reply-box { + background-color: rgba(0, 242, 234, 0.05); + border-left: 3px solid var(--tiktok-cyan); + padding: 8px 12px; + margin-top: 5px; + font-size: 0.9rem; + font-style: italic; + color: var(--tiktok-cyan); } @keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; +.pulse { + animation: pulse-animation 2s infinite; } -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +@keyframes pulse-animation { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } } -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); +.card { + border-radius: 12px; } -.chat-input-area form { - display: flex; - gap: 0.75rem; +.form-control, .form-select { + border-radius: 8px; + padding: 0.6rem 0.75rem; } -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; +.form-control:focus, .form-select:focus { + background-color: #1a1a1a; + border-color: var(--tiktok-cyan); + box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.1); + color: #fff; } -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); +.toast { + background-color: var(--bg-surface); + color: var(--text-light); + border: 1px solid var(--border-secondary); } -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; +/* AI Avatar Styles Improved */ +.ai-avatar-container { + text-align: center; + margin-bottom: 2rem; + position: relative; } -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); +.ai-avatar { + width: 160px; + height: 220px; + border-radius: 80px 80px 20px 20px; + background: linear-gradient(135deg, var(--tiktok-cyan), var(--tiktok-red)); + padding: 5px; + display: inline-block; + box-shadow: 0 10px 40px rgba(0, 242, 234, 0.3); + overflow: hidden; + position: relative; + animation: float-sway 6s ease-in-out infinite, breath 4s ease-in-out infinite; } -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; +.ai-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 75px 75px 15px 15px; + background: #000; + filter: brightness(1.1) contrast(1.1); } -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); +.ai-avatar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.5), transparent); + pointer-events: none; } -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); +.ai-avatar-glow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-shadow: inset 0 0 20px rgba(0, 242, 234, 0.4); + pointer-events: none; + z-index: 2; + animation: glow-pulse 3s infinite; } -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; +.ai-badge { + position: absolute; + bottom: 5px; + left: 50%; + transform: translateX(-50%); + background-color: var(--tiktok-cyan); + color: #000; + font-size: 0.7rem; + font-weight: 700; + padding: 2px 12px; + border-radius: 20px; + box-shadow: 0 4px 6px rgba(0,0,0,0.4); + z-index: 3; } -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; +@keyframes float-sway { + 0% { transform: translateY(0px) rotate(-1deg); } + 50% { transform: translateY(-20px) rotate(1deg); } + 100% { transform: translateY(0px) rotate(-1deg); } } -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } +@keyframes breath { + 0% { transform: scale(1); } + 50% { transform: scale(1.02); } + 100% { transform: scale(1); } +} + +@keyframes glow-pulse { + 0% { opacity: 0.3; } + 50% { opacity: 1; } + 100% { opacity: 0.3; } } .admin-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; + font-size: 0.8rem; + color: var(--text-secondary); + text-decoration: none; + transition: color 0.2s; + display: flex; + align-items: center; + gap: 5px; } .admin-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; + color: var(--tiktok-cyan); } - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} \ No newline at end of file diff --git a/assets/images/ai_avatar.jpg b/assets/images/ai_avatar.jpg new file mode 100644 index 0000000..6358d81 Binary files /dev/null and b/assets/images/ai_avatar.jpg differ diff --git a/assets/js/main.js b/assets/js/main.js index d349598..47480e5 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,239 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + const connectBtn = document.getElementById('connectBtn'); + const disconnectBtn = document.getElementById('disconnectBtn'); + const tiktokUsernameInput = document.getElementById('tiktokUsername'); + const connectionStatus = document.getElementById('connectionStatus'); + const commentFeed = document.getElementById('commentFeed'); + const emptyFeed = document.getElementById('emptyFeed'); + const voiceSelect = document.getElementById('voiceSelect'); + const rateRange = document.getElementById('rateRange'); + const rateValue = document.getElementById('rateValue'); + const autoReplyToggle = document.getElementById('autoReplyToggle'); + const simulateBtn = document.getElementById('simulateBtn'); + const manualCommentInput = document.getElementById('manualComment'); + const historyTableBody = document.getElementById('historyTableBody'); + const commentCountBadge = document.getElementById('commentCount'); + const toastContainer = document.getElementById('toastContainer'); - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; + let isConnected = false; + let commentCount = 0; + let synth = window.speechSynthesis; + let voices = []; + let pollInterval = null; + let lastEventId = 0; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + // Load voices + function populateVoiceList() { + voices = synth.getVoices().sort(function (a, b) { + const aname = a.name.toUpperCase(); + const bname = b.name.toUpperCase(); + if (aname < bname) return -1; + else if (aname > bname) return 1; + return 0; + }); + voiceSelect.innerHTML = ''; + voices.forEach((voice, i) => { + const option = document.createElement('option'); + option.textContent = `${voice.name} (${voice.lang})`; + if (voice.default) option.textContent += ' -- DEFAULT'; + option.setAttribute('data-lang', voice.lang); + option.setAttribute('data-name', voice.name); + voiceSelect.appendChild(option); + }); + } - appendMessage(message, 'visitor'); - chatInput.value = ''; + populateVoiceList(); + if (speechSynthesis.onvoiceschanged !== undefined) { + speechSynthesis.onvoiceschanged = populateVoiceList; + } + rateRange.addEventListener('input', () => { + rateValue.textContent = rateRange.value; + }); + + async function startBridge(username) { try { - const response = await fetch('api/chat.php', { + const resp = await fetch(`api/bridge_control.php?action=start&username=${username}`); + const data = await resp.json(); + if (data.success) { + isConnected = true; + connectBtn.classList.add('d-none'); + disconnectBtn.classList.remove('d-none'); + connectionStatus.innerHTML = ' Live connection active: @' + username; + emptyFeed.classList.add('d-none'); + showToast('Connected', `Started listening to @${username}. Looking for comments...`, 'success'); + + startPolling(username); + } else { + showToast('Bridge Error', data.error || 'Failed to start bridge', 'danger'); + } + } catch (err) { + showToast('Network Error', 'Could not reach bridge control.', 'danger'); + } + } + + async function stopBridge(username) { + try { + await fetch(`api/bridge_control.php?action=stop&username=${username}`); + isConnected = false; + connectBtn.classList.remove('d-none'); + disconnectBtn.classList.add('d-none'); + connectionStatus.innerHTML = ' Disconnected'; + showToast('Disconnected', 'Stopped bridge.', 'warning'); + + if (pollInterval) clearInterval(pollInterval); + } catch (err) { + console.error(err); + } + } + + function startPolling(username) { + if (pollInterval) clearInterval(pollInterval); + + pollInterval = setInterval(async () => { + if (!isConnected) return; + + try { + const resp = await fetch(`api/get_updates.php?username=${username}`); + const data = await resp.json(); + + if (data.events && data.events.length > 0) { + data.events.forEach(event => { + // Only add if not already seen + if (event.id > lastEventId) { + addEventToFeed(event); + lastEventId = event.id; + } + }); + } + } catch (err) { + console.error('Polling error:', err); + } + }, 3000); // Poll every 3 seconds + } + + function addEventToFeed(event) { + commentCount++; + commentCountBadge.textContent = `${commentCount} events`; + + const isSystem = event.comment.includes('sent') && event.comment.includes('x'); + + const commentItem = document.createElement('div'); + commentItem.className = isSystem ? 'comment-item border-start border-4 border-tiktok-red' : 'comment-item'; + commentItem.innerHTML = ` +
+
+ ${event.author}: + ${event.comment} +
+ ${new Date(event.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} +
+
AI: ${event.reply}
+ `; + + commentFeed.prepend(commentItem); + + // TTS + speak(event.reply); + + // Update history table + updateHistoryTable(event); + } + + connectBtn.addEventListener('click', () => { + const username = tiktokUsernameInput.value.trim(); + if (!username) { + showToast('Error', 'Please enter a TikTok username.', 'danger'); + return; + } + startBridge(username); + }); + + disconnectBtn.addEventListener('click', () => { + const username = tiktokUsernameInput.value.trim(); + stopBridge(username); + }); + + simulateBtn.addEventListener('click', async () => { + const commentText = manualCommentInput.value.trim(); + const username = tiktokUsernameInput.value.trim(); + if (!commentText || !username) return; + + try { + // Manually insert into DB to test + await fetch('api/process_comment.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) + body: JSON.stringify({ author: 'TestUser', comment: commentText, username }) }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + manualCommentInput.value = ''; + // Polling will pick it up + } catch (err) { + console.error(err); } }); -}); + + function speak(text) { + if (!text) return; + + const utterThis = new SpeechSynthesisUtterance(text); + const selectedOption = voiceSelect.selectedOptions[0]?.getAttribute('data-name'); + if (selectedOption) { + for (let i = 0; i < voices.length; i++) { + if (voices[i].name === selectedOption) { + utterThis.voice = voices[i]; + } + } + } + utterThis.pitch = 1; + utterThis.rate = rateRange.value; + synth.speak(utterThis); + } + + function updateHistoryTable(event) { + const row = document.createElement('tr'); + row.className = 'border-secondary'; + const time = new Date(event.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + row.innerHTML = ` + ${time} + ${event.author} + ${event.comment} + ${event.reply} + `; + + if (historyTableBody.firstChild && historyTableBody.firstChild.tagName === 'TR' && historyTableBody.firstChild.innerText.includes('No history yet')) { + historyTableBody.innerHTML = ''; + } + + historyTableBody.prepend(row); + + if (historyTableBody.children.length > 20) { + historyTableBody.removeChild(historyTableBody.lastChild); + } + } + + function showToast(title, message, type = 'info') { + const toastId = 'toast-' + Date.now(); + const toastHtml = ` + + `; + + toastContainer.insertAdjacentHTML('beforeend', toastHtml); + const toastElement = document.getElementById(toastId); + const toast = new bootstrap.Toast(toastElement); + toast.show(); + + toastElement.addEventListener('hidden.bs.toast', () => { + toastElement.remove(); + }); + } +}); \ No newline at end of file diff --git a/db/migrations/add_user_details.sql b/db/migrations/add_user_details.sql new file mode 100644 index 0000000..7362489 --- /dev/null +++ b/db/migrations/add_user_details.sql @@ -0,0 +1,6 @@ +-- Add email and role columns to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255) AFTER username; +ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(50) DEFAULT 'user' AFTER password; + +-- Set existing admin user to admin role +UPDATE users SET role = 'admin' WHERE username = 'admin'; diff --git a/db/migrations/openai_api_key.sql b/db/migrations/openai_api_key.sql new file mode 100644 index 0000000..c4adf82 --- /dev/null +++ b/db/migrations/openai_api_key.sql @@ -0,0 +1 @@ +INSERT INTO site_settings (setting_key, setting_value) VALUES ('openai_api_key', '') ON DUPLICATE KEY UPDATE setting_key=setting_key; diff --git a/db/migrations/site_settings.sql b/db/migrations/site_settings.sql new file mode 100644 index 0000000..311499e --- /dev/null +++ b/db/migrations/site_settings.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS site_settings ( + setting_key VARCHAR(255) PRIMARY KEY, + setting_value TEXT +); + +INSERT INTO site_settings (setting_key, setting_value) VALUES ('head_ads', '') ON DUPLICATE KEY UPDATE setting_key=setting_key; +INSERT INTO site_settings (setting_key, setting_value) VALUES ('body_ads', '') ON DUPLICATE KEY UPDATE setting_key=setting_key; diff --git a/db/migrations/users.sql b/db/migrations/users.sql new file mode 100644 index 0000000..bd721d9 --- /dev/null +++ b/db/migrations/users.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Default admin user (password is 'admin123') +INSERT IGNORE INTO users (username, password) VALUES ('admin', '$2y$10$2jqIQAXSAx9.G80gTO4yjO7gUmhxfGsBpVq9Iv.HJZlTIKy0i2Ypa'); \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..8a79150 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS tiktok_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + comment_author VARCHAR(255) NOT NULL, + comment_text TEXT NOT NULL, + ai_reply TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/index.php b/index.php index 7205f3d..050e521 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,215 @@ query("SELECT setting_key, setting_value FROM site_settings"); +$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); +$head_ads = $settings['head_ads'] ?? ''; +$body_ads = $settings['body_ads'] ?? ''; ?> - New Style - - - - - + TikTok Live AI Assistant + + - - - - - - - - + + + + + + - - + + + + + - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + + +
- + + + +
+
+ +
+ +
+
+ AI Avatar +
+
AI ACTIVE
+
+

Your AI Assistant

+

I am here to interact with your live stream audience!

+
+ +
+
+
Configuration
+ +
+ +
+ @ + +
+
+ +
+ + +
+ +
+ +
TTS Settings
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+ + +
+
+
+
Live Comment Feed
+ 0 comments +
+
+
+
+

Enter a username and connect to start seeing live comments.

+
+
+
+ +
+
+
+ + +
+
+
+
+
Recent AI Interactions
+
+
+ + + + + + + + + + + query("SELECT * FROM tiktok_history ORDER BY created_at DESC LIMIT 5"); + $rows = $stmt->fetchAll(); + if (empty($rows)) { + echo ''; + } else { + foreach ($rows as $row) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + } + } catch (Exception $e) { + echo ''; + } + ?> + +
TimeUserCommentAI Reply
No history yet.
" . date('H:i:s', strtotime($row['created_at'])) . "" . htmlspecialchars($row['comment_author']) . "" . htmlspecialchars($row['comment_text']) . "" . htmlspecialchars($row['ai_reply']) . "
Error loading history.
+
+
+
+
+
+ + + +
+ + + + + + - + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..1d9454b --- /dev/null +++ b/login.php @@ -0,0 +1,98 @@ +prepare("SELECT * FROM users WHERE username = ? OR email = ?"); + $stmt->execute([$username, $username]); // Allow login with username or email + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role'] ?? 'user'; + + if ($_SESSION['role'] === 'admin') { + header('Location: admin.php'); + } else { + header('Location: index.php'); + } + exit; + } else { + $error = "Invalid credentials."; + } + } else { + $error = "Please fill in all fields."; + } +} +?> + + + + + + Login - TikTok Live AI + + + + + + + +
+
+

TikTok AI Login

+

Please login to continue

+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+

Don't have an account? Register here

+ ← Back to Site +
+
+ + + \ No newline at end of file diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..ed56897 --- /dev/null +++ b/logout.php @@ -0,0 +1,7 @@ +=10.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz", + "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callable-instance": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz", + "integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz", + "integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/tiktok-live-connector": { + "version": "2.1.1-beta1", + "resolved": "https://registry.npmjs.org/tiktok-live-connector/-/tiktok-live-connector-2.1.1-beta1.tgz", + "integrity": "sha512-K//OhuyMqq9EAb/2S5jHlW14fsFtO2cXQmRG5ZGQe6Cykb89h4qnkZMdMarzujYxb1jaadzfWKlB2dlz6OmCTA==", + "funding": [ + "https://buymeacoffee.com/zerody" + ], + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.2.5", + "@eulerstream/euler-api-sdk": "0.1.7", + "axios": "^1.12.2", + "callable-instance": "^2.0.0", + "protobufjs": "^6.11.2", + "typed-emitter": "^2.1.0", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4a44b60 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "workspace", + "version": "1.0.0", + "description": "", + "main": "tiktok_bridge.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "mysql2": "^3.18.2", + "tiktok-live-connector": "^2.1.1-beta1", + "ws": "^8.19.0" + } +} diff --git a/register.php b/register.php new file mode 100644 index 0000000..b3135cb --- /dev/null +++ b/register.php @@ -0,0 +1,113 @@ +prepare("SELECT id FROM users WHERE username = ? OR email = ?"); + $stmt->execute([$username, $email]); + if ($stmt->fetch()) { + $error = "Username or email already exists."; + } else { + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + $stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, 'user')"); + if ($stmt->execute([$username, $email, $hashed_password])) { + $success = "Registration successful! You can now login."; + } else { + $error = "Registration failed. Please try again."; + } + } + } + } else { + $error = "Please fill in all fields."; + } +} +?> + + + + + + Register - TikTok Live AI + + + + + + + +
+
+

Join TikTok AI

+

Create your account to get started

+
+ + +
+ + + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ + +
+

Already have an account? Login here

+ ← Back to Site +
+
+ + + diff --git a/tiktok_bridge.js b/tiktok_bridge.js new file mode 100644 index 0000000..a67092f --- /dev/null +++ b/tiktok_bridge.js @@ -0,0 +1,84 @@ +const { WebcastPushConnection } = require('tiktok-live-connector'); +const mysql = require('mysql2/promise'); + +const [,, username, dbHost, dbUser, dbPass, dbName] = process.argv; + +if (!username) { + console.error('Usage: node tiktok_bridge.js '); + process.exit(1); +} + +async function start() { + console.log(`TikTok Bridge starting for @${username}`); + + let connection; + try { + connection = await mysql.createConnection({ + host: dbHost || '127.0.0.1', + user: dbUser || 'root', + password: dbPass || '', + database: dbName || 'app_db' + }); + console.log('Connected to database'); + } catch (err) { + console.error('DB Connection failed:', err); + process.exit(1); + } + + const tiktokConnection = new WebcastPushConnection(username); + + tiktokConnection.connect().then(state => { + console.log(`Connected to TikTok Live @${username} (Room ID: ${state.roomId})`); + }).catch(err => { + console.error('TikTok Connection failed:', err); + process.exit(1); + }); + + tiktokConnection.on('chat', async (data) => { + console.log(`[Chat] ${data.uniqueId}: ${data.comment}`); + try { + await connection.execute( + 'INSERT INTO tiktok_history (username, comment_author, comment_text, created_at) VALUES (?, ?, ?, NOW())', + [username, data.uniqueId, data.comment] + ); + } catch (err) { + console.error('Error inserting chat:', err); + } + }); + + tiktokConnection.on('gift', async (data) => { + const giftMsg = `sent ${data.repeatCount}x ${data.giftName}!`; + console.log(`[Gift] ${data.uniqueId}: ${giftMsg}`); + try { + await connection.execute( + 'INSERT INTO tiktok_history (username, comment_author, comment_text, created_at) VALUES (?, ?, ?, NOW())', + [username, data.uniqueId, giftMsg] + ); + } catch (err) { + console.error('Error inserting gift:', err); + } + }); + + tiktokConnection.on('disconnected', () => { + console.log('TikTok connection disconnected'); + process.exit(0); + }); + + tiktokConnection.on('error', (err) => { + console.error('TikTok error:', err); + }); + + // Keep alive + setInterval(async () => { + try { + await connection.query('SELECT 1'); + } catch (err) { + console.error('DB connection lost, reconnecting...'); + connection = await mysql.createConnection({ + host: dbHost, user: dbUser, password: dbPass, database: dbName + }); + } + }, 30000); +} + +start(); \ No newline at end of file