Skip to main content

combat_service/
services.rs

1//! Reaching the other services as this service itself: it logs in to the
2//! auth service with its own key (registered there with `auth-service
3//! add-service`) to get a token for each campaign it works for. Character
4//! sheets are kept in step through it (see [`crate::sync`]), and monsters'
5//! stat blocks fetched, to add them to a fight.
6
7use std::{collections::HashMap, sync::Arc};
8
9use chrono::Utc;
10use edda_auth::{DeviceKey, PublicKey, protocol::Session};
11use edda_client::{ClientError, auth::AuthClient, monsters::MonsterClient};
12use edda_types::monster::Monster;
13use tokio::sync::Mutex;
14use uuid::Uuid;
15
16/// Which other services to use, and how to log in to them.
17#[derive(Debug, Clone)]
18pub struct ServicesConfig {
19    pub auth_service: String,
20    /// The auth service's token key, checked when logging in.
21    pub auth_key: PublicKey,
22    /// This service's own key.
23    pub key: Arc<DeviceKey>,
24    /// Where character sheets are kept in step with combats: hit points
25    /// written back, slots and feature uses spent. `None` leaves sheets
26    /// alone.
27    pub character_service: Option<String>,
28    /// Where monsters' stat blocks come from, to add monsters to a fight.
29    /// `None` refuses `AddMonsters` and fights from plans.
30    pub monster_service: Option<String>,
31}
32
33/// Logged in to each campaign this service works for.
34pub struct ServiceLogin {
35    auth: AuthClient,
36    auth_key: PublicKey,
37    key: Arc<DeviceKey>,
38    /// A token per campaign, reused until shortly before it expires.
39    sessions: Mutex<HashMap<Uuid, Session>>,
40}
41
42impl ServiceLogin {
43    /// Logs in as configured, when a token is first needed.
44    pub fn new(config: &ServicesConfig) -> Self {
45        Self {
46            auth: AuthClient::new(&config.auth_service),
47            auth_key: config.auth_key,
48            key: config.key.clone(),
49            sessions: Mutex::new(HashMap::new()),
50        }
51    }
52
53    /// A token for `campaign`.
54    pub async fn token(&self, campaign: Uuid) -> Result<String, ClientError> {
55        let mut sessions = self.sessions.lock().await;
56        let fresh = |s: &Session| s.claims.expires_at > Utc::now() + chrono::Duration::minutes(1);
57        if let Some(session) = sessions.get(&campaign).filter(|s| fresh(s)) {
58            return Ok(session.token.clone());
59        }
60        let session = self
61            .auth
62            .login(campaign, &self.key, Some(self.auth_key))
63            .await?;
64        let token = session.token.clone();
65        sessions.insert(campaign, session);
66        Ok(token)
67    }
68}
69
70/// The campaigns' monsters, from the monster service.
71pub struct Monsters {
72    login: Arc<ServiceLogin>,
73    url: String,
74}
75
76impl Monsters {
77    /// The monster service at `url`, logged in to with `login`.
78    pub fn new(login: Arc<ServiceLogin>, url: String) -> Self {
79        Self { login, url }
80    }
81
82    /// A monster `campaign` can use: one of its own or a shared one.
83    pub async fn get(&self, campaign: Uuid, id: Uuid) -> Result<Monster, ClientError> {
84        let token = self.login.token(campaign).await?;
85        MonsterClient::new(&self.url)
86            .with_token(&token)
87            .get(id)
88            .await
89    }
90}