Skip to main content

wowlab_node/
claim.rs

1use wowlab_common::node_http::{NodeRegistrationAccessRule, NodeRegistrationRequest};
2use wowlab_parsers::AccessRule;
3
4use crate::{
5    config::NodeConfig,
6    sentinel::{SentinelClient, SentinelError},
7};
8
9const TOKEN_PREFIX: &str = "wlab_claim_";
10const TOKEN_LEN: usize = 43;
11const DEFAULT_CORE_FALLBACK: i32 = 4;
12
13/// Invalid claim-token syntax.
14#[derive(Debug, thiserror::Error)]
15#[error("{kind}")]
16pub struct ClaimTokenError {
17    kind: ClaimTokenErrorKind,
18}
19
20#[derive(Debug, thiserror::Error)]
21enum ClaimTokenErrorKind {
22    #[error("")]
23    Empty,
24    #[error("Token must start with '{TOKEN_PREFIX}'")]
25    WrongPrefix,
26    #[error("Token must be {TOKEN_LEN} characters (currently {actual})")]
27    WrongLength { actual: usize },
28}
29
30impl ClaimTokenError {
31    const fn new(kind: ClaimTokenErrorKind) -> Self {
32        Self { kind }
33    }
34
35    #[must_use]
36    pub const fn is_empty(&self) -> bool {
37        matches!(self.kind, ClaimTokenErrorKind::Empty)
38    }
39}
40
41/// Validate a claim token format.
42///
43/// # Errors
44///
45/// Returns a user-facing validation error when the token is empty, has the wrong prefix, or has the wrong length.
46pub fn validate_token(token: &str) -> Result<(), ClaimTokenError> {
47    if token.is_empty() {
48        return Err(ClaimTokenError::new(ClaimTokenErrorKind::Empty));
49    }
50
51    if !token.starts_with(TOKEN_PREFIX) {
52        return Err(ClaimTokenError::new(ClaimTokenErrorKind::WrongPrefix));
53    }
54
55    if token.len() != TOKEN_LEN {
56        return Err(ClaimTokenError::new(ClaimTokenErrorKind::WrongLength {
57            actual: token.len(),
58        }));
59    }
60
61    Ok(())
62}
63
64pub(crate) fn default_name() -> String {
65    hostname::get()
66        .ok()
67        .and_then(|h| h.into_string().ok())
68        .unwrap_or_else(|| "WoW Lab Node".to_string())
69}
70
71pub(crate) fn total_cores() -> i32 {
72    i32::try_from(wowlab_common::sys::logical_cores()).unwrap_or(DEFAULT_CORE_FALLBACK)
73}
74
75pub(crate) fn default_enabled_cores() -> i32 {
76    i32::try_from(wowlab_common::sys::optimal_concurrency()).unwrap_or(DEFAULT_CORE_FALLBACK)
77}
78
79pub(crate) fn platform() -> String {
80    let os = std::env::consts::OS;
81    let arch = std::env::consts::ARCH;
82
83    format!("{os}-{arch}")
84}
85
86pub(crate) async fn register(
87    client: &SentinelClient,
88    config: &NodeConfig,
89    token_claim: &str,
90) -> Result<(), SentinelError> {
91    let platform = platform();
92    let version = env!("CARGO_PKG_VERSION");
93    let access_rules = config
94        .access
95        .rules
96        .iter()
97        .map(|rule| match rule {
98            AccessRule::Public => NodeRegistrationAccessRule {
99                access_type: "public".to_string(),
100                target_id: None,
101            },
102            AccessRule::Discord(id) => NodeRegistrationAccessRule {
103                access_type: "discord".to_string(),
104                target_id: Some(id.clone()),
105            },
106            AccessRule::Friends(id) => NodeRegistrationAccessRule {
107                access_type: "friends".to_string(),
108                target_id: Some(id.clone()),
109            },
110        })
111        .collect();
112
113    client
114        .register_node(&NodeRegistrationRequest {
115            token_claim: token_claim.to_string(),
116            hostname: Some(config.name.clone()),
117            total_cores: Some(config.total_cores),
118            enabled_cores: Some(config.enabled_cores),
119            platform: Some(platform),
120            version: Some(version.to_string()),
121            access_rules,
122        })
123        .await?;
124    tracing::info!(name = %config.name, "Registered node");
125
126    Ok(())
127}