Skip to main content

gsm_core/platforms/webchat/
session.rs

1use std::{collections::HashMap, sync::Arc};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use greentic_types::TenantCtx;
6use time::OffsetDateTime;
7use tokio::sync::RwLock;
8
9/// In-memory representation of the WebChat conversation session.
10#[derive(Clone, Debug)]
11pub struct WebchatSession {
12    pub conversation_id: String,
13    pub tenant_ctx: TenantCtx,
14    pub bearer_token: String,
15    pub watermark: Option<String>,
16    pub last_seen_at: OffsetDateTime,
17    pub proactive_ok: bool,
18}
19
20impl WebchatSession {
21    pub fn new(conversation_id: String, tenant_ctx: TenantCtx, bearer_token: String) -> Self {
22        Self {
23            conversation_id,
24            tenant_ctx,
25            bearer_token,
26            watermark: None,
27            last_seen_at: OffsetDateTime::now_utc(),
28            proactive_ok: true,
29        }
30    }
31}
32
33#[async_trait]
34pub trait WebchatSessionStore: Send + Sync {
35    async fn get(&self, conversation_id: &str) -> Result<Option<WebchatSession>>;
36    async fn upsert(&self, session: WebchatSession) -> Result<()>;
37    async fn update_watermark(
38        &self,
39        conversation_id: &str,
40        watermark: Option<String>,
41    ) -> Result<()>;
42    async fn update_bearer_token(&self, conversation_id: &str, token: String) -> Result<()>;
43    async fn set_proactive(&self, conversation_id: &str, proactive_ok: bool) -> Result<()>;
44    async fn list_by_tenant(
45        &self,
46        env: &str,
47        tenant: &str,
48        team: Option<&str>,
49    ) -> Result<Vec<WebchatSession>>;
50}
51
52/// Simple in-memory session store used by the standalone server.
53#[derive(Default)]
54pub struct MemorySessionStore {
55    inner: RwLock<HashMap<String, WebchatSession>>,
56}
57
58impl MemorySessionStore {
59    pub fn new() -> Self {
60        Self::default()
61    }
62}
63
64#[async_trait]
65impl WebchatSessionStore for MemorySessionStore {
66    async fn get(&self, conversation_id: &str) -> Result<Option<WebchatSession>> {
67        let guard = self.inner.read().await;
68        Ok(guard.get(conversation_id).cloned())
69    }
70
71    async fn upsert(&self, session: WebchatSession) -> Result<()> {
72        let mut guard = self.inner.write().await;
73        guard.insert(session.conversation_id.clone(), session);
74        Ok(())
75    }
76
77    async fn update_watermark(
78        &self,
79        conversation_id: &str,
80        watermark: Option<String>,
81    ) -> Result<()> {
82        let mut guard = self.inner.write().await;
83        if let Some(existing) = guard.get_mut(conversation_id) {
84            existing.watermark = watermark;
85            existing.last_seen_at = OffsetDateTime::now_utc();
86        }
87        Ok(())
88    }
89
90    async fn update_bearer_token(&self, conversation_id: &str, token: String) -> Result<()> {
91        let mut guard = self.inner.write().await;
92        if let Some(existing) = guard.get_mut(conversation_id) {
93            existing.bearer_token = token;
94            existing.last_seen_at = OffsetDateTime::now_utc();
95        }
96        Ok(())
97    }
98
99    async fn set_proactive(&self, conversation_id: &str, proactive_ok: bool) -> Result<()> {
100        let mut guard = self.inner.write().await;
101        if let Some(existing) = guard.get_mut(conversation_id) {
102            existing.proactive_ok = proactive_ok;
103            existing.last_seen_at = OffsetDateTime::now_utc();
104        }
105        Ok(())
106    }
107
108    async fn list_by_tenant(
109        &self,
110        env: &str,
111        tenant: &str,
112        team: Option<&str>,
113    ) -> Result<Vec<WebchatSession>> {
114        let env_lower = env.to_ascii_lowercase();
115        let tenant_lower = tenant.to_ascii_lowercase();
116        let team_lower = team.map(|value| value.to_ascii_lowercase());
117
118        let guard = self.inner.read().await;
119        Ok(guard
120            .values()
121            .filter(|session| {
122                session
123                    .tenant_ctx
124                    .env
125                    .as_ref()
126                    .eq_ignore_ascii_case(&env_lower)
127                    && session
128                        .tenant_ctx
129                        .tenant
130                        .as_ref()
131                        .eq_ignore_ascii_case(&tenant_lower)
132                    && match (&team_lower, session.tenant_ctx.team.as_ref()) {
133                        (Some(expected), Some(actual)) => {
134                            actual.as_ref().eq_ignore_ascii_case(expected)
135                        }
136                        (Some(_), None) => false,
137                        (None, _) => true,
138                    }
139            })
140            .cloned()
141            .collect())
142    }
143}
144
145pub type SharedSessionStore = Arc<dyn WebchatSessionStore>;