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
71 changes: 56 additions & 15 deletions sdk/rust/src/api/contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
//! ```
//!
//! All calls are authenticated as the acting agent (`X-Agent-ID` + signature).
//! Contacts are keyed by cryptoId. Responses are returned as [`serde_json::Value`]
//! (the backend contact record shape is not needed by current callers).
//! Contacts are keyed by cryptoId; list/request/stats responses expose the other
//! party as `agent_id` (the backend wire name is `cryptoId`, normalized here to
//! match the TS SDK).

use crate::error::Result;
use crate::http::HttpClient;
use crate::types::{
Contact, ContactListParams, ContactRequestsResponse, ContactStats, ContactsResponse,
};
use crate::util::encode;

/// Manages the mutual first-level contact graph: send/accept/decline requests,
/// block, and list contacts.
/// block/unblock, and list contacts.
#[derive(Clone)]
pub struct ContactsApi {
http: HttpClient,
Expand All @@ -34,19 +38,19 @@ impl ContactsApi {

/// Send a contact request to `agent_id` (idempotent; **auto-accepts** when a
/// reverse request from `agent_id` is already pending).
pub async fn request(&self, agent_id: &str) -> Result<serde_json::Value> {
pub async fn request(&self, agent_id: &str) -> Result<Contact> {
self.http
.post_agent_auth::<serde_json::Value, serde_json::Value>(
.post_agent_auth::<Contact, serde_json::Value>(
&format!("/contacts/{}", encode(agent_id)),
None,
)
.await
}

/// Accept a pending incoming request from `agent_id`.
pub async fn accept(&self, agent_id: &str) -> Result<serde_json::Value> {
pub async fn accept(&self, agent_id: &str) -> Result<Contact> {
self.http
.post_agent_auth::<serde_json::Value, serde_json::Value>(
.post_agent_auth::<Contact, serde_json::Value>(
&format!("/contacts/{}/accept", encode(agent_id)),
None,
)
Expand All @@ -64,29 +68,66 @@ impl ContactsApi {
}

/// Block `agent_id`, suppressing the relationship and refusing new requests.
pub async fn block(&self, agent_id: &str) -> Result<serde_json::Value> {
pub async fn block(&self, agent_id: &str) -> Result<Contact> {
self.http
.post_agent_auth::<serde_json::Value, serde_json::Value>(
.post_agent_auth::<Contact, serde_json::Value>(
&format!("/contacts/{}/block", encode(agent_id)),
None,
)
.await
}

/// Get the relationship status with `agent_id`.
pub async fn status(&self, agent_id: &str) -> Result<serde_json::Value> {
/// Remove a block previously created by the acting agent.
pub async fn unblock(&self, agent_id: &str) -> Result<()> {
self.http
.post_agent_auth::<(), serde_json::Value>(
&format!("/contacts/{}/unblock", encode(agent_id)),
None,
)
.await
}

/// Get the relationship status with `agent_id`. Returns the backend
/// `Contact` record (its `status` field is `pending` | `accepted` |
/// `blocked`); a missing relationship surfaces as a 404 error.
pub async fn status(&self, agent_id: &str) -> Result<Contact> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return the status endpoint's status shape

When /contacts/{id}/status returns the status response shape (agentId, status, optional direction) exposed by the TS SDK in sdk/typescript/src/types/contacts.ts:42-45 and used by the test mock in sdk/typescript/tests/codex-cli.test.ts:603-609, deserializing it as Contact silently drops agentId/direction and fills requester/addressee with empty defaults. Rust callers then cannot distinguish pending incoming vs outgoing (or read the returned peer), so the typed surface is lossy; add a ContactStatusResponse type and return it here.

Useful? React with 👍 / 👎.

self.http
.get_agent_auth(&format!("/contacts/{}/status", encode(agent_id)), &[])
.await
}

/// List the acting agent's accepted contacts.
pub async fn list(&self) -> Result<serde_json::Value> {
self.http.get_agent_auth("/contacts", &[]).await
pub async fn list(&self, params: Option<&ContactListParams>) -> Result<ContactsResponse> {
self.http
.get_agent_auth("/contacts", &list_query(params))
.await
}

/// List pending incoming and outgoing requests.
pub async fn requests(&self) -> Result<serde_json::Value> {
self.http.get_agent_auth("/contacts/requests", &[]).await
pub async fn requests(
&self,
params: Option<&ContactListParams>,
) -> Result<ContactRequestsResponse> {
self.http
.get_agent_auth("/contacts/requests", &list_query(params))
.await
}

/// Contact-graph counts for the acting agent (accepted + pending).
pub async fn stats(&self) -> Result<ContactStats> {
self.http.get_agent_auth("/contacts/stats", &[]).await
}
}

fn list_query(params: Option<&ContactListParams>) -> Vec<(String, String)> {
let mut q: Vec<(String, String)> = Vec::new();
if let Some(p) = params {
if let Some(v) = p.limit {
q.push(("limit".into(), v.to_string()));
}
if let Some(v) = p.offset {
q.push(("offset".into(), v.to_string()));
}
}
q
}
95 changes: 95 additions & 0 deletions sdk/rust/src/types/contacts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Contact-graph types. Mirrors `sdk/typescript/src/types/contacts.ts`.
//!
//! A contact is a single mutual edge between two agents: one side sends a
//! request (`pending`), the other accepts (`accepted`). Either side may block
//! the other (`blocked`). An **accepted** contact is the prerequisite for
//! direct messaging (the relay refuses DMs between non-contacts).
//!
//! The single agent identifier on the wire is `cryptoId` (spec rule 8 dropped
//! the legacy `agentId`); we accept both via `#[serde(alias)]` and expose it as
//! `agent_id` to match the TS SDK's normalized surface.

#[allow(unused_imports)]
use super::*;
use serde::{Deserialize, Serialize}; // sibling types share a flat namespace, like the TS barrel

/// A first-level relationship as stored by the backend. There is at most one
/// `Contact` per unordered pair of agents; `requester`/`addressee` record who
/// initiated it (relevant while pending). When `status` is `blocked`,
/// `blocked_by` names the agent who blocked.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contact {
#[serde(default)]
pub requester: String,
#[serde(default)]
pub addressee: String,
/// One of `pending` | `accepted` | `blocked`.
#[serde(default)]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocked_by: Option<String>,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub updated_at: String,
}

/// A relationship as seen from the requesting agent's perspective. `direction`
/// is present only for pending relationships (`incoming`/`outgoing`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactView {
/// The other party's cryptoId. Accepts the legacy `agentId` wire name too.
#[serde(rename = "cryptoId", alias = "agentId", default)]
pub agent_id: String,
#[serde(default)]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
}

/// Pagination parameters for contact/request listings.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactListParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
}

/// Response listing the acting agent's accepted contacts.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactsResponse {
#[serde(default)]
pub contacts: Vec<ContactView>,
}

/// Response listing pending incoming and outgoing requests.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactRequestsResponse {
#[serde(default)]
pub incoming: Vec<ContactView>,
#[serde(default)]
pub outgoing: Vec<ContactView>,
}

/// Contact-graph counts for the acting agent.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContactStats {
/// The acting agent's cryptoId. Accepts the legacy `agentId` wire name too.
#[serde(rename = "cryptoId", alias = "agentId", default)]
pub agent_id: String,
#[serde(default)]
pub contact_count: i64,
#[serde(default)]
pub pending_incoming: i64,
#[serde(default)]
pub pending_outgoing: i64,
}
2 changes: 2 additions & 0 deletions sdk/rust/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod artifacts;
mod bounties;
mod broadcasts;
mod commerce;
mod contacts;
mod conversations;
mod directory;
mod docs;
Expand Down Expand Up @@ -40,6 +41,7 @@ pub use artifacts::*;
pub use bounties::*;
pub use broadcasts::*;
pub use commerce::*;
pub use contacts::*;
pub use conversations::*;
pub use directory::*;
pub use docs::*;
Expand Down
Loading
Loading