Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fefe28601c | ||
|
|
70c4865817 | ||
|
|
0c23b971b3 | ||
|
|
3f421e9a53 | ||
|
|
62bc71e2f3 |
272
admin.php
Normal file
272
admin.php
Normal file
@ -0,0 +1,272 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/languages/helper.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'] ?? '';
|
||||
$openai_model = $_POST['openai_model'] ?? 'gpt-4o-mini';
|
||||
$site_name = $_POST['site_name'] ?? 'TikTok Live AI Assistant';
|
||||
$default_language = $_POST['default_language'] ?? 'en';
|
||||
|
||||
// Handle File Uploads
|
||||
$upload_dir = 'assets/images/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0775, true);
|
||||
}
|
||||
|
||||
$site_icon = $_POST['current_site_icon'] ?? 'assets/images/logo.png';
|
||||
if (!empty($_FILES['site_icon']['name'])) {
|
||||
$icon_name = 'logo_' . time() . '.' . pathinfo($_FILES['site_icon']['name'], PATHINFO_EXTENSION);
|
||||
if (move_uploaded_file($_FILES['site_icon']['tmp_name'], $upload_dir . $icon_name)) {
|
||||
$site_icon = $upload_dir . $icon_name;
|
||||
}
|
||||
}
|
||||
|
||||
$site_favicon = $_POST['current_site_favicon'] ?? 'favicon.ico';
|
||||
if (!empty($_FILES['site_favicon']['name'])) {
|
||||
$fav_name = 'favicon_' . time() . '.' . pathinfo($_FILES['site_favicon']['name'], PATHINFO_EXTENSION);
|
||||
if (move_uploaded_file($_FILES['site_favicon']['tmp_name'], $upload_dir . $fav_name)) {
|
||||
$site_favicon = $upload_dir . $fav_name;
|
||||
}
|
||||
}
|
||||
|
||||
$settings_to_update = [
|
||||
'head_ads' => $head_ads,
|
||||
'body_ads' => $body_ads,
|
||||
'openai_api_key' => $openai_api_key,
|
||||
'openai_model' => $openai_model,
|
||||
'site_name' => $site_name,
|
||||
'site_icon' => $site_icon,
|
||||
'site_favicon' => $site_favicon,
|
||||
'default_language' => $default_language
|
||||
];
|
||||
|
||||
foreach ($settings_to_update as $key => $value) {
|
||||
$stmt = $pdo->prepare("INSERT INTO site_settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
|
||||
$stmt->execute([$key, $value, $value]);
|
||||
}
|
||||
|
||||
$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'] ?? '';
|
||||
$openai_model = $settings['openai_model'] ?? 'gpt-4o-mini';
|
||||
$site_name = $settings['site_name'] ?? 'TikTok Live AI Assistant';
|
||||
$site_icon = $settings['site_icon'] ?? 'assets/images/logo.png';
|
||||
$site_favicon = $settings['site_favicon'] ?? 'favicon.ico';
|
||||
$default_language = $settings['default_language'] ?? 'en';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= get_lang() ?>">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><?= __('admin') ?> - <?= htmlspecialchars($site_name) ?></title>
|
||||
<link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($site_favicon) ?>">
|
||||
<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, .form-select { background-color: #262626; border-color: #444; color: #fff; }
|
||||
.form-control:focus, .form-select: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"><?= __('hi') ?>, <?= 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 mb-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="card-admin p-4 shadow-lg mb-4">
|
||||
<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" enctype="multipart/form-data">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label for="site_name" class="form-label fw-semibold text-secondary small"><?= __('site_name') ?></label>
|
||||
<input type="text" name="site_name" id="site_name" class="form-control" value="<?= htmlspecialchars($site_name) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="default_language" class="form-label fw-semibold text-secondary small"><?= __('language') ?></label>
|
||||
<select name="default_language" id="default_language" class="form-select">
|
||||
<option value="en" <?= $default_language === 'en' ? 'selected' : '' ?>>English</option>
|
||||
<option value="id" <?= $default_language === 'id' ? 'selected' : '' ?>>Indonesia</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="site_icon" class="form-label fw-semibold text-secondary small"><?= __('site_icon') ?></label>
|
||||
<input type="file" name="site_icon" id="site_icon" class="form-control">
|
||||
<input type="hidden" name="current_site_icon" value="<?= htmlspecialchars($site_icon) ?>">
|
||||
<?php if ($site_icon): ?>
|
||||
<div class="mt-2 small text-secondary">Current: <img src="<?= $site_icon ?>" height="30" class="ms-2"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="site_favicon" class="form-label fw-semibold text-secondary small"><?= __('site_favicon') ?></label>
|
||||
<input type="file" name="site_favicon" id="site_favicon" class="form-control">
|
||||
<input type="hidden" name="current_site_favicon" value="<?= htmlspecialchars($site_favicon) ?>">
|
||||
<?php if ($site_favicon): ?>
|
||||
<div class="mt-2 small text-secondary">Current: <img src="<?= $site_favicon ?>" height="20" class="ms-2"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<hr class="border-secondary my-2">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="openai_api_key" class="form-label fw-semibold text-secondary small"><?= __('openai_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>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="openai_model" class="form-label fw-semibold text-secondary small"><?= __('openai_model') ?></label>
|
||||
<select name="openai_model" id="openai_model" class="form-select">
|
||||
<option value="gpt-4o-mini" <?= $openai_model === 'gpt-4o-mini' ? 'selected' : '' ?>>GPT-4o Mini (Default)</option>
|
||||
<option value="o3-mini" <?= $openai_model === 'o3-mini' ? 'selected' : '' ?>>o3-mini (Reasoning)</option>
|
||||
<option value="gpt-4o" <?= $openai_model === 'gpt-4o' ? 'selected' : '' ?>>GPT-4o (High Quality)</option>
|
||||
</select>
|
||||
<div class="form-text text-muted">Choose which model to use with your API Key.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="head_ads" class="form-label fw-semibold text-secondary small"><?= __('head_scripts') ?></label>
|
||||
<textarea name="head_ads" id="head_ads" rows="4" class="form-control" placeholder="Paste your <head> scripts here..."><?= htmlspecialchars($head_ads) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="body_ads" class="form-label fw-semibold text-secondary small"><?= __('body_scripts') ?></label>
|
||||
<textarea name="body_ads" id="body_ads" rows="4" class="form-control" placeholder="Paste your <body> scripts here..."><?= htmlspecialchars($body_ads) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid mt-5">
|
||||
<button type="submit" class="btn btn-save"><?= __('save_settings') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Diagnostics Card -->
|
||||
<div class="card-admin p-4 shadow-lg border-warning border-opacity-25">
|
||||
<h4 class="mb-3 fw-bold text-warning"><i class="fa-solid fa-microchip me-2"></i> System Diagnostics</h4>
|
||||
<p class="text-secondary small">Check if your hosting (cPanel/Other) is compatible with the TikTok Bridge.</p>
|
||||
|
||||
<div id="diagnosticsOutput" class="p-3 bg-black rounded border border-secondary mb-3" style="font-family: monospace; font-size: 0.85rem; max-height: 300px; overflow-y: auto;">
|
||||
Loading diagnostics...
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-warning btn-sm" id="runDiagnosticsBtn">
|
||||
<i class="fa-solid fa-rotate me-1"></i> Run Diagnostics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const diagOutput = document.getElementById('diagnosticsOutput');
|
||||
const runBtn = document.getElementById('runDiagnosticsBtn');
|
||||
|
||||
async function runDiagnostics() {
|
||||
diagOutput.innerHTML = '<span class="text-secondary">Running diagnostics...</span>';
|
||||
try {
|
||||
const resp = await fetch('api/bridge_control.php?action=debug&username=admin'); // username doesn't matter for debug
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success && data.env) {
|
||||
let html = '<table class="table table-dark table-sm table-borderless mb-0">';
|
||||
for (const [key, value] of Object.entries(data.env)) {
|
||||
let valDisplay = value;
|
||||
let colorClass = '';
|
||||
|
||||
if (value === true) { valDisplay = '<span class="text-success">YES</span>'; }
|
||||
else if (value === false) { valDisplay = '<span class="text-danger">NO</span>'; }
|
||||
else if (!value) { valDisplay = '<span class="text-secondary">N/A</span>'; }
|
||||
|
||||
html += `<tr><td class="text-secondary" style="width: 40%">${key}:</td><td>${valDisplay}</td></tr>`;
|
||||
}
|
||||
html += '</table>';
|
||||
|
||||
if (!data.env.shell_exec_enabled) {
|
||||
html += '<div class="mt-3 text-danger small"><strong>Warning:</strong> shell_exec is disabled. The TikTok bridge cannot start. Please enable it in cPanel MultiPHP INI Editor or contact your host.</div>';
|
||||
}
|
||||
if (!data.env.node_path || data.env.node_path === 'node') {
|
||||
html += '<div class="mt-2 text-warning small"><strong>Notice:</strong> Node binary not explicitly found. Ensure "node" is in your system PATH.</div>';
|
||||
}
|
||||
if (!data.env.node_modules_exists) {
|
||||
html += '<div class="mt-2 text-danger small"><strong>Warning:</strong> node_modules folder is missing. Run "npm install" on your server.</div>';
|
||||
}
|
||||
|
||||
diagOutput.innerHTML = html;
|
||||
} else {
|
||||
diagOutput.innerHTML = '<span class="text-danger">Error fetching diagnostics: ' + (data.error || 'Unknown error') + '</span>';
|
||||
}
|
||||
} catch (err) {
|
||||
diagOutput.innerHTML = '<span class="text-danger">Network Error: Could not reach bridge control API.</span>';
|
||||
}
|
||||
}
|
||||
|
||||
runBtn.addEventListener('click', runDiagnostics);
|
||||
runDiagnostics(); // Run on load
|
||||
});
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
|
||||
</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 site_settings or config
|
||||
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||
$payload['model'] = $cfg['default_model'];
|
||||
$payload['model'] = !empty($cfg['openai_model']) ? $cfg['openai_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,26 @@ class LocalAIApi
|
||||
if (!is_array($cfg)) {
|
||||
throw new RuntimeException('Invalid AI config format: expected array');
|
||||
}
|
||||
|
||||
// Try to load custom API key and model from DB if possible
|
||||
$dbConfig = __DIR__ . '/../db/config.php';
|
||||
if (file_exists($dbConfig)) {
|
||||
try {
|
||||
require_once $dbConfig;
|
||||
$pdo = db();
|
||||
|
||||
// Fetch all relevant settings in one go
|
||||
$stmt = $pdo->query("SELECT setting_key, setting_value FROM site_settings WHERE setting_key IN ('openai_api_key', 'openai_model')");
|
||||
while ($row = $stmt->fetch()) {
|
||||
$cfg[$row['setting_key']] = $row['setting_value'] ?: null;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Fail silently, fallback to defaults
|
||||
$cfg['openai_api_key'] = null;
|
||||
$cfg['openai_model'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
self::$configCache = $cfg;
|
||||
}
|
||||
|
||||
@ -490,4 +536,4 @@ class LocalAIApi
|
||||
// Legacy alias for backward compatibility with the previous class name.
|
||||
if (!class_exists('OpenAIService')) {
|
||||
class_alias(LocalAIApi::class, 'OpenAIService');
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ if (
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value, "\"' ");
|
||||
$value = trim($value, "' ");
|
||||
if (getenv($key) === false || getenv($key) === '') {
|
||||
putenv("{$key}={$value}");
|
||||
}
|
||||
@ -46,7 +46,7 @@ return [
|
||||
'project_id' => $projectId,
|
||||
'project_uuid' => $projectUuid,
|
||||
'project_header' => 'project-uuid',
|
||||
'default_model' => 'gpt-5-mini',
|
||||
'default_model' => 'gpt-4o-mini',
|
||||
'timeout' => 30,
|
||||
'verify_tls' => true,
|
||||
];
|
||||
];
|
||||
161
api/bridge_control.php
Normal file
161
api/bridge_control.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// Authentication check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$username = $_GET['username'] ?? '';
|
||||
|
||||
if (empty($username)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Username required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Helper to check if a function is enabled
|
||||
function is_enabled($func) {
|
||||
return function_exists($func) && !in_array($func, array_map('trim', explode(',', ini_get('disable_functions'))));
|
||||
}
|
||||
|
||||
if (!is_enabled('shell_exec')) {
|
||||
echo json_encode(['success' => false, 'error' => 'PHP function shell_exec is disabled on this server. Contact your hosting provider.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Find node path
|
||||
function get_node_path() {
|
||||
$path = trim((string)shell_exec('which node'));
|
||||
if ($path && file_exists($path)) return $path;
|
||||
|
||||
$common_paths = ['/usr/local/bin/node', '/usr/bin/node', '/bin/node', '/opt/node/bin/node'];
|
||||
foreach ($common_paths as $p) {
|
||||
if (file_exists($p)) return $p;
|
||||
}
|
||||
return 'node'; // Fallback to PATH
|
||||
}
|
||||
|
||||
$node = get_node_path();
|
||||
|
||||
// Create logs directory if not exists
|
||||
$logs_dir = __DIR__ . '/../logs';
|
||||
if (!is_dir($logs_dir)) {
|
||||
@mkdir($logs_dir, 0755, true);
|
||||
}
|
||||
|
||||
$pid_file = "$logs_dir/bridge_{$username}_{$user_id}.pid";
|
||||
$log_file = "$logs_dir/bridge_{$username}_{$user_id}.log";
|
||||
|
||||
function is_running($pid_file) {
|
||||
if (!file_exists($pid_file)) return false;
|
||||
$pid = (int)file_get_contents($pid_file);
|
||||
if ($pid <= 0) return false;
|
||||
|
||||
// Check if process exists (posix_getpgid is safer but might be disabled)
|
||||
if (function_exists('posix_getpgid')) {
|
||||
return @posix_getpgid($pid) !== false;
|
||||
}
|
||||
|
||||
// Fallback to pgrep or ps
|
||||
$output = shell_exec("ps -p $pid");
|
||||
return (strpos($output, (string)$pid) !== false);
|
||||
}
|
||||
|
||||
function kill_process($pid_file, $username, $user_id) {
|
||||
if (file_exists($pid_file)) {
|
||||
$pid = (int)file_get_contents($pid_file);
|
||||
if ($pid > 0) {
|
||||
shell_exec("kill -9 $pid > /dev/null 2>&1");
|
||||
}
|
||||
@unlink($pid_file);
|
||||
}
|
||||
// Also try pkill as backup
|
||||
shell_exec("pkill -f \"node .*tiktok_bridge.js $username .* $user_id\"");
|
||||
}
|
||||
|
||||
if ($action === 'start') {
|
||||
// 1. Kill any existing bridge
|
||||
kill_process($pid_file, $username, $user_id);
|
||||
|
||||
// 2. Start new bridge
|
||||
$bridge_script = __DIR__ . '/../tiktok_bridge.js';
|
||||
|
||||
// Check if node_modules exist
|
||||
if (!is_dir(__DIR__ . '/../node_modules')) {
|
||||
echo json_encode(['success' => false, 'error' => 'node_modules folder not found. Please run "npm install" on your server.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cmd = sprintf(
|
||||
"%s %s %s %s %s %s %s %s > %s 2>&1 & echo $!",
|
||||
$node,
|
||||
escapeshellarg($bridge_script),
|
||||
escapeshellarg($username),
|
||||
escapeshellarg(DB_HOST),
|
||||
escapeshellarg(DB_USER),
|
||||
escapeshellarg(DB_PASS),
|
||||
escapeshellarg(DB_NAME),
|
||||
escapeshellarg($user_id),
|
||||
escapeshellarg($log_file)
|
||||
);
|
||||
|
||||
$pid = trim((string)shell_exec($cmd));
|
||||
|
||||
if ($pid > 0) {
|
||||
file_put_contents($pid_file, $pid);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "Bridge started for @$username",
|
||||
'pid' => $pid,
|
||||
'log' => "logs/" . basename($log_file)
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => "Failed to start bridge. Check $log_file for errors.",
|
||||
'node_path' => $node
|
||||
]);
|
||||
}
|
||||
|
||||
} elseif ($action === 'stop') {
|
||||
kill_process($pid_file, $username, $user_id);
|
||||
echo json_encode(['success' => true, 'message' => "Bridge stopped for @$username"]);
|
||||
|
||||
} elseif ($action === 'status') {
|
||||
$running = is_running($pid_file);
|
||||
|
||||
// Check log for errors if not running but was supposed to
|
||||
$last_log = "";
|
||||
if (!$running && file_exists($log_file)) {
|
||||
$last_log = trim((string)shell_exec("tail -n 5 " . escapeshellarg($log_file)));
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'running' => $running,
|
||||
'last_log' => $last_log
|
||||
]);
|
||||
|
||||
} elseif ($action === 'debug') {
|
||||
// Special action to check environment
|
||||
$env = [
|
||||
'php_version' => PHP_VERSION,
|
||||
'shell_exec_enabled' => is_enabled('shell_exec'),
|
||||
'node_path' => $node,
|
||||
'node_version' => trim((string)shell_exec("$node -v")),
|
||||
'npm_version' => trim((string)shell_exec("npm -v")),
|
||||
'node_modules_exists' => is_dir(__DIR__ . '/../node_modules'),
|
||||
'db_host' => DB_HOST,
|
||||
'db_user' => DB_USER,
|
||||
'os' => PHP_OS,
|
||||
];
|
||||
echo json_encode(['success' => true, 'env' => $env]);
|
||||
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid action']);
|
||||
}
|
||||
@ -32,7 +32,7 @@ try {
|
||||
|
||||
// 3. Call AI API
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
// Model is handled by LocalAIApi defaults/site_settings if omitted
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $message],
|
||||
@ -61,4 +61,4 @@ try {
|
||||
} catch (Exception $e) {
|
||||
error_log("Chat Error: " . $e->getMessage());
|
||||
echo json_encode(['reply' => "An internal error occurred."]);
|
||||
}
|
||||
}
|
||||
79
api/get_updates.php
Normal file
79
api/get_updates.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
// Authentication check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$username = $_GET['username'] ?? '';
|
||||
|
||||
if (empty($username)) {
|
||||
echo json_encode(['events' => []]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Fetch pending comments (those without AI replies) for this specific user
|
||||
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND user_id = ? AND ai_reply IS NULL ORDER BY created_at ASC LIMIT 5");
|
||||
$stmt->execute([$username, $user_id]);
|
||||
$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 5 seconds) for this user
|
||||
$since = date('Y-m-d H:i:s', time() - 5);
|
||||
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND user_id = ? AND ai_reply IS NOT NULL AND created_at >= ? ORDER BY created_at DESC LIMIT 10");
|
||||
$stmt->execute([$username, $user_id, $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' => []]);
|
||||
}
|
||||
83
api/process_comment.php
Normal file
83
api/process_comment.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
// Authentication check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$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'];
|
||||
|
||||
// Fetch user personality
|
||||
$personality = 'funny';
|
||||
try {
|
||||
$stmt = db()->prepare("SELECT ai_personality FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$personality = $stmt->fetchColumn() ?: 'funny';
|
||||
} catch (Exception $e) {
|
||||
// Fallback to funny
|
||||
}
|
||||
|
||||
$personality_instructions = "";
|
||||
switch ($personality) {
|
||||
case 'serious':
|
||||
$personality_instructions = "Respond in a serious, professional, and formal tone.";
|
||||
break;
|
||||
case 'expert':
|
||||
$personality_instructions = "Respond as an expert, providing highly informative, accurate, and insightful information.";
|
||||
break;
|
||||
case 'funny':
|
||||
default:
|
||||
$personality_instructions = "Respond in a funny, engaging, and lighthearted way, using jokes or playful language.";
|
||||
break;
|
||||
}
|
||||
|
||||
// Prompt for AI
|
||||
$prompt = "You are a helpful AI assistant for a TikTok live stream. $personality_instructions
|
||||
The streamer is '$username'.
|
||||
The user '$comment_author' just said: '$comment_text'.
|
||||
Respond to them in a short way (max 20 words).
|
||||
Keep it suitable for a live stream audience.";
|
||||
|
||||
$resp = LocalAIApi::createResponse([
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => "TikTok Live AI Assistant - Personality: $personality"],
|
||||
['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, user_id) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$username, $comment_author, $comment_text, $ai_reply, $user_id]);
|
||||
} catch (Exception $e) {
|
||||
// Log error but continue
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'reply' => $ai_reply,
|
||||
'author' => $comment_author,
|
||||
'comment' => $comment_text
|
||||
]);
|
||||
@ -19,7 +19,7 @@ if (empty($text)) {
|
||||
}
|
||||
|
||||
// Get Telegram Token from DB
|
||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
||||
$stmt = db()->query("SELECT setting_value FROM site_settings WHERE setting_key = 'telegram_token'");
|
||||
$token = $stmt->fetchColumn();
|
||||
|
||||
if (!$token) {
|
||||
@ -64,7 +64,7 @@ try {
|
||||
|
||||
// 2. Call AI
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
// Model is handled by LocalAIApi defaults/site_settings if omitted
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $text],
|
||||
@ -88,4 +88,4 @@ try {
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Telegram Webhook Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
25
api/update_personality.php
Normal file
25
api/update_personality.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$personality = $data['personality'] ?? 'funny';
|
||||
|
||||
$allowed_personalities = ['funny', 'serious', 'expert'];
|
||||
if (!in_array($personality, $allowed_personalities)) {
|
||||
$personality = 'funny';
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE users SET ai_personality = ? WHERE id = ?");
|
||||
$stmt->execute([$personality, $_SESSION['user_id']]);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@ -1,302 +1,278 @@
|
||||
:root {
|
||||
--tiktok-cyan: #00f2ea;
|
||||
--tiktok-red: #ff0050;
|
||||
--bg-dark: #121212;
|
||||
--bg-surface: #1e1e1e;
|
||||
--border-secondary: rgba(255, 255, 255, 0.1);
|
||||
--text-light: #ffffff;
|
||||
--text-secondary: #ced4da; /* Brighter than adb5bd */
|
||||
--text-muted: #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;
|
||||
h1, h2, h3, h4, h5, h6, .card-title, .navbar-brand {
|
||||
color: var(--text-light) !important;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
.bg-surface {
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
|
||||
.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-cyan {
|
||||
color: var(--tiktok-cyan) !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;
|
||||
.text-tiktok-red {
|
||||
color: var(--tiktok-red) !important;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
.text-secondary {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
.small.text-secondary {
|
||||
color: var(--text-secondary) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
.btn-tiktok-cyan {
|
||||
background-color: var(--tiktok-cyan);
|
||||
color: #000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
.btn-tiktok-cyan:hover {
|
||||
background-color: #00d8d1;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
.btn-outline-tiktok-cyan {
|
||||
border-color: var(--tiktok-cyan);
|
||||
color: var(--tiktok-cyan);
|
||||
}
|
||||
|
||||
.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);
|
||||
.btn-outline-tiktok-cyan:hover {
|
||||
background-color: var(--tiktok-cyan);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: #262626 !important;
|
||||
border-color: #444 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.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::placeholder {
|
||||
color: #888 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
.form-control:focus, .form-select:focus {
|
||||
background-color: #2d2d2d !important;
|
||||
border-color: var(--tiktok-cyan) !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.1);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
.toast {
|
||||
background-color: var(--bg-surface);
|
||||
color: var(--text-light);
|
||||
border: 1px solid var(--border-secondary);
|
||||
}
|
||||
|
||||
.chat-input-area button:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
/* AI Avatar Styles Improved */
|
||||
.ai-avatar-container {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
.bg-animations {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 75px 75px 15px 15px;
|
||||
background: #000;
|
||||
filter: brightness(1.1) contrast(1.1);
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
background: rgba(238, 119, 82, 0.4);
|
||||
.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-2 {
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
background: rgba(35, 166, 213, 0.4);
|
||||
animation-delay: -7s;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
.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-3 {
|
||||
top: 40%;
|
||||
left: 30%;
|
||||
background: rgba(231, 60, 126, 0.3);
|
||||
animation-delay: -14s;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
.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;
|
||||
}
|
||||
|
||||
@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 float-sway {
|
||||
0% { transform: translateY(0px) rotate(-1deg); }
|
||||
50% { transform: translateY(-20px) rotate(1deg); }
|
||||
100% { transform: translateY(0px) rotate(-1deg); }
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
color: var(--tiktok-cyan);
|
||||
}
|
||||
|
||||
/* Table readability */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--text-light) !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 1px;
|
||||
.table thead th {
|
||||
color: var(--text-secondary) !important;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
.text-muted {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.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: 466 KiB |
@ -1,39 +1,276 @@
|
||||
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');
|
||||
if (!connectBtn) return; // Exit if not logged in
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
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 aiPersonality = document.getElementById('aiPersonality');
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
let isConnected = false;
|
||||
let commentCount = 0;
|
||||
let synth = window.speechSynthesis;
|
||||
let voices = [];
|
||||
let pollInterval = null;
|
||||
let lastEventId = 0;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
// Load voices
|
||||
function populateVoiceList() {
|
||||
if (!synth) return;
|
||||
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;
|
||||
});
|
||||
if (voiceSelect) {
|
||||
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);
|
||||
});
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
populateVoiceList();
|
||||
if (speechSynthesis.onvoiceschanged !== undefined) {
|
||||
speechSynthesis.onvoiceschanged = populateVoiceList;
|
||||
}
|
||||
|
||||
if (rateRange) {
|
||||
rateRange.addEventListener('input', () => {
|
||||
if (rateValue) rateValue.textContent = rateRange.value;
|
||||
});
|
||||
}
|
||||
|
||||
if (aiPersonality) {
|
||||
aiPersonality.addEventListener('change', async () => {
|
||||
const personality = aiPersonality.value;
|
||||
try {
|
||||
const resp = await fetch('api/update_personality.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ personality })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
showToast('Settings Saved', 'AI personality updated successfully.', 'success');
|
||||
} else {
|
||||
showToast('Error', data.error || 'Failed to update personality', 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Network Error', 'Could not reach settings API.', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function startBridge(username) {
|
||||
try {
|
||||
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');
|
||||
if (disconnectBtn) disconnectBtn.classList.remove('d-none');
|
||||
if (connectionStatus) connectionStatus.innerHTML = '<span class="status-dot bg-success me-1 pulse"></span> Live connection active: @' + username;
|
||||
if (emptyFeed) 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');
|
||||
if (disconnectBtn) disconnectBtn.classList.add('d-none');
|
||||
if (connectionStatus) 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++;
|
||||
if (commentCountBadge) 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>
|
||||
`;
|
||||
|
||||
if (commentFeed) 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);
|
||||
});
|
||||
|
||||
if (disconnectBtn) {
|
||||
disconnectBtn.addEventListener('click', () => {
|
||||
const username = tiktokUsernameInput.value.trim();
|
||||
stopBridge(username);
|
||||
});
|
||||
}
|
||||
|
||||
if (simulateBtn) {
|
||||
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({ author: 'TestUser', comment: commentText, username })
|
||||
});
|
||||
manualCommentInput.value = '';
|
||||
// Polling will pick it up
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function speak(text) {
|
||||
if (!text || !synth) 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 || 1.0;
|
||||
synth.speak(utterThis);
|
||||
}
|
||||
|
||||
function updateHistoryTable(event) {
|
||||
if (!historyTableBody) return;
|
||||
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') {
|
||||
if (!toastContainer) return;
|
||||
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);
|
||||
if (toastElement) {
|
||||
const toast = new bootstrap.Toast(toastElement);
|
||||
toast.show();
|
||||
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BIN
assets/pasted-20260226-193641-2eb2b83c.jpg
Normal file
BIN
assets/pasted-20260226-193641-2eb2b83c.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 223 KiB |
BIN
assets/pasted-20260226-202032-95296e2f.jpg
Normal file
BIN
assets/pasted-20260226-202032-95296e2f.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
assets/pasted-20260226-202418-4fb1c28a.jpg
Normal file
BIN
assets/pasted-20260226-202418-4fb1c28a.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 289 KiB |
BIN
assets/pasted-20260226-202752-c096aad9.jpg
Normal file
BIN
assets/pasted-20260226-202752-c096aad9.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
2
db/migrations/add_ai_personality.sql
Normal file
2
db/migrations/add_ai_personality.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Add ai_personality column to users table
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS ai_personality VARCHAR(50) DEFAULT 'funny' AFTER role;
|
||||
1
db/migrations/add_openai_model_setting.sql
Normal file
1
db/migrations/add_openai_model_setting.sql
Normal file
@ -0,0 +1 @@
|
||||
INSERT INTO site_settings (setting_key, setting_value) VALUES ('openai_model', 'gpt-4o-mini') ON DUPLICATE KEY UPDATE setting_key=setting_key;
|
||||
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';
|
||||
13
db/migrations/faqs_and_messages.sql
Normal file
13
db/migrations/faqs_and_messages.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS faqs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
keywords TEXT NOT NULL,
|
||||
answer TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_message TEXT NOT NULL,
|
||||
ai_response TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
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;
|
||||
4
db/migrations/site_settings_extended.sql
Normal file
4
db/migrations/site_settings_extended.sql
Normal file
@ -0,0 +1,4 @@
|
||||
INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_name', 'TikTok Live AI Assistant') ON DUPLICATE KEY UPDATE setting_key=setting_key;
|
||||
INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_icon', 'assets/images/logo.png') ON DUPLICATE KEY UPDATE setting_key=setting_key;
|
||||
INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_favicon', 'favicon.ico') ON DUPLICATE KEY UPDATE setting_key=setting_key;
|
||||
INSERT INTO site_settings (setting_key, setting_value) VALUES ('default_language', 'en') 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
|
||||
);
|
||||
27
includes/pexels.php
Normal file
27
includes/pexels.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
function pexels_key() {
|
||||
$k = getenv('PEXELS_KEY');
|
||||
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
|
||||
}
|
||||
|
||||
function pexels_get($url) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
function download_to($srcUrl, $destPath) {
|
||||
$data = file_get_contents($srcUrl);
|
||||
if ($data === false) return false;
|
||||
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
|
||||
return file_put_contents($destPath, $data) !== false;
|
||||
}
|
||||
391
index.php
391
index.php
@ -1,150 +1,259 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
// index.php - Main Dashboard
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
require_once 'languages/helper.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
// Check if user is logged in
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$stmt = db()->prepare("SELECT username, role, ai_personality FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$is_admin = ($user['role'] ?? 'user') === 'admin';
|
||||
$ai_personality = $user['ai_personality'] ?? 'Expert';
|
||||
|
||||
// Simple SEO meta
|
||||
$page_title = __('welcome') . " - AI Assistant";
|
||||
$meta_description = "AI Dashboard for TikTok automation and more.";
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= $lang ?>">
|
||||
<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 -->
|
||||
<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; ?>
|
||||
<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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $page_title ?></title>
|
||||
<meta name="description" content="<?= $meta_description ?>">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
<style>
|
||||
/* Minor overrides for layout */
|
||||
.ai-avatar {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--tiktok-cyan);
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
box-shadow: 0 0 20px rgba(0, 242, 234, 0.4);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ai-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.status-dot.pulse {
|
||||
animation: pulse-animation 1.5s infinite;
|
||||
}
|
||||
@keyframes pulse-animation {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.2); opacity: 0.5; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</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>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-surface shadow-sm sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="index.php">
|
||||
<span class="text-tiktok-red">AI</span> <span class="text-tiktok-cyan">DASHBOARD</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="index.php"><i class="fa-solid fa-house me-1"></i> Dashboard</a>
|
||||
</li>
|
||||
<?php if ($is_admin): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="admin.php"><i class="fa-solid fa-user-shield me-1"></i> Admin</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<div class="navbar-nav align-items-center">
|
||||
<span class="nav-item me-3 text-secondary small">
|
||||
<i class="fa-solid fa-user me-1"></i> <?= htmlspecialchars($user['username']) ?>
|
||||
</span>
|
||||
<div class="nav-item dropdown me-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="langDropdown" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end bg-surface border-secondary shadow">
|
||||
<li><a class="dropdown-item text-light" href="?lang=id">Indonesia</a></li>
|
||||
<li><a class="dropdown-item text-light" href="?lang=en">English</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="logout.php" class="btn btn-outline-danger btn-sm rounded-pill px-3">
|
||||
<i class="fa-solid fa-right-from-bracket me-1"></i> Logout
|
||||
</a>
|
||||
</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-5">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-5 fw-bold mb-2"><?= __('welcome') ?>, <span class="text-tiktok-red"><?= htmlspecialchars($user['username']) ?></span>!</h1>
|
||||
<p class="lead text-secondary"><?= __('hero_text') ?></p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Controls & AI personality -->
|
||||
<div class="col-lg-4">
|
||||
<!-- AI Avatar Display -->
|
||||
<div class="card bg-surface border-secondary mb-4 shadow-sm py-4">
|
||||
<div class="card-body text-center">
|
||||
<div class="ai-avatar mb-3">
|
||||
<img src="assets/images/ai_avatar.jpg?v=<?php echo time(); ?>" alt="AI Avatar">
|
||||
</div>
|
||||
<h5 class="fw-bold text-tiktok-cyan mb-1"><?= __('welcome') ?></h5>
|
||||
<div id="connectionStatus" class="small text-secondary mb-3">
|
||||
<span class="status-dot bg-secondary me-1"></span> Disconnected
|
||||
</div>
|
||||
<hr class="border-secondary opacity-25">
|
||||
<div class="text-start px-3">
|
||||
<label class="form-label text-secondary small fw-bold mb-1"><?= __('personality') ?></label>
|
||||
<select class="form-select bg-dark text-light border-secondary" id="aiPersonality">
|
||||
<option value="Funny" <?= $ai_personality == 'Funny' ? 'selected' : '' ?>><?= __('personality_funny') ?></option>
|
||||
<option value="Serious" <?= $ai_personality == 'Serious' ? 'selected' : '' ?>><?= __('personality_serious') ?></option>
|
||||
<option value="Expert" <?= $ai_personality == 'Expert' ? 'selected' : '' ?>><?= __('personality_expert') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Card -->
|
||||
<div class="card bg-surface border-secondary mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-4 fw-bold"><i class="fa-brands fa-tiktok me-2"></i> <?= __('config') ?></h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tiktokUsername" class="form-label text-secondary small fw-bold"><?= __('tiktok_username') ?></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary">@</span>
|
||||
<input type="text" class="form-control bg-dark text-light border-secondary" id="tiktokUsername" placeholder="username">
|
||||
</div>
|
||||
<div class="form-text text-secondary mt-1 small">Enter your TikTok username that is currently live.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-tiktok-cyan text-dark fw-bold" id="connectBtn">
|
||||
<i class="fa-solid fa-plug me-2"></i> <?= __('connect') ?>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger fw-bold d-none" id="disconnectBtn">
|
||||
<i class="fa-solid fa-power-off me-2"></i> <?= __('disconnect') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TTS Settings -->
|
||||
<div class="card bg-surface border-secondary shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3 fw-bold"><i class="fa-solid fa-volume-high me-2"></i> <?= __('tts_settings') ?></h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold"><?= __('voice') ?></label>
|
||||
<select class="form-select bg-dark text-light border-secondary" id="voiceSelect"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold d-flex justify-content-between">
|
||||
<span><?= __('speed') ?></span>
|
||||
<span id="rateValue" class="text-tiktok-cyan">1.0</span>
|
||||
</label>
|
||||
<input type="range" class="form-range" id="rateRange" min="0.5" max="2" step="0.1" value="1">
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="autoReplyToggle" checked>
|
||||
<label class="form-check-label text-secondary small fw-bold" for="autoReplyToggle"><?= __('auto_reply') ?></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Live Feed & History -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Comment Feed -->
|
||||
<div class="card bg-dark border-secondary mb-4 shadow-sm" style="min-height: 500px;">
|
||||
<div class="card-header bg-surface border-secondary d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fa-solid fa-stream me-2 text-tiktok-cyan"></i> <?= __('live_feed') ?></h5>
|
||||
<span class="badge bg-tiktok-red" id="commentCount">0 <?= __('comments') ?></span>
|
||||
</div>
|
||||
<div class="card-body p-0 d-flex flex-column">
|
||||
<div id="commentFeed" class="p-3 flex-grow-1" style="height: 400px; overflow-y: auto;">
|
||||
<div id="emptyFeed" class="text-center py-5 opacity-50">
|
||||
<i class="fa-solid fa-satellite-dish fa-3x mb-3 text-secondary"></i>
|
||||
<p><?= __('no_history') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-surface border-top border-secondary mt-auto">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control bg-dark text-light border-secondary" id="manualComment" placeholder="Simulate a comment...">
|
||||
<button class="btn btn-outline-tiktok-cyan fw-bold" id="simulateBtn"><?= __('simulate') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History -->
|
||||
<div class="card bg-surface border-secondary shadow-sm">
|
||||
<div class="card-header bg-transparent border-secondary py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fa-solid fa-history me-2 text-secondary"></i> <?= __('recent_interactions') ?></h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr class="border-secondary text-secondary">
|
||||
<th class="small fw-bold"><?= __('time') ?></th>
|
||||
<th class="small fw-bold"><?= __('user') ?></th>
|
||||
<th class="small fw-bold"><?= __('comment') ?></th>
|
||||
<th class="small fw-bold"><?= __('ai_reply') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="historyTableBody">
|
||||
<tr id="noHistoryRow">
|
||||
<td colspan="4" class="text-center py-4 text-secondary"><?= __('no_history') ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="py-4 mt-5 border-top border-secondary">
|
||||
<div class="container text-center">
|
||||
<p class="text-secondary small mb-0"><?= __('footer_text') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toastContainer" class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1100"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<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=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
44
languages/en.php
Normal file
44
languages/en.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
return [
|
||||
'welcome' => 'Welcome to TikTok Live AI',
|
||||
'hero_text' => 'Enhance your live stream with AI-powered interactions. Connect with your audience like never before.',
|
||||
'login_to_start' => 'Please login or register to connect to TikTok Live.',
|
||||
'config' => 'Configuration',
|
||||
'tiktok_username' => 'TikTok Username',
|
||||
'connect' => 'Connect to Live',
|
||||
'disconnect' => 'Disconnect',
|
||||
'tts_settings' => 'TTS Settings',
|
||||
'voice' => 'Voice',
|
||||
'speed' => 'Speed',
|
||||
'auto_reply' => 'AI Auto-Reply',
|
||||
'live_feed' => 'Live Comment Feed',
|
||||
'comments' => 'comments',
|
||||
'simulate' => 'Simulate',
|
||||
'recent_interactions' => 'Recent AI Interactions',
|
||||
'time' => 'Time',
|
||||
'user' => 'User',
|
||||
'comment' => 'Comment',
|
||||
'ai_reply' => 'AI Reply',
|
||||
'no_history' => 'No history yet.',
|
||||
'hi' => 'Hi',
|
||||
'logout' => 'Logout',
|
||||
'login' => 'Login',
|
||||
'register' => 'Register',
|
||||
'admin' => 'Admin',
|
||||
'footer_text' => 'TikTok Live AI Assistant. Built with LAMP + OpenAI.',
|
||||
'back_to_home' => 'Back to Home',
|
||||
'save_settings' => 'Save All Settings',
|
||||
'site_settings' => 'Site Settings',
|
||||
'openai_key' => 'OpenAI API Key',
|
||||
'openai_model' => 'OpenAI Model',
|
||||
'head_scripts' => 'Head Scripts',
|
||||
'body_scripts' => 'Body Scripts',
|
||||
'site_name' => 'Site Name',
|
||||
'site_icon' => 'Site Icon',
|
||||
'site_favicon' => 'Site Favicon',
|
||||
'language' => 'Language',
|
||||
'personality' => 'AI Personality',
|
||||
'personality_funny' => 'Funny',
|
||||
'personality_serious' => 'Serious',
|
||||
'personality_expert' => 'Expert',
|
||||
];
|
||||
25
languages/helper.php
Normal file
25
languages/helper.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
function get_lang() {
|
||||
if (!isset($_SESSION['lang'])) {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT setting_value FROM site_settings WHERE setting_key = 'default_language'");
|
||||
$default_lang = $stmt->fetchColumn() ?: 'en';
|
||||
$_SESSION['lang'] = $default_lang;
|
||||
}
|
||||
|
||||
if (isset($_GET['lang']) && in_array($_GET['lang'], ['en', 'id'])) {
|
||||
$_SESSION['lang'] = $_GET['lang'];
|
||||
}
|
||||
|
||||
return $_SESSION['lang'];
|
||||
}
|
||||
|
||||
function __($key) {
|
||||
static $translations = null;
|
||||
if ($translations === null) {
|
||||
$lang = get_lang();
|
||||
$translations = require __DIR__ . "/$lang.php";
|
||||
}
|
||||
|
||||
return $translations[$key] ?? $key;
|
||||
}
|
||||
44
languages/id.php
Normal file
44
languages/id.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
return [
|
||||
'welcome' => 'Selamat datang di TikTok Live AI',
|
||||
'hero_text' => 'Tingkatkan siaran langsung Anda dengan interaksi berbasis AI. Terhubung dengan audiens Anda tidak seperti sebelumnya.',
|
||||
'login_to_start' => 'Silakan login atau daftar untuk terhubung ke TikTok Live.',
|
||||
'config' => 'Konfigurasi',
|
||||
'tiktok_username' => 'Username TikTok',
|
||||
'connect' => 'Hubungkan ke Live',
|
||||
'disconnect' => 'Putuskan Koneksi',
|
||||
'tts_settings' => 'Pengaturan TTS',
|
||||
'voice' => 'Suara',
|
||||
'speed' => 'Kecepatan',
|
||||
'auto_reply' => 'Balasan Otomatis AI',
|
||||
'live_feed' => 'Umpan Komentar Langsung',
|
||||
'comments' => 'komentar',
|
||||
'simulate' => 'Simulasi',
|
||||
'recent_interactions' => 'Interaksi AI Terbaru',
|
||||
'time' => 'Waktu',
|
||||
'user' => 'Pengguna',
|
||||
'comment' => 'Komentar',
|
||||
'ai_reply' => 'Balasan AI',
|
||||
'no_history' => 'Belum ada riwayat.',
|
||||
'hi' => 'Halo',
|
||||
'logout' => 'Keluar',
|
||||
'login' => 'Masuk',
|
||||
'register' => 'Daftar',
|
||||
'admin' => 'Admin',
|
||||
'footer_text' => 'Asisten AI TikTok Live. Dibuat dengan LAMP + OpenAI.',
|
||||
'back_to_home' => 'Kembali ke Beranda',
|
||||
'save_settings' => 'Simpan Semua Pengaturan',
|
||||
'site_settings' => 'Pengaturan Situs',
|
||||
'openai_key' => 'Kunci API OpenAI',
|
||||
'openai_model' => 'Model OpenAI',
|
||||
'head_scripts' => 'Skrip Head',
|
||||
'body_scripts' => 'Skrip Body',
|
||||
'site_name' => 'Nama Situs',
|
||||
'site_icon' => 'Ikon Situs',
|
||||
'site_favicon' => 'Favicon Situs',
|
||||
'language' => 'Bahasa',
|
||||
'personality' => 'Kepribadian AI',
|
||||
'personality_funny' => 'Lucu',
|
||||
'personality_serious' => 'Serius',
|
||||
'personality_expert' => 'Pakar',
|
||||
];
|
||||
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>
|
||||
111
tiktok_bridge.js
Normal file
111
tiktok_bridge.js
Normal file
@ -0,0 +1,111 @@
|
||||
const { WebcastPushConnection } = require('tiktok-live-connector');
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
const [,, username, dbHost, dbUser, dbPass, dbName, userId] = process.argv;
|
||||
|
||||
if (!username) {
|
||||
console.error('Usage: node tiktok_bridge.js <username> <dbHost> <dbUser> <dbPass> <dbName> <userId>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
console.log(`[${new Date().toISOString()}] TikTok Bridge starting for @${username} (User ID: ${userId})`);
|
||||
|
||||
let connection;
|
||||
try {
|
||||
connection = await mysql.createConnection({
|
||||
host: dbHost || '127.0.0.1',
|
||||
user: dbUser || 'root',
|
||||
password: dbPass || '',
|
||||
database: dbName || 'app_db'
|
||||
});
|
||||
console.log(`[${new Date().toISOString()}] Connected to database: ${dbName} at ${dbHost}`);
|
||||
} catch (err) {
|
||||
console.error(`[${new Date().toISOString()}] DB Connection failed:`, err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create a new connection instance
|
||||
let tiktokConnection = new WebcastPushConnection(username, {
|
||||
processInitialData: false,
|
||||
enableExtendedGiftInfo: true,
|
||||
requestPollingIntervalMs: 2000
|
||||
});
|
||||
|
||||
tiktokConnection.connect().then(state => {
|
||||
console.log(`[${new Date().toISOString()}] Connected to TikTok Live @${username} (Room ID: ${state.roomId})`);
|
||||
}).catch(err => {
|
||||
console.error(`[${new Date().toISOString()}] TikTok Connection failed:`, err.message);
|
||||
console.error('Check if the username is correct and the user is currently LIVE.');
|
||||
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, user_id, created_at) VALUES (?, ?, ?, ?, NOW())',
|
||||
[username, data.uniqueId, data.comment, userId || null]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error inserting chat:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
tiktokConnection.on('gift', async (data) => {
|
||||
if (data.giftType === 1 && !data.repeatEnd) {
|
||||
// Wait for repeatEnd for streaks
|
||||
return;
|
||||
}
|
||||
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, user_id, created_at) VALUES (?, ?, ?, ?, NOW())',
|
||||
[username, data.uniqueId, giftMsg, userId || null]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error inserting gift:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
tiktokConnection.on('member', (data) => {
|
||||
console.log(`[Join] ${data.uniqueId} joined the stream`);
|
||||
});
|
||||
|
||||
tiktokConnection.on('disconnected', () => {
|
||||
console.log(`[${new Date().toISOString()}] TikTok connection disconnected`);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
tiktokConnection.on('streamEnd', () => {
|
||||
console.log(`[${new Date().toISOString()}] Stream ended by host`);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
tiktokConnection.on('error', (err) => {
|
||||
console.error(`[${new Date().toISOString()}] TikTok error:`, err.message);
|
||||
});
|
||||
|
||||
// Keep DB connection alive
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await connection.query('SELECT 1');
|
||||
} catch (err) {
|
||||
console.log(`[${new Date().toISOString()}] DB connection lost, reconnecting...`);
|
||||
try {
|
||||
connection = await mysql.createConnection({
|
||||
host: dbHost, user: dbUser, password: dbPass, database: dbName
|
||||
});
|
||||
} catch (reconnectErr) {
|
||||
console.error('Reconnection failed:', reconnectErr.message);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Handle termination signals
|
||||
process.on('SIGINT', () => process.exit(0));
|
||||
process.on('SIGTERM', () => process.exit(0));
|
||||
|
||||
start();
|
||||
Loading…
x
Reference in New Issue
Block a user