Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use flake

watch_file flake.nix
watch_file flake.lock
watch_file rust-toolchain.toml

dotenv_if_exists .env
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated files
/target/

# The library shouldn't decide about the exact versions of
# its dependencies, but let the downstream crate decide.
Cargo.lock
Cargo.lock
.claude/settings.local.json
1 change: 1 addition & 0 deletions .ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.direnv
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

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.

## Commands

### Build and Test
- `cargo build` - Build the library
- `cargo test` - Run unit tests
- `cargo clippy` - Run linter (note: very strict clippy rules enabled)
- `cargo fmt` - Format code
- `cargo doc` - Generate documentation

### Examples
- `cargo run --example hello-world` - Basic message sending example
- `cargo run --example auto-reply` - Bot that automatically replies to messages
- `cargo run --example adaptivecard` - Demonstrates AdaptiveCard usage
- `cargo run --example device-authentication` - Shows device authentication flow

### Development
- `cargo test --lib` - Run library tests only
- `cargo clippy --all-targets --all-features` - Full clippy check
- `cargo build --all-targets` - Build everything including examples

## Architecture

### Core Components

- **`Webex` struct** (`src/lib.rs:92-100`) - Main API client with token-based authentication
- **`WebexEventStream`** (`src/lib.rs:102-108`) - WebSocket event stream handler for real-time events
- **`RestClient`** (`src/lib.rs:247-251`) - Low-level HTTP client wrapper
- **Types module** (`src/types.rs`) - All API data structures and serialization
- **AdaptiveCard module** (`src/adaptive_card.rs`) - Support for interactive cards
- **Auth module** (`src/auth.rs`) - Device authentication flows
- **Error module** (`src/error.rs`) - Comprehensive error handling

### Key Patterns

- **Generic API methods**: `get<T>()`, `list<T>()`, `delete<T>()` work with any `Gettable` type
- **Device registration**: Automatic device setup and caching for WebSocket connections
- **Message handling**: Supports both direct messages and room messages with threading
- **Event streaming**: WebSocket-based real-time event processing with automatic reconnection

### Authentication Flow

1. Token-based authentication for REST API calls
2. Device registration with Webex for WebSocket connections
3. Mercury URL discovery for optimal WebSocket endpoint
4. Automatic device cleanup and recreation as needed

## Important Notes

- Uses Rust 1.76 toolchain (see `rust-toolchain.toml`)
- Very strict clippy configuration with pedantic and nursery lints enabled
- All public APIs must have documentation (`#![deny(missing_docs)]`)
- WebSocket connections require device registration and token authentication
- Mercury URL caching reduces API calls for device discovery
1 change: 1 addition & 0 deletions CLAUDE.md
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ features = ["v4"]

