/voices: the voices the configured account can use, names and ids only.

The list is kept for a day: it changes when someone adds a voice to the account, and asking costs a call out. A failure to refresh serves the voices already known and says they are stale; with none known the route says plainly that the list cannot be read, so the app keeps the voice it has.

7use std::sync::{Mutex, PoisonError};
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;

How long a list is served before the account is asked again.

17pub const VOICES_KEPT_MS: u64 = 24 * 60 * 60 * 1000;

POST /voices. Needs the voice and the clock.

20#[derive(Default)]
21pub struct Voices {
22    known: Mutex<Option<(Millis, Vec<VoiceInfo>)>>,
23}
25impl sealed::Sealed for Voices {}
26
27#[derive(Serialize)]
28struct Reply<'a> {
29    voices: &'a [VoiceInfo],

The household's base voice, which a character with none of its own speaks in.

31    default: Option<&'a whiskers_ports::VoiceId>,

The list is an older one, because the account could not be asked just now.

33    stale: bool,
34}
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}