Skip to main content

elizaos_plugin_nextcloud_talk/
error.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Result type used throughout the Nextcloud Talk plugin.
5pub type Result<T> = std::result::Result<T, NextcloudTalkError>;
6
7#[derive(Debug, Error)]
8/// Errors produced by the Nextcloud Talk plugin.
9pub enum NextcloudTalkError {
10    #[error("Nextcloud Talk service not initialized - call start() first")]
11    /// The Nextcloud Talk service has not been started yet.
12    ServiceNotInitialized,
13
14    #[error("Nextcloud Talk service is already running")]
15    /// The Nextcloud Talk service is already running.
16    AlreadyRunning,
17
18    #[error("Failed to connect to Nextcloud Talk: {0}")]
19    /// A connection attempt to Nextcloud Talk failed.
20    ConnectionFailed(String),
21
22    #[error("Nextcloud Talk API error: {0}")]
23    /// A Nextcloud Talk API call failed.
24    ApiError(String),
25
26    #[error("Configuration error: {0}")]
27    /// Configuration values were invalid or inconsistent.
28    ConfigError(String),
29
30    #[error("Missing required setting: {0}")]
31    /// A required configuration setting was missing.
32    MissingSetting(String),
33
34    #[error("Invalid room token: {0}")]
35    /// A provided room token was invalid.
36    InvalidRoomToken(String),
37
38    #[error("Invalid argument: {0}")]
39    /// A caller provided an invalid argument.
40    InvalidArgument(String),
41
42    #[error("Message too long: {length} characters (max: {max})")]
43    /// A message exceeded Nextcloud Talk's maximum allowed length.
44    MessageTooLong {
45        /// The message length that was attempted.
46        length: usize,
47        /// The maximum supported length.
48        max: usize,
49    },
50
51    #[error("Room not found: {0}")]
52    /// The requested room could not be found.
53    RoomNotFound(String),
54
55    #[error("User not found: {0}")]
56    /// The requested user could not be found.
57    UserNotFound(String),
58
59    #[error("Permission denied: {0}")]
60    /// The operation is not permitted.
61    PermissionDenied(String),
62
63    #[error("Authentication failed: {0}")]
64    /// Authentication with Nextcloud Talk failed.
65    AuthenticationFailed(String),
66
67    #[error("Signature verification failed")]
68    /// HMAC signature verification failed.
69    SignatureVerificationFailed,
70
71    #[error("Rate limited by Nextcloud Talk API, retry after {retry_after_secs}s")]
72    /// Nextcloud Talk API rate limit was hit.
73    RateLimited {
74        /// Suggested delay before retrying the request.
75        retry_after_secs: u64,
76    },
77
78    #[error("Operation timed out after {timeout_ms}ms")]
79    /// The operation did not complete within the expected time.
80    Timeout {
81        /// Timeout duration in milliseconds.
82        timeout_ms: u64,
83    },
84
85    #[error("Internal error: {0}")]
86    /// An internal error occurred.
87    Internal(String),
88
89    #[error("Serialization error: {0}")]
90    /// Serialization/deserialization failed.
91    SerializationError(String),
92
93    #[error("Action validation failed: {0}")]
94    /// An action failed validation.
95    ValidationFailed(String),
96
97    #[error("Webhook error: {0}")]
98    /// An error occurred in the webhook server.
99    WebhookError(String),
100}
101
102impl NextcloudTalkError {
103    /// Returns `true` if retrying the operation might succeed.
104    pub fn is_retryable(&self) -> bool {
105        matches!(
106            self,
107            NextcloudTalkError::RateLimited { .. }
108                | NextcloudTalkError::Timeout { .. }
109                | NextcloudTalkError::ConnectionFailed(_)
110        )
111    }
112
113    /// Returns an optional suggested retry delay (in seconds).
114    pub fn retry_after_secs(&self) -> Option<u64> {
115        match self {
116            NextcloudTalkError::RateLimited { retry_after_secs } => Some(*retry_after_secs),
117            NextcloudTalkError::Timeout { timeout_ms } => Some(*timeout_ms / 2000),
118            _ => None,
119        }
120    }
121}
122
123impl From<serde_json::Error> for NextcloudTalkError {
124    fn from(err: serde_json::Error) -> Self {
125        NextcloudTalkError::SerializationError(err.to_string())
126    }
127}
128
129impl From<std::io::Error> for NextcloudTalkError {
130    fn from(err: std::io::Error) -> Self {
131        NextcloudTalkError::Internal(format!("I/O error: {}", err))
132    }
133}
134
135impl From<reqwest::Error> for NextcloudTalkError {
136    fn from(err: reqwest::Error) -> Self {
137        NextcloudTalkError::ApiError(err.to_string())
138    }
139}
140
141#[derive(Debug)]
142/// Error wrapper that adds context to an underlying error.
143pub struct ErrorContext<E: fmt::Display> {
144    error: E,
145    context: String,
146}
147
148impl<E: fmt::Display> fmt::Display for ErrorContext<E> {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "{}: {}", self.context, self.error)
151    }
152}
153
154impl<E: fmt::Display + fmt::Debug> std::error::Error for ErrorContext<E> {}
155
156/// Extension trait for attaching context strings to results.
157pub trait WithContext<T, E: fmt::Display> {
158    /// Maps an error into an [`ErrorContext`] produced by the given closure.
159    fn with_context<F: FnOnce() -> String>(self, f: F) -> std::result::Result<T, ErrorContext<E>>;
160}
161
162impl<T, E: fmt::Display> WithContext<T, E> for std::result::Result<T, E> {
163    fn with_context<F: FnOnce() -> String>(self, f: F) -> std::result::Result<T, ErrorContext<E>> {
164        self.map_err(|e| ErrorContext {
165            error: e,
166            context: f(),
167        })
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_error_display() {
177        let err = NextcloudTalkError::MissingSetting("NEXTCLOUD_URL".to_string());
178        assert!(err.to_string().contains("NEXTCLOUD_URL"));
179    }
180
181    #[test]
182    fn test_error_retryable() {
183        assert!(NextcloudTalkError::RateLimited {
184            retry_after_secs: 10
185        }
186        .is_retryable());
187        assert!(NextcloudTalkError::Timeout { timeout_ms: 5000 }.is_retryable());
188        assert!(!NextcloudTalkError::ServiceNotInitialized.is_retryable());
189    }
190
191    #[test]
192    fn test_retry_after() {
193        let err = NextcloudTalkError::RateLimited {
194            retry_after_secs: 10,
195        };
196        assert_eq!(err.retry_after_secs(), Some(10));
197
198        let err = NextcloudTalkError::ServiceNotInitialized;
199        assert_eq!(err.retry_after_secs(), None);
200    }
201}