verifierProf($seance); return view('seances.visio-prof', [ 'seance' => $seance->load(['classe', 'lab']), 'jeton' => $seance->visioOuverte() ? $this->jetons->pour($seance, 'prof-'.$prof->id, $prof->name, true) : null, ]); } public function ouvrir(Seance $seance): RedirectResponse { $this->verifierProf($seance); abort_if($seance->classe->estArchivee() || $seance->statut === Seance::TERMINEE, 409); if (! $seance->visioOuverte()) { $seance->update([ 'visio_ouverte_a' => now(), 'visio_salle' => 'syllabus-'.$seance->id.'-'.Str::lower(Str::random(16)), 'visio_chat' => false, ]); } return redirect()->route('seances.visio.prof', $seance); } public function fermer(Seance $seance): RedirectResponse { $this->verifierProf($seance); $seance->update(['visio_ouverte_a' => null, 'visio_salle' => null, 'visio_chat' => false]); Cache::forget($this->cle($seance)); return redirect()->route('seances.attente', $seance); } public function etatProf(Seance $seance): JsonResponse { $this->verifierProf($seance); $etat = $this->etat($seance); $limite = time() - self::PRESENCE_SECONDES; $mains = collect($etat['mains'])->sortBy('depuis')->map(fn ($m, $id) => $m + [ 'eleve_id' => $id, 'participant_id' => $etat['participants'][$id] ?? null, ])->values(); $attente = collect($etat['presences']) ->filter(fn ($p, $id) => $p['vu'] >= $limite && ! isset($etat['participants'][$id])) ->map(fn ($p) => ['nom' => $p['nom']])->values(); return response()->json([ 'ouverte' => $seance->visioOuverte(), 'chat' => $seance->visio_chat, 'mains' => $mains, 'attente' => $attente, ]); } /** * Le prof a donné la parole dans Jitsi (askToUnmute, qui autorise le * micro quand la modération est active) : la main est baissée. */ public function parole(Seance $seance, Eleve $eleve): JsonResponse { $this->verifierProf($seance); abort_if($eleve->classe_id !== $seance->classe_id, 404); $this->modifier($seance, function (array $etat) use ($eleve) { unset($etat['mains'][$eleve->id]); return $etat; }); return response()->json(['ok' => true]); } public function chat(Request $request, Seance $seance): JsonResponse { $this->verifierProf($seance); $seance->update(['visio_chat' => $request->boolean('actif')]); return response()->json(['chat' => $seance->visio_chat]); } // ---- Élève ------------------------------------------------------------ public function eleve(Seance $seance): View { $eleve = $this->verifierEleve($seance); return view('seances.visio-eleve', [ 'seance' => $seance->load(['classe', 'lab']), 'eleve' => $eleve, 'jeton' => $seance->visioOuverte() ? $this->jetons->pour($seance, 'eleve-'.$eleve->id, $this->nomVisible($eleve), false) : null, ]); } /** * Interrogé régulièrement par la page de l'élève : sert aussi de signe de * présence (salle d'attente côté prof). */ public function etatEleve(Seance $seance): JsonResponse { $eleve = $this->verifierEleve($seance); $etat = $this->modifier($seance, function (array $etat) use ($eleve) { $etat['presences'][$eleve->id] = ['nom' => $this->nomVisible($eleve), 'vu' => time()]; return $etat; }); return response()->json([ 'ouverte' => $seance->visioOuverte(), 'chat' => $seance->visio_chat, 'main_levee' => isset($etat['mains'][$eleve->id]), ]); } public function main(Request $request, Seance $seance): JsonResponse { $eleve = $this->verifierEleve($seance); $levee = $request->boolean('levee'); $this->modifier($seance, function (array $etat) use ($eleve, $levee) { if ($levee) { $etat['mains'][$eleve->id] ??= ['nom' => $this->nomVisible($eleve), 'depuis' => time()]; } else { unset($etat['mains'][$eleve->id]); } return $etat; }); return response()->json(['main_levee' => $levee]); } public function participant(Request $request, Seance $seance): JsonResponse { $eleve = $this->verifierEleve($seance); $participant = $request->validate(['participant_id' => ['required', 'string', 'max:64', 'regex:/^[A-Za-z0-9_-]+$/']])['participant_id']; $this->modifier($seance, function (array $etat) use ($eleve, $participant) { $etat['participants'][$eleve->id] = $participant; return $etat; }); return response()->json(['ok' => true]); } // ---- Interne ---------------------------------------------------------- private function verifierProf(Seance $seance): User { $prof = Auth::guard('web')->user(); abort_if($seance->classe->user_id !== $prof->id, 403); return $prof; } private function verifierEleve(Seance $seance): Eleve { $eleve = Auth::guard('eleve')->user(); abort_if($eleve->classe_id !== $seance->classe_id || $seance->classe->estArchivee(), 403); return $eleve; } /** * Minimisation : visible de toute la classe, donc prénom et initiale du nom. */ private function nomVisible(Eleve $eleve): string { return $eleve->prenom.($eleve->nom ? ' '.mb_strtoupper(mb_substr($eleve->nom, 0, 1)).'.' : ''); } /** * Lu aussi par le tableau de bord du prof (ClasseController), d'où la * visibilité publique de la clé et de ce compteur — jamais du contenu * complet de l'état (noms des élèves), seulement un chiffre. */ public static function cleEtat(Seance $seance): string { return "visio:{$seance->id}"; } public static function nombreMainsLevees(Seance $seance): int { return count(self::mainsLevees($seance)); } /** * Mains levées, premier arrivé en premier (même tri que etatProf) : nom * déjà minimisé (prénom + initiale), rien de plus que ce qu'affiche * déjà la page de visio. * * @return list */ public static function mainsLevees(Seance $seance): array { /** @var array $mains */ $mains = (Cache::get(self::cleEtat($seance)) ?? [])['mains'] ?? []; return collect($mains) ->sortBy('depuis') ->map(fn ($m, $id) => ['eleve_id' => (int) $id, 'nom' => $m['nom'], 'depuis' => $m['depuis']]) ->values() ->all(); } private function cle(Seance $seance): string { return self::cleEtat($seance); } /** * @return array{mains: array, participants: array, presences: array} */ private function etat(Seance $seance): array { return Cache::get($this->cle($seance)) ?? ['mains' => [], 'participants' => [], 'presences' => []]; } /** * Lecture-modification-écriture sous verrou : deux élèves qui lèvent la * main au même instant ne s'écrasent pas. * * @param callable(array{mains: array, participants: array, presences: array}): array{mains: array, participants: array, presences: array} $changement * @return array{mains: array, participants: array, presences: array} */ private function modifier(Seance $seance, callable $changement): array { return Cache::lock($this->cle($seance).':verrou', 5)->block(3, function () use ($seance, $changement) { $etat = $changement($this->etat($seance)); Cache::put($this->cle($seance), $etat, self::DUREE_ETAT); return $etat; }); } }