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:
Ty Clifford
2026-06-09 00:16:32 -04:00
parent ccdbc47642
commit a1971f9515
11 changed files with 143 additions and 1110 deletions
+71 -16
View File
@@ -56,6 +56,13 @@ function getConfig(bool $reload = false): array {
if (!file_exists(CONFIG_FILE)) throw new RuntimeException('config.json not found');
$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 (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;
}
@@ -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 {
$db->exec("CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -226,6 +278,7 @@ function initDB(PDO $db): void {
'voice_mime' => 'TEXT',
'voice_duration' => 'REAL',
]);
removeLegacyGroupSchema($db, true);
$db->exec("CREATE TABLE IF NOT EXISTS plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -368,11 +421,9 @@ function seedPlans(PDO $db): void {
$insertFeature->execute([$planId, $key, json_encode($value)]);
}
}
$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.'");
$db->exec("DELETE FROM plan_features WHERE feature_key LIKE 'group%'");
$db->exec("UPDATE plans SET description='Configurable premium chat features.'
WHERE lower(description) LIKE '%group%'");
$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]);
}
@@ -388,6 +439,7 @@ function plans(bool $activeOnly = false): array {
foreach ($featureStmt->fetchAll() as $feature) {
$row['features'][$feature['feature_key']] = json_decode($feature['value_json'], true);
}
$row['features'] = visiblePlanFeatures($row['features']);
$row['id'] = (int)$row['id'];
$row['price_cents'] = (int)$row['price_cents'];
$row['active'] = (bool)$row['active'];
@@ -395,6 +447,14 @@ function plans(bool $activeOnly = false): array {
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 {
foreach (plans(false) as $plan) if ($plan['id'] === $planId) return $plan;
return null;
@@ -425,7 +485,7 @@ function userPublicPayload(array $user): array {
'email_verified' => !empty($user['email_verified_at']),
'plan' => ['id' => $plan['id'], 'slug' => $plan['slug'], 'name' => $plan['name']],
'plan_source' => !empty($user['manual_plan_id']) ? 'admin' : 'billing',
'features' => $plan['features'],
'features' => visiblePlanFeatures($plan['features']),
'subscription' => [
'status' => $user['subscription_status'] ?? 'free',
'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');
}
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 {
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'",
'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 INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
return $db;
@@ -567,8 +623,7 @@ function archiveYesterdayIfNeeded(): void {
$flag = ROOT_DIR . '/db/.archived_' . $yesterday;
if (file_exists($flag)) return;
$db = getDB();
$publicOnly = publicMessagesOnly($db);
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=?$publicOnly ORDER BY id");
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=? ORDER BY id");
$stmt->execute([$yesterday]);
$rows = $stmt->fetchAll();
if ($rows) {
@@ -590,7 +645,7 @@ function archiveYesterdayIfNeeded(): void {
$meta->execute(['message_count', (string)count($rows)]);
$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);
}
@@ -600,7 +655,7 @@ function personalArchiveRows(array $user, string $search = '', int $limit = 500)
$includeVoice = (bool)featureValue($user, 'voice_archive', false);
$rows = [];
$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];
if (!$includeVoice) $sql .= " AND message_type='text'";
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]) {
$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];
if (!$includeVoice) $archiveSql .= " AND message_type='text'";
if ($search !== '') { $archiveSql .= " AND message LIKE ?"; $archiveParams[] = '%' . $search . '%'; }