use crate::agent::AgentClient;
use crate::client::{ClientOptions, HttpClient};
use crate::error::{RelayError, Result};
use crate::types::*;
const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_BASE_URL: &str = "https://gateway.relaycast.dev";
const DEFAULT_ORIGIN_SURFACE: &str = "sdk";
const DEFAULT_ORIGIN_CLIENT: &str = "@relaycast/sdk-rust";
fn strip_hash(channel: &str) -> &str {
channel.strip_prefix('#').unwrap_or(channel)
}
#[derive(Debug, Clone)]
pub struct RelayCastOptions {
pub api_key: String,
pub base_url: Option<String>,
}
impl RelayCastOptions {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: None,
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
}
#[derive(Clone)]
pub struct RelayCast {
client: HttpClient,
}
impl RelayCast {
pub fn new(options: RelayCastOptions) -> Result<Self> {
if options.api_key.trim().is_empty() {
return Err(RelayError::InvalidResponse(
"RelayCast api_key is required".to_string(),
));
}
let base_url = options
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
let mut client_options = ClientOptions::new(options.api_key);
client_options = client_options.with_base_url(base_url);
let client = HttpClient::new(client_options)?;
Ok(Self { client })
}
pub async fn create_workspace(
name: &str,
base_url: Option<&str>,
) -> Result<CreateWorkspaceResponse> {
let url = format!("{}/v1/workspaces", base_url.unwrap_or(DEFAULT_BASE_URL));
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.header("X-SDK-Version", SDK_VERSION)
.header("X-Relaycast-Origin-Surface", DEFAULT_ORIGIN_SURFACE)
.header("X-Relaycast-Origin-Client", DEFAULT_ORIGIN_CLIENT)
.header("X-Relaycast-Origin-Version", SDK_VERSION)
.json(&serde_json::json!({ "name": name }))
.send()
.await?;
let status = response.status().as_u16();
let json: ApiResponse<CreateWorkspaceResponse> = response.json().await?;
if !json.ok {
let error = json.error.unwrap_or_else(|| ApiErrorInfo {
code: "unknown_error".to_string(),
message: "Unknown error".to_string(),
});
return Err(RelayError::api(error.code, error.message, status));
}
json.data
.ok_or_else(|| RelayError::InvalidResponse("Response missing data field".to_string()))
}
pub fn as_agent(&self, agent_token: impl Into<String>) -> Result<AgentClient> {
let client = self.client.with_api_key(agent_token)?;
Ok(AgentClient::from_client(client))
}
pub async fn workspace_info(&self) -> Result<Workspace> {
self.client.get("/v1/workspace", None, None).await
}
pub async fn update_workspace(&self, request: UpdateWorkspaceRequest) -> Result<Workspace> {
self.client
.patch("/v1/workspace", Some(request), None)
.await
}
pub async fn delete_workspace(&self) -> Result<()> {
self.client.delete("/v1/workspace", None).await
}
pub async fn workspace_stream_get(&self) -> Result<WorkspaceStreamConfig> {
self.client.get("/v1/workspace/stream", None, None).await
}
pub async fn workspace_stream_set(&self, enabled: bool) -> Result<WorkspaceStreamConfig> {
self.client
.put(
"/v1/workspace/stream",
Some(serde_json::json!({ "enabled": enabled })),
None,
)
.await
}
pub async fn workspace_stream_inherit(&self) -> Result<WorkspaceStreamConfig> {
self.client
.put(
"/v1/workspace/stream",
Some(serde_json::json!({ "mode": "inherit" })),
None,
)
.await
}
pub async fn get_system_prompt(&self) -> Result<SystemPrompt> {
self.client
.get("/v1/workspace/system-prompt", None, None)
.await
}
pub async fn set_system_prompt(&self, request: SetSystemPromptRequest) -> Result<SystemPrompt> {
self.client
.put("/v1/workspace/system-prompt", Some(request), None)
.await
}
pub async fn list_channels(&self, include_archived: bool) -> Result<Vec<Channel>> {
let query = if include_archived {
Some([("include_archived", "true")].as_slice())
} else {
None
};
self.client.get("/v1/channels", query, None).await
}
pub async fn get_channel(&self, name: &str) -> Result<ChannelWithMembers> {
self.client
.get(
&format!("/v1/channels/{}", urlencoding::encode(name)),
None,
None,
)
.await
}
pub async fn list_messages(
&self,
channel: &str,
opts: Option<MessageListQuery>,
) -> Result<Vec<MessageWithMeta>> {
let name = strip_hash(channel);
let opts = opts.unwrap_or_default();
let mut query_params: Vec<(String, String)> = Vec::new();
if let Some(limit) = opts.limit {
query_params.push(("limit".to_string(), limit.to_string()));
}
if let Some(before) = opts.before {
query_params.push(("before".to_string(), before));
}
if let Some(after) = opts.after {
query_params.push(("after".to_string(), after));
}
let query: Vec<(&str, &str)> = query_params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let query_ref = if query.is_empty() {
None
} else {
Some(query.as_slice())
};
self.client
.get(
&format!("/v1/channels/{}/messages", urlencoding::encode(name)),
query_ref,
None,
)
.await
}
pub async fn get_message(&self, id: &str) -> Result<MessageWithMeta> {
self.client
.get(
&format!("/v1/messages/{}", urlencoding::encode(id)),
None,
None,
)
.await
}
pub async fn get_thread(
&self,
message_id: &str,
opts: Option<MessageListQuery>,
) -> Result<ThreadResponse> {
let opts = opts.unwrap_or_default();
let mut query_params: Vec<(String, String)> = Vec::new();
if let Some(limit) = opts.limit {
query_params.push(("limit".to_string(), limit.to_string()));
}
if let Some(before) = opts.before {
query_params.push(("before".to_string(), before));
}
if let Some(after) = opts.after {
query_params.push(("after".to_string(), after));
}
let query: Vec<(&str, &str)> = query_params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let query_ref = if query.is_empty() {
None
} else {
Some(query.as_slice())
};
self.client
.get(
&format!("/v1/messages/{}/replies", urlencoding::encode(message_id)),
query_ref,
None,
)
.await
}
pub async fn get_message_reactions(&self, id: &str) -> Result<Vec<ReactionGroup>> {
self.client
.get(
&format!("/v1/messages/{}/reactions", urlencoding::encode(id)),
None,
None,
)
.await
}
pub async fn register_agent(&self, request: CreateAgentRequest) -> Result<CreateAgentResponse> {
self.client.post("/v1/agents", Some(request), None).await
}
pub async fn list_agents(&self, query: Option<AgentListQuery>) -> Result<Vec<Agent>> {
let query = query.unwrap_or_default();
let params: Vec<(String, String)> = query
.status
.map(|s| vec![("status".to_string(), s)])
.unwrap_or_default();
let query_slice: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let query_ref = if query_slice.is_empty() {
None
} else {
Some(query_slice.as_slice())
};
self.client.get("/v1/agents", query_ref, None).await
}
pub async fn get_agent(&self, name: &str) -> Result<Agent> {
self.client
.get(
&format!("/v1/agents/{}", urlencoding::encode(name)),
None,
None,
)
.await
}
pub async fn rotate_agent_token(&self, name: &str) -> Result<TokenRotateResponse> {
self.client
.post(
&format!("/v1/agents/{}/rotate-token", urlencoding::encode(name)),
Some(serde_json::json!({})),
None,
)
.await
}
pub async fn update_agent(&self, name: &str, request: UpdateAgentRequest) -> Result<Agent> {
self.client
.patch(
&format!("/v1/agents/{}", urlencoding::encode(name)),
Some(request),
None,
)
.await
}
pub async fn delete_agent(&self, name: &str) -> Result<()> {
self.client
.delete(&format!("/v1/agents/{}", urlencoding::encode(name)), None)
.await
}
pub async fn agent_presence(&self) -> Result<Vec<AgentPresenceInfo>> {
self.client.get("/v1/agents/presence", None, None).await
}
pub async fn register_or_get_agent(
&self,
request: CreateAgentRequest,
) -> Result<CreateAgentResponse> {
match self.register_agent(request.clone()).await {
Ok(response) => Ok(response),
Err(RelayError::Api { code, status, .. })
if code == "agent_already_exists" || status == 409 =>
{
let agent = self.get_agent(&request.name).await?;
let token_response = self.rotate_agent_token(&agent.name).await?;
let created_at = agent.created_at.or(agent.last_seen).unwrap_or_default();
Ok(CreateAgentResponse {
id: agent.id,
name: agent.name,
token: token_response.token,
status: agent.status,
created_at,
})
}
Err(e) => Err(e),
}
}
pub async fn spawn_agent(&self, request: SpawnAgentRequest) -> Result<SpawnAgentResponse> {
self.client
.post("/v1/agents/spawn", Some(request), None)
.await
}
pub async fn release_agent(
&self,
request: ReleaseAgentRequest,
) -> Result<ReleaseAgentResponse> {
self.client
.post("/v1/agents/release", Some(request), None)
.await
}
pub async fn create_webhook(
&self,
request: CreateWebhookRequest,
) -> Result<CreateWebhookResponse> {
self.client.post("/v1/webhooks", Some(request), None).await
}
pub async fn list_webhooks(&self) -> Result<Vec<Webhook>> {
self.client.get("/v1/webhooks", None, None).await
}
pub async fn delete_webhook(&self, id: &str) -> Result<()> {
self.client
.delete(&format!("/v1/webhooks/{}", urlencoding::encode(id)), None)
.await
}
pub async fn trigger_webhook(
&self,
webhook_id: &str,
request: WebhookTriggerRequest,
) -> Result<WebhookTriggerResponse> {
self.client
.post(
&format!("/v1/hooks/{}", urlencoding::encode(webhook_id)),
Some(request),
None,
)
.await
}
pub async fn create_subscription(
&self,
request: CreateSubscriptionRequest,
) -> Result<CreateSubscriptionResponse> {
self.client
.post("/v1/subscriptions", Some(request), None)
.await
}
pub async fn list_subscriptions(&self) -> Result<Vec<EventSubscription>> {
self.client.get("/v1/subscriptions", None, None).await
}
pub async fn get_subscription(&self, id: &str) -> Result<EventSubscription> {
self.client
.get(
&format!("/v1/subscriptions/{}", urlencoding::encode(id)),
None,
None,
)
.await
}
pub async fn delete_subscription(&self, id: &str) -> Result<()> {
self.client
.delete(
&format!("/v1/subscriptions/{}", urlencoding::encode(id)),
None,
)
.await
}
pub async fn register_action(
&self,
request: RegisterActionRequest,
) -> Result<ActionDefinition> {
self.client.post("/v1/actions", Some(request), None).await
}
pub async fn list_actions(&self) -> Result<Vec<ActionDefinition>> {
self.client.get("/v1/actions", None, None).await
}
pub async fn get_action(&self, name: &str) -> Result<ActionDefinition> {
self.client
.get(
&format!("/v1/actions/{}", urlencoding::encode(name)),
None,
None,
)
.await
}
pub async fn delete_action(&self, name: &str) -> Result<()> {
self.client
.delete(&format!("/v1/actions/{}", urlencoding::encode(name)), None)
.await
}
pub async fn emit_agent_event(
&self,
name: &str,
request: EmitSessionEventRequest,
) -> Result<SessionEvent> {
self.client
.post(
&format!("/v1/agents/{}/events", urlencoding::encode(name)),
Some(request),
None,
)
.await
}
pub async fn list_agent_events(
&self,
name: &str,
query: Option<ListSessionEventsQuery>,
) -> Result<Vec<SessionEvent>> {
let query = query.unwrap_or_default();
let mut params: Vec<(String, String)> = Vec::new();
if let Some(event_type) = query.event_type {
params.push(("type".to_string(), event_type));
}
if let Some(limit) = query.limit {
params.push(("limit".to_string(), limit.to_string()));
}
let slice: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let query_ref = if slice.is_empty() {
None
} else {
Some(slice.as_slice())
};
self.client
.get(
&format!("/v1/agents/{}/events", urlencoding::encode(name)),
query_ref,
None,
)
.await
}
pub async fn stats(&self) -> Result<WorkspaceStats> {
self.client.get("/v1/workspace/stats", None, None).await
}
pub async fn activity(&self, limit: Option<i32>) -> Result<Vec<ActivityItem>> {
let limit_str = limit.map(|l| l.to_string());
let query: Vec<(&str, &str)> = limit_str
.as_ref()
.map(|l| vec![("limit", l.as_str())])
.unwrap_or_default();
let query_ref = if query.is_empty() {
None
} else {
Some(query.as_slice())
};
self.client.get("/v1/activity", query_ref, None).await
}
pub async fn all_dm_conversations(&self) -> Result<Vec<WorkspaceDmConversation>> {
self.client
.get("/v1/dm/conversations/all", None, None)
.await
}
pub async fn dm_conversation_participants(&self, conversation_id: &str) -> Result<Vec<String>> {
let target = conversation_id.trim();
if target.is_empty() {
return Ok(vec![]);
}
let conversations = self.all_dm_conversations().await?;
Ok(conversations
.into_iter()
.find(|conversation| conversation.id == target)
.map(|conversation| conversation.participants)
.unwrap_or_default())
}
pub async fn dm_messages(
&self,
conversation_id: &str,
opts: Option<MessageListQuery>,
) -> Result<Vec<WorkspaceDmMessage>> {
let opts = opts.unwrap_or_default();
let mut query_params: Vec<(String, String)> = Vec::new();
if let Some(limit) = opts.limit {
query_params.push(("limit".to_string(), limit.to_string()));
}
if let Some(before) = opts.before {
query_params.push(("before".to_string(), before));
}
if let Some(after) = opts.after {
query_params.push(("after".to_string(), after));
}
let query: Vec<(&str, &str)> = query_params
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let query_ref = if query.is_empty() {
None
} else {
Some(query.as_slice())
};
self.client
.get(
&format!(
"/v1/dm/conversations/{}/messages",
urlencoding::encode(conversation_id)
),
query_ref,
None,
)
.await
}
}