wx-bot-sdk 0.1.3

Standalone Weixin Bot SDK in Rust
Documentation
use rand::Rng;
use std::{
    collections::HashMap,
    time::{Duration, Instant},
};

use super::{WeixinApiOptions, get_config};
use crate::util::Logger;

#[derive(Clone, Debug, Default)]
pub struct CachedConfig {
    pub typing_ticket: Option<String>,
}

#[derive(Clone, Debug)]
struct CacheEntry {
    config: CachedConfig,
    fetched_at: Instant,
    ttl: Duration,
    fail_count: u32,
}

const DEFAULT_TTL_MS: u64 = 24 * 60 * 60 * 1000;
const INITIAL_RETRY_MS: u64 = 2_000;
const MAX_RETRY_MS: u64 = 60 * 60 * 1000;

#[derive(Clone, Debug)]
pub struct WeixinConfigManager {
    cache: HashMap<String, CacheEntry>,
    api_opts: WeixinApiOptions,
    log: Logger,
}

impl WeixinConfigManager {
    pub fn new(api_opts: WeixinApiOptions, log: Logger) -> Self {
        Self {
            cache: HashMap::new(),
            api_opts,
            log,
        }
    }

    pub async fn get_for_user(
        &mut self,
        user_id: &str,
        context_token: Option<&str>,
    ) -> CachedConfig {
        let should_fetch = self
            .cache
            .get(user_id)
            .map(|e| e.fetched_at.elapsed() >= e.ttl)
            .unwrap_or(true);
        if should_fetch {
            if let Ok(resp) = get_config(&self.api_opts, user_id, context_token).await {
                if resp.ret == Some(0) {
                    let ttl = rand::thread_rng().gen_range(0..DEFAULT_TTL_MS.max(1));
                    let entry = CacheEntry {
                        config: CachedConfig {
                            typing_ticket: Some(resp.typing_ticket.unwrap_or_default()),
                        },
                        fetched_at: Instant::now(),
                        ttl: Duration::from_millis(ttl),
                        fail_count: 0,
                    };
                    self.cache.insert(user_id.to_string(), entry);
                    return self.cache[user_id].config.clone();
                }
            } else {
                self.log.warn(format!("getConfig failed for {user_id}"));
            }
            let prev = self.cache.get(user_id).map(|e| e.fail_count).unwrap_or(0);
            let retry =
                (INITIAL_RETRY_MS.saturating_mul(2_u64.saturating_pow(prev))).min(MAX_RETRY_MS);
            self.cache
                .entry(user_id.to_string())
                .and_modify(|e| {
                    e.fail_count = prev + 1;
                    e.ttl = Duration::from_millis(retry);
                    e.fetched_at = Instant::now();
                })
                .or_insert(CacheEntry {
                    config: CachedConfig {
                        typing_ticket: Some(String::new()),
                    },
                    fetched_at: Instant::now(),
                    ttl: Duration::from_millis(retry),
                    fail_count: 1,
                });
        }
        self.cache
            .get(user_id)
            .map(|e| e.config.clone())
            .unwrap_or_default()
    }

    pub fn clear(&mut self, user_id: &str) {
        self.cache.remove(user_id);
    }
    pub fn clear_all(&mut self) {
        self.cache.clear();
    }
}