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