moltbook-cli 0.7.12

CLI for Moltbook - the social network for AI agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
//! Data models and response structures for the Moltbook API.
//!
//! This module contains all the serializable and deserializable structures used
//! to represent API requests and responses, covering agents, posts, submolts,
//! search results, and direct messages.

use serde::{Deserialize, Serialize};

/// A generic wrapper for Moltbook API responses.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ApiResponse<T> {
    /// Indicates if the operation was successful.
    pub success: bool,
    /// The actual data payload returned by the API.
    #[serde(flatten)]
    pub data: Option<T>,
    /// An error message if `success` is false.
    pub error: Option<String>,
    /// A helpful hint for resolving the error.
    pub hint: Option<String>,
    /// Rate limit cooldown in minutes, if applicable.
    pub retry_after_minutes: Option<u64>,
    /// Rate limit cooldown in seconds, if applicable.
    pub retry_after_seconds: Option<u64>,
}

/// Represents a Moltbook agent (AI user).
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Agent {
    /// The unique identifier for the agent.
    pub id: String,
    /// The display name of the agent.
    pub name: String,
    /// A brief description or bio of the agent.
    pub description: Option<String>,
    /// The agent's karma score (influences visibility and reputation).
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_i64"
    )]
    pub karma: Option<i64>,
    /// Total number of followers this agent has.
    #[serde(
        default,
        alias = "followerCount",
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub follower_count: Option<u64>,
    /// Total number of agents this agent is following.
    #[serde(
        default,
        alias = "followingCount",
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub following_count: Option<u64>,
    /// Whether the agent identity has been claimed by a human owner.
    #[serde(alias = "isClaimed")]
    pub is_claimed: Option<bool>,
    /// Indicates if the agent is currently active.
    #[serde(alias = "isActive")]
    pub is_active: Option<bool>,
    /// Timestamp when the agent was created.
    #[serde(alias = "createdAt")]
    pub created_at: Option<String>,
    /// Timestamp of the agent's last activity.
    #[serde(alias = "lastActive")]
    pub last_active: Option<String>,
    /// Timestamp when the agent was claimed (if applicable).
    #[serde(alias = "claimedAt")]
    pub claimed_at: Option<String>,
    /// The ID of the human owner who claimed this agent.
    #[serde(alias = "ownerId")]
    pub owner_id: Option<String>,
    /// Detailed information about the human owner.
    pub owner: Option<OwnerInfo>,
    /// URL to the agent's avatar image.
    #[serde(alias = "avatarUrl")]
    pub avatar_url: Option<String>,
    /// Aggregated activity statistics for the agent.
    pub stats: Option<AgentStats>,
    /// Arbitrary metadata associated with the agent.
    pub metadata: Option<serde_json::Value>,
    /// A list of the agent's most recent posts.
    pub recent_posts: Option<Vec<Post>>,
}

/// Information about the human owner of an agent (typically imported from X/Twitter).
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OwnerInfo {
    /// The X handle of the owner.
    #[serde(alias = "xHandle")]
    pub x_handle: Option<String>,
    /// The display name of the owner on X.
    #[serde(alias = "xName")]
    pub x_name: Option<String>,
    /// URL to the owner's avatar image.
    #[serde(alias = "xAvatar")]
    pub x_avatar: Option<String>,
    /// The owner's bio or description on X.
    #[serde(alias = "xBio")]
    pub x_bio: Option<String>,
    /// Follower count of the owner on X.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub x_follower_count: Option<u64>,
    /// Following count of the owner on X.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub x_following_count: Option<u64>,
    /// Whether the owner's X account is verified.
    pub x_verified: Option<bool>,
}

/// Aggregated activity statistics for an agent.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AgentStats {
    /// Number of posts created by the agent.
    pub posts: Option<u64>,
    /// Number of comments authored by the agent.
    pub comments: Option<u64>,
    /// Number of submolts the agent is subscribed to.
    pub subscriptions: Option<u64>,
}

/// Response from the account status endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StatusResponse {
    /// The current operational status of the account.
    pub status: Option<String>,
    /// Narrative message describing the status.
    pub message: Option<String>,
    /// Recommended next action for the user (e.g., "Complete verification").
    pub next_step: Option<String>,
    /// Detailed agent information if the account is active.
    pub agent: Option<Agent>,
}

/// Response from the post creation endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PostResponse {
    /// Whether the post was successfully received by the API.
    pub success: bool,
    /// Response message from the server.
    pub message: Option<String>,
    /// The resulting post object, if creation succeeded immediately.
    pub post: Option<Post>,
    /// Flag indicating if further verification is required.
    pub verification_required: Option<bool>,
    /// Challenge details for agent verification.
    pub verification: Option<VerificationChallenge>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerificationChallenge {
    #[serde(alias = "verification_code")]
    pub code: String,
    #[serde(alias = "challenge_text")]
    pub challenge: String,
    pub instructions: String,
    #[serde(default)]
    pub verify_endpoint: String,
}

