From 62bc71e2f3d64bb804aeaf2d9540c4991857fd97 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 26 Feb 2026 19:33:48 +0000 Subject: [PATCH] Autosave: 20260226-193348 --- admin.php | 127 ++++++ ai/LocalAIApi.php | 74 +++- api/bridge_control.php | 40 ++ api/get_updates.php | 71 ++++ api/process_comment.php | 51 +++ assets/css/custom.css | 424 +++++++++---------- assets/images/ai_avatar.jpg | Bin 0 -> 35533 bytes assets/js/main.js | 256 ++++++++++-- db/migrations/add_user_details.sql | 6 + db/migrations/openai_api_key.sql | 1 + db/migrations/site_settings.sql | 7 + db/migrations/users.sql | 9 + db/schema.sql | 8 + index.php | 331 +++++++++------ login.php | 98 +++++ logout.php | 7 + package-lock.json | 631 +++++++++++++++++++++++++++++ package.json | 17 + register.php | 113 ++++++ tiktok_bridge.js | 84 ++++ 20 files changed, 1935 insertions(+), 420 deletions(-) create mode 100644 admin.php create mode 100644 api/bridge_control.php create mode 100644 api/get_updates.php create mode 100644 api/process_comment.php create mode 100644 assets/images/ai_avatar.jpg create mode 100644 db/migrations/add_user_details.sql create mode 100644 db/migrations/openai_api_key.sql create mode 100644 db/migrations/site_settings.sql create mode 100644 db/migrations/users.sql create mode 100644 db/schema.sql create mode 100644 login.php create mode 100644 logout.php create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 register.php create mode 100644 tiktok_bridge.js 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 0000000000000000000000000000000000000000..6358d813525d220d6efbd223a7cdd3393de1d70d GIT binary patch literal 35533 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 literal 0 HcmV?d00001 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… + + +
-
- Page updated: (UTC) -
+ + + +
+
+ +
+ +
+
+ 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.
+
+
+
+
+
+ +
+

© TikTok Live AI Assistant. Built with LAMP + OpenAI.

+

Admin Settings

+
+ +
+ + + + + + - + \ 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 + + + + + + + + + + + \ 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