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