Skip to content

Commit f132301

Browse files
authored
Merge pull request #61 from sgrimee/master
feat: add leave_room, improve error handling, and refactor codebase
2 parents 6192708 + 77dc046 commit f132301

18 files changed

Lines changed: 1206 additions & 101 deletions

.envrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use flake
2+
3+
watch_file flake.nix
4+
watch_file flake.lock
5+
watch_file rust-toolchain.toml
6+
7+
dotenv_if_exists .env

.gitignore

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Generated files
22
/target/
33

4-
# The library shouldn't decide about the exact versions of
5-
# its dependencies, but let the downstream crate decide.
6-
Cargo.lock
4+
Cargo.lock
5+
.claude/settings.local.json

.ignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.direnv

AGENTS.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is `webex-rust`, an asynchronous Rust library providing a minimal interface to Webex Teams APIs. It's designed primarily for building bots but supports general API interactions.
8+
9+
## Commands
10+
11+
### Build and Test
12+
- `cargo build` - Build the library
13+
- `cargo test` - Run unit tests
14+
- `cargo clippy` - Run linter (note: very strict clippy rules enabled)
15+
- `cargo fmt` - Format code
16+
- `cargo doc` - Generate documentation
17+
18+
### Examples
19+
- `cargo run --example hello-world` - Basic message sending example
20+
- `cargo run --example auto-reply` - Bot that automatically replies to messages
21+
- `cargo run --example adaptivecard` - Demonstrates AdaptiveCard usage
22+
- `cargo run --example device-authentication` - Shows device authentication flow
23+
24+
### Development
25+
- `cargo test --lib` - Run library tests only
26+
- `cargo clippy --all-targets --all-features` - Full clippy check
27+
- `cargo build --all-targets` - Build everything including examples
28+
29+
## Architecture
30+
31+
### Core Components
32+
33+
- **`Webex` struct** (`src/lib.rs:92-100`) - Main API client with token-based authentication
34+
- **`WebexEventStream`** (`src/lib.rs:102-108`) - WebSocket event stream handler for real-time events
35+
- **`RestClient`** (`src/lib.rs:247-251`) - Low-level HTTP client wrapper
36+
- **Types module** (`src/types.rs`) - All API data structures and serialization
37+
- **AdaptiveCard module** (`src/adaptive_card.rs`) - Support for interactive cards
38+
- **Auth module** (`src/auth.rs`) - Device authentication flows
39+
- **Error module** (`src/error.rs`) - Comprehensive error handling
40+
41+
### Key Patterns
42+
43+
- **Generic API methods**: `get<T>()`, `list<T>()`, `delete<T>()` work with any `Gettable` type
44+
- **Device registration**: Automatic device setup and caching for WebSocket connections
45+
- **Message handling**: Supports both direct messages and room messages with threading
46+
- **Event streaming**: WebSocket-based real-time event processing with automatic reconnection
47+
48+
### Authentication Flow
49+
50+
1. Token-based authentication for REST API calls
51+
2. Device registration with Webex for WebSocket connections
52+
3. Mercury URL discovery for optimal WebSocket endpoint
53+
4. Automatic device cleanup and recreation as needed
54+
55+
## Important Notes
56+
57+
- Uses Rust 1.76 toolchain (see `rust-toolchain.toml`)
58+
- Very strict clippy configuration with pedantic and nursery lints enabled
59+
- All public APIs must have documentation (`#![deny(missing_docs)]`)
60+
- WebSocket connections require device registration and token authentication
61+
- Mercury URL caching reduces API calls for device discovery

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@ features = ["v4"]
5050

5151
[dev-dependencies]
5252
env_logger = "0.11.5"
53+
mockito = "1.5.0"
54+
tokio-test = "0.4.4"