[dev-dependencies]
env_logger = "0.11.5"
mockito = "1.5.0"
tokio-test = "0.4.4"
16 changes: 5 additions & 11 deletions examples/adaptivecard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async fn main() {
let event = match eventstream.next().await {
Ok(event) => event,
Err(e) => {
println!("Eventstream failed: {}", e);
println!("Eventstream failed: {e}");
continue;
}
};
Expand All @@ -57,16 +57,13 @@ async fn handle_adaptive_card(webex: &webex::Webex, event: &webex::Event) {
match webex.get(&event.try_global_id().unwrap()).await {
Ok(a) => a,
Err(e) => {
println!("Error: {}", e);
println!("Error: {e}");
return;
}
};
let which_card = actions.inputs.as_ref().and_then(|inputs| inputs.get("id"));
match which_card {
None => println!(
"ERROR: expected card to have both inputs and id, got {:?}",
actions
),
None => println!("ERROR: expected card to have both inputs and id, got {actions:?}"),
Some(s) => match s.as_str() {
// s is serde::Value so we have to check if it's actually a string (as_str produces an
// Option)
Expand All @@ -87,10 +84,7 @@ async fn handle_adaptive_card_init(webex: &webex::Webex, actions: &webex::Attach
.as_ref()
.and_then(|inputs| inputs.get("input2"));
if let (Some(input1), Some(input2)) = (input1, input2) {
println!(
"Recieved initial adaptive card, inputs {} and {}",
input1, input2
);
println!("Recieved initial adaptive card, inputs {input1} and {input2}");
return;
}

Expand All @@ -113,7 +107,7 @@ async fn respond_to_message(webex: &webex::Webex, config: &Config, event: &webex
let message: webex::Message = match webex.get(&event.try_global_id().unwrap()).await {
Ok(msg) => msg,
Err(e) => {
println!("Failed to get message: {}", e);
println!("Failed to get message: {e}");
return;
}
};
Expand Down
6 changes: 3 additions & 3 deletions examples/auto-reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const BOT_EMAIL: &str = "BOT_EMAIL";
#[tokio::main]
async fn main() {
let token = env::var(BOT_ACCESS_TOKEN)
.unwrap_or_else(|_| panic!("{} not specified in environment", BOT_ACCESS_TOKEN));
let bot_email = env::var(BOT_EMAIL)
.unwrap_or_else(|_| panic!("{} not specified in environment", BOT_EMAIL));
.unwrap_or_else(|_| panic!("{BOT_ACCESS_TOKEN} not specified in environment"));
let bot_email =
env::var(BOT_EMAIL).unwrap_or_else(|_| panic!("{BOT_EMAIL} not specified in environment"));

let webex = webex::Webex::new(token.as_str()).await;
let mut event_stream = webex.event_stream().await.expect("event stream");
Expand Down
4 changes: 2 additions & 2 deletions examples/device-authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const INTEGRATION_CLIENT_SECRET: &str = "INTEGRATION_CLIENT_SECRET";
#[tokio::main]
async fn main() {
let client_id = env::var(INTEGRATION_CLIENT_ID)
.unwrap_or_else(|_| panic!("{} not specified in environment", INTEGRATION_CLIENT_ID));
.unwrap_or_else(|_| panic!("{INTEGRATION_CLIENT_ID} not specified in environment"));
let client_secret = env::var(INTEGRATION_CLIENT_SECRET)
.unwrap_or_else(|_| panic!("{} not specified in environment", INTEGRATION_CLIENT_SECRET));
.unwrap_or_else(|_| panic!("{INTEGRATION_CLIENT_SECRET} not specified in environment"));

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

Expand Down
6 changes: 3 additions & 3 deletions examples/hello-world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ const DEST_EMAIL: &str = "DEST_EMAIL";
#[tokio::main]
async fn main() {
let token = env::var(BOT_ACCESS_TOKEN)
.unwrap_or_else(|_| panic!("{} not specified in environment", BOT_ACCESS_TOKEN));
.unwrap_or_else(|_| panic!("{BOT_ACCESS_TOKEN} not specified in environment"));
let to_email = env::var(DEST_EMAIL)
.unwrap_or_else(|_| panic!("{} not specified in environment", DEST_EMAIL));
.unwrap_or_else(|_| panic!("{DEST_EMAIL} not specified in environment"));

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

let msg_to_send = webex::types::MessageOut {
to_person_email: Some(to_email),
Expand Down
100 changes: 100 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
description = "Rust dev using fenix";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils = {
url = "github:numtide/flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
};

outputs = {
self,
nixpkgs,
fenix,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = import nixpkgs {
inherit system;
overlays = [
fenix.overlays.default
];
};

# get Rust version from toolchain file
toolchain = with fenix.packages.${system};
fromToolchainFile {
file = ./rust-toolchain.toml;
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
};
in {
devShells.default = pkgs.mkShell {
# build environment
nativeBuildInputs = with pkgs; [
clang
openssl.dev
pkg-config
toolchain
];

# runtime environment
buildInputs = with pkgs;
[
bacon
# git-cliff
rust-analyzer
toolchain
]
++ lib.optionals pkgs.stdenv.isDarwin [
# linking will fail if clang is not in nativeBuildInputs
pkgs.darwin.apple_sdk.frameworks.CoreServices
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
};
}
);
}
Loading