1//! `/voices`: the voices the configured account can use, names and ids only. 2//! 3//! The list is kept for a day: it changes when someone adds a voice to the account, and asking costs a call 4//! out. A failure to refresh serves the voices already known and says they are stale; with none known the 5//! route says plainly that the list cannot be read, so the app keeps the voice it has. 6 7use std::sync::{Mutex, PoisonError}; 8 9use ::log::{debug, info, warn}; 10use serde::Serialize; 11use whiskers_ports::{Clock, HasClock, HasVoice, Millis, Voice, VoiceInfo, VoicesError}; 12 13use super::{Route, sealed}; 14use crate::Response; 15 16/// How long a list is served before the account is asked again. 17pub const VOICES_KEPT_MS: u64 = 24 * 60 * 60 * 1000; 18 19/// `POST /voices`. Needs the voice and the clock. 20#[derive(Default)] 21pub struct Voices { 22 known: Mutex<Option<(Millis, Vec<VoiceInfo>)>>, 23} 24 25impl sealed::Sealed for Voices {} 26 27#[derive(Serialize)] 28struct Reply<'a> { 29 voices: &'a [VoiceInfo], 30 /// The household's base voice, which a character with none of its own speaks in. 31 default: Option<&'a whiskers_ports::VoiceId>, 32 /// The list is an older one, because the account could not be asked just now. 33 stale: bool, 34} 35 36impl<C: HasVoice + HasClock> Route<C> for Voices { 37 const PATHS: &'static [&'static str] = &["/voices"]; 38 async fn answer(&self, adapter: &C, _: &str, _: &str) -> Response { 39 let now = adapter.clock().now(); 40 let default = adapter.voice().base_voice().await; 41 let known = self.known.lock().unwrap_or_else(PoisonError::into_inner).clone(); 42 if let Some((at, list)) = &known 43 && now.get().saturating_sub(at.get()) < VOICES_KEPT_MS 44 { 45 debug!("/voices: {} voices from the list kept", list.len()); 46 return Response::of(&Reply { voices: list, default: default.as_ref(), stale: false }); 47 } 48 match adapter.voice().voices().await { 49 Ok(list) => { 50 info!("/voices: the account lists {} voices", list.len()); 51 *self.known.lock().unwrap_or_else(PoisonError::into_inner) = Some((now, list.clone())); 52 Response::of(&Reply { voices: &list, default: default.as_ref(), stale: false }) 53 } 54 Err(VoicesError::NotConfigured) => { 55 warn!("/voices refused: the voice is not configured"); 56 Response::text(503, VoicesError::NotConfigured.to_string()) 57 } 58 Err(e) => match known { 59 Some((_, list)) => { 60 warn!("/voices: the account could not be asked ({e}); serving {} voices already known", list.len()); 61 Response::of(&Reply { voices: &list, default: default.as_ref(), stale: true }) 62 } 63 None => { 64 warn!("/voices: the account could not be asked and none is known: {e}"); 65 Response::text(502, format!("the voice list cannot be read: {e}")) 66 } 67 }, 68 } 69 } 70}