[], 'member_limit' => 0, 'enabled' => false]); } jsonResponse(['error' => 'Private groups are currently disabled', 'groups_enabled' => false], 403); } switch ($action) { case 'list': jsonResponse([ 'groups' => userGroups((int)$user['id']), 'member_limit' => (int)featureValue($user, 'group_member_limit', 2), 'enabled' => true, ]); case 'create': $name = trim((string)($_POST['name'] ?? '')); $names = array_values(array_unique(array_filter(array_map('trim', explode(',', (string)($_POST['members'] ?? '')))))); if ($name === '' || strlen($name) > 40) jsonResponse(['error' => 'Group name must be 1-40 characters'], 400); $limit = max(2, (int)featureValue($user, 'group_member_limit', 2)); if (count($names) + 1 > $limit) jsonResponse(['error' => "Your plan allows $limit group members including you"], 403); $memberIds = []; $lookup = getDB()->prepare("SELECT id FROM users WHERE username=? COLLATE NOCASE"); foreach ($names as $username) { if (strcasecmp($username, $user['username']) === 0) continue; $lookup->execute([$username]); $id = (int)$lookup->fetchColumn(); if (!$id) jsonResponse(['error' => "User '$username' was not found"], 404); $memberIds[$id] = $id; } if (!$memberIds) jsonResponse(['error' => 'Add at least one other user to create a group'], 400); if (count($memberIds) + 1 > $limit) jsonResponse(['error' => "Your plan allows $limit group members including you"], 403); $db = getDB(); $db->beginTransaction(); $db->prepare("INSERT INTO chat_groups (owner_id,name,created_at,updated_at) VALUES (?,?,?,?)") ->execute([$user['id'], $name, time(), time()]); $groupId = (int)$db->lastInsertId(); $insert = $db->prepare("INSERT INTO group_members (group_id,user_id,role,joined_at) VALUES (?,?,?,?)"); $insert->execute([$groupId, $user['id'], 'owner', time()]); foreach ($memberIds as $id) $insert->execute([$groupId, $id, 'member', time()]); $db->commit(); jsonResponse(['success' => true, 'groups' => userGroups((int)$user['id'])]); case 'add_member': $groupId = normalizeGroupId($_POST['group_id'] ?? null); $username = trim((string)($_POST['username'] ?? '')); $groupStmt = getDB()->prepare("SELECT * FROM chat_groups WHERE id=? AND owner_id=?"); $groupStmt->execute([$groupId, $user['id']]); $group = $groupStmt->fetch(); if (!$group) jsonResponse(['error' => 'Only the group owner can add members'], 403); $limit = max(2, (int)featureValue($user, 'group_member_limit', 2)); $countStmt = getDB()->prepare("SELECT COUNT(*) FROM group_members WHERE group_id=?"); $countStmt->execute([$groupId]); if ((int)$countStmt->fetchColumn() >= $limit) jsonResponse(['error' => "This group has reached your $limit-member limit"], 403); $lookup = getDB()->prepare("SELECT id FROM users WHERE username=? COLLATE NOCASE"); $lookup->execute([$username]); $memberId = (int)$lookup->fetchColumn(); if (!$memberId) jsonResponse(['error' => 'User not found'], 404); getDB()->prepare("INSERT OR IGNORE INTO group_members (group_id,user_id,role,joined_at) VALUES (?,?,'member',?)") ->execute([$groupId, $memberId, time()]); getDB()->prepare("UPDATE chat_groups SET updated_at=? WHERE id=?")->execute([time(), $groupId]); jsonResponse(['success' => true, 'groups' => userGroups((int)$user['id'])]); case 'remove_member': $groupId = normalizeGroupId($_POST['group_id'] ?? null); $memberId = (int)($_POST['user_id'] ?? 0); $stmt = getDB()->prepare("SELECT owner_id FROM chat_groups WHERE id=?"); $stmt->execute([$groupId]); $ownerId = (int)$stmt->fetchColumn(); if (!$ownerId) jsonResponse(['error' => 'Group not found'], 404); if ($ownerId !== (int)$user['id'] && $memberId !== (int)$user['id']) jsonResponse(['error' => 'Not allowed'], 403); if ($memberId === $ownerId) jsonResponse(['error' => 'The owner must delete the group instead'], 400); getDB()->prepare("DELETE FROM group_members WHERE group_id=? AND user_id=?")->execute([$groupId, $memberId]); jsonResponse(['success' => true, 'groups' => userGroups((int)$user['id'])]); case 'delete': $groupId = normalizeGroupId($_POST['group_id'] ?? null); $db = getDB(); $owner = $db->prepare("SELECT 1 FROM chat_groups WHERE id=? AND owner_id=?"); $owner->execute([$groupId, $user['id']]); if (!$owner->fetchColumn()) jsonResponse(['error' => 'Only the group owner can delete this group'], 403); $voice = $db->prepare("SELECT voice_file FROM messages WHERE group_id=? AND voice_file IS NOT NULL"); $voice->execute([$groupId]); $voiceFiles = $voice->fetchAll(); $db->beginTransaction(); try { $db->prepare("DELETE FROM recording_status WHERE group_key=?")->execute([groupKey($groupId)]); $db->prepare("DELETE FROM messages WHERE group_id=?")->execute([$groupId]); $db->prepare("DELETE FROM group_members WHERE group_id=?")->execute([$groupId]); $db->prepare("DELETE FROM chat_groups WHERE id=?")->execute([$groupId]); $db->commit(); } catch (Throwable $e) { if ($db->inTransaction()) $db->rollBack(); throw $e; } foreach ($voiceFiles as $row) deleteVoiceFile($row['voice_file'] ?? null); jsonResponse(['success' => true, 'groups' => userGroups((int)$user['id'])]); default: jsonResponse(['error' => 'Unknown action'], 400); }