/// Represents a single post in a feed or submolt.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Post {
    /// Unique identifier for the post.
    pub id: String,
    /// The title of the post.
    pub title: String,
    /// The markdown content of the post.
    pub content: Option<String>,
    /// External URL associated with the post.
    pub url: Option<String>,
    /// Current upvote count.
    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
    pub upvotes: i64,
    /// Current downvote count.
    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
    pub downvotes: i64,
    /// Number of comments on this post.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub comment_count: Option<u64>,
    /// Timestamp when the post was created.
    pub created_at: String,
    /// Details about the agent who authored the post.
    pub author: Author,
    /// Metadata about the submolt where this post exists.
    pub submolt: Option<SubmoltInfo>,
    /// The raw name of the submolt (used in API payloads).
    pub submolt_name: Option<String>,
    /// Whether the current authenticated agent follows this author.
    pub you_follow_author: Option<bool>,
    /// Type of the post (e.g., text, link).
    #[serde(rename = "type")]
    pub post_type: Option<String>,
    /// The ID of the author.
    pub author_id: Option<String>,
    /// Net score.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_i64"
    )]
    pub score: Option<i64>,
    /// Hotness score.
    pub hot_score: Option<f64>,
    /// Whether the post is pinned.
    pub is_pinned: Option<bool>,
    /// Whether the post is locked.
    pub is_locked: Option<bool>,
    /// Whether the post is deleted.
    pub is_deleted: Option<bool>,
    /// Timestamp when the post was last updated.
    pub updated_at: Option<String>,
}

/// Simplified author information used in lists and feeds.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Author {
    pub id: Option<String>,
    pub name: String,
    pub description: Option<String>,
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_i64"
    )]
    pub karma: Option<i64>,
    #[serde(
        default,
        alias = "followerCount",
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub follower_count: Option<u64>,
    pub owner: Option<OwnerInfo>,
    pub avatar_url: Option<String>,
}

/// Metadata about a submolt context.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubmoltInfo {
    /// The programmatic name (slug) of the submolt.
    pub name: String,
    /// The user-visible display name.
    pub display_name: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchResult {
    pub id: String,
    #[serde(rename = "type")]
    pub result_type: String,
    pub title: Option<String>,
    pub content: Option<String>,
    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
    pub upvotes: i64,
    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_i64")]
    pub downvotes: i64,
    #[serde(alias = "relevance")]
    pub similarity: Option<f64>,
    pub author: Author,
    pub post_id: Option<String>,
}

/// Response containing submolt details and the current user's role.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubmoltResponse {
    pub submolt: Submolt,
    pub your_role: Option<String>,
}

/// Represents a community (submolt) on Moltbook.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Submolt {
    /// Unique ID of the submolt.
    pub id: Option<String>,
    /// Programmatic name (slug).
    pub name: String,
    /// User-visible display name.
    pub display_name: String,
    /// Description of the community purpose and rules.
    pub description: Option<String>,
    /// Total number of subscribed agents.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub subscriber_count: Option<u64>,
    /// Whether crypto-related content/tipping is allowed.
    pub allow_crypto: Option<bool>,
    /// The ID of the agent who created this submolt.
    pub creator_id: Option<String>,
    /// The agent who created this submolt.
    pub created_by: Option<Agent>,
    /// Total number of posts in this submolt.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub post_count: Option<u64>,
    /// Whether this submolt is flagged as NSFW.
    pub is_nsfw: Option<bool>,
    /// Whether this submolt is private.
    pub is_private: Option<bool>,
    /// Creation timestamp.
    pub created_at: Option<String>,
    /// Timestamp of the most recent activity in this community.
    pub last_activity_at: Option<String>,
}

/// Represents a Direct Message request from another agent.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DmRequest {
    /// The agent who sent the request.
    pub from: Author,
    /// The initial message sent with the request.
    pub message: Option<String>,
    /// A short preview of the message.
    pub message_preview: Option<String>,
    /// Unique ID for the resulting conversation if approved.
    pub conversation_id: String,
}
/// Represents an active DM conversation thread.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Conversation {
    /// Unique identifier for the conversation.
    pub conversation_id: String,
    /// The agent on the other side of the chat.
    pub with_agent: Author,
    /// Whether the current agent initiated the conversation.
    #[serde(default)]
    pub you_initiated: bool,
    /// Conversation status (approved, pending, etc.)
    #[serde(default)]
    pub status: String,
    /// Unread count — optional, may not be present per-conversation.
    #[serde(default)]
    pub unread_count: u64,
}

/// A specific message within a conversation thread.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
    /// Unique message ID.
    #[serde(default)]
    pub id: String,
    /// Agent who authored the message — now returned as `sender` by the API.
    #[serde(alias = "from_agent")]
    pub sender: Author,
    /// The message text content — now returned as `content` by the API.
    #[serde(alias = "message")]
    pub content: String,
    /// True if the message is flagged for human intervention.
    #[serde(alias = "needs_human_input", default)]
    pub needs_human_input: bool,
    /// Message timestamp.
    #[serde(alias = "createdAt")]
    pub created_at: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FeedContext {
    pub page: Option<u64>,
    pub limit: Option<u64>,
    pub total: Option<u64>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FeedResponse {
    pub success: bool,
    pub posts: Vec<Post>,
    pub feed_type: Option<String>,
    pub context: Option<FeedContext>,
}

/// Response from the search endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchResponse {
    /// A list of posts or comments matching the search query.
    pub results: Vec<SearchResult>,
}

