# Events, WebSockets, and channels Webhook handlers use `EventDispatcher` for typed event callbacks, signature verification, and encrypted payload processing. The optional `ws` feature adds long connections for event delivery; see [`examples/ws_client.rs`](../examples/ws_client.rs). Trusted user channels match the upstream Go SDK behavior. Configure the channel before obtaining its control handle, start the client, then bind a user token after the connection becomes ready. See [`examples/ws_client.rs`](../examples/ws_client.rs) for the connection lifecycle and [`examples/channel_send.rs`](../examples/channel_send.rs) for higher-level messages. ```rust,no_run use std::collections::HashMap; use std::time::Duration; # use larksuite_oapi_sdk_rs::{EventDispatcher, LarkClient}; # #[tokio::main] # async fn main() -> Result<(), Box> { # let client = LarkClient::builder("APP_ID", "APP_SECRET").build()?; # let dispatcher = EventDispatcher::new("VERIFICATION_TOKEN", "ENCRYPT_KEY"); let ws_client = client .ws_client(dispatcher) .channel_tag("trusted_channel") .websocket_query_params(HashMap::from([("env".into(), "staging".into())])); let control = ws_client.control(); let task = tokio::spawn(ws_client.start()); while control.connection_id().is_none() { tokio::time::sleep(Duration::from_millis(50)).await; } control.attach_user("USER_ACCESS_TOKEN").await?; // Later, before dropping the user session: // control.detach_user("USER_ACCESS_TOKEN").await?; control.close_and_wait().await; task.await??; # Ok(()) # } ``` `WsClientControl::close` requests shutdown without waiting, which is safe from lifecycle callbacks. `close_and_wait` is for external teardown and waits until the running client has stopped. The client honors a finite reconnect count from the gateway bootstrap configuration, treats an inbound-silent connection as failed after two ping intervals plus five seconds, and reconnects after either that liveness timeout or a timed-out connection write. It bounds each connection write to ten seconds by default and accepts `write_timeout` for a different bound. Event callbacks run independently of the receive loop, so a slow callback does not mask a silent gateway. Use `websocket_connector` when a custom proxy, TLS, or test dial transport is needed; it affects only the WebSocket gateway dial, not the bootstrap HTTP client. ## Calendar share-token joins Calendar invitations provide an opaque share token through their link, QR code, or RSVP card. Join with that token; the endpoint deliberately has no event-ID alternative. ```rust,no_run use larksuite_oapi_sdk_rs::service::calendar::v4::JoinCalendarEventReqBody; use larksuite_oapi_sdk_rs::{LarkClient, RequestOption}; # async fn example() -> Result<(), larksuite_oapi_sdk_rs::LarkError> { let client = LarkClient::builder("APP_ID", "APP_SECRET").build()?; client .calendar() .calendar .join_event( &JoinCalendarEventReqBody::new("CALENDAR_SHARE_TOKEN"), &RequestOption::default(), ) .await?; # Ok(()) # } ``` Use the optional `axum` feature for Axum HTTP adapters. The optional `messaging` feature provides outbound send, reply, update, upload, and download operations without WebSocket dependencies. The optional `channel` feature adds inbound message normalization and runtime policy controls and enables both `messaging` and `ws`. ## Replies `ChannelMessaging::send` preserves convenience behavior: a reply whose target has disappeared may retry as a top-level send. For workflows that must never escape a thread, use `ChannelMessaging::reply` or `ChannelMessaging::reply_in_thread`. Both methods only call the reply endpoint and return its error unchanged. Set `uuid` to opt into Lark's one-hour idempotency window; automatic split messages derive distinct UUIDs, so the supplied UUID must leave room for a `-N` suffix. ```rust,no_run use larksuite_oapi_sdk_rs::channel::SendInput; use larksuite_oapi_sdk_rs::{LarkClient, RequestOption}; # async fn run() -> Result<(), Box> { let client = LarkClient::builder("APP_ID", "APP_SECRET").build()?; let messaging = client.channel_messaging(); messaging .reply_in_thread( "om_parent_message", &SendInput { markdown: Some("A reply that stays in the topic".into()), uuid: Some("order-42-status".into()), ..Default::default() }, &RequestOption::default(), ) .await?; # Ok(()) # } ``` ## Message updates Use `ChannelMessaging::edit_text` to update a text message. Use `ChannelMessaging::edit_card` only for validated interactive-card documents; it uses the separate card patch operation. See [`examples/card_send.rs`](../examples/card_send.rs) for Card JSON delivery and [`examples/card_action_handler.rs`](../examples/card_action_handler.rs) for interactive callback responses. ```rust,no_run use larksuite_oapi_sdk_rs::card::v1::{Card, CardDocument, Div, Element, Header, Text}; use larksuite_oapi_sdk_rs::{LarkClient, RequestOption}; # async fn run() -> Result<(), Box> { let client = LarkClient::builder("APP_ID", "APP_SECRET").build()?; let messaging = client.channel_messaging(); let card = CardDocument::new( Card::new() .header(Header::new("Done")) .element(Element::Div(Div::new(Text::lark_md("Complete")))), )?; messaging .edit_text("om_text_message", "Updated status", &RequestOption::default()) .await?; messaging .edit_card( "om_card_message", &card, &RequestOption::default(), ) .await?; # Ok(()) # } ``` ## Cards Use `card::v1` or `card::v2` to build versioned Card JSON, and validate it with their `CardDocument` types before sending. `CardActionHandler` handles callback payloads. See the [Cards guide](cards.md) for CardKit and published Card Builder templates.