#[cfg(feature = "server")]
use async_trait::async_trait;
use crate::domain::{A2AError, TaskIdParams, TaskPushNotificationConfig};
fn validate_push_notification_url(config: &TaskPushNotificationConfig) -> Result<(), A2AError> {
if config.url.trim().is_empty() {
return Err(A2AError::ValidationError {
field: "url".to_string(),
message: "Webhook URL cannot be empty".to_string(),
});
}
match url::Url::parse(&config.url) {
Ok(parsed_url) => {
let scheme = parsed_url.scheme();
if scheme != "https" {
let is_localhost = parsed_url
.host_str()
.map(|h| h == "localhost" || h == "127.0.0.1" || h == "::1")
.unwrap_or(false);
if scheme != "http" || !is_localhost {
return Err(A2AError::ValidationError {
field: "url".to_string(),
message: "Webhook URL must use HTTPS (HTTP is only allowed for localhost)"
.to_string(),
});
}
}
}
Err(_) => {
return Err(A2AError::ValidationError {
field: "url".to_string(),
message: "Invalid webhook URL format".to_string(),
});
}
}
Ok(())
}
pub trait NotificationManager {
fn set_task_notification(
&self,
config: &TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError>;
fn get_task_notification(&self, task_id: &str) -> Result<TaskPushNotificationConfig, A2AError>;
fn remove_task_notification(&self, task_id: &str) -> Result<(), A2AError>;
fn has_task_notification(&self, task_id: &str) -> Result<bool, A2AError> {
match self.get_task_notification(task_id) {
Ok(_) => Ok(true),
Err(A2AError::TaskNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
fn validate_notification_config(
&self,
config: &TaskPushNotificationConfig,
) -> Result<(), A2AError> {
validate_push_notification_url(config)
}
fn send_test_notification(&self, config: &TaskPushNotificationConfig) -> Result<(), A2AError> {
self.validate_notification_config(config)?;
Ok(())
}
}
#[cfg(feature = "server")]
#[async_trait]
pub trait AsyncNotificationManager: Send + Sync {
async fn set_task_notification(
&self,
config: &TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError>;
async fn get_task_notification(
&self,
task_id: &str,
) -> Result<TaskPushNotificationConfig, A2AError>;
async fn remove_task_notification(&self, task_id: &str) -> Result<(), A2AError>;
async fn has_task_notification(&self, task_id: &str) -> Result<bool, A2AError> {
match self.get_task_notification(task_id).await {
Ok(_) => Ok(true),
Err(A2AError::TaskNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
async fn validate_notification_config(
&self,
config: &TaskPushNotificationConfig,
) -> Result<(), A2AError> {
validate_push_notification_url(config)
}
async fn send_test_notification(
&self,
config: &TaskPushNotificationConfig,
) -> Result<(), A2AError> {
self.validate_notification_config(config).await?;
Ok(())
}
async fn set_task_notification_validated(
&self,
config: &TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError> {
if config.task_id.trim().is_empty() {
return Err(A2AError::ValidationError {
field: "task_id".to_string(),
message: "Task ID cannot be empty".to_string(),
});
}
self.validate_notification_config(config).await?;
self.set_task_notification(config).await
}
async fn get_task_notification_validated(
&self,
params: &TaskIdParams,
) -> Result<TaskPushNotificationConfig, A2AError> {
if params.id.trim().is_empty() {
return Err(A2AError::ValidationError {
field: "task_id".to_string(),
message: "Task ID cannot be empty".to_string(),
});
}
self.get_task_notification(¶ms.id).await
}
async fn notify_task_status_update(
&self,
task_id: &str,
_status_update: &crate::domain::TaskStatusUpdateEvent,
) -> Result<(), A2AError> {
if !self.has_task_notification(task_id).await? {
return Ok(()); }
let _config = self.get_task_notification(task_id).await?;
Ok(())
}
async fn notify_task_artifact_update(
&self,
task_id: &str,
_artifact_update: &crate::domain::TaskArtifactUpdateEvent,
) -> Result<(), A2AError> {
if !self.has_task_notification(task_id).await? {
return Ok(()); }
let _config = self.get_task_notification(task_id).await?;
Ok(())
}
}