-
Groups is fully removed: No homepage navigation, Account feature, tier setting, API, or client code. Legacy group tables, entitlements, messages, voice files, and config are purged automatically. New cache-busted client: cyberchat-app.js?v=20260609.3. Browser and SQLite migration tests passed with no console errors.
This commit is contained in:
@@ -11,6 +11,15 @@
|
|||||||
</IfModule>
|
</IfModule>
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Always revalidate HTML so new asset versions take effect immediately.
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
<FilesMatch "\.(html?)$">
|
||||||
|
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
|
Header set Pragma "no-cache"
|
||||||
|
Header set Expires "0"
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
# Deny direct access to storage and internal PHP libraries
|
# Deny direct access to storage and internal PHP libraries
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
|
|||||||
@@ -138,12 +138,12 @@ Set a PDO MySQL DSN in the dashboard, enable the mirror, and optionally enable a
|
|||||||
## Embedding
|
## Embedding
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="/chat/assets/css/cyberchat.css">
|
<link rel="stylesheet" href="/chat/assets/css/cyberchat.css?v=20260609.3">
|
||||||
<div id="my-chat" style="width:100%;height:600px"></div>
|
<div id="my-chat" style="width:100%;height:600px"></div>
|
||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '/chat';
|
window.CYBERCHAT_BASE = '/chat';
|
||||||
</script>
|
</script>
|
||||||
<script src="/chat/assets/js/cyberchat-v4.js"></script>
|
<script src="/chat/assets/js/cyberchat-app.js?v=20260609.3"></script>
|
||||||
<script>
|
<script>
|
||||||
CyberChat.init('#my-chat');
|
CyberChat.init('#my-chat');
|
||||||
</script>
|
</script>
|
||||||
@@ -163,7 +163,7 @@ api/auth.php Registration, login, 2FA, sessions
|
|||||||
api/billing.php Stripe checkout, portal, and cancellation
|
api/billing.php Stripe checkout, portal, and cancellation
|
||||||
api/messages.php Text, voice, polling, and recording presence
|
api/messages.php Text, voice, polling, and recording presence
|
||||||
api/stripe-webhook.php Stripe event synchronization
|
api/stripe-webhook.php Stripe event synchronization
|
||||||
assets/js/cyberchat-v4.js Browser application
|
assets/js/cyberchat-app.js Browser application
|
||||||
lib/mailer.php Reusable Mailgun sender
|
lib/mailer.php Reusable Mailgun sender
|
||||||
lib/stripe.php Stripe REST client and subscription sync
|
lib/stripe.php Stripe REST client and subscription sync
|
||||||
lib/mysql_mirror.php Optional SQLite-to-MySQL mirror
|
lib/mysql_mirror.php Optional SQLite-to-MySQL mirror
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ session_start();
|
|||||||
|
|
||||||
function cfg(): array {
|
function cfg(): array {
|
||||||
static $c = null;
|
static $c = null;
|
||||||
if ($c === null) $c = json_decode(file_get_contents(CONFIG_FILE), true) ?? [];
|
if ($c === null) $c = getConfig();
|
||||||
return $c;
|
return $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +52,6 @@ function ensureAdminMessageColumns(PDO $d): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function adminPublicMessagePredicate(PDO $d, string $alias = ''): string {
|
|
||||||
$prefix = $alias !== '' ? rtrim($alias, '.') . '.' : '';
|
|
||||||
return isset(tableColumns($d, 'messages')['group_id']) ? "{$prefix}group_id IS NULL" : '1=1';
|
|
||||||
}
|
|
||||||
|
|
||||||
function archiveDB(string $year, string $month, string $day): ?PDO {
|
function archiveDB(string $year, string $month, string $day): ?PDO {
|
||||||
$tpl = cfgVal('database.archive_path', 'archive/{year}/{month}/{day}.sqlite');
|
$tpl = cfgVal('database.archive_path', 'archive/{year}/{month}/{day}.sqlite');
|
||||||
$path = storagePath(str_replace(['{year}','{month}','{day}'], [$year,$month,$day], $tpl));
|
$path = storagePath(str_replace(['{year}','{month}','{day}'], [$year,$month,$day], $tpl));
|
||||||
@@ -65,6 +60,8 @@ function archiveDB(string $year, string $month, string $day): ?PDO {
|
|||||||
$a->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$a->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
$a->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
$a->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
ensureAdminMessageColumns($a);
|
ensureAdminMessageColumns($a);
|
||||||
|
removeLegacyGroupSchema($a, false);
|
||||||
|
$a->exec("CREATE INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
|
||||||
return $a;
|
return $a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +88,12 @@ function createArchiveDB(string $year, string $month, string $day): PDO {
|
|||||||
day_key TEXT NOT NULL
|
day_key TEXT NOT NULL
|
||||||
)");
|
)");
|
||||||
ensureAdminMessageColumns($a);
|
ensureAdminMessageColumns($a);
|
||||||
|
removeLegacyGroupSchema($a, false);
|
||||||
$a->exec("CREATE TABLE IF NOT EXISTS archive_meta (
|
$a->exec("CREATE TABLE IF NOT EXISTS archive_meta (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT
|
value TEXT
|
||||||
)");
|
)");
|
||||||
|
$a->exec("CREATE INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
|
||||||
return $a;
|
return $a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +154,7 @@ function refreshArchiveMeta(PDO $archive): void {
|
|||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT
|
value TEXT
|
||||||
)");
|
)");
|
||||||
$count = (int)$archive->query(
|
$count = (int)$archive->query("SELECT COUNT(*) FROM messages")->fetchColumn();
|
||||||
"SELECT COUNT(*) FROM messages WHERE " . adminPublicMessagePredicate($archive)
|
|
||||||
)->fetchColumn();
|
|
||||||
$meta = $archive->prepare("INSERT OR REPLACE INTO archive_meta(key,value) VALUES(?,?)");
|
$meta = $archive->prepare("INSERT OR REPLACE INTO archive_meta(key,value) VALUES(?,?)");
|
||||||
$meta->execute(['archived_at', (string)time()]);
|
$meta->execute(['archived_at', (string)time()]);
|
||||||
$meta->execute(['message_count', (string)$count]);
|
$meta->execute(['message_count', (string)$count]);
|
||||||
@@ -199,8 +196,7 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($act === 'archive_voice_clip') {
|
if ($act === 'archive_voice_clip') {
|
||||||
$id = (int)($_POST['msg_id'] ?? 0);
|
$id = (int)($_POST['msg_id'] ?? 0);
|
||||||
$live = db();
|
$live = db();
|
||||||
$stmt = $live->prepare("SELECT * FROM messages WHERE id = ? AND message_type = 'voice' AND "
|
$stmt = $live->prepare("SELECT * FROM messages WHERE id = ? AND message_type = 'voice'");
|
||||||
. adminPublicMessagePredicate($live));
|
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch();
|
||||||
if (!$row) {
|
if (!$row) {
|
||||||
@@ -241,8 +237,7 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($adb->inTransaction()) $adb->rollBack();
|
if ($adb->inTransaction()) $adb->rollBack();
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
$delete = $live->prepare("DELETE FROM messages WHERE id = ? AND message_type = 'voice' AND "
|
$delete = $live->prepare("DELETE FROM messages WHERE id = ? AND message_type = 'voice'");
|
||||||
. adminPublicMessagePredicate($live));
|
|
||||||
$delete->execute([$id]);
|
$delete->execute([$id]);
|
||||||
if ($delete->rowCount() !== 1) {
|
if ($delete->rowCount() !== 1) {
|
||||||
throw new RuntimeException('The live clip changed before it could be removed.');
|
throw new RuntimeException('The live clip changed before it could be removed.');
|
||||||
@@ -259,11 +254,10 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$src = $_POST['msg_src'] ?? 'live'; // 'live' or 'archive:Y-m-d'
|
$src = $_POST['msg_src'] ?? 'live'; // 'live' or 'archive:Y-m-d'
|
||||||
if ($src === 'live') {
|
if ($src === 'live') {
|
||||||
$live = db();
|
$live = db();
|
||||||
$public = adminPublicMessagePredicate($live);
|
$row = $live->prepare("SELECT voice_file FROM messages WHERE id = ?");
|
||||||
$row = $live->prepare("SELECT voice_file FROM messages WHERE id = ? AND $public");
|
|
||||||
$row->execute([$id]);
|
$row->execute([$id]);
|
||||||
deleteRecordingsForRows($row->fetchAll());
|
deleteRecordingsForRows($row->fetchAll());
|
||||||
$live->prepare("DELETE FROM messages WHERE id = ? AND $public")->execute([$id]);
|
$live->prepare("DELETE FROM messages WHERE id = ?")->execute([$id]);
|
||||||
flash("Message #$id deleted.");
|
flash("Message #$id deleted.");
|
||||||
} else {
|
} else {
|
||||||
[$_, $day] = explode(':', $src, 2);
|
[$_, $day] = explode(':', $src, 2);
|
||||||
@@ -295,8 +289,8 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
if ($src === 'live') {
|
if ($src === 'live') {
|
||||||
$live = db();
|
$live = db();
|
||||||
$live->prepare("UPDATE messages SET message = ?, username = ? WHERE id = ? AND "
|
$live->prepare("UPDATE messages SET message = ?, username = ? WHERE id = ?")
|
||||||
. adminPublicMessagePredicate($live))->execute([$txt, $username, $id]);
|
->execute([$txt, $username, $id]);
|
||||||
flash("Message #$id updated.");
|
flash("Message #$id updated.");
|
||||||
} else {
|
} else {
|
||||||
[$_, $day] = explode(':', $src, 2);
|
[$_, $day] = explode(':', $src, 2);
|
||||||
@@ -314,12 +308,11 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$ids = array_map('intval', $_POST['msg_ids'] ?? []);
|
$ids = array_map('intval', $_POST['msg_ids'] ?? []);
|
||||||
if ($ids) {
|
if ($ids) {
|
||||||
$live = db();
|
$live = db();
|
||||||
$public = adminPublicMessagePredicate($live);
|
|
||||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||||
$rows = $live->prepare("SELECT voice_file FROM messages WHERE id IN ($ph) AND $public");
|
$rows = $live->prepare("SELECT voice_file FROM messages WHERE id IN ($ph)");
|
||||||
$rows->execute($ids);
|
$rows->execute($ids);
|
||||||
deleteRecordingsForRows($rows->fetchAll());
|
deleteRecordingsForRows($rows->fetchAll());
|
||||||
$live->prepare("DELETE FROM messages WHERE id IN ($ph) AND $public")->execute($ids);
|
$live->prepare("DELETE FROM messages WHERE id IN ($ph)")->execute($ids);
|
||||||
flash(count($ids) . ' message(s) deleted.');
|
flash(count($ids) . ' message(s) deleted.');
|
||||||
}
|
}
|
||||||
redirect($_POST['return_qs'] ?? '');
|
redirect($_POST['return_qs'] ?? '');
|
||||||
@@ -328,11 +321,10 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($act === 'delete_day_live') {
|
if ($act === 'delete_day_live') {
|
||||||
$day = $_POST['day_key'];
|
$day = $_POST['day_key'];
|
||||||
$live = db();
|
$live = db();
|
||||||
$public = adminPublicMessagePredicate($live);
|
$rows = $live->prepare("SELECT voice_file FROM messages WHERE day_key = ?");
|
||||||
$rows = $live->prepare("SELECT voice_file FROM messages WHERE day_key = ? AND $public");
|
|
||||||
$rows->execute([$day]);
|
$rows->execute([$day]);
|
||||||
deleteRecordingsForRows($rows->fetchAll());
|
deleteRecordingsForRows($rows->fetchAll());
|
||||||
$st = $live->prepare("DELETE FROM messages WHERE day_key = ? AND $public");
|
$st = $live->prepare("DELETE FROM messages WHERE day_key = ?");
|
||||||
$st->execute([$day]);
|
$st->execute([$day]);
|
||||||
flash("All messages for $day deleted (" . $st->rowCount() . " rows).");
|
flash("All messages for $day deleted (" . $st->rowCount() . " rows).");
|
||||||
redirect('tab=messages');
|
redirect('tab=messages');
|
||||||
@@ -356,9 +348,8 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
if ($act === 'clear_all_live') {
|
if ($act === 'clear_all_live') {
|
||||||
$live = db();
|
$live = db();
|
||||||
$public = adminPublicMessagePredicate($live);
|
deleteRecordingsForRows($live->query("SELECT voice_file FROM messages")->fetchAll());
|
||||||
deleteRecordingsForRows($live->query("SELECT voice_file FROM messages WHERE $public")->fetchAll());
|
$live->exec("DELETE FROM messages");
|
||||||
$live->exec("DELETE FROM messages WHERE $public");
|
|
||||||
flash("All live messages cleared.");
|
flash("All live messages cleared.");
|
||||||
redirect('tab=messages');
|
redirect('tab=messages');
|
||||||
}
|
}
|
||||||
@@ -474,12 +465,11 @@ $stats = [];
|
|||||||
if ($isLoggedIn) {
|
if ($isLoggedIn) {
|
||||||
try {
|
try {
|
||||||
$live = db();
|
$live = db();
|
||||||
$public = adminPublicMessagePredicate($live);
|
|
||||||
$stats['users'] = $live->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
$stats['users'] = $live->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
||||||
$stats['messages'] = $live->query("SELECT COUNT(*) FROM messages WHERE $public")->fetchColumn();
|
$stats['messages'] = $live->query("SELECT COUNT(*) FROM messages")->fetchColumn();
|
||||||
$stats['voice'] = $live->query("SELECT COUNT(*) FROM messages WHERE message_type = 'voice' AND $public")->fetchColumn();
|
$stats['voice'] = $live->query("SELECT COUNT(*) FROM messages WHERE message_type = 'voice'")->fetchColumn();
|
||||||
$stats['sessions'] = $live->query("SELECT COUNT(*) FROM sessions")->fetchColumn();
|
$stats['sessions'] = $live->query("SELECT COUNT(*) FROM sessions")->fetchColumn();
|
||||||
$stats['today'] = $live->query("SELECT COUNT(*) FROM messages WHERE day_key = '" . date('Y-m-d') . "' AND $public")->fetchColumn();
|
$stats['today'] = $live->query("SELECT COUNT(*) FROM messages WHERE day_key = '" . date('Y-m-d') . "'")->fetchColumn();
|
||||||
$timeout = (int)cfgVal('security.session_timeout_hours', 24) * 3600;
|
$timeout = (int)cfgVal('security.session_timeout_hours', 24) * 3600;
|
||||||
$stats['active'] = db()->query("SELECT COUNT(*) FROM sessions WHERE last_active > " . (time() - $timeout))->fetchColumn();
|
$stats['active'] = db()->query("SELECT COUNT(*) FROM sessions WHERE last_active > " . (time() - $timeout))->fetchColumn();
|
||||||
} catch (Exception $e) { $stats = ['users'=>'?','messages'=>'?','voice'=>'?','sessions'=>'?','today'=>'?','active'=>'?']; }
|
} catch (Exception $e) { $stats = ['users'=>'?','messages'=>'?','voice'=>'?','sessions'=>'?','today'=>'?','active'=>'?']; }
|
||||||
@@ -496,8 +486,8 @@ if ($isLoggedIn) {
|
|||||||
$sz = filesize($f);
|
$sz = filesize($f);
|
||||||
$cnt = 0;
|
$cnt = 0;
|
||||||
try {
|
try {
|
||||||
$x = new PDO('sqlite:'.$f);
|
$x = archiveDB($m[1], $m[2], $m[3]);
|
||||||
$cnt = $x->query("SELECT COUNT(*) FROM messages WHERE " . adminPublicMessagePredicate($x))->fetchColumn();
|
if ($x) $cnt = $x->query("SELECT COUNT(*) FROM messages")->fetchColumn();
|
||||||
} catch(Exception $e) {}
|
} catch(Exception $e) {}
|
||||||
$archives[] = ['day'=>$day,'path'=>$f,'size'=>$sz,'count'=>$cnt,'year'=>$m[1],'month'=>$m[2],'daynum'=>$m[3]];
|
$archives[] = ['day'=>$day,'path'=>$f,'size'=>$sz,'count'=>$cnt,'year'=>$m[1],'month'=>$m[2],'daynum'=>$m[3]];
|
||||||
}
|
}
|
||||||
@@ -969,7 +959,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
<?php
|
<?php
|
||||||
$live = db();
|
$live = db();
|
||||||
$recent = $live->query("SELECT m.*,u.username as uname FROM messages m LEFT JOIN users u ON u.id=m.user_id
|
$recent = $live->query("SELECT m.*,u.username as uname FROM messages m LEFT JOIN users u ON u.id=m.user_id
|
||||||
WHERE " . adminPublicMessagePredicate($live, 'm') . " ORDER BY m.created_at DESC LIMIT 8")->fetchAll();
|
ORDER BY m.created_at DESC LIMIT 8")->fetchAll();
|
||||||
if (empty($recent)): ?>
|
if (empty($recent)): ?>
|
||||||
<div class="empty">NO MESSAGES YET</div>
|
<div class="empty">NO MESSAGES YET</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -1048,7 +1038,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
$fType = trim($_GET['type'] ?? '');
|
$fType = trim($_GET['type'] ?? '');
|
||||||
|
|
||||||
$live = db();
|
$live = db();
|
||||||
$where = [adminPublicMessagePredicate($live)]; $params = [];
|
$where = []; $params = [];
|
||||||
if ($fUser) { $where[] = "username LIKE ?"; $params[] = '%'.$fUser.'%'; }
|
if ($fUser) { $where[] = "username LIKE ?"; $params[] = '%'.$fUser.'%'; }
|
||||||
if ($fText) { $where[] = "message LIKE ?"; $params[] = '%'.$fText.'%'; }
|
if ($fText) { $where[] = "message LIKE ?"; $params[] = '%'.$fText.'%'; }
|
||||||
if ($fDay) { $where[] = "day_key = ?"; $params[] = $fDay; }
|
if ($fDay) { $where[] = "day_key = ?"; $params[] = $fDay; }
|
||||||
@@ -1057,8 +1047,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
$msgs = $live->prepare($sql); $msgs->execute($params); $msgs = $msgs->fetchAll();
|
$msgs = $live->prepare($sql); $msgs->execute($params); $msgs = $msgs->fetchAll();
|
||||||
|
|
||||||
// Days available
|
// Days available
|
||||||
$days = $live->query("SELECT DISTINCT day_key FROM messages WHERE "
|
$days = $live->query("SELECT DISTINCT day_key FROM messages ORDER BY day_key DESC")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
. adminPublicMessagePredicate($live) . " ORDER BY day_key DESC")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
@@ -1203,7 +1192,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
$voiceRows = [];
|
$voiceRows = [];
|
||||||
if ($vSource !== 'archive') {
|
if ($vSource !== 'archive') {
|
||||||
$live = db();
|
$live = db();
|
||||||
$vWhere = ["message_type = 'voice'", adminPublicMessagePredicate($live)];
|
$vWhere = ["message_type = 'voice'"];
|
||||||
$vParams = [];
|
$vParams = [];
|
||||||
if ($vUser !== '') { $vWhere[] = 'username LIKE ?'; $vParams[] = '%' . $vUser . '%'; }
|
if ($vUser !== '') { $vWhere[] = 'username LIKE ?'; $vParams[] = '%' . $vUser . '%'; }
|
||||||
if ($vDay !== '') { $vWhere[] = 'day_key = ?'; $vParams[] = $vDay; }
|
if ($vDay !== '') { $vWhere[] = 'day_key = ?'; $vParams[] = $vDay; }
|
||||||
@@ -1221,8 +1210,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
try {
|
try {
|
||||||
$vAdb = archiveDB($arc['year'], $arc['month'], $arc['daynum']);
|
$vAdb = archiveDB($arc['year'], $arc['month'], $arc['daynum']);
|
||||||
if (!$vAdb) continue;
|
if (!$vAdb) continue;
|
||||||
$vSql = "SELECT * FROM messages WHERE message_type = 'voice' AND "
|
$vSql = "SELECT * FROM messages WHERE message_type = 'voice'";
|
||||||
. adminPublicMessagePredicate($vAdb);
|
|
||||||
$vParams = [];
|
$vParams = [];
|
||||||
if ($vUser !== '') { $vSql .= ' AND username LIKE ?'; $vParams[] = '%' . $vUser . '%'; }
|
if ($vUser !== '') { $vSql .= ' AND username LIKE ?'; $vParams[] = '%' . $vUser . '%'; }
|
||||||
$vSql .= ' ORDER BY created_at DESC LIMIT 500';
|
$vSql .= ' ORDER BY created_at DESC LIMIT 500';
|
||||||
@@ -1242,8 +1230,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
$voiceRows = array_slice($voiceRows, 0, 500);
|
$voiceRows = array_slice($voiceRows, 0, 500);
|
||||||
$live = db();
|
$live = db();
|
||||||
$voiceDays = array_unique(array_merge(
|
$voiceDays = array_unique(array_merge(
|
||||||
$live->query("SELECT DISTINCT day_key FROM messages WHERE message_type = 'voice' AND "
|
$live->query("SELECT DISTINCT day_key FROM messages WHERE message_type = 'voice' ORDER BY day_key DESC")->fetchAll(PDO::FETCH_COLUMN),
|
||||||
. adminPublicMessagePredicate($live) . " ORDER BY day_key DESC")->fetchAll(PDO::FETCH_COLUMN),
|
|
||||||
array_column($archives, 'day')
|
array_column($archives, 'day')
|
||||||
));
|
));
|
||||||
rsort($voiceDays);
|
rsort($voiceDays);
|
||||||
@@ -1356,7 +1343,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
if ($adb) {
|
if ($adb) {
|
||||||
$fAUser = trim($_GET['au'] ?? '');
|
$fAUser = trim($_GET['au'] ?? '');
|
||||||
$fAText = trim($_GET['at'] ?? '');
|
$fAText = trim($_GET['at'] ?? '');
|
||||||
$awhere = [adminPublicMessagePredicate($adb)]; $aparams = [];
|
$awhere = []; $aparams = [];
|
||||||
if ($fAUser) { $awhere[] = "username LIKE ?"; $aparams[] = '%'.$fAUser.'%'; }
|
if ($fAUser) { $awhere[] = "username LIKE ?"; $aparams[] = '%'.$fAUser.'%'; }
|
||||||
if ($fAText) { $awhere[] = "message LIKE ?"; $aparams[] = '%'.$fAText.'%'; }
|
if ($fAText) { $awhere[] = "message LIKE ?"; $aparams[] = '%'.$fAText.'%'; }
|
||||||
$asql = "SELECT * FROM messages" . ($awhere ? " WHERE ".implode(" AND ", $awhere) : "") . " ORDER BY created_at DESC LIMIT 500";
|
$asql = "SELECT * FROM messages" . ($awhere ? " WHERE ".implode(" AND ", $awhere) : "") . " ORDER BY created_at DESC LIMIT 500";
|
||||||
@@ -1539,8 +1526,7 @@ td.actions{white-space:nowrap;width:1px}
|
|||||||
$live = db();
|
$live = db();
|
||||||
$allUsers = $live->query("
|
$allUsers = $live->query("
|
||||||
SELECT u.*,p.name AS billing_plan_name,mp.name AS manual_plan_name,
|
SELECT u.*,p.name AS billing_plan_name,mp.name AS manual_plan_name,
|
||||||
(SELECT COUNT(*) FROM messages m WHERE m.user_id = u.id AND "
|
(SELECT COUNT(*) FROM messages m WHERE m.user_id = u.id) as msg_count,
|
||||||
. adminPublicMessagePredicate($live, 'm') . ") as msg_count,
|
|
||||||
(SELECT COUNT(*) FROM sessions s WHERE s.user_id = u.id) as sess_count
|
(SELECT COUNT(*) FROM sessions s WHERE s.user_id = u.id) as sess_count
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN plans p ON p.id=u.plan_id
|
LEFT JOIN plans p ON p.id=u.plan_id
|
||||||
|
|||||||
+2
-7
@@ -30,10 +30,6 @@ function messagePayload(array $row): array {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function publicMessageCondition(PDO $db): string {
|
|
||||||
return isset(tableColumns($db, 'messages')['group_id']) ? ' AND group_id IS NULL' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendTextMessage(): never {
|
function sendTextMessage(): never {
|
||||||
$user = authRequired();
|
$user = authRequired();
|
||||||
$message = trim((string)($_POST['message'] ?? ''));
|
$message = trim((string)($_POST['message'] ?? ''));
|
||||||
@@ -155,17 +151,16 @@ function pollMessages(): never {
|
|||||||
$since = max(0, (int)($_GET['since'] ?? 0));
|
$since = max(0, (int)($_GET['since'] ?? 0));
|
||||||
$limit = max(1, min(250, (int)getConfigVal('chat.messages_per_page', 100)));
|
$limit = max(1, min(250, (int)getConfigVal('chat.messages_per_page', 100)));
|
||||||
$db = getDB();
|
$db = getDB();
|
||||||
$publicOnly = publicMessageCondition($db);
|
|
||||||
|
|
||||||
if ($since === 0) {
|
if ($since === 0) {
|
||||||
$stmt = $db->prepare("SELECT * FROM messages
|
$stmt = $db->prepare("SELECT * FROM messages
|
||||||
WHERE day_key=?$publicOnly ORDER BY id DESC LIMIT ?");
|
WHERE day_key=? ORDER BY id DESC LIMIT ?");
|
||||||
$stmt->execute([dayKey(), $limit]);
|
$stmt->execute([dayKey(), $limit]);
|
||||||
$rows = array_reverse($stmt->fetchAll());
|
$rows = array_reverse($stmt->fetchAll());
|
||||||
$hasMore = false;
|
$hasMore = false;
|
||||||
} else {
|
} else {
|
||||||
$stmt = $db->prepare("SELECT * FROM messages
|
$stmt = $db->prepare("SELECT * FROM messages
|
||||||
WHERE day_key=?$publicOnly AND id>? ORDER BY id LIMIT ?");
|
WHERE day_key=? AND id>? ORDER BY id LIMIT ?");
|
||||||
$stmt->execute([dayKey(), $since, $limit + 1]);
|
$stmt->execute([dayKey(), $since, $limit + 1]);
|
||||||
$rows = $stmt->fetchAll();
|
$rows = $stmt->fetchAll();
|
||||||
$hasMore = count($rows) > $limit;
|
$hasMore = count($rows) > $limit;
|
||||||
|
|||||||
@@ -1178,9 +1178,6 @@ html, body {
|
|||||||
.cc-inline-btn.danger { color: var(--r); border-color: var(--r); }
|
.cc-inline-btn.danger { color: var(--r); border-color: var(--r); }
|
||||||
.cc-inline-btn:disabled { opacity: .35; cursor: not-allowed; }
|
.cc-inline-btn:disabled { opacity: .35; cursor: not-allowed; }
|
||||||
.cc-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
.cc-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
.cc-member-list { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
||||||
.cc-member-list span { padding: 4px 7px; border: 1px solid var(--bd); border-radius: 2px; font: 10px var(--mono); }
|
|
||||||
.cc-member-list button { color: var(--r); background: none; border: 0; margin-left: 4px; }
|
|
||||||
.cc-empty-card { padding: 30px; color: var(--t1); border: 1px dashed var(--bd); text-align: center; font: 10px var(--mono); }
|
.cc-empty-card { padding: 30px; color: var(--t1); border: 1px dashed var(--bd); text-align: center; font: 10px var(--mono); }
|
||||||
.cc-search-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; margin-bottom: 12px; }
|
.cc-search-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; margin-bottom: 12px; }
|
||||||
.cc-archive-list { display: flex; flex-direction: column; gap: 6px; }
|
.cc-archive-list { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
};
|
};
|
||||||
let root;
|
let root;
|
||||||
let wakeListenersBound = false;
|
let wakeListenersBound = false;
|
||||||
|
const displayedFeatures = new Set([
|
||||||
|
'voice_messages', 'recording_status', 'auto_voice_send', 'auto_voice_play',
|
||||||
|
'username_color', 'text_archive_days', 'text_export', 'voice_archive',
|
||||||
|
'voice_export', 'email_2fa'
|
||||||
|
]);
|
||||||
const $ = (s, c) => (c || root).querySelector(s);
|
const $ = (s, c) => (c || root).querySelector(s);
|
||||||
const $$ = (s, c) => Array.from((c || root).querySelectorAll(s));
|
const $$ = (s, c) => Array.from((c || root).querySelectorAll(s));
|
||||||
const esc = value => String(value == null ? '' : value)
|
const esc = value => String(value == null ? '' : value)
|
||||||
@@ -27,8 +32,8 @@
|
|||||||
|
|
||||||
function base() {
|
function base() {
|
||||||
if (window.CYBERCHAT_BASE != null) return String(window.CYBERCHAT_BASE).replace(/\/$/, '');
|
if (window.CYBERCHAT_BASE != null) return String(window.CYBERCHAT_BASE).replace(/\/$/, '');
|
||||||
const script = Array.from(document.scripts).find(item => item.src.includes('cyberchat-v4.js'));
|
const script = Array.from(document.scripts).find(item => item.src.includes('cyberchat-app.js'));
|
||||||
return script ? script.src.replace(/\/assets\/js\/cyberchat-v4\.js.*$/, '') : '';
|
return script ? script.src.replace(/\/assets\/js\/cyberchat-app\.js.*$/, '') : '';
|
||||||
}
|
}
|
||||||
function apiUrl(file, query = '') { return base() + '/api/' + file + (query ? '?' + query : ''); }
|
function apiUrl(file, query = '') { return base() + '/api/' + file + (query ? '?' + query : ''); }
|
||||||
function publicUrl(path) {
|
function publicUrl(path) {
|
||||||
@@ -54,6 +59,10 @@
|
|||||||
return state.user?.features && Object.prototype.hasOwnProperty.call(state.user.features, key)
|
return state.user?.features && Object.prototype.hasOwnProperty.call(state.user.features, key)
|
||||||
? state.user.features[key] : fallback;
|
? state.user.features[key] : fallback;
|
||||||
}
|
}
|
||||||
|
function visibleFeatures() {
|
||||||
|
return Object.entries(state.user?.features || {})
|
||||||
|
.filter(([key]) => displayedFeatures.has(String(key).toLowerCase()));
|
||||||
|
}
|
||||||
function toast(message, type = 'error') {
|
function toast(message, type = 'error') {
|
||||||
const area = $('#cc-toasts');
|
const area = $('#cc-toasts');
|
||||||
if (!area) return;
|
if (!area) return;
|
||||||
@@ -681,7 +690,7 @@
|
|||||||
</section></div>
|
</section></div>
|
||||||
${showBilling ? billingHtml(billing) : ''}
|
${showBilling ? billingHtml(billing) : ''}
|
||||||
<section class="cc-card cc-feature-card"><h3>CURRENT FEATURES</h3><div class="cc-feature-list">
|
<section class="cc-card cc-feature-card"><h3>CURRENT FEATURES</h3><div class="cc-feature-list">
|
||||||
${Object.entries(state.user.features).map(([key, value]) => `<span><b>${esc(key.replace(/_/g, ' '))}</b> ${esc(String(value))}</span>`).join('')}</div></section>
|
${visibleFeatures().map(([key, value]) => `<span><b>${esc(key.replace(/_/g, ' '))}</b> ${esc(String(value))}</span>`).join('')}</div></section>
|
||||||
</div>`;
|
</div>`;
|
||||||
$('#logout').addEventListener('click', () => logout(true));
|
$('#logout').addEventListener('click', () => logout(true));
|
||||||
$('#email-send').addEventListener('click', requestEmail);
|
$('#email-send').addEventListener('click', requestEmail);
|
||||||
File diff suppressed because it is too large
Load Diff
+71
-16
@@ -56,6 +56,13 @@ function getConfig(bool $reload = false): array {
|
|||||||
if (!file_exists(CONFIG_FILE)) throw new RuntimeException('config.json not found');
|
if (!file_exists(CONFIG_FILE)) throw new RuntimeException('config.json not found');
|
||||||
$config = json_decode((string)file_get_contents(CONFIG_FILE), true);
|
$config = json_decode((string)file_get_contents(CONFIG_FILE), true);
|
||||||
if (!is_array($config)) throw new RuntimeException('config.json is invalid: ' . json_last_error_msg());
|
if (!is_array($config)) throw new RuntimeException('config.json is invalid: ' . json_last_error_msg());
|
||||||
|
if (array_key_exists('groups', $config)) {
|
||||||
|
unset($config['groups']);
|
||||||
|
$json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($json !== false && is_writable(CONFIG_FILE)) {
|
||||||
|
file_put_contents(CONFIG_FILE, $json . PHP_EOL, LOCK_EX);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
@@ -165,6 +172,51 @@ function ensureColumns(PDO $db, string $table, array $additions): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeLegacyGroupSchema(PDO $db, bool $mainDatabase): void {
|
||||||
|
$messageColumns = tableColumns($db, 'messages');
|
||||||
|
if (isset($messageColumns['group_id'])) {
|
||||||
|
$voiceFiles = $db->query("SELECT DISTINCT voice_file FROM messages
|
||||||
|
WHERE group_id IS NOT NULL AND voice_file IS NOT NULL AND voice_file != ''")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
$db->exec('PRAGMA foreign_keys=OFF');
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
$db->exec("DROP TABLE IF EXISTS messages_without_groups");
|
||||||
|
$db->exec("CREATE TABLE messages_without_groups (
|
||||||
|
id INTEGER PRIMARY KEY" . ($mainDatabase ? ' AUTOINCREMENT' : '') . ",
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
message_type TEXT NOT NULL DEFAULT 'text',
|
||||||
|
voice_file TEXT,
|
||||||
|
voice_mime TEXT,
|
||||||
|
voice_duration REAL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
day_key TEXT NOT NULL" .
|
||||||
|
($mainDatabase ? ", FOREIGN KEY(user_id) REFERENCES users(id)" : '') . "
|
||||||
|
)");
|
||||||
|
$db->exec("INSERT INTO messages_without_groups
|
||||||
|
(id,user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
|
||||||
|
SELECT id,user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key
|
||||||
|
FROM messages WHERE group_id IS NULL");
|
||||||
|
$db->exec("DROP TABLE messages");
|
||||||
|
$db->exec("ALTER TABLE messages_without_groups RENAME TO messages");
|
||||||
|
$db->exec("DROP TABLE IF EXISTS group_members");
|
||||||
|
$db->exec("DROP TABLE IF EXISTS chat_groups");
|
||||||
|
$db->commit();
|
||||||
|
foreach ($voiceFiles as $voiceFile) deleteVoiceFile((string)$voiceFile);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($db->inTransaction()) $db->rollBack();
|
||||||
|
throw $e;
|
||||||
|
} finally {
|
||||||
|
$db->exec('PRAGMA foreign_keys=ON');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$db->exec("DROP TABLE IF EXISTS group_members");
|
||||||
|
$db->exec("DROP TABLE IF EXISTS chat_groups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initDB(PDO $db): void {
|
function initDB(PDO $db): void {
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS users (
|
$db->exec("CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -226,6 +278,7 @@ function initDB(PDO $db): void {
|
|||||||
'voice_mime' => 'TEXT',
|
'voice_mime' => 'TEXT',
|
||||||
'voice_duration' => 'REAL',
|
'voice_duration' => 'REAL',
|
||||||
]);
|
]);
|
||||||
|
removeLegacyGroupSchema($db, true);
|
||||||
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS plans (
|
$db->exec("CREATE TABLE IF NOT EXISTS plans (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -368,11 +421,9 @@ function seedPlans(PDO $db): void {
|
|||||||
$insertFeature->execute([$planId, $key, json_encode($value)]);
|
$insertFeature->execute([$planId, $key, json_encode($value)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$db->exec("DELETE FROM plan_features WHERE feature_key IN ('groups_available','group_member_limit')");
|
$db->exec("DELETE FROM plan_features WHERE feature_key LIKE 'group%'");
|
||||||
$db->exec("UPDATE plans SET description='Premium voice, identity, security, and 90-day personal archives.'
|
$db->exec("UPDATE plans SET description='Configurable premium chat features.'
|
||||||
WHERE slug='plus' AND description='Premium voice, identity, security, and groups for four.'");
|
WHERE lower(description) LIKE '%group%'");
|
||||||
$db->exec("UPDATE plans SET description='Premium voice, security, and one year of personal archives.'
|
|
||||||
WHERE slug='pro' AND description='Expanded groups and one year of personal archives.'");
|
|
||||||
$freeId = (int)$db->query("SELECT id FROM plans WHERE slug = 'free'")->fetchColumn();
|
$freeId = (int)$db->query("SELECT id FROM plans WHERE slug = 'free'")->fetchColumn();
|
||||||
$db->prepare("UPDATE users SET plan_id = ? WHERE plan_id IS NULL")->execute([$freeId]);
|
$db->prepare("UPDATE users SET plan_id = ? WHERE plan_id IS NULL")->execute([$freeId]);
|
||||||
}
|
}
|
||||||
@@ -388,6 +439,7 @@ function plans(bool $activeOnly = false): array {
|
|||||||
foreach ($featureStmt->fetchAll() as $feature) {
|
foreach ($featureStmt->fetchAll() as $feature) {
|
||||||
$row['features'][$feature['feature_key']] = json_decode($feature['value_json'], true);
|
$row['features'][$feature['feature_key']] = json_decode($feature['value_json'], true);
|
||||||
}
|
}
|
||||||
|
$row['features'] = visiblePlanFeatures($row['features']);
|
||||||
$row['id'] = (int)$row['id'];
|
$row['id'] = (int)$row['id'];
|
||||||
$row['price_cents'] = (int)$row['price_cents'];
|
$row['price_cents'] = (int)$row['price_cents'];
|
||||||
$row['active'] = (bool)$row['active'];
|
$row['active'] = (bool)$row['active'];
|
||||||
@@ -395,6 +447,14 @@ function plans(bool $activeOnly = false): array {
|
|||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visiblePlanFeatures(array $features): array {
|
||||||
|
return array_filter(
|
||||||
|
$features,
|
||||||
|
fn(string $key): bool => !str_starts_with(strtolower($key), 'group'),
|
||||||
|
ARRAY_FILTER_USE_KEY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function planById(int $planId): ?array {
|
function planById(int $planId): ?array {
|
||||||
foreach (plans(false) as $plan) if ($plan['id'] === $planId) return $plan;
|
foreach (plans(false) as $plan) if ($plan['id'] === $planId) return $plan;
|
||||||
return null;
|
return null;
|
||||||
@@ -425,7 +485,7 @@ function userPublicPayload(array $user): array {
|
|||||||
'email_verified' => !empty($user['email_verified_at']),
|
'email_verified' => !empty($user['email_verified_at']),
|
||||||
'plan' => ['id' => $plan['id'], 'slug' => $plan['slug'], 'name' => $plan['name']],
|
'plan' => ['id' => $plan['id'], 'slug' => $plan['slug'], 'name' => $plan['name']],
|
||||||
'plan_source' => !empty($user['manual_plan_id']) ? 'admin' : 'billing',
|
'plan_source' => !empty($user['manual_plan_id']) ? 'admin' : 'billing',
|
||||||
'features' => $plan['features'],
|
'features' => visiblePlanFeatures($plan['features']),
|
||||||
'subscription' => [
|
'subscription' => [
|
||||||
'status' => $user['subscription_status'] ?? 'free',
|
'status' => $user['subscription_status'] ?? 'free',
|
||||||
'period_end' => isset($user['subscription_period_end']) ? (int)$user['subscription_period_end'] : null,
|
'period_end' => isset($user['subscription_period_end']) ? (int)$user['subscription_period_end'] : null,
|
||||||
@@ -506,11 +566,6 @@ function dayKey(): string {
|
|||||||
return date('Y-m-d');
|
return date('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
function publicMessagesOnly(PDO $db, string $alias = ''): string {
|
|
||||||
$prefix = $alias !== '' ? rtrim($alias, '.') . '.' : '';
|
|
||||||
return isset(tableColumns($db, 'messages')['group_id']) ? " AND {$prefix}group_id IS NULL" : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function voiceUploadDir(): string {
|
function voiceUploadDir(): string {
|
||||||
return storagePath((string)getConfigVal('voice.upload_dir', 'uploads/voice'));
|
return storagePath((string)getConfigVal('voice.upload_dir', 'uploads/voice'));
|
||||||
}
|
}
|
||||||
@@ -544,6 +599,7 @@ function getArchiveDB(string $year, string $month, string $day): PDO {
|
|||||||
'message_type' => "TEXT NOT NULL DEFAULT 'text'",
|
'message_type' => "TEXT NOT NULL DEFAULT 'text'",
|
||||||
'voice_file' => 'TEXT', 'voice_mime' => 'TEXT', 'voice_duration' => 'REAL',
|
'voice_file' => 'TEXT', 'voice_mime' => 'TEXT', 'voice_duration' => 'REAL',
|
||||||
]);
|
]);
|
||||||
|
removeLegacyGroupSchema($db, false);
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS archive_meta (key TEXT PRIMARY KEY,value TEXT)");
|
$db->exec("CREATE TABLE IF NOT EXISTS archive_meta (key TEXT PRIMARY KEY,value TEXT)");
|
||||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
|
||||||
return $db;
|
return $db;
|
||||||
@@ -567,8 +623,7 @@ function archiveYesterdayIfNeeded(): void {
|
|||||||
$flag = ROOT_DIR . '/db/.archived_' . $yesterday;
|
$flag = ROOT_DIR . '/db/.archived_' . $yesterday;
|
||||||
if (file_exists($flag)) return;
|
if (file_exists($flag)) return;
|
||||||
$db = getDB();
|
$db = getDB();
|
||||||
$publicOnly = publicMessagesOnly($db);
|
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=? ORDER BY id");
|
||||||
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=?$publicOnly ORDER BY id");
|
|
||||||
$stmt->execute([$yesterday]);
|
$stmt->execute([$yesterday]);
|
||||||
$rows = $stmt->fetchAll();
|
$rows = $stmt->fetchAll();
|
||||||
if ($rows) {
|
if ($rows) {
|
||||||
@@ -590,7 +645,7 @@ function archiveYesterdayIfNeeded(): void {
|
|||||||
$meta->execute(['message_count', (string)count($rows)]);
|
$meta->execute(['message_count', (string)count($rows)]);
|
||||||
$archive->commit();
|
$archive->commit();
|
||||||
}
|
}
|
||||||
$db->prepare("DELETE FROM messages WHERE day_key=?$publicOnly")->execute([$yesterday]);
|
$db->prepare("DELETE FROM messages WHERE day_key=?")->execute([$yesterday]);
|
||||||
file_put_contents($flag, date('c'), LOCK_EX);
|
file_put_contents($flag, date('c'), LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +655,7 @@ function personalArchiveRows(array $user, string $search = '', int $limit = 500)
|
|||||||
$includeVoice = (bool)featureValue($user, 'voice_archive', false);
|
$includeVoice = (bool)featureValue($user, 'voice_archive', false);
|
||||||
$rows = [];
|
$rows = [];
|
||||||
$db = getDB();
|
$db = getDB();
|
||||||
$sql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?" . publicMessagesOnly($db);
|
$sql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?";
|
||||||
$params = [$user['id'], $cutoff];
|
$params = [$user['id'], $cutoff];
|
||||||
if (!$includeVoice) $sql .= " AND message_type='text'";
|
if (!$includeVoice) $sql .= " AND message_type='text'";
|
||||||
if ($search !== '') { $sql .= " AND message LIKE ?"; $params[] = '%' . $search . '%'; }
|
if ($search !== '') { $sql .= " AND message LIKE ?"; $params[] = '%' . $search . '%'; }
|
||||||
@@ -611,7 +666,7 @@ function personalArchiveRows(array $user, string $search = '', int $limit = 500)
|
|||||||
|
|
||||||
foreach (archiveFilesSince($cutoff) as [, $year, $month, $day]) {
|
foreach (archiveFilesSince($cutoff) as [, $year, $month, $day]) {
|
||||||
$archive = getArchiveDB($year, $month, $day);
|
$archive = getArchiveDB($year, $month, $day);
|
||||||
$archiveSql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?" . publicMessagesOnly($archive);
|
$archiveSql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?";
|
||||||
$archiveParams = [$user['id'], $cutoff];
|
$archiveParams = [$user['id'], $cutoff];
|
||||||
if (!$includeVoice) $archiveSql .= " AND message_type='text'";
|
if (!$includeVoice) $archiveSql .= " AND message_type='text'";
|
||||||
if ($search !== '') { $archiveSql .= " AND message LIKE ?"; $archiveParams[] = '%' . $search . '%'; }
|
if ($search !== '') { $archiveSql .= " AND message LIKE ?"; $archiveParams[] = '%' . $search . '%'; }
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,7 @@
|
|||||||
<title>CyberChat — Embed Example</title>
|
<title>CyberChat — Embed Example</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260608.6">
|
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260609.3">
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
<span class="kw"><link</span> href=<span class="str">"https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@700;900&display=swap"</span> rel=<span class="str">"stylesheet"</span><span class="kw">></span>
|
<span class="kw"><link</span> href=<span class="str">"https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;600;700&family=Orbitron:wght@700;900&display=swap"</span> rel=<span class="str">"stylesheet"</span><span class="kw">></span>
|
||||||
|
|
||||||
<span class="cm"><!-- 2. CyberChat stylesheet --></span>
|
<span class="cm"><!-- 2. CyberChat stylesheet --></span>
|
||||||
<span class="kw"><link</span> rel=<span class="str">"stylesheet"</span> href=<span class="str">"assets/css/cyberchat.css?v=20260608.6"</span><span class="kw">></span>
|
<span class="kw"><link</span> rel=<span class="str">"stylesheet"</span> href=<span class="str">"assets/css/cyberchat.css?v=20260609.3"</span><span class="kw">></span>
|
||||||
|
|
||||||
<span class="cm"><!-- 3. Container with any dimensions --></span>
|
<span class="cm"><!-- 3. Container with any dimensions --></span>
|
||||||
<span class="kw"><div</span> id=<span class="str">"my-chat"</span> style=<span class="str">"width:100%; height:600px"</span><span class="kw">></div></span>
|
<span class="kw"><div</span> id=<span class="str">"my-chat"</span> style=<span class="str">"width:100%; height:600px"</span><span class="kw">></div></span>
|
||||||
|
|
||||||
<span class="cm"><!-- 4. Script + init --></span>
|
<span class="cm"><!-- 4. Script + init --></span>
|
||||||
<span class="kw"><script</span> src=<span class="str">"assets/js/cyberchat-v4.js?v=20260608.6"</span><span class="kw">></script></span>
|
<span class="kw"><script</span> src=<span class="str">"assets/js/cyberchat-app.js?v=20260609.3"</span><span class="kw">></script></span>
|
||||||
<span class="kw"><script></span>
|
<span class="kw"><script></span>
|
||||||
window.CYBERCHAT_BASE = <span class="str">'/path/to/cyberchat'</span>; <span class="cm">// adjust to your install path</span>
|
window.CYBERCHAT_BASE = <span class="str">'/path/to/cyberchat'</span>; <span class="cm">// adjust to your install path</span>
|
||||||
CyberChat.init(<span class="str">'#my-chat'</span>);
|
CyberChat.init(<span class="str">'#my-chat'</span>);
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '';
|
window.CYBERCHAT_BASE = '';
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/cyberchat-v4.js?v=20260608.6"></script>
|
<script src="assets/js/cyberchat-app.js?v=20260609.3"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
CyberChat.init('#chat-here');
|
CyberChat.init('#chat-here');
|
||||||
|
|||||||
+5
-2
@@ -2,19 +2,22 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="description" content="CyberChat — Secure Real-Time Chat">
|
<meta name="description" content="CyberChat — Secure Real-Time Chat">
|
||||||
<title>Chat @ TyClifford.com</title>
|
<title>Chat @ TyClifford.com</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260608.6">
|
<link rel="stylesheet" href="assets/css/cyberchat.css?v=20260609.3">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script>
|
<script>
|
||||||
window.CYBERCHAT_BASE = '';
|
window.CYBERCHAT_BASE = '';
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/cyberchat-v4.js?v=20260608.6"></script>
|
<script src="assets/js/cyberchat-app.js?v=20260609.3"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
CyberChat.init('#app');
|
CyberChat.init('#app');
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ server {
|
|||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~* \.html?$ {
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
# Never execute files from the voice upload directory
|
# Never execute files from the voice upload directory
|
||||||
location ~ ^/uploads/voice/.*\.(php|phtml|phar|cgi|pl|py|sh)$ {
|
location ~ ^/uploads/voice/.*\.(php|phtml|phar|cgi|pl|py|sh)$ {
|
||||||
deny all;
|
deny all;
|
||||||
|
|||||||
Reference in New Issue
Block a user