use std::collections::HashMap;
use std::sync::Arc;
use log::{debug, info};
use reqwest::Client;
use serde::Deserialize;
use serde_json::json;
use tokio::sync::mpsc;
use tokio_tungstenite::connect_async_with_config;
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
use url::Url;
use super::session::{Session, SessionOptions};
type EventHandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
#[derive(Debug, Deserialize)]
struct WsEndpointApiResponse<T> {
#[serde(default)]
code: i32,
#[serde(default)]
msg: String,
data: Option<T>,
}
fn map_ws_api_error(code: i32, message: String) -> WsClientError {
match code {
1 | 1000040343 => WsClientError::ServerError { code, message },
_ => WsClientError::ClientError { code, message },
}
}
fn extract_endpoint_response(
resp: WsEndpointApiResponse<EndPointResponse>,
) -> WsClientResult<EndPointResponse> {
if resp.code != 0 {
return Err(map_ws_api_error(resp.code, resp.msg));
}
let end_point = resp.data.ok_or(WsClientError::UnexpectedResponse)?;
if end_point.url.as_ref().is_none_or(|url| url.is_empty()) {
return Err(WsClientError::ServerError {
code: 500,
message: "No available endpoint".to_string(),
});
}
Ok(end_point)
}
#[derive(Debug, Deserialize)]
struct RawEventEnvelope {
header: RawEventHeader,
}
#[derive(Debug, Deserialize)]
struct RawEventHeader {
#[serde(default)]
event_type: String,
}
pub trait EventHandler: Send + Sync + 'static {
fn handle(&self, payload: &[u8]) -> EventHandlerResult;
}
#[derive(Clone)]
pub struct EventDispatcherHandler {
payload_tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
raw_handlers: HashMap<String, Arc<dyn EventHandler>>,
}
impl std::fmt::Debug for EventDispatcherHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventDispatcherHandler")
.field(
"payload_tx",
&self.payload_tx.as_ref().map(|_| "configured"),
)
.field(
"raw_handler_keys",
&self.raw_handlers.keys().collect::<Vec<_>>(),
)
.finish()
}
}
impl EventDispatcherHandler {
pub const RAW_EVENT_KEY: &'static str = "raw";
pub fn builder() -> Self {
Self {
payload_tx: None,
raw_handlers: HashMap::new(),
}
}
pub fn build(self) -> Self {
self
}
pub fn payload_sender(mut self, payload_tx: mpsc::UnboundedSender<Vec<u8>>) -> Self {
self.payload_tx = Some(payload_tx);
self
}
pub fn register_raw<S, H>(mut self, key: S, handler: H) -> Result<Self, String>
where
S: Into<String>,
H: EventHandler,
{
let key = key.into();
if key.trim().is_empty() {
return Err("processor key cannot be empty".to_string());
}
if self.raw_handlers.contains_key(&key) {
return Err(format!("processor already registered, type: {key}"));
}
self.raw_handlers.insert(key, Arc::new(handler));
Ok(self)
}
fn extract_event_type(payload: &[u8]) -> Option<String> {
serde_json::from_slice::<RawEventEnvelope>(payload)
.ok()
.map(|event| event.header.event_type)
.filter(|event_type| !event_type.trim().is_empty())
}
fn dispatch_raw_handler(&self, key: &str, payload: &[u8]) -> Result<(), String> {
if let Some(handler) = self.raw_handlers.get(key) {
handler
.handle(payload)
.map_err(|err| format!("处理原始事件 {key} 失败: {err}"))?;
}
Ok(())
}
pub fn do_without_validation(&self, payload: &[u8]) -> Result<(), String> {
if let Some(payload_tx) = &self.payload_tx {
payload_tx
.send(payload.to_vec())
.map_err(|e| format!("转发事件负载失败: {e}"))?;
}
if let Some(event_type) = Self::extract_event_type(payload) {
self.dispatch_raw_handler(&event_type, payload)?;
}
self.dispatch_raw_handler(Self::RAW_EVENT_KEY, payload)?;
Ok(())
}
}
const END_POINT_URL: &str = "/callback/ws/endpoint";
pub struct LarkWsClient;
impl LarkWsClient {
pub async fn open(
config: Arc<openlark_core::config::Config>,
event_handler: EventDispatcherHandler,
) -> WsClientResult<()> {
Self::open_with(config, event_handler, SessionOptions::default()).await
}
pub(crate) async fn open_with(
config: Arc<openlark_core::config::Config>,
event_handler: EventDispatcherHandler,
options: SessionOptions,
) -> WsClientResult<()> {
let end_point = Self::get_conn_url(&config).await?;
let conn_url = end_point.url.ok_or(WsClientError::UnexpectedResponse)?;
let client_config = end_point
.client_config
.ok_or(WsClientError::UnexpectedResponse)?;
let url = Url::parse(&conn_url)?;
let query_pairs: HashMap<_, _> = url.query_pairs().into_iter().collect();
let service_id = query_pairs
.get("service_id")
.ok_or(WsClientError::UnexpectedResponse)?
.parse()
.map_err(|_| WsClientError::UnexpectedResponse)?;
let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
.max_message_size(Some(config.max_response_size() as usize))
.max_frame_size(Some(config.max_response_size() as usize));
let (conn, _response) = connect_async_with_config(conn_url, Some(ws_config), false).await?;
info!("connected to {url}");
Session::new(service_id, client_config, conn, event_handler, options)
.run()
.await
}
async fn get_conn_url(
config: &Arc<openlark_core::config::Config>,
) -> WsClientResult<EndPointResponse> {
let body = json!({
"AppID": config.app_id(),
"AppSecret": config.app_secret()
});
let mut http_builder = Client::builder();
if let Some(timeout) = config.req_timeout() {
http_builder = http_builder.timeout(timeout);
}
let http_client = http_builder.build()?;
let base_url = config.base_url().trim_end_matches('/');
let req = http_client
.post(format!("{base_url}{END_POINT_URL}"))
.header("locale", "zh")
.json(&body)
.send()
.await?;
let resp = req
.json::<WsEndpointApiResponse<EndPointResponse>>()
.await?;
debug!("{:?}", resp.data);
extract_endpoint_response(resp)
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct EndPointResponse {
#[serde(rename = "URL")]
pub url: Option<String>,
#[serde(rename = "ClientConfig")]
pub client_config: Option<ClientConfig>,
}
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct ClientConfig {
#[serde(rename = "PingInterval")]
pub(crate) ping_interval: i32,
}
pub type WsClientResult<T> = Result<T, WsClientError>;
#[derive(Debug, thiserror::Error)]
pub enum WsClientError {
#[error("unexpected response")]
UnexpectedResponse,
#[error("Request error: {0}")]
RequestError(#[from] reqwest::Error),
#[error("Url parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Server error: {code}, {message}")]
ServerError {
code: i32,
message: String,
},
#[error("Client error: {code}, {message}")]
ClientError {
code: i32,
message: String,
},
#[error("connection closed")]
ConnectionClosed {
reason: Option<WsCloseReason>,
},
#[error("WebSocket error: {0}")]
WsError(Box<tokio_tungstenite::tungstenite::Error>),
#[error("Prost error: {0}")]
ProstError(#[from] prost::DecodeError),
#[error("malformed control frame: {message}")]
MalformedControlFrame {
message: String,
},
#[error("invalid session state transition: {kind}")]
InvalidStateTransition {
kind: InvalidStateKind,
},
#[error("event handler panicked")]
HandlerPanicked,
#[error("handler backlog full: {message}")]
BacklogFull {
message: String,
},
#[error("invalid frame method: {method}")]
InvalidFrameMethod {
method: i32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InvalidStateKind {
ExpectedActive {
actual: &'static str,
},
DataWhileClosing,
AlreadyClosed,
WorkerGone,
}
impl std::fmt::Display for InvalidStateKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedActive { actual } => {
write!(f, "session is {actual}, expected Active")
}
Self::DataWhileClosing => {
write!(f, "received data frame while session is Closing")
}
Self::AlreadyClosed => write!(f, "session already Closed"),
Self::WorkerGone => write!(f, "handler worker is gone"),
}
}
}
impl From<tokio_tungstenite::tungstenite::Error> for WsClientError {
fn from(error: tokio_tungstenite::tungstenite::Error) -> Self {
WsClientError::WsError(Box::new(error))
}
}
#[derive(Debug, Clone)]
pub struct WsCloseReason {
pub code: CloseCode,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::{
WsClientError, WsEndpointApiResponse, extract_endpoint_response, map_ws_api_error,
};
#[test]
fn test_ws_endpoint_error_response_not_treated_as_success() {
let payload = r#"{"code":400,"msg":"Bad Request"}"#;
let parsed = serde_json::from_str::<WsEndpointApiResponse<serde_json::Value>>(payload)
.expect("endpoint response should deserialize");
assert_eq!(parsed.code, 400);
assert_eq!(parsed.msg, "Bad Request");
assert!(parsed.data.is_none());
let mapped = map_ws_api_error(parsed.code, parsed.msg);
assert!(matches!(
mapped,
WsClientError::ClientError { code: 400, .. }
));
}
#[test]
fn test_ws_endpoint_success_without_data_returns_unexpected_response() {
let resp = WsEndpointApiResponse::<super::EndPointResponse> {
code: 0,
msg: "success".to_string(),
data: None,
};
let result = extract_endpoint_response(resp);
assert!(matches!(result, Err(WsClientError::UnexpectedResponse)));
}
#[test]
fn test_ws_endpoint_server_error_mapping_is_preserved() {
let mapped = map_ws_api_error(1000040343, "No available endpoint".to_string());
assert!(matches!(
mapped,
WsClientError::ServerError {
code: 1000040343,
..
}
));
}
}