gsm_core/platforms/webchat/
session.rs1use 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#[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#[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>;