examples/adaptivecard.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async fn main() {
3434
let event = match eventstream.next().await {
3535
Ok(event) => event,
3636
Err(e) => {
37-
println!("Eventstream failed: {}", e);
37+
println!("Eventstream failed: {e}");
3838
continue;
3939
}
4040
};
@@ -57,16 +57,13 @@ async fn handle_adaptive_card(webex: &webex::Webex, event: &webex::Event) {
5757
match webex.get(&event.try_global_id().unwrap()).await {
5858
Ok(a) => a,
5959
Err(e) => {
60-
println!("Error: {}", e);
60+
println!("Error: {e}");
6161
return;
6262
}
6363
};
6464
let which_card = actions.inputs.as_ref().and_then(|inputs| inputs.get("id"));
6565
match which_card {
66-
None => println!(
67-
"ERROR: expected card to have both inputs and id, got {:?}",
68-
actions
69-
),
66+
None => println!("ERROR: expected card to have both inputs and id, got {actions:?}"),
7067
Some(s) => match s.as_str() {
7168
// s is serde::Value so we have to check if it's actually a string (as_str produces an
7269
// Option)
@@ -87,10 +84,7 @@ async fn handle_adaptive_card_init(webex: &webex::Webex, actions: &webex::Attach
8784
.as_ref()
8885
.and_then(|inputs| inputs.get("input2"));
8986
if let (Some(input1), Some(input2)) = (input1, input2) {
90-
println!(
91-
"Recieved initial adaptive card, inputs {} and {}",
92-
input1, input2
93-
);
87+
println!("Recieved initial adaptive card, inputs {input1} and {input2}");
9488
return;
9589
}
9690

@@ -113,7 +107,7 @@ async fn respond_to_message(webex: &webex::Webex, config: &Config, event: &webex
113107
let message: webex::Message = match webex.get(&event.try_global_id().unwrap()).await {
114108
Ok(msg) => msg,
115109
Err(e) => {
116-
println!("Failed to get message: {}", e);
110+
println!("Failed to get message: {e}");
117111
return;
118112
}
119113
};

examples/auto-reply.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ const BOT_EMAIL: &str = "BOT_EMAIL";
2626
#[tokio::main]
2727
async fn main() {
2828
let token = env::var(BOT_ACCESS_TOKEN)
29-
.unwrap_or_else(|_| panic!("{} not specified in environment", BOT_ACCESS_TOKEN));
30-
let bot_email = env::var(BOT_EMAIL)
31-
.unwrap_or_else(|_| panic!("{} not specified in environment", BOT_EMAIL));
29+
.unwrap_or_else(|_| panic!("{BOT_ACCESS_TOKEN} not specified in environment"));
30+
let bot_email =
31+
env::var(BOT_EMAIL).unwrap_or_else(|_| panic!("{BOT_EMAIL} not specified in environment"));
3232

3333
let webex = webex::Webex::new(token.as_str()).await;
3434
let mut event_stream = webex.event_stream().await.expect("event stream");

examples/device-authentication.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const INTEGRATION_CLIENT_SECRET: &str = "INTEGRATION_CLIENT_SECRET";
77
#[tokio::main]
88
async fn main() {
99
let client_id = env::var(INTEGRATION_CLIENT_ID)
10-
.unwrap_or_else(|_| panic!("{} not specified in environment", INTEGRATION_CLIENT_ID));
10+
.unwrap_or_else(|_| panic!("{INTEGRATION_CLIENT_ID} not specified in environment"));
1111
let client_secret = env::var(INTEGRATION_CLIENT_SECRET)
12-
.unwrap_or_else(|_| panic!("{} not specified in environment", INTEGRATION_CLIENT_SECRET));
12+
.unwrap_or_else(|_| panic!("{INTEGRATION_CLIENT_SECRET} not specified in environment"));
1313

1414
let authenticator = DeviceAuthenticator::new(&client_id, &client_secret);
1515

examples/hello-world.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ const DEST_EMAIL: &str = "DEST_EMAIL";
2424
#[tokio::main]
2525
async fn main() {
2626
let token = env::var(BOT_ACCESS_TOKEN)
27-
.unwrap_or_else(|_| panic!("{} not specified in environment", BOT_ACCESS_TOKEN));
27+
.unwrap_or_else(|_| panic!("{BOT_ACCESS_TOKEN} not specified in environment"));
2828
let to_email = env::var(DEST_EMAIL)
29-
.unwrap_or_else(|_| panic!("{} not specified in environment", DEST_EMAIL));
29+
.unwrap_or_else(|_| panic!("{DEST_EMAIL} not specified in environment"));
3030

3131
let webex = webex::Webex::new(token.as_str()).await;
32-
let text = format!("Hello, {}", to_email);
32+
let text = format!("Hello, {to_email}");
3333

3434
let msg_to_send = webex::types::MessageOut {
3535
to_person_email: Some(to_email),

0 commit comments

Comments
 (0)