- Implemented.
Replaced polling with a single queued cursor worker, timeout recovery, backoff, deduplication, chronological insertion, and capped DOM history in cyberchat-v4.js. Removed Groups UI, API, configuration, tier features, admin controls, and archive exposure. Legacy group records remain stored but inaccessible. Added configurable polling limits in config.json.
This commit is contained in:
+32
-90
@@ -69,14 +69,6 @@ function getConfigVal(string $path, mixed $default = null): mixed {
|
||||
return $value;
|
||||
}
|
||||
|
||||
function groupsEnabled(): bool {
|
||||
return (bool)getConfigVal('groups.enabled', true);
|
||||
}
|
||||
|
||||
function groupsAvailableForUser(array $user): bool {
|
||||
return groupsEnabled() && (bool)featureValue($user, 'groups_available', true);
|
||||
}
|
||||
|
||||
function setConfigValues(array $changes): void {
|
||||
$config = getConfig();
|
||||
foreach ($changes as $path => $value) {
|
||||
@@ -214,28 +206,9 @@ function initDB(PDO $db): void {
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)");
|
||||
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS chat_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)");
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS group_members (
|
||||
group_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
joined_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(group_id, user_id),
|
||||
FOREIGN KEY(group_id) REFERENCES chat_groups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)");
|
||||
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
group_id INTEGER,
|
||||
username TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
@@ -245,11 +218,9 @@ function initDB(PDO $db): void {
|
||||
voice_duration REAL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
day_key TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id),
|
||||
FOREIGN KEY(group_id) REFERENCES chat_groups(id) ON DELETE CASCADE
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
)");
|
||||
ensureColumns($db, 'messages', [
|
||||
'group_id' => 'INTEGER',
|
||||
'message_type' => "TEXT NOT NULL DEFAULT 'text'",
|
||||
'voice_file' => 'TEXT',
|
||||
'voice_mime' => 'TEXT',
|
||||
@@ -315,11 +286,15 @@ function initDB(PDO $db): void {
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)");
|
||||
$recordingColumns = tableColumns($db, 'recording_status');
|
||||
if ($recordingColumns && !isset($recordingColumns['channel_key'])) {
|
||||
$db->exec("DROP TABLE recording_status");
|
||||
}
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS recording_status (
|
||||
group_key TEXT NOT NULL,
|
||||
channel_key TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(group_key, user_id),
|
||||
PRIMARY KEY(channel_key, user_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)");
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS visitor_events (
|
||||
@@ -339,7 +314,6 @@ function initDB(PDO $db): void {
|
||||
)");
|
||||
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_msg_day ON messages(day_key)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_msg_group ON messages(group_id, id)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_msg_user_created ON messages(user_id, created_at)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sess_user ON sessions(user_id)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_sess_active ON sessions(last_active)");
|
||||
@@ -357,28 +331,25 @@ function seedPlans(PDO $db): void {
|
||||
'price' => 0, 'sort' => 0,
|
||||
'features' => [
|
||||
'voice_messages' => true, 'recording_status' => false, 'auto_voice_send' => false,
|
||||
'auto_voice_play' => false, 'groups_available' => true,
|
||||
'group_member_limit' => 2, 'username_color' => false, 'text_archive_days' => 30,
|
||||
'auto_voice_play' => false, 'username_color' => false, 'text_archive_days' => 30,
|
||||
'text_export' => true, 'voice_archive' => false, 'voice_export' => false, 'email_2fa' => false,
|
||||
],
|
||||
],
|
||||
'plus' => [
|
||||
'name' => 'Plus', 'description' => 'Premium voice, identity, security, and groups for four.',
|
||||
'name' => 'Plus', 'description' => 'Premium voice, identity, security, and 90-day personal archives.',
|
||||
'price' => 499, 'sort' => 10,
|
||||
'features' => [
|
||||
'voice_messages' => true, 'recording_status' => true, 'auto_voice_send' => true,
|
||||
'auto_voice_play' => true, 'groups_available' => true,
|
||||
'group_member_limit' => 4, 'username_color' => true, 'text_archive_days' => 90,
|
||||
'auto_voice_play' => true, 'username_color' => true, 'text_archive_days' => 90,
|
||||
'text_export' => true, 'voice_archive' => true, 'voice_export' => true, 'email_2fa' => true,
|
||||
],
|
||||
],
|
||||
'pro' => [
|
||||
'name' => 'Pro', 'description' => 'Expanded groups and one year of personal archives.',
|
||||
'name' => 'Pro', 'description' => 'Premium voice, security, and one year of personal archives.',
|
||||
'price' => 999, 'sort' => 20,
|
||||
'features' => [
|
||||
'voice_messages' => true, 'recording_status' => true, 'auto_voice_send' => true,
|
||||
'auto_voice_play' => true, 'groups_available' => true,
|
||||
'group_member_limit' => 8, 'username_color' => true, 'text_archive_days' => 365,
|
||||
'auto_voice_play' => true, 'username_color' => true, 'text_archive_days' => 365,
|
||||
'text_export' => true, 'voice_archive' => true, 'voice_export' => true, 'email_2fa' => true,
|
||||
],
|
||||
],
|
||||
@@ -397,8 +368,11 @@ function seedPlans(PDO $db): void {
|
||||
$insertFeature->execute([$planId, $key, json_encode($value)]);
|
||||
}
|
||||
}
|
||||
$db->exec("INSERT OR IGNORE INTO plan_features (plan_id,feature_key,value_json)
|
||||
SELECT id,'groups_available','true' FROM plans");
|
||||
$db->exec("DELETE FROM plan_features WHERE feature_key IN ('groups_available','group_member_limit')");
|
||||
$db->exec("UPDATE plans SET description='Premium voice, identity, security, and 90-day personal archives.'
|
||||
WHERE slug='plus' AND description='Premium voice, identity, security, and groups for four.'");
|
||||
$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();
|
||||
$db->prepare("UPDATE users SET plan_id = ? WHERE plan_id IS NULL")->execute([$freeId]);
|
||||
}
|
||||
@@ -532,43 +506,9 @@ function dayKey(): string {
|
||||
return date('Y-m-d');
|
||||
}
|
||||
|
||||
function normalizeGroupId(mixed $value): ?int {
|
||||
$id = (int)$value;
|
||||
return $id > 0 ? $id : null;
|
||||
}
|
||||
|
||||
function groupKey(?int $groupId): string {
|
||||
return $groupId ? 'group:' . $groupId : 'lobby';
|
||||
}
|
||||
|
||||
function requireGroupAccess(array $user, ?int $groupId): void {
|
||||
if ($groupId === null) return;
|
||||
$stmt = getDB()->prepare("SELECT 1 FROM group_members WHERE group_id=? AND user_id=?");
|
||||
$stmt->execute([$groupId, $user['id']]);
|
||||
if (!$stmt->fetchColumn()) jsonResponse(['error' => 'You are not a member of this group'], 403);
|
||||
}
|
||||
|
||||
function userGroups(int $userId): array {
|
||||
$stmt = getDB()->prepare("SELECT g.id,g.name,g.owner_id,g.created_at,gm.role,
|
||||
(SELECT COUNT(*) FROM group_members x WHERE x.group_id=g.id) AS member_count,
|
||||
(SELECT MAX(m.id) FROM messages m WHERE m.group_id=g.id) AS latest_message_id,
|
||||
(SELECT MAX(m.created_at) FROM messages m WHERE m.group_id=g.id) AS latest_message_at
|
||||
FROM chat_groups g JOIN group_members gm ON gm.group_id=g.id
|
||||
WHERE gm.user_id=? ORDER BY g.updated_at DESC,g.id DESC");
|
||||
$stmt->execute([$userId]);
|
||||
$groups = $stmt->fetchAll();
|
||||
$members = getDB()->prepare("SELECT u.id,u.username,u.color,gm.role FROM group_members gm
|
||||
JOIN users u ON u.id=gm.user_id WHERE gm.group_id=? ORDER BY gm.role='owner' DESC,u.username");
|
||||
foreach ($groups as &$group) {
|
||||
$group['id'] = (int)$group['id'];
|
||||
$group['owner_id'] = (int)$group['owner_id'];
|
||||
$group['member_count'] = (int)$group['member_count'];
|
||||
$group['latest_message_id'] = (int)($group['latest_message_id'] ?? 0);
|
||||
$group['latest_message_at'] = (int)($group['latest_message_at'] ?? 0);
|
||||
$members->execute([$group['id']]);
|
||||
$group['members'] = $members->fetchAll();
|
||||
}
|
||||
return $groups;
|
||||
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 {
|
||||
@@ -595,13 +535,13 @@ function getArchiveDB(string $year, string $month, string $day): PDO {
|
||||
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$db->exec('PRAGMA journal_mode=WAL');
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, group_id INTEGER,
|
||||
id INTEGER PRIMARY KEY, 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,day_key TEXT NOT NULL
|
||||
)");
|
||||
ensureColumns($db, 'messages', [
|
||||
'group_id' => 'INTEGER', 'message_type' => "TEXT NOT NULL DEFAULT 'text'",
|
||||
'message_type' => "TEXT NOT NULL DEFAULT 'text'",
|
||||
'voice_file' => 'TEXT', 'voice_mime' => 'TEXT', 'voice_duration' => 'REAL',
|
||||
]);
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS archive_meta (key TEXT PRIMARY KEY,value TEXT)");
|
||||
@@ -627,7 +567,8 @@ function archiveYesterdayIfNeeded(): void {
|
||||
$flag = ROOT_DIR . '/db/.archived_' . $yesterday;
|
||||
if (file_exists($flag)) return;
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=? ORDER BY id");
|
||||
$publicOnly = publicMessagesOnly($db);
|
||||
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=?$publicOnly ORDER BY id");
|
||||
$stmt->execute([$yesterday]);
|
||||
$rows = $stmt->fetchAll();
|
||||
if ($rows) {
|
||||
@@ -635,11 +576,11 @@ function archiveYesterdayIfNeeded(): void {
|
||||
$archive = getArchiveDB($year, $month, $day);
|
||||
$archive->beginTransaction();
|
||||
$insert = $archive->prepare("INSERT OR IGNORE INTO messages
|
||||
(id,user_id,group_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
(id,user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||
foreach ($rows as $row) {
|
||||
$insert->execute([
|
||||
$row['id'], $row['user_id'], $row['group_id'] ?? null, $row['username'], $row['color'],
|
||||
$row['id'], $row['user_id'], $row['username'], $row['color'],
|
||||
$row['message'], $row['message_type'], $row['voice_file'], $row['voice_mime'],
|
||||
$row['voice_duration'], $row['created_at'], $row['day_key'],
|
||||
]);
|
||||
@@ -649,7 +590,7 @@ function archiveYesterdayIfNeeded(): void {
|
||||
$meta->execute(['message_count', (string)count($rows)]);
|
||||
$archive->commit();
|
||||
}
|
||||
$db->prepare("DELETE FROM messages WHERE day_key=?")->execute([$yesterday]);
|
||||
$db->prepare("DELETE FROM messages WHERE day_key=?$publicOnly")->execute([$yesterday]);
|
||||
file_put_contents($flag, date('c'), LOCK_EX);
|
||||
}
|
||||
|
||||
@@ -658,18 +599,19 @@ function personalArchiveRows(array $user, string $search = '', int $limit = 500)
|
||||
$cutoff = time() - ($days * 86400);
|
||||
$includeVoice = (bool)featureValue($user, 'voice_archive', false);
|
||||
$rows = [];
|
||||
$sql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?";
|
||||
$db = getDB();
|
||||
$sql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?" . publicMessagesOnly($db);
|
||||
$params = [$user['id'], $cutoff];
|
||||
if (!$includeVoice) $sql .= " AND message_type='text'";
|
||||
if ($search !== '') { $sql .= " AND message LIKE ?"; $params[] = '%' . $search . '%'; }
|
||||
$stmt = getDB()->prepare($sql . " ORDER BY created_at DESC LIMIT ?");
|
||||
$stmt = $db->prepare($sql . " ORDER BY created_at DESC LIMIT ?");
|
||||
$params[] = $limit;
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
foreach (archiveFilesSince($cutoff) as [, $year, $month, $day]) {
|
||||
$archive = getArchiveDB($year, $month, $day);
|
||||
$archiveSql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?";
|
||||
$archiveSql = "SELECT * FROM messages WHERE user_id=? AND created_at>=?" . publicMessagesOnly($archive);
|
||||
$archiveParams = [$user['id'], $cutoff];
|
||||
if (!$includeVoice) $archiveSql .= " AND message_type='text'";
|
||||
if ($search !== '') { $archiveSql .= " AND message LIKE ?"; $archiveParams[] = '%' . $search . '%'; }
|
||||
|
||||
Reference in New Issue
Block a user