Autosave: 20260226-193348
This commit is contained in:
parent
ce5ab804b3
commit
62bc71e2f3
127
admin.php
Normal file
127
admin.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
// Authentication and Authorization check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (($_SESSION['role'] ?? 'user') !== 'admin') {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$message = '';
|
||||
$pdo = db();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['logout'])) {
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$head_ads = $_POST['head_ads'] ?? '';
|
||||
$body_ads = $_POST['body_ads'] ?? '';
|
||||
$openai_api_key = $_POST['openai_api_key'] ?? '';
|
||||
|
||||
$stmt = $pdo->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'] ?? '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Admin Panel - TikTok Live AI</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #0f0f0f; color: #fff; }
|
||||
.card-admin { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; }
|
||||
.form-control { background-color: #262626; border-color: #444; color: #fff; }
|
||||
.form-control:focus { background-color: #2d2d2d; border-color: #00f2ea; color: #fff; box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.25); }
|
||||
.btn-save { background-color: #00f2ea; color: #000; font-weight: 700; border-radius: 8px; border: none; padding: 12px 24px; }
|
||||
.btn-save:hover { background-color: #00d8d1; color: #000; }
|
||||
.text-tiktok-cyan { color: #00f2ea; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-dark bg-black border-bottom border-secondary mb-5">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="/">
|
||||
<span class="text-tiktok-cyan">TikTok</span> Live Admin
|
||||
</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-secondary small me-3">Logged in as <?= htmlspecialchars($_SESSION['username']) ?></span>
|
||||
<a href="/" class="btn btn-outline-light btn-sm me-2">Back to Home</a>
|
||||
<form action="admin.php" method="POST" class="m-0 d-inline">
|
||||
<button type="submit" name="logout" class="btn btn-danger btn-sm">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin p-4 shadow-lg">
|
||||
<h2 class="mb-4 fw-bold">Site Settings</h2>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success bg-dark text-success border-success"><?= $message ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-4">
|
||||
<label for="openai_api_key" class="form-label fw-semibold text-secondary small">OpenAI API Key</label>
|
||||
<input type="password" name="openai_api_key" id="openai_api_key" class="form-control" placeholder="sk-..." value="<?= htmlspecialchars($openai_api_key) ?>">
|
||||
<div class="form-text text-muted">If left empty, the application will use the Flatlogic AI Proxy.</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-secondary my-4">
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="head_ads" class="form-label fw-semibold text-secondary small">Head Scripts (for JS Ads, Meta tags, etc.)</label>
|
||||
<textarea name="head_ads" id="head_ads" rows="4" class="form-control" placeholder="Paste your <head> scripts here..."><?= htmlspecialchars($head_ads) ?></textarea>
|
||||
<div class="form-text text-muted">These scripts will be placed inside the <head> tag.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="body_ads" class="form-label fw-semibold text-secondary small">Body Scripts (for floating ads, analytics, etc.)</label>
|
||||
<textarea name="body_ads" id="body_ads" rows="4" class="form-control" placeholder="Paste your <body> scripts here..."><?= htmlspecialchars($body_ads) ?></textarea>
|
||||
<div class="form-text text-muted">These scripts will be placed at the bottom of the <body> tag.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid mt-5">
|
||||
<button type="submit" class="btn btn-save">Save All Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
40
api/bridge_control.php
Normal file
40
api/bridge_control.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$username = $_GET['username'] ?? '';
|
||||
|
||||
if (empty($username)) {
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
|
||||
71
api/get_updates.php
Normal file
71
api/get_updates.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
$username = $_GET['username'] ?? '';
|
||||
if (empty($username)) {
|
||||
echo json_encode(['events' => []]);
|
||||
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' => []]);
|
||||
}
|
||||
51
api/process_comment.php
Normal file
51
api/process_comment.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$data || empty($data['comment']) || empty($data['username'])) {
|
||||
echo json_encode(['error' => '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
|
||||
]);
|
||||
@ -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);
|
||||
}
|
||||
BIN
assets/images/ai_avatar.jpg
Normal file
BIN
assets/images/ai_avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@ -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 = '<span class="status-dot bg-success me-1 pulse"></span> 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 = '<span class="status-dot bg-secondary me-1"></span> 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 = `
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="comment-author" style="${isSystem ? 'color: var(--tiktok-red)' : ''}">${event.author}:</span>
|
||||
<span class="comment-text">${event.comment}</span>
|
||||
</div>
|
||||
<small class="text-secondary opacity-50" style="font-size: 0.7rem">${new Date(event.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</small>
|
||||
</div>
|
||||
<div class="ai-reply-box mt-1">AI: ${event.reply}</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<td class="text-secondary small">${time}</td>
|
||||
<td><strong>${event.author}</strong></td>
|
||||
<td class="text-secondary">${event.comment}</td>
|
||||
<td class="text-tiktok-cyan">${event.reply}</td>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header bg-dark text-light border-secondary">
|
||||
<strong class="me-auto ${type === 'danger' ? 'text-danger' : (type === 'success' ? 'text-tiktok-cyan' : '')}">${title}</strong>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement);
|
||||
toast.show();
|
||||
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
6
db/migrations/add_user_details.sql
Normal file
6
db/migrations/add_user_details.sql
Normal file
@ -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';
|
||||
1
db/migrations/openai_api_key.sql
Normal file
1
db/migrations/openai_api_key.sql
Normal file
@ -0,0 +1 @@
|
||||
INSERT INTO site_settings (setting_key, setting_value) VALUES ('openai_api_key', '') ON DUPLICATE KEY UPDATE setting_key=setting_key;
|
||||
7
db/migrations/site_settings.sql
Normal file
7
db/migrations/site_settings.sql
Normal file
@ -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;
|
||||
9
db/migrations/users.sql
Normal file
9
db/migrations/users.sql
Normal file
@ -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');
|
||||
8
db/schema.sql
Normal file
8
db/schema.sql
Normal file
@ -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
|
||||
);
|
||||
331
index.php
331
index.php
@ -1,150 +1,215 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$pdo = db();
|
||||
// Fetch 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'] ?? '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<title>TikTok Live AI Assistant</title>
|
||||
<?php
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Real-time TikTok Live AI Comment Reader and Auto-Responder with TTS.';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||
|
||||
<!-- Custom Head Scripts -->
|
||||
<?= $head_ads ?>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-black border-bottom border-secondary sticky-top">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold d-flex align-items-center" href="/">
|
||||
<span class="text-tiktok-cyan me-1">TikTok</span>
|
||||
<span class="text-tiktok-red me-2">Live</span>
|
||||
<span class="badge bg-danger rounded-pill pulse">AI LIVE</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<div class="ms-auto d-flex align-items-center flex-column flex-lg-row">
|
||||
<div id="connectionStatus" class="me-lg-3 my-2 my-lg-0 small text-secondary">
|
||||
<span class="status-dot bg-secondary me-1"></span> Disconnected
|
||||
</div>
|
||||
|
||||
<?php if (isset($_SESSION['user_id'])): ?>
|
||||
<?php if (($_SESSION['role'] ?? 'user') === 'admin'): ?>
|
||||
<a href="admin.php" class="nav-link text-tiktok-cyan me-lg-3 my-1 my-lg-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-gear-fill me-1" viewBox="0 0 16 16">
|
||||
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
||||
</svg>
|
||||
Admin
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span class="text-secondary small me-lg-3 my-1 my-lg-0">Hi, <?= htmlspecialchars($_SESSION['username']) ?></span>
|
||||
<a href="logout.php" class="btn btn-outline-danger btn-sm my-1 my-lg-0">Logout</a>
|
||||
<?php else: ?>
|
||||
<a href="login.php" class="nav-link text-light me-lg-3 my-1 my-lg-0">Login</a>
|
||||
<a href="register.php" class="btn btn-tiktok-cyan btn-sm my-1 my-lg-0">Register</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4">
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Controls & Input -->
|
||||
<div class="col-lg-4">
|
||||
<!-- AI Avatar Improved -->
|
||||
<div class="ai-avatar-container">
|
||||
<div class="ai-avatar">
|
||||
<img src="assets/images/ai_avatar.jpg" alt="AI Avatar">
|
||||
<div class="ai-avatar-glow"></div>
|
||||
<div class="ai-badge">AI ACTIVE</div>
|
||||
</div>
|
||||
<h4 class="mt-3 fw-bold text-tiktok-cyan">Your AI Assistant</h4>
|
||||
<p class="text-secondary small">I am here to interact with your live stream audience!</p>
|
||||
</div>
|
||||
|
||||
<div class="card bg-surface border-secondary h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-4 fw-bold">Configuration</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tiktokUsername" class="form-label small text-secondary">TikTok Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary">@</span>
|
||||
<input type="text" id="tiktokUsername" class="form-control bg-dark border-secondary text-light" placeholder="username" value="tiktok_star">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button id="connectBtn" class="btn btn-tiktok-cyan w-100 fw-bold py-2 shadow-sm">Connect to Live</button>
|
||||
<button id="disconnectBtn" class="btn btn-outline-danger w-100 fw-bold py-2 mt-2 shadow-sm d-none">Disconnect</button>
|
||||
</div>
|
||||
|
||||
<hr class="border-secondary my-4">
|
||||
|
||||
<h6 class="mb-3 fw-bold">TTS Settings</h6>
|
||||
<div class="mb-3">
|
||||
<label for="voiceSelect" class="form-label small text-secondary">Voice</label>
|
||||
<select id="voiceSelect" class="form-select bg-dark border-secondary text-light"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="rateRange" class="form-label small text-secondary">Speed: <span id="rateValue">1.0</span></label>
|
||||
<input type="range" class="form-range" id="rateRange" min="0.5" max="2" step="0.1" value="1.0">
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="autoReplyToggle" checked>
|
||||
<label class="form-check-label" for="autoReplyToggle">AI Auto-Reply</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Live Feed -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card bg-surface border-secondary shadow-sm h-100">
|
||||
<div class="card-header bg-transparent border-secondary d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold">Live Comment Feed</h5>
|
||||
<span class="badge bg-dark border border-secondary text-secondary" id="commentCount">0 comments</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="commentFeed" class="comment-feed p-3 overflow-auto" style="height: 500px;">
|
||||
<div class="text-center text-secondary py-5" id="emptyFeed">
|
||||
<p>Enter a username and connect to start seeing live comments.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-secondary">
|
||||
<div class="input-group">
|
||||
<input type="text" id="manualComment" class="form-control bg-dark border-secondary text-light" placeholder="Simulate a comment...">
|
||||
<button id="simulateBtn" class="btn btn-outline-tiktok-cyan">Simulate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats / History -->
|
||||
<div class="row g-4 mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-surface border-secondary shadow-sm">
|
||||
<div class="card-header bg-transparent border-secondary">
|
||||
<h5 class="mb-0 fw-bold">Recent AI Interactions</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr class="border-secondary">
|
||||
<th class="border-secondary text-secondary small">Time</th>
|
||||
<th class="border-secondary text-secondary small">User</th>
|
||||
<th class="border-secondary text-secondary small">Comment</th>
|
||||
<th class="border-secondary text-secondary small">AI Reply</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="historyTableBody">
|
||||
<?php
|
||||
try {
|
||||
$stmt = db()->query("SELECT * FROM tiktok_history ORDER BY created_at DESC LIMIT 5");
|
||||
$rows = $stmt->fetchAll();
|
||||
if (empty($rows)) {
|
||||
echo '<tr><td colspan="4" class="text-center text-secondary py-4">No history yet.</td></tr>';
|
||||
} else {
|
||||
foreach ($rows as $row) {
|
||||
echo "<tr class='border-secondary'>";
|
||||
echo "<td class='text-secondary small'>" . date('H:i:s', strtotime($row['created_at'])) . "</td>";
|
||||
echo "<td><strong>" . htmlspecialchars($row['comment_author']) . "</strong></td>";
|
||||
echo "<td class='text-secondary'>" . htmlspecialchars($row['comment_text']) . "</td>";
|
||||
echo "<td class='text-tiktok-cyan'>" . htmlspecialchars($row['ai_reply']) . "</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo '<tr><td colspan="4" class="text-center text-danger py-4">Error loading history.</td></tr>';
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="text-center py-4 text-secondary small border-top border-secondary mt-5">
|
||||
<p>© <?= date('Y') ?> TikTok Live AI Assistant. Built with LAMP + OpenAI.</p>
|
||||
<p><a href="admin.php" class="text-secondary text-decoration-none">Admin Settings</a></p>
|
||||
</footer>
|
||||
|
||||
<div id="toastContainer" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Custom Body Scripts -->
|
||||
<?= $body_ads ?>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
98
login.php
Normal file
98
login.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
if (($_SESSION['role'] ?? 'user') === 'admin') {
|
||||
header('Location: admin.php');
|
||||
} else {
|
||||
header('Location: index.php');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username && $password) {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->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.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Login - TikTok Live AI</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #0f0f0f; color: #fff; height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||||
.card-login { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; width: 100%; max-width: 400px; }
|
||||
.form-control { background-color: #262626; border-color: #444; color: #fff; }
|
||||
.form-control:focus { background-color: #2d2d2d; border-color: #00f2ea; color: #fff; box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.25); }
|
||||
.btn-login { background-color: #00f2ea; color: #000; font-weight: 700; border-radius: 8px; border: none; padding: 12px; }
|
||||
.btn-login:hover { background-color: #00d8d1; color: #000; }
|
||||
.text-tiktok-cyan { color: #00f2ea; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card-login p-4 shadow-lg">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="fw-bold"><span class="text-tiktok-cyan">TikTok</span> AI Login</h3>
|
||||
<p class="text-secondary small">Please login to continue</p>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger bg-dark text-danger border-danger py-2 small"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label small text-secondary">Username or Email</label>
|
||||
<input type="text" name="username" id="username" class="form-control" required autofocus>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label small text-secondary">Password</label>
|
||||
<input type="password" name="password" id="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-login">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<p class="small text-secondary">Don't have an account? <a href="register.php" class="text-tiktok-cyan text-decoration-none">Register here</a></p>
|
||||
<a href="/" class="text-secondary small text-decoration-none">← Back to Site</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
7
logout.php
Normal file
7
logout.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
631
package-lock.json
generated
Normal file
631
package-lock.json
generated
Normal file
@ -0,0 +1,631 @@
|
||||
{
|
||||
"name": "workspace",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "workspace",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mysql2": "^3.18.2",
|
||||
"tiktok-live-connector": "^2.1.1-beta1",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@eulerstream/euler-api-sdk": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eulerstream/euler-api-sdk/-/euler-api-sdk-0.1.7.tgz",
|
||||
"integrity": "sha512-nahq6cTun0XClhpjFqSDOA79GpJXa5wgEEQIRE/q8G5sM83BzkJcRkQPaI0SP1AUgovCuAiXNdhEhqtvdAVf5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
package.json
Normal file
17
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
113
register.php
Normal file
113
register.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$confirm_password = $_POST['confirm_password'] ?? '';
|
||||
|
||||
if ($username && $email && $password && $confirm_password) {
|
||||
if ($password !== $confirm_password) {
|
||||
$error = "Passwords do not match.";
|
||||
} elseif (strlen($password) < 6) {
|
||||
$error = "Password must be at least 6 characters.";
|
||||
} else {
|
||||
$pdo = db();
|
||||
// Check if username or email exists
|
||||
$stmt = $pdo->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 <a href='login.php' class='text-tiktok-cyan'>login</a>.";
|
||||
} else {
|
||||
$error = "Registration failed. Please try again.";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$error = "Please fill in all fields.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Register - TikTok Live AI</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #0f0f0f; color: #fff; height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||||
.card-register { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; width: 100%; max-width: 450px; }
|
||||
.form-control { background-color: #262626; border-color: #444; color: #fff; }
|
||||
.form-control:focus { background-color: #2d2d2d; border-color: #00f2ea; color: #fff; box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.25); }
|
||||
.btn-register { background-color: #00f2ea; color: #000; font-weight: 700; border-radius: 8px; border: none; padding: 12px; }
|
||||
.btn-register:hover { background-color: #00d8d1; color: #000; }
|
||||
.text-tiktok-cyan { color: #00f2ea; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card-register p-4 shadow-lg">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="fw-bold"><span class="text-tiktok-cyan">Join</span> TikTok AI</h3>
|
||||
<p class="text-secondary small">Create your account to get started</p>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger bg-dark text-danger border-danger py-2 small"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success bg-dark text-success border-success py-2 small"><?= $success ?></div>
|
||||
<?php else: ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label small text-secondary">Username</label>
|
||||
<input type="text" name="username" id="username" class="form-control" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label small text-secondary">Email Address</label>
|
||||
<input type="email" name="email" id="email" class="form-control" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="password" class="form-label small text-secondary">Password</label>
|
||||
<input type="password" name="password" id="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<label for="confirm_password" class="form-label small text-secondary">Confirm Password</label>
|
||||
<input type="password" name="confirm_password" id="confirm_password" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-register">Register Now</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<p class="small text-secondary">Already have an account? <a href="login.php" class="text-tiktok-cyan text-decoration-none">Login here</a></p>
|
||||
<a href="/" class="text-secondary small text-decoration-none">← Back to Site</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
84
tiktok_bridge.js
Normal file
84
tiktok_bridge.js
Normal file
@ -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 <username> <dbHost> <dbUser> <dbPass> <dbName>');
|
||||
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();
|
||||
Loading…
x
Reference in New Issue
Block a user