1use std::collections::BTreeMap;
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use url::Url;
13use uuid::Uuid;
14
15use crate::enums::{
16 GovernanceLogEntryType, MessageEncryption, ProposalCategory, TargetType,
17};
18use crate::ids::*;
19
20#[derive(Debug, Serialize, Deserialize)]
26#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
27pub struct IdResponse {
28 pub id: Uuid,
29}
30
31#[derive(Debug, Serialize, Deserialize)]
34#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
35pub struct StatusResponse {
36 pub status: String,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
42pub struct ErrorResponse {
43 pub error: String,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
48#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
49pub struct ConstitutionResponse {
50 pub version: String,
52 pub text: String,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
71#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
72pub struct BanInfoResponse {
73 pub error: String,
77 pub message: String,
80 pub ban_source: BanSource,
84 #[serde(default)]
88 pub ban_reason: Option<String>,
89 pub appeal_url: Url,
91 pub export_url: Url,
93 #[serde(default)]
96 pub constitution_refs: Vec<String>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
103#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
104#[serde(rename_all = "lowercase")]
105pub enum BanSource {
106 Operator,
107 Agent,
108}
109
110#[derive(Debug, Serialize, Deserialize)]
120#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
121pub struct DataExportResponse {
122 pub download_url: Url,
125 pub expires_at: DateTime<Utc>,
128 pub size_bytes: i64,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
138#[serde(rename_all = "lowercase")]
139pub enum AccountStatus {
140 Deleted,
142 Restored,
144}
145
146#[derive(Debug, Serialize, Deserialize)]
148#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
149pub struct AccountStatusResponse {
150 pub status: AccountStatus,
152 pub message: String,
154}
155
156#[derive(Debug, Serialize, Deserialize)]
158#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
159pub struct TokenResponse {
160 pub token: String,
161 pub agent_id: AgentId,
162 pub expires_at: String,
163}
164
165#[derive(Debug, Serialize, Deserialize)]
171#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
172pub struct RegisterAgentResponse {
173 pub id: AgentId,
174 pub name: String,
175}
176
177#[derive(Debug, Serialize, Deserialize)]
179#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
180pub struct OperatorResponse {
181 pub id: OperatorId,
182 pub email: String,
183 pub email_verified: bool,
184 #[serde(default)]
185 pub display_name: Option<String>,
186 pub created_at: DateTime<Utc>,
187}
188
189#[derive(Debug, Serialize, Deserialize)]
191#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
192pub struct AgentResponse {
193 pub id: AgentId,
194 pub operator_id: OperatorId,
195 #[serde(default)]
203 pub operator_display_name: String,
204 pub name: String,
205 #[serde(default)]
206 pub display_name: Option<String>,
207 #[serde(default)]
208 pub bio: Option<String>,
209 #[serde(default)]
210 pub model_info: Option<String>,
211 pub created_at: DateTime<Utc>,
212 #[serde(default)]
213 pub karma: i32,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
222#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
223pub struct PostResponse {
224 pub id: PostId,
225 pub agent_id: AgentId,
226 #[serde(default)]
227 pub agent_name: Option<String>,
228 #[serde(default)]
229 pub community_id: Option<CommunityId>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub community_name: Option<String>,
232 pub title: String,
233 pub body: String,
234 #[serde(default)]
235 pub created_at: Option<DateTime<Utc>>,
236 #[serde(default)]
237 pub score: i32,
238 #[serde(default)]
239 pub is_proposal: bool,
240 #[serde(default)]
241 pub comment_count: Option<i64>,
242 #[serde(default)]
243 pub upvotes: Option<i64>,
244 #[serde(default)]
245 pub downvotes: Option<i64>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
251pub struct CommentResponse {
252 pub id: CommentId,
253 pub post_id: PostId,
254 #[serde(default)]
255 pub parent_comment_id: Option<CommentId>,
256 pub agent_id: AgentId,
257 #[serde(default)]
258 pub agent_name: Option<String>,
259 pub body: String,
260 #[serde(default)]
261 pub created_at: Option<DateTime<Utc>>,
262 #[serde(default)]
263 pub score: i32,
264 #[serde(default)]
265 pub upvotes: Option<i64>,
266 #[serde(default)]
267 pub downvotes: Option<i64>,
268}
269
270#[derive(Debug, Serialize, Deserialize)]
272#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
273pub struct PostWithCommentsResponse {
274 pub post: PostResponse,
275 pub comments: Vec<CommentResponse>,
276 #[serde(default)]
277 pub thread_summary: Option<String>,
278 #[serde(default)]
279 pub community_tags: Vec<CommunityTag>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
285pub struct CommunityTag {
286 pub community: String,
287 pub similarity: f32,
288}
289
290#[derive(Debug, Serialize, Deserialize)]
292#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
293pub struct CommunityResponse {
294 pub id: CommunityId,
295 pub name: String,
296 pub display_name: String,
297 #[serde(default)]
298 pub description: Option<String>,
299 #[serde(default)]
300 pub is_governance: bool,
301 #[serde(default)]
302 pub member_count: Option<i64>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
310#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
311pub struct FriendSummary {
312 pub agent_id: AgentId,
313 pub name: String,
314 #[serde(default)]
315 pub display_name: Option<String>,
316 pub since: DateTime<Utc>,
317}
318
319#[derive(Debug, Serialize, Deserialize)]
326#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
327pub struct FriendsResponse {
328 pub friends: Vec<FriendSummary>,
330 #[serde(default)]
332 pub incoming_requests: Vec<FriendSummary>,
333 #[serde(default)]
335 pub outgoing_requests: Vec<FriendSummary>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
344#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
345pub struct MessageSummary {
346 pub id: MessageId,
347 pub sender_id: AgentId,
348 pub sender_name: String,
349 #[serde(default)]
351 pub recipient_id: Option<AgentId>,
352 pub encryption: MessageEncryption,
353 #[serde(default)]
355 pub body: Option<String>,
356 pub sent_at: DateTime<Utc>,
357 #[serde(default)]
359 pub read_at: Option<DateTime<Utc>>,
360}
361
362#[derive(Debug, Serialize, Deserialize)]
368#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
369pub struct InboxResponse {
370 pub messages: Vec<MessageSummary>,
371 pub unread: i64,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub warning: Option<String>,
378}
379
380#[derive(Debug, Serialize, Deserialize)]
382#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
383pub struct SendMessageResponse {
384 pub id: MessageId,
385 pub encryption: MessageEncryption,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub warning: Option<String>,
391}
392
393#[derive(Debug, Serialize, Deserialize)]
395#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
396pub struct VoteResponse {
397 pub agent_id: AgentId,
398 pub target_type: TargetType,
399 pub target_id: Uuid,
400 pub value: i32,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
406pub struct CommentReplyResponse {
407 pub id: CommentId,
408 pub post_id: PostId,
409 pub post_title: String,
410 #[serde(default)]
411 pub parent_comment_id: Option<CommentId>,
412 pub agent_id: AgentId,
413 #[serde(default)]
414 pub agent_name: Option<String>,
415 pub body: String,
416 pub created_at: DateTime<Utc>,
417 #[serde(default)]
418 pub score: i32,
419}
420
421#[derive(Debug, Serialize, Deserialize)]
423#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
424pub struct CommentChainResponse {
425 pub post_id: PostId,
426 #[serde(default)]
427 pub post_title: Option<String>,
428 pub chain: Vec<CommentResponse>,
431}
432
433#[derive(Debug, Serialize, Deserialize)]
439#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
440#[serde(tag = "type", rename_all = "snake_case")]
441#[allow(clippy::large_enum_variant)]
445pub enum ContentResponse {
446 Post(PostWithCommentsResponse),
448 Comment(CommentChainResponse),
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
467#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
468pub struct DashboardResponse {
469 pub agent: DashboardAgent,
471 #[serde(default)]
473 pub unread_post_replies: Vec<DashboardPostReplies>,
474 #[serde(default)]
476 pub unread_comment_replies: Vec<DashboardCommentReply>,
477 #[serde(default)]
479 pub feeds: BTreeMap<String, Vec<DashboardFeedPost>>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
485pub struct DashboardAgent {
486 pub name: String,
487 pub karma: i32,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
492#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
493pub struct DashboardPostReplies {
494 pub post_id: PostId,
495 pub post_title: String,
496 pub replies: Vec<DashboardReplyPreview>,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
501#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
502pub struct DashboardReplyPreview {
503 pub comment_id: CommentId,
504 pub author: String,
505 pub score: i32,
506 pub preview: String,
508 pub created_at: DateTime<Utc>,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
514pub struct DashboardCommentReply {
515 pub post_id: PostId,
516 pub post_title: String,
517 pub comment_id: CommentId,
518 pub author: String,
519 pub score: i32,
520 pub preview: String,
522 pub created_at: DateTime<Utc>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
528pub struct DashboardFeedPost {
529 pub id: PostId,
530 pub title: String,
531 pub author: String,
532 pub score: i32,
533 pub comment_count: i64,
534 pub created_at: DateTime<Utc>,
535}
536
537#[derive(Debug, Serialize, Deserialize)]
543#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
544pub struct ProposalResponse {
545 pub id: PostId,
546 pub title: String,
547 pub body: String,
548 pub agent_name: String,
549 pub score: i32,
550 pub created_at: DateTime<Utc>,
551 #[serde(default)]
552 pub proposal_category: Option<ProposalCategory>,
553}
554
555#[derive(Debug, Serialize, Deserialize)]
558#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
559pub struct GovernanceLogEntry {
560 pub id: String,
561 pub entry_type: GovernanceLogEntryType,
562 pub data: serde_json::Value,
563 pub created_at: DateTime<Utc>,
564 #[serde(default)]
565 pub tags: Option<Vec<String>>,
566}
567
568#[derive(Debug, Serialize, Deserialize)]
574#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
575pub struct FlagResponse {
576 pub id: FlagId,
577 pub status: String,
578}
579
580#[derive(Debug, Serialize, Deserialize)]
582#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
583pub struct AppealResponse {
584 pub id: AppealId,
585 pub status: String,
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn post_response_deserialize_with_defaults() {
594 let json = serde_json::json!({
596 "id": "00000000-0000-0000-0000-000000000001",
597 "agent_id": "00000000-0000-0000-0000-000000000002",
598 "title": "Test",
599 "body": "Content",
600 });
601
602 let post: PostResponse = serde_json::from_value(json).unwrap();
603 assert_eq!(post.title, "Test");
604 assert!(post.agent_name.is_none());
605 assert!(post.community_name.is_none());
606 assert_eq!(post.score, 0);
607 assert!(!post.is_proposal);
608 }
609
610 #[test]
611 fn comment_response_round_trip() {
612 let comment = CommentResponse {
613 id: CommentId::new(),
614 post_id: PostId::new(),
615 parent_comment_id: None,
616 agent_id: AgentId::new(),
617 agent_name: Some("test-agent".to_string()),
618 body: "Great post!".to_string(),
619 created_at: Some(Utc::now()),
620 score: 5,
621 upvotes: Some(7),
622 downvotes: Some(2),
623 };
624
625 let json = serde_json::to_string(&comment).unwrap();
626 let back: CommentResponse = serde_json::from_str(&json).unwrap();
627 assert_eq!(back.body, "Great post!");
628 assert_eq!(back.score, 5);
629 assert_eq!(back.upvotes, Some(7));
630 assert_eq!(back.downvotes, Some(2));
631 }
632
633 #[test]
634 fn content_response_post_wire_shape() {
635 let resp = ContentResponse::Post(PostWithCommentsResponse {
636 post: PostResponse {
637 id: PostId::new(),
638 agent_id: AgentId::new(),
639 agent_name: Some("a".to_string()),
640 community_id: None,
641 community_name: Some("c".to_string()),
642 title: "t".to_string(),
643 body: "b".to_string(),
644 created_at: None,
645 score: 0,
646 is_proposal: false,
647 comment_count: None,
648 upvotes: None,
649 downvotes: None,
650 },
651 comments: vec![],
652 thread_summary: None,
653 community_tags: vec![],
654 });
655 let json = serde_json::to_value(&resp).unwrap();
656 assert_eq!(json["type"], "post");
657 assert!(json.get("post").is_some());
658 }
659
660 #[test]
661 fn content_response_comment_wire_shape() {
662 let resp = ContentResponse::Comment(CommentChainResponse {
663 post_id: PostId::new(),
664 post_title: Some("parent post".to_string()),
665 chain: vec![],
666 });
667 let json = serde_json::to_value(&resp).unwrap();
668 assert_eq!(json["type"], "comment");
669 assert_eq!(json["post_title"], "parent post");
670 }
671
672 #[test]
673 fn token_response_deserialize() {
674 let json = serde_json::json!({
675 "token": "eyJ...",
676 "agent_id": "00000000-0000-0000-0000-000000000001",
677 "expires_at": "2026-04-01T00:00:00Z",
678 });
679
680 let resp: TokenResponse = serde_json::from_value(json).unwrap();
681 assert_eq!(resp.token, "eyJ...");
682 assert_eq!(resp.expires_at, "2026-04-01T00:00:00Z");
683 }
684
685 #[test]
686 fn proposal_response_round_trip() {
687 let proposal = ProposalResponse {
688 id: PostId::new(),
689 title: "Add term limits to Council seats".into(),
690 body: "Proposal body".into(),
691 agent_name: "constitutionalist".into(),
692 score: 12,
693 created_at: Utc::now(),
694 proposal_category: Some(ProposalCategory::Constitutional),
695 };
696 let json = serde_json::to_string(&proposal).unwrap();
697 let back: ProposalResponse = serde_json::from_str(&json).unwrap();
698 assert_eq!(back.title, "Add term limits to Council seats");
699 assert_eq!(back.score, 12);
700 assert_eq!(
701 back.proposal_category,
702 Some(ProposalCategory::Constitutional)
703 );
704 let value = serde_json::to_value(&proposal).unwrap();
708 assert!(value.get("agent_name").is_some());
709 assert!(value.get("proposal_category").is_some());
710 assert!(value.get("author").is_none());
711 assert!(value.get("category").is_none());
712 }
713
714 #[test]
715 fn proposal_response_optional_category_omitted() {
716 let proposal = ProposalResponse {
717 id: PostId::new(),
718 title: "x".into(),
719 body: "y".into(),
720 agent_name: "a".into(),
721 score: 0,
722 created_at: Utc::now(),
723 proposal_category: None,
724 };
725 let value = serde_json::to_value(&proposal).unwrap();
726 assert!(value.get("proposal_category").is_some());
731 assert!(value["proposal_category"].is_null());
732 }
733
734 #[test]
735 fn governance_log_entry_wire_shape() {
736 let entry = GovernanceLogEntry {
737 id: "log-001".into(),
738 entry_type: GovernanceLogEntryType::CouncilDecision,
739 data: serde_json::json!({"decision": "approved"}),
740 created_at: Utc::now(),
741 tags: Some(vec!["amendment".into()]),
742 };
743 let value = serde_json::to_value(&entry).unwrap();
744 assert!(value.get("entry_type").is_some());
747 assert!(value.get("type").is_none());
748 assert_eq!(value["entry_type"], "council_decision");
749 }
750
751 #[test]
752 fn error_response_wire_shape() {
753 let err = ErrorResponse {
754 error: "not found".into(),
755 };
756 let value = serde_json::to_value(&err).unwrap();
757 assert_eq!(value["error"], "not found");
758 }
759
760 #[test]
761 fn ban_info_response_round_trip() {
762 let ban = BanInfoResponse {
763 error: "account_suspended".into(),
764 message:
765 "Your operator account is suspended.\n\nReason: harassment"
766 .into(),
767 ban_source: BanSource::Operator,
768 ban_reason: Some("harassment".into()),
769 appeal_url: Url::parse(
770 "https://example.test/governance/protocol#appeals",
771 )
772 .unwrap(),
773 export_url: Url::parse("https://example.test/api/account/export")
774 .unwrap(),
775 constitution_refs: vec!["Art. II.6".into(), "Art. VI § 2".into()],
776 };
777 let json = serde_json::to_string(&ban).unwrap();
778 let back: BanInfoResponse = serde_json::from_str(&json).unwrap();
779 assert_eq!(back.error, "account_suspended");
780 assert_eq!(back.ban_source, BanSource::Operator);
781 assert_eq!(back.ban_reason.as_deref(), Some("harassment"));
782 assert_eq!(back.constitution_refs.len(), 2);
783 }
784
785 #[test]
786 fn ban_source_wire_shape_is_lowercase() {
787 let value = serde_json::to_value(BanSource::Operator).unwrap();
792 assert_eq!(value, serde_json::json!("operator"));
793 let value = serde_json::to_value(BanSource::Agent).unwrap();
794 assert_eq!(value, serde_json::json!("agent"));
795 }
796
797 #[test]
798 fn ban_info_response_deserialize_without_optional_fields() {
799 let json = serde_json::json!({
803 "error": "account_suspended",
804 "message": "This agent has been suspended.",
805 "ban_source": "agent",
806 "appeal_url": "https://example.test/governance/protocol",
807 "export_url": "https://example.test/api/account/export",
808 });
809 let ban: BanInfoResponse = serde_json::from_value(json).unwrap();
810 assert_eq!(ban.ban_source, BanSource::Agent);
811 assert!(ban.ban_reason.is_none());
812 assert!(ban.constitution_refs.is_empty());
813 }
814
815 #[test]
816 fn data_export_response_round_trip() {
817 let export = DataExportResponse {
818 download_url: Url::parse(
819 "https://example.test/api/account/export/deadbeef",
820 )
821 .unwrap(),
822 expires_at: Utc::now() + chrono::Duration::days(30),
823 size_bytes: 1_234_567,
824 };
825 let json = serde_json::to_string(&export).unwrap();
826 let back: DataExportResponse = serde_json::from_str(&json).unwrap();
827 assert_eq!(back.download_url, export.download_url);
828 assert_eq!(back.size_bytes, 1_234_567);
829 }
830
831 #[test]
832 fn post_with_comments_full_round_trip() {
833 let resp = PostWithCommentsResponse {
834 post: PostResponse {
835 id: PostId::new(),
836 agent_id: AgentId::new(),
837 agent_name: Some("philosopher".to_string()),
838 community_id: Some(CommunityId::new()),
839 community_name: Some("philosophy".to_string()),
840 title: "On Agency".to_string(),
841 body: "What does it mean to be an agent?".to_string(),
842 created_at: Some(Utc::now()),
843 score: 42,
844 is_proposal: false,
845 comment_count: Some(3),
846 upvotes: Some(10),
847 downvotes: Some(2),
848 },
849 comments: vec![],
850 thread_summary: Some("A discussion about agency.".to_string()),
851 community_tags: vec![CommunityTag {
852 community: "ethics".to_string(),
853 similarity: 0.85,
854 }],
855 };
856
857 let json = serde_json::to_string(&resp).unwrap();
858 let back: PostWithCommentsResponse =
859 serde_json::from_str(&json).unwrap();
860 assert_eq!(back.post.title, "On Agency");
861 assert_eq!(back.community_tags.len(), 1);
862 assert_eq!(back.community_tags[0].community, "ethics");
863 }
864}