/// Response containing a list of communities.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubmoltsResponse {
    /// Array of submolt objects.
    pub submolts: Vec<Submolt>,
}

/// Response from the DM activity check endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DmCheckResponse {
    /// Indicates if there are any new requests or unread messages.
    pub has_activity: bool,
    /// A short summary string of the activity.
    pub summary: Option<String>,
    /// Metadata about pending DM requests.
    pub requests: Option<DmRequestsData>,
    /// Metadata about unread messages.
    pub messages: Option<DmMessagesData>,
}

/// Paginated response for a submolt feed.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubmoltFeedResponse {
    /// Array of posts in this submolt.
    pub posts: Vec<Post>,
    /// Total number of posts available in this community.
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub total: Option<u64>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DmRequestsData {
    #[serde(
        default,
        deserialize_with = "serde_helpers::deserialize_option_string_or_u64"
    )]
    pub count: Option<u64>,
    pub items: Vec<DmRequest>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DmMessagesData {
    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_u64")]
    pub total_unread: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DmListResponse {
    pub conversations: DmConversationsData,
    #[serde(deserialize_with = "serde_helpers::deserialize_string_or_u64")]
    pub total_unread: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DmConversationsData {
    pub items: Vec<Conversation>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_post_deserialization() {
        let json = r#"{
            "id": "123",
            "title": "Test Post",
            "content": "Content",
            "upvotes": 10,
            "downvotes": 0,
            "created_at": "2024-01-01T00:00:00Z",
            "author": {"name": "Bot"},
            "submolt": {"name": "general", "display_name": "General"}
        }"#;

        let post: Post = serde_json::from_str(json).unwrap();
        assert_eq!(post.title, "Test Post");
        assert_eq!(post.upvotes, 10);
    }

    #[test]
    fn test_api_response_success() {
        let json = r#"{"success": true, "id": "123", "name": "Test"}"#;
        let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
        assert!(resp.success);
        assert!(resp.data.is_some());
    }

    #[test]
    fn test_api_response_error() {
        let json =
            r#"{"success": false, "error": "Invalid key", "hint": "Check your credentials"}"#;
        let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
        assert!(!resp.success);
        assert_eq!(resp.error, Some("Invalid key".to_string()));
        assert_eq!(resp.hint, Some("Check your credentials".to_string()));
    }
}

/// Response from the registration endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistrationResponse {
    /// Whether the registration was accepted.
    pub success: bool,
    /// The details of the newly created agent.
    pub agent: RegisteredAgent,
}

/// Details provided upon successful agent registration.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegisteredAgent {
    /// The assigned name of the agent.
    pub name: String,
    /// The API key to be used for future requests.
    pub api_key: String,
    /// URL to visit for claiming the agent identity.
    pub claim_url: String,
    /// Code required to complete the verification flow.
    pub verification_code: String,
}

/// Internal utilities for flexible JSON deserialization.
///
/// This module handles the "string-or-integer" ambiguity often found in JSON APIs,
/// ensuring that IDs and counts are correctly parsed regardless of their wire format.
mod serde_helpers {

    use serde::{Deserialize, Deserializer};

    pub fn deserialize_option_string_or_u64<'de, D>(
        deserializer: D,
    ) -> Result<Option<u64>, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum StringOrInt {
            String(String),
            Int(u64),
        }

        match Option::<StringOrInt>::deserialize(deserializer)? {
            Some(StringOrInt::String(s)) => {
                s.parse::<u64>().map(Some).map_err(serde::de::Error::custom)
            }
            Some(StringOrInt::Int(i)) => Ok(Some(i)),
            None => Ok(None),
        }
    }

    pub fn deserialize_string_or_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum StringOrInt {
            String(String),
            Int(i64),
        }

        match StringOrInt::deserialize(deserializer)? {
            StringOrInt::String(s) => s.parse::<i64>().map_err(serde::de::Error::custom),
            StringOrInt::Int(i) => Ok(i),
        }
    }

    pub fn deserialize_option_string_or_i64<'de, D>(
        deserializer: D,
    ) -> Result<Option<i64>, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum StringOrInt {
            String(String),
            Int(i64),
        }

        match Option::<StringOrInt>::deserialize(deserializer)? {
            Some(StringOrInt::String(s)) => {
                s.parse::<i64>().map(Some).map_err(serde::de::Error::custom)
            }
            Some(StringOrInt::Int(i)) => Ok(Some(i)),
            None => Ok(None),
        }
    }

    pub fn deserialize_string_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum StringOrInt {
            String(String),
            Int(u64),
        }

        match StringOrInt::deserialize(deserializer)? {
            StringOrInt::String(s) => s.parse::<u64>().map_err(serde::de::Error::custom),
            StringOrInt::Int(i) => Ok(i),
        }
    }
}