38443-vm/api_v1_voice.php
2026-02-18 19:16:04 +00:00

256 lines
8.3 KiB
PHP

<?php // Vocal secours — WebRTC P2P + signalisation PHP + participants + PTT + VOX (gating via GainNode)
// Mutualisé OVH : nécessite ./data écrivable.
declare(strict_types=1);
require_once "auth/session.php";
header("X-Content-Type-Options: nosniff");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
$DATA_DIR = __DIR__ . "/data";
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
$user = getCurrentUser();
$current_user_id = $user ? (int)$user["id"] : 0;
function room_id(string $s): string {
$s = preg_replace("~[^a-zA-Z0-9_\-]~", "", $s);
return $s !== "" ? $s : "secours";
}
function peer_id(): string {
return bin2hex(random_bytes(8));
}
function room_log_file(string $room): string {
return __DIR__ . "/data/" . $room . ".log";
}
function room_participants_file(string $room): string {
return __DIR__ . "/data/" . $room . ".participants.json";
}
function chat_log_file_for_today(): string {
// Un fichier par jour : YYYY-MM-DD.chat.log
$d = date("Y-m-d");
return __DIR__ . "/data/" . $d . ".chat.log";
}
function now_ms(): int {
return (int) floor(microtime(true) * 1000);
}
function json_out($data, int $code = 200): void {
http_response_code($code);
header("Content-Type: application/json; charset=utf-8");
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function read_json_file(string $path): array {
if (!file_exists($path)) return [];
$raw = @file_get_contents($path);
if ($raw === false || $raw === "") return [];
$j = json_decode($raw, true);
return is_array($j) ? $j : [];
}
function write_json_file(string $path, array $data): void {
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
}
function tail_lines(string $path, int $maxLines = 120): array {
if (!is_file($path)) return [];
$fp = fopen($path, "rb");
if (!$fp) return [];
$lines = [];
fseek($fp, 0, SEEK_END);
$pos = ftell($fp);
$buffer = "";
while ($pos > 0 && count($lines) < $maxLines) {
$readSize = min($pos, 4096);
$pos -= $readSize;
fseek($fp, $pos);
$chunk = fread($fp, $readSize);
$buffer = $chunk . $buffer;
$chunkLines = explode("\n", $buffer);
$buffer = array_shift($chunkLines);
while (!empty($chunkLines)) {
$line = array_pop($chunkLines);
if (trim($line) !== "") {
array_unshift($lines, trim($line));
if (count($lines) >= $maxLines) break;
}
}
}
fclose($fp);
return $lines;
}
// Logic for signaling
$action = $_REQUEST["action"] ?? "";
$room = room_id($_REQUEST["room"] ?? "secours");
$my_id = $_REQUEST["peer_id"] ?? "";
if ($action === "join") {
$name = $_REQUEST["name"] ?? "User";
$p_file = room_participants_file($room);
$ps = read_json_file($p_file);
// Cleanup old participants (> 10s)
$stale_time = now_ms() - 10000;
foreach ($ps as $id => $p) {
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
}
$new_id = substr($_REQUEST["peer_id"] ?: peer_id(), 0, 16);
$ps[$new_id] = [
"id" => $new_id,
"user_id" => $current_user_id,
"name" => $name,
"avatar_url" => $user["avatar_url"] ?? "",
"last_seen" => now_ms()
];
write_json_file($p_file, $ps);
// DB Integration for sidebar
if ($current_user_id > 0) {
try {
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, last_seen, peer_id, name) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, last_seen = ?, peer_id = ?, name = ?");
$res = $stmt->execute([$current_user_id, $room, now_ms(), $new_id, $name, $room, now_ms(), $new_id, $name]);
if (!$res) {
error_log("Failed to insert voice session for user $current_user_id in room $room");
}
} catch (Exception $e) {
error_log("Voice session DB error: " . $e->getMessage());
}
}
json_out(["success" => true, "peer_id" => $new_id, "participants" => $ps]);
}
if ($action === "poll") {
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
$p_file = room_participants_file($room);
$ps = read_json_file($p_file);
if (isset($ps[$my_id])) {
$ps[$my_id]["last_seen"] = now_ms();
}
$stale_time = now_ms() - 10000;
foreach ($ps as $id => $p) {
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
}
write_json_file($p_file, $ps);
// Update DB last_seen
if ($current_user_id > 0) {
try {
$stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ?");
$stmt->execute([now_ms(), $current_user_id]);
} catch (Exception $e) {}
}
// Read signals from DB
$signals = [];
try {
$stmt = db()->prepare("SELECT id, from_peer_id as `from`, data FROM voice_signals WHERE to_peer_id = ? ORDER BY id ASC");
$stmt->execute([$my_id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($rows)) {
$ids = [];
foreach ($rows as $r) {
$signals[] = [
"from" => $r["from"],
"data" => json_decode($r["data"], true)
];
$ids[] = $r["id"];
}
// Delete signals we just read
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt_del = db()->prepare("DELETE FROM voice_signals WHERE id IN ($placeholders)");
$stmt_del->execute($ids);
}
// Periodic cleanup of old signals (> 1 minute)
if (rand(1, 20) === 1) {
$old_time = now_ms() - 60000;
$stmt_clean = db()->prepare("DELETE FROM voice_signals WHERE created_at_ms < ?");
$stmt_clean->execute([$old_time]);
}
} catch (Exception $e) {
error_log("Signal poll error: " . $e->getMessage());
}
json_out(["success" => true, "participants" => $ps, "signals" => $signals]);
}
if ($action === "signal") {
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
$to = $_REQUEST["to"] ?? "";
$data = $_REQUEST["data"] ?? "";
if (!$to || !$data) json_out(["error" => "Missing to/data"], 400);
try {
$stmt = db()->prepare("INSERT INTO voice_signals (room_id, from_peer_id, to_peer_id, data, created_at_ms) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$room, $my_id, $to, $data, now_ms()]);
json_out(["success" => true]);
} catch (Exception $e) {
json_out(["error" => "Signal send failed: " . $e->getMessage()], 500);
}
}
if ($action === "list_all") {
// Periodic cleanup of the DB table (stale sessions > 15s)
if (rand(1, 10) === 1) {
try {
$stale_db_time = now_ms() - 15000;
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?");
$stmt->execute([$stale_db_time]);
} catch (Exception $e) {}
}
try {
$stmt = db()->prepare("
SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url
FROM voice_sessions vs
JOIN users u ON vs.user_id = u.id
WHERE vs.last_seen > ?
");
$stale_db_time = now_ms() - 15000;
$stmt->execute([$stale_db_time]);
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$by_channel = [];
foreach ($sessions as $s) {
$by_channel[$s['channel_id']][] = $s;
}
json_out(["success" => true, "channels" => $by_channel]);
} catch (Exception $e) {
json_out(["error" => $e->getMessage()], 500);
}
}
if ($action === "leave") {
if ($my_id) {
$p_file = room_participants_file($room);
$ps = read_json_file($p_file);
unset($ps[$my_id]);
write_json_file($p_file, $ps);
}
if ($current_user_id > 0) {
try {
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?");
$stmt->execute([$current_user_id]);
} catch (Exception $e) {}
}
json_out(["success" => true]);
}
json_out(["error" => "Unknown action"], 404);