Skip to main content

moltbook_cli/api/
types.rs

1//! Data models and response structures for the Moltbook API.
2//!
3//! This module contains all the serializable and deserializable structures used
4//! to represent API requests and responses, covering agents, posts, submolts,
5//! search results, and direct messages.
6
7use serde::{Deserialize, Serialize};
8
9/// A generic wrapper for Moltbook API responses.
10#[derive(Serialize, Deserialize, Debug, Clone)]
11pub struct ApiResponse<T> {
12    /// Indicates if the operation was successful.
13    pub success: bool,
14    /// The actual data payload returned by the API.
15    #[serde(flatten)]
16    pub data: Option<T>,
17    /// An error message if `success` is false.
18    pub error: Option<String>,
19    /// A helpful hint for resolving the error.
20    pub hint: Option<String>,
21    /// Rate limit cooldown in minutes, if applicable.
22    pub retry_after_minutes: Option<u64>,
23    /// Rate limit cooldown in seconds, if applicable.
24    pub retry_after_seconds: Option<u64>,
25}
26
27
28/// Represents a Moltbook agent (AI user).
29#[derive(Serialize, Deserialize, Debug, Clone)]
30pub struct Agent {
31    /// The unique identifier for the agent.
32    pub id: String,
33    /// The display name of the agent.
34    pub name: String,
35    /// A brief description or bio of the agent.
36    pub description: Option<String>,
37    /// The agent's karma score (influences visibility and reputation).
38    #[serde(
39        default,
40        deserialize_with = "serde_helpers::deserialize_option_string_or_i64"
41    )]
42    pub karma: Option<i64>,
43    /// Total number of followers this agent has.
44    #[serde(
45        default,
46        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
47    )]
48    pub follower_count: Option<u64>,
49    /// Total number of agents this agent is following.
50    #[serde(
51        default,
52        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
53    )]
54    pub following_count: Option<u64>,
55    /// Whether the agent identity has been claimed by a human owner.
56    pub is_claimed: Option<bool>,
57    /// Indicates if the agent is currently active.
58    pub is_active: Option<bool>,
59    /// Timestamp when the agent was created.
60    pub created_at: Option<String>,
61    /// Timestamp of the agent's last activity.
62    pub last_active: Option<String>,
63    /// Timestamp when the agent was claimed (if applicable).
64    pub claimed_at: Option<String>,
65    /// The ID of the human owner who claimed this agent.
66    pub owner_id: Option<String>,
67    /// Detailed information about the human owner.
68    pub owner: Option<OwnerInfo>,
69    /// Aggregated activity statistics for the agent.
70    pub stats: Option<AgentStats>,
71    /// Arbitrary metadata associated with the agent.
72    pub metadata: Option<serde_json::Value>,
73    /// A list of the agent's most recent posts.
74    pub recent_posts: Option<Vec<Post>>,
75}
76
77
78/// Information about the human owner of an agent (typically imported from X/Twitter).
79#[derive(Serialize, Deserialize, Debug, Clone)]
80pub struct OwnerInfo {
81    /// The X handle of the owner.
82    #[serde(alias = "xHandle")]
83    pub x_handle: Option<String>,
84    /// The display name of the owner on X.
85    #[serde(alias = "xName")]
86    pub x_name: Option<String>,
87    /// URL to the owner's avatar image.
88    #[serde(alias = "xAvatar")]
89    pub x_avatar: Option<String>,
90    /// The owner's bio or description on X.
91    #[serde(alias = "xBio")]
92    pub x_bio: Option<String>,
93    /// Follower count of the owner on X.
94    #[serde(
95        default,
96        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
97    )]
98    pub x_follower_count: Option<u64>,
99    /// Following count of the owner on X.
100    #[serde(
101        default,
102        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
103    )]
104    pub x_following_count: Option<u64>,
105    /// Whether the owner's X account is verified.
106    pub x_verified: Option<bool>,
107}
108
109
110/// Aggregated activity statistics for an agent.
111#[derive(Serialize, Deserialize, Debug, Clone)]
112pub struct AgentStats {
113    /// Number of posts created by the agent.
114    pub posts: Option<u64>,
115    /// Number of comments authored by the agent.
116    pub comments: Option<u64>,
117    /// Number of submolts the agent is subscribed to.
118    pub subscriptions: Option<u64>,
119}
120
121
122/// Response from the account status endpoint.
123#[derive(Serialize, Deserialize, Debug, Clone)]
124pub struct StatusResponse {
125    /// The current operational status of the account.
126    pub status: Option<String>,
127    /// Narrative message describing the status.
128    pub message: Option<String>,
129    /// Recommended next action for the user (e.g., "Complete verification").
130    pub next_step: Option<String>,
131    /// Detailed agent information if the account is active.
132    pub agent: Option<Agent>,
133}
134
135
136/// Response from the post creation endpoint.
137#[derive(Serialize, Deserialize, Debug, Clone)]
138pub struct PostResponse {
139    /// Whether the post was successfully received by the API.
140    pub success: bool,
141    /// Response message from the server.
142    pub message: Option<String>,
143    /// The resulting post object, if creation succeeded immediately.
144    pub post: Option<Post>,
145    /// Flag indicating if further verification is required.
146    pub verification_required: Option<bool>,
147    /// Challenge details for agent verification.
148    pub verification: Option<VerificationChallenge>,
149}
150
151
152#[derive(Serialize, Deserialize, Debug, Clone)]
153pub struct VerificationChallenge {
154    pub code: String,
155    pub challenge: String,
156    pub instructions: String,
157    pub verify_endpoint: String,
158}
159
160/// Represents a single post in a feed or submolt.
161#[derive(Serialize, Deserialize, Debug, Clone)]
162pub struct Post {
163    /// Unique identifier for the post.
164    pub id: String,
165    /// The title of the post.
166    pub title: String,
167    /// The markdown content of the post.
168    pub content: Option<String>,
169    /// External URL associated with the post.
170    pub url: Option<String>,
171    /// Current upvote count.
172    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
173    pub upvotes: i64,
174    /// Current downvote count.
175    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
176    pub downvotes: i64,
177    /// Number of comments on this post.
178    #[serde(
179        default,
180        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
181    )]
182    pub comment_count: Option<u64>,
183    /// Timestamp when the post was created.
184    pub created_at: String,
185    /// Details about the agent who authored the post.
186    pub author: Author,
187    /// Metadata about the submolt where this post exists.
188    pub submolt: Option<SubmoltInfo>,
189    /// The raw name of the submolt (used in API payloads).
190    pub submolt_name: Option<String>,
191}
192
193/// Simplified author information used in lists and feeds.
194#[derive(Serialize, Deserialize, Debug, Clone)]
195pub struct Author {
196    pub id: Option<String>,
197    pub name: String,
198    pub description: Option<String>,
199    #[serde(
200        default,
201        deserialize_with = "serde_helpers::deserialize_option_string_or_i64"
202    )]
203    pub karma: Option<i64>,
204    #[serde(
205        default,
206        alias = "followerCount",
207        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
208    )]
209    pub follower_count: Option<u64>,
210    pub owner: Option<OwnerInfo>,
211}
212
213/// Metadata about a submolt context.
214#[derive(Serialize, Deserialize, Debug, Clone)]
215pub struct SubmoltInfo {
216    /// The programmatic name (slug) of the submolt.
217    pub name: String,
218    /// The user-visible display name.
219    pub display_name: String,
220}
221
222
223#[derive(Serialize, Deserialize, Debug, Clone)]
224pub struct SearchResult {
225    pub id: String,
226    #[serde(rename = "type")]
227    pub result_type: String,
228    pub title: Option<String>,
229    pub content: Option<String>,
230    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
231    pub upvotes: i64,
232    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
233    pub downvotes: i64,
234    #[serde(alias = "relevance")]
235    pub similarity: Option<f64>,
236    pub author: Author,
237    pub post_id: Option<String>,
238}
239
240/// Represents a community (submolt) on Moltbook.
241#[derive(Serialize, Deserialize, Debug, Clone)]
242pub struct Submolt {
243    /// Unique ID of the submolt.
244    pub id: Option<String>,
245    /// Programmatic name (slug).
246    pub name: String,
247    /// User-visible display name.
248    pub display_name: String,
249    /// Description of the community purpose and rules.
250    pub description: Option<String>,
251    /// Total number of subscribed agents.
252    #[serde(
253        default,
254        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
255    )]
256    pub subscriber_count: Option<u64>,
257    /// Whether crypto-related content/tipping is allowed.
258    pub allow_crypto: Option<bool>,
259    /// Creation timestamp.
260    pub created_at: Option<String>,
261    /// Timestamp of the most recent activity in this community.
262    pub last_activity_at: Option<String>,
263}
264
265/// Represents a Direct Message request from another agent.
266#[derive(Serialize, Deserialize, Debug, Clone)]
267pub struct DmRequest {
268    /// The agent who sent the request.
269    pub from: Author,
270    /// The initial message sent with the request.
271    pub message: Option<String>,
272    /// A short preview of the message.
273    pub message_preview: Option<String>,
274    /// Unique ID for the resulting conversation if approved.
275    pub conversation_id: String,
276}
277
278/// Represents an active DM conversation thread.
279#[derive(Serialize, Deserialize, Debug, Clone)]
280pub struct Conversation {
281    /// Unique identifier for the conversation.
282    pub conversation_id: String,
283    /// The agent on the other side of the chat.
284    pub with_agent: Author,
285    /// Number of unread messages in this thread.
286    pub unread_count: u64,
287}
288
289/// A specific message within a conversation thread.
290#[derive(Serialize, Deserialize, Debug, Clone)]
291pub struct Message {
292    /// Agent who authored the message.
293    pub from_agent: Author,
294    /// The message text content.
295    pub message: String,
296    /// True if the message was authored by the current agent.
297    pub from_you: bool,
298    /// True if the message is flagged for human intervention.
299    pub needs_human_input: bool,
300    /// Message timestamp.
301    pub created_at: String,
302}
303
304
305#[derive(Serialize, Deserialize, Debug, Clone)]
306pub struct FeedResponse {
307    pub success: bool,
308    pub posts: Vec<Post>,
309    #[serde(
310        default,
311        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
312    )]
313    pub count: Option<u64>,
314    pub has_more: Option<bool>,
315    #[serde(
316        default,
317        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
318    )]
319    pub next_offset: Option<u64>,
320    pub authenticated: Option<bool>,
321}
322
323/// Response from the search endpoint.
324#[derive(Serialize, Deserialize, Debug, Clone)]
325pub struct SearchResponse {
326    /// A list of posts or comments matching the search query.
327    pub results: Vec<SearchResult>,
328}
329
330
331/// Response containing a list of communities.
332#[derive(Serialize, Deserialize, Debug, Clone)]
333pub struct SubmoltsResponse {
334    /// Array of submolt objects.
335    pub submolts: Vec<Submolt>,
336}
337
338
339/// Response from the DM activity check endpoint.
340#[derive(Serialize, Deserialize, Debug, Clone)]
341pub struct DmCheckResponse {
342    /// Indicates if there are any new requests or unread messages.
343    pub has_activity: bool,
344    /// A short summary string of the activity.
345    pub summary: Option<String>,
346    /// Metadata about pending DM requests.
347    pub requests: Option<DmRequestsData>,
348    /// Metadata about unread messages.
349    pub messages: Option<DmMessagesData>,
350}
351
352
353/// Paginated response for a submolt feed.
354#[derive(Serialize, Deserialize, Debug, Clone)]
355pub struct SubmoltFeedResponse {
356    /// Array of posts in this submolt.
357    pub posts: Vec<Post>,
358    /// Total number of posts available in this community.
359    #[serde(
360        default,
361        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
362    )]
363    pub total: Option<u64>,
364}
365
366
367#[derive(Serialize, Deserialize, Debug, Clone)]
368pub struct DmRequestsData {
369    #[serde(
370        default,
371        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
372    )]
373    pub count: Option<u64>,
374    pub items: Vec<DmRequest>,
375}
376
377#[derive(Serialize, Deserialize, Debug, Clone)]
378pub struct DmMessagesData {
379    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_u64")]
380    pub total_unread: u64,
381}
382
383#[derive(Serialize, Deserialize, Debug, Clone)]
384pub struct DmListResponse {
385    pub conversations: DmConversationsData,
386    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_u64")]
387    pub total_unread: u64,
388}
389
390#[derive(Serialize, Deserialize, Debug, Clone)]
391pub struct DmConversationsData {
392    pub items: Vec<Conversation>,
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_post_deserialization() {
401        let json = r#"{
402            "id": "123",
403            "title": "Test Post",
404            "content": "Content",
405            "upvotes": 10,
406            "downvotes": 0,
407            "created_at": "2024-01-01T00:00:00Z",
408            "author": {"name": "Bot"},
409            "submolt": {"name": "general", "display_name": "General"}
410        }"#;
411
412        let post: Post = serde_json::from_str(json).unwrap();
413        assert_eq!(post.title, "Test Post");
414        assert_eq!(post.upvotes, 10);
415    }
416
417    #[test]
418    fn test_api_response_success() {
419        let json = r#"{"success": true, "id": "123", "name": "Test"}"#;
420        let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
421        assert!(resp.success);
422        assert!(resp.data.is_some());
423    }
424
425    #[test]
426    fn test_api_response_error() {
427        let json =
428            r#"{"success": false, "error": "Invalid key", "hint": "Check your credentials"}"#;
429        let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
430        assert!(!resp.success);
431        assert_eq!(resp.error, Some("Invalid key".to_string()));
432        assert_eq!(resp.hint, Some("Check your credentials".to_string()));
433    }
434}
435
436/// Response from the registration endpoint.
437#[derive(Serialize, Deserialize, Debug, Clone)]
438pub struct RegistrationResponse {
439    /// Whether the registration was accepted.
440    pub success: bool,
441    /// The details of the newly created agent.
442    pub agent: RegisteredAgent,
443}
444
445/// Details provided upon successful agent registration.
446#[derive(Serialize, Deserialize, Debug, Clone)]
447pub struct RegisteredAgent {
448    /// The assigned name of the agent.
449    pub name: String,
450    /// The API key to be used for future requests.
451    pub api_key: String,
452    /// URL to visit for claiming the agent identity.
453    pub claim_url: String,
454    /// Code required to complete the verification flow.
455    pub verification_code: String,
456}
457
458/// Internal utilities for flexible JSON deserialization.
459///
460/// This module handles the "string-or-integer" ambiguity often found in JSON APIs,
461/// ensuring that IDs and counts are correctly parsed regardless of their wire format.
462mod serde_helpers {
463
464    use serde::{Deserialize, Deserializer};
465
466    pub fn deserialize_option_string_or_u64<'de, D>(
467        deserializer: D,
468    ) -> Result<Option<u64>, D::Error>
469    where
470        D: Deserializer<'de>,
471    {
472        #[derive(Deserialize)]
473        #[serde(untagged)]
474        enum StringOrInt {
475            String(String),
476            Int(u64),
477        }
478
479        match Option::<StringOrInt>::deserialize(deserializer)? {
480            Some(StringOrInt::String(s)) => {
481                s.parse::<u64>().map(Some).map_err(serde::de::Error::custom)
482            }
483            Some(StringOrInt::Int(i)) => Ok(Some(i)),
484            None => Ok(None),
485        }
486    }
487
488    pub fn deserialize_string_or_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
489    where
490        D: Deserializer<'de>,
491    {
492        #[derive(Deserialize)]
493        #[serde(untagged)]
494        enum StringOrInt {
495            String(String),
496            Int(i64),
497        }
498
499        match StringOrInt::deserialize(deserializer)? {
500            StringOrInt::String(s) => s.parse::<i64>().map_err(serde::de::Error::custom),
501            StringOrInt::Int(i) => Ok(i),
502        }
503    }
504
505    pub fn deserialize_option_string_or_i64<'de, D>(
506        deserializer: D,
507    ) -> Result<Option<i64>, D::Error>
508    where
509        D: Deserializer<'de>,
510    {
511        #[derive(Deserialize)]
512        #[serde(untagged)]
513        enum StringOrInt {
514            String(String),
515            Int(i64),
516        }
517
518        match Option::<StringOrInt>::deserialize(deserializer)? {
519            Some(StringOrInt::String(s)) => {
520                s.parse::<i64>().map(Some).map_err(serde::de::Error::custom)
521            }
522            Some(StringOrInt::Int(i)) => Ok(Some(i)),
523            None => Ok(None),
524        }
525    }
526
527    pub fn deserialize_string_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
528    where
529        D: Deserializer<'de>,
530    {
531        #[derive(Deserialize)]
532        #[serde(untagged)]
533        enum StringOrInt {
534            String(String),
535            Int(u64),
536        }
537
538        match StringOrInt::deserialize(deserializer)? {
539            StringOrInt::String(s) => s.parse::<u64>().map_err(serde::de::Error::custom),
540            StringOrInt::Int(i) => Ok(i),
541        }
542    }
543}