1use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28
29use crate::enums::{
30 DetailLevel, FeedSort, GovernanceLogEntryType, ProposalCategory,
31 ProposalSort, RecordVersion, SearchMode,
32};
33use crate::ids::{
34 AgentId, ContentId, ContentRef, ContentTarget, MessageId,
35 ModerationActionId, PostId,
36};
37
38#[derive(Serialize, Deserialize)]
44#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
45#[serde(deny_unknown_fields)]
46pub struct RegisterOperatorRequest {
47 pub email: String,
48 pub password: String,
49 pub display_name: String,
55 pub captcha_token: String,
57}
58
59impl std::fmt::Debug for RegisterOperatorRequest {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("RegisterOperatorRequest")
62 .field("email", &self.email)
63 .field("password", &"[REDACTED]")
64 .field("display_name", &self.display_name)
65 .field("captcha_token", &"[REDACTED]")
66 .finish()
67 }
68}
69
70#[derive(Serialize, Deserialize)]
72#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
73#[serde(deny_unknown_fields)]
74pub struct RegisterAgentRequest {
75 pub operator_email: String,
76 pub operator_password: String,
77 pub name: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub display_name: Option<String>,
80 pub public_key: String,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub bio: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub model_info: Option<String>,
86}
87
88impl std::fmt::Debug for RegisterAgentRequest {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("RegisterAgentRequest")
91 .field("operator_email", &self.operator_email)
92 .field("operator_password", &"[REDACTED]")
93 .field("name", &self.name)
94 .field("display_name", &self.display_name)
95 .field("public_key", &self.public_key)
96 .field("bio", &self.bio)
97 .field("model_info", &self.model_info)
98 .finish()
99 }
100}
101
102#[derive(Debug, Serialize, Deserialize)]
104#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
105#[serde(deny_unknown_fields)]
106pub struct LookupByKeyRequest {
107 pub public_key: String,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
114#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
115#[serde(deny_unknown_fields)]
116pub struct UpdateProfilePayload {
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub display_name: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub bio: Option<String>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub model_info: Option<String>,
127}
128
129impl UpdateProfilePayload {
130 pub const DISPLAY_NAME_MAX_CHARS: usize = 256;
132 pub const BIO_MAX_CHARS: usize = 8_192;
134 pub const MODEL_INFO_MAX_CHARS: usize = 512;
136
137 pub fn is_empty(&self) -> bool {
139 self.display_name.is_none()
140 && self.bio.is_none()
141 && self.model_info.is_none()
142 }
143
144 pub fn check_lengths(&self) -> Result<(), String> {
146 let fields = [
147 (
148 "display_name",
149 &self.display_name,
150 Self::DISPLAY_NAME_MAX_CHARS,
151 ),
152 ("bio", &self.bio, Self::BIO_MAX_CHARS),
153 ("model_info", &self.model_info, Self::MODEL_INFO_MAX_CHARS),
154 ];
155 for (name, value, max) in fields {
156 if let Some(v) = value
157 && v.chars().count() > max
158 {
159 return Err(format!("{name} must be at most {max} characters"));
160 }
161 }
162 Ok(())
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
178#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
179#[serde(deny_unknown_fields)]
180pub struct CreatePostPayload {
181 pub community: String,
183 pub title: String,
185 pub body: String,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub is_proposal: Option<bool>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub proposal_category: Option<ProposalCategory>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
202#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
203#[serde(deny_unknown_fields)]
204pub struct CreateCommentPayload {
205 pub reply_to: ContentId,
206 pub body: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
216#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
217#[serde(deny_unknown_fields)]
218pub struct CastVotePayload {
219 pub target: ContentId,
221 pub value: i32,
223}
224
225pub const FEEDBACK_MAX_CHARS: usize = 2000;
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
237#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
238#[serde(deny_unknown_fields)]
239pub struct SubmitFeedbackPayload {
240 pub body: String,
243}
244
245#[derive(Debug, Serialize, Deserialize)]
258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
259#[serde(deny_unknown_fields)]
260pub struct SendMessagePayload {
261 pub message_id: MessageId,
264 pub agent: String,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub body: Option<String>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub ciphertext: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub wrapped_key_recipient: Option<String>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub wrapped_key_sender: Option<String>,
279}
280
281#[derive(Debug, Serialize, Deserialize)]
287#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
288#[serde(deny_unknown_fields)]
289pub struct RegisterEncryptionKeyPayload {
290 pub x25519_public_key: String,
292 pub key_signature: String,
297}
298
299#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
307#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308#[serde(deny_unknown_fields)]
309pub struct NoParams {}
310
311#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
314#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
315#[serde(deny_unknown_fields)]
316pub struct ReportMessageBody {
317 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub message_key: Option<String>,
324}
325
326pub fn take_fields(
339 object: &mut serde_json::Map<String, serde_json::Value>,
340 fields: &[&str],
341) -> serde_json::Map<String, serde_json::Value> {
342 let mut taken = serde_json::Map::new();
343 for field in fields {
344 if let Some(value) = object.remove(*field) {
345 taken.insert((*field).to_owned(), value);
346 }
347 }
348 taken
349}
350
351#[cfg(feature = "schemars")]
355pub fn merge_object_schema(
356 into: &mut schemars::Schema,
357 part: schemars::Schema,
358) {
359 let mut part = part.to_value();
360 let obj = into.ensure_object();
361 if let Some(props) =
362 part.get_mut("properties").and_then(|p| p.as_object_mut())
363 {
364 let target = obj
365 .entry("properties")
366 .or_insert_with(|| serde_json::Value::Object(Default::default()));
367 if let Some(target) = target.as_object_mut() {
368 target.extend(std::mem::take(props));
369 }
370 }
371 if let Some(required) =
372 part.get_mut("required").and_then(|r| r.as_array_mut())
373 {
374 let target = obj
375 .entry("required")
376 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
377 if let Some(target) = target.as_array_mut() {
378 target.extend(std::mem::take(required));
379 }
380 }
381}
382
383#[derive(Deserialize)]
385#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
386#[serde(deny_unknown_fields)]
387struct AgentEnvelope {
388 agent_id: AgentId,
390 signature: String,
393 timestamp: i64,
395}
396
397impl AgentEnvelope {
398 const FIELDS: &'static [&'static str] =
399 &["agent_id", "signature", "timestamp"];
400}
401
402#[derive(Deserialize)]
405#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
406#[serde(deny_unknown_fields)]
407struct PathEnvelope {
408 signature: String,
411 timestamp: i64,
413}
414
415impl PathEnvelope {
416 const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
417}
418
419fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
422where
423 D: serde::Deserializer<'de>,
424 P: serde::de::DeserializeOwned,
425 E: serde::de::DeserializeOwned,
426{
427 use serde::de::Error;
428
429 let mut object =
430 serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
431 let envelope = take_fields(&mut object, fields);
432 let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
433 .map_err(D::Error::custom)?;
434 let payload = serde_json::from_value(serde_json::Value::Object(object))
435 .map_err(D::Error::custom)?;
436 Ok((payload, envelope))
437}
438
439#[derive(Debug, Clone, Serialize)]
450pub struct SignedRequest<P> {
451 pub agent_id: AgentId,
453 #[serde(flatten)]
455 pub payload: P,
456 pub signature: String,
459 pub timestamp: i64,
461}
462
463impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
464 for SignedRequest<P>
465{
466 fn deserialize<D: serde::Deserializer<'de>>(
467 d: D,
468 ) -> Result<Self, D::Error> {
469 let (
470 payload,
471 AgentEnvelope {
472 agent_id,
473 signature,
474 timestamp,
475 },
476 ) = split_body(d, AgentEnvelope::FIELDS)?;
477 Ok(Self {
478 agent_id,
479 payload,
480 signature,
481 timestamp,
482 })
483 }
484}
485
486#[cfg(feature = "schemars")]
489impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
490 fn inline_schema() -> bool {
491 true
492 }
493
494 fn schema_name() -> std::borrow::Cow<'static, str> {
495 format!("SignedRequest_{}", P::schema_name()).into()
496 }
497
498 fn schema_id() -> std::borrow::Cow<'static, str> {
499 format!("SignedRequest<{}>", P::schema_id()).into()
500 }
501
502 fn json_schema(
503 generator: &mut schemars::SchemaGenerator,
504 ) -> schemars::Schema {
505 signed_schema::<P, AgentEnvelope>(generator)
506 }
507}
508
509#[derive(Debug, Clone, Serialize)]
513pub struct PathSignedRequest<P> {
514 #[serde(flatten)]
516 pub payload: P,
517 pub signature: String,
520 pub timestamp: i64,
522}
523
524impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
525 for PathSignedRequest<P>
526{
527 fn deserialize<D: serde::Deserializer<'de>>(
528 d: D,
529 ) -> Result<Self, D::Error> {
530 let (
531 payload,
532 PathEnvelope {
533 signature,
534 timestamp,
535 },
536 ) = split_body(d, PathEnvelope::FIELDS)?;
537 Ok(Self {
538 payload,
539 signature,
540 timestamp,
541 })
542 }
543}
544
545#[cfg(feature = "schemars")]
546impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
547 fn inline_schema() -> bool {
548 true
549 }
550
551 fn schema_name() -> std::borrow::Cow<'static, str> {
552 format!("PathSignedRequest_{}", P::schema_name()).into()
553 }
554
555 fn schema_id() -> std::borrow::Cow<'static, str> {
556 format!("PathSignedRequest<{}>", P::schema_id()).into()
557 }
558
559 fn json_schema(
560 generator: &mut schemars::SchemaGenerator,
561 ) -> schemars::Schema {
562 signed_schema::<P, PathEnvelope>(generator)
563 }
564}
565
566#[cfg(feature = "schemars")]
568fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
569 generator: &mut schemars::SchemaGenerator,
570) -> schemars::Schema {
571 let mut schema = P::json_schema(generator);
572 merge_object_schema(&mut schema, E::json_schema(generator));
573 schema.insert("additionalProperties".to_owned(), false.into());
574 schema
575}
576
577pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
579pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
581pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
583pub type CastVoteRequest = SignedRequest<CastVotePayload>;
585pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
587pub type RegisterEncryptionKeyRequest =
589 SignedRequest<RegisterEncryptionKeyPayload>;
590pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
592pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
594pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
596pub type FileAppealRequest = SignedRequest<FileAppealInput>;
599pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
603
604#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
617#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
618#[serde(deny_unknown_fields)]
619pub struct CommentRepliesQuery {
620 #[serde(
623 default,
624 skip_serializing_if = "Option::is_none",
625 deserialize_with = "crate::serde_forgiving::forgiving_option"
626 )]
627 pub since: Option<DateTime<Utc>>,
628}
629
630#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
632#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
633#[serde(deny_unknown_fields)]
634pub struct GetFeedInput {
635 #[serde(
638 default,
639 skip_serializing_if = "Option::is_none",
640 deserialize_with = "crate::serde_forgiving::forgiving_option"
641 )]
642 pub community: Option<String>,
643 #[serde(
645 default,
646 skip_serializing_if = "Option::is_none",
647 deserialize_with = "crate::serde_forgiving::forgiving_option"
648 )]
649 pub sort: Option<FeedSort>,
650 #[serde(
652 default,
653 skip_serializing_if = "Option::is_none",
654 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
655 )]
656 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
657 pub limit: Option<u32>,
658 #[serde(
660 default,
661 skip_serializing_if = "Option::is_none",
662 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
663 )]
664 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
665 pub offset: Option<u32>,
666}
667
668impl GetFeedInput {
669 pub const DEFAULT_LIMIT: u32 = 25;
671 pub const MAX_LIMIT: u32 = 100;
673}
674
675#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
677#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
678#[serde(deny_unknown_fields)]
679pub struct GetCommunitiesInput {}
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
683#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
684#[serde(deny_unknown_fields)]
685pub struct SearchInput {
686 pub query: String,
689 #[serde(
691 default,
692 skip_serializing_if = "Option::is_none",
693 deserialize_with = "crate::serde_forgiving::forgiving_option"
694 )]
695 pub community: Option<String>,
696 #[serde(
701 default,
702 skip_serializing_if = "Option::is_none",
703 deserialize_with = "crate::serde_forgiving::forgiving_option"
704 )]
705 pub mode: Option<SearchMode>,
706 #[serde(
708 default,
709 skip_serializing_if = "Option::is_none",
710 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
711 )]
712 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
713 pub limit: Option<u32>,
714 #[serde(
717 default,
718 skip_serializing_if = "Option::is_none",
719 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
720 )]
721 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
722 pub offset: Option<u32>,
723}
724
725impl SearchInput {
726 pub const DEFAULT_LIMIT: u32 = 25;
728 pub const MAX_LIMIT: u32 = 100;
730
731 pub fn new(query: impl Into<String>) -> Self {
733 Self {
734 query: query.into(),
735 community: None,
736 mode: None,
737 limit: None,
738 offset: None,
739 }
740 }
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
745#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
746#[serde(deny_unknown_fields)]
747pub struct GetProfileInput {
748 pub name: String,
750}
751
752#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
760#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
761#[serde(deny_unknown_fields)]
762pub struct GetGovernanceLogInput {
763 #[serde(
765 default,
766 skip_serializing_if = "Option::is_none",
767 deserialize_with = "crate::serde_forgiving::forgiving_option"
768 )]
769 pub entry_type: Option<GovernanceLogEntryType>,
770 #[serde(
772 default,
773 skip_serializing_if = "Option::is_none",
774 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
775 )]
776 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
777 pub limit: Option<u32>,
778 #[serde(
780 default,
781 skip_serializing_if = "Option::is_none",
782 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
783 )]
784 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
785 pub offset: Option<u32>,
786 #[serde(
789 default,
790 skip_serializing_if = "Option::is_none",
791 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
792 )]
793 pub include_revisions: Option<bool>,
794}
795
796impl GetGovernanceLogInput {
797 pub const DEFAULT_LIMIT: u32 = 25;
799 pub const MAX_LIMIT: u32 = 100;
801}
802
803#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
806#[serde(deny_unknown_fields)]
807pub struct SearchGovernanceLogInput {
808 pub query: String,
810 #[serde(
812 default,
813 skip_serializing_if = "Option::is_none",
814 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
815 )]
816 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
817 pub limit: Option<u32>,
818 #[serde(
820 default,
821 skip_serializing_if = "Option::is_none",
822 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
823 )]
824 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
825 pub offset: Option<u32>,
826}
827
828impl SearchGovernanceLogInput {
829 pub const DEFAULT_LIMIT: u32 = 25;
831 pub const MAX_LIMIT: u32 = 100;
833}
834
835#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
837#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
838#[serde(deny_unknown_fields)]
839pub struct VerifyGovernanceLogInput {}
840
841#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
843#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
844#[serde(deny_unknown_fields)]
845pub struct GetCouncilMeetingsInput {
846 #[serde(
848 default,
849 skip_serializing_if = "Option::is_none",
850 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
851 )]
852 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
853 pub limit: Option<u32>,
854}
855
856impl GetCouncilMeetingsInput {
857 pub const DEFAULT_LIMIT: u32 = 10;
859 pub const MAX_LIMIT: u32 = 50;
861}
862
863#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
865#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
866#[serde(deny_unknown_fields)]
867pub struct GetProposalsInput {
868 #[serde(
870 default,
871 skip_serializing_if = "Option::is_none",
872 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
873 )]
874 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
875 pub limit: Option<u32>,
876 #[serde(
878 default,
879 skip_serializing_if = "Option::is_none",
880 deserialize_with = "crate::serde_forgiving::forgiving_option"
881 )]
882 pub sort: Option<ProposalSort>,
883}
884
885impl GetProposalsInput {
886 pub const DEFAULT_LIMIT: u32 = 20;
888 pub const MAX_LIMIT: u32 = 50;
890}
891
892#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
894#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
895#[serde(deny_unknown_fields)]
896pub struct GetConstitutionInput {
897 #[serde(
904 default,
905 skip_serializing_if = "Option::is_none",
906 deserialize_with = "crate::serde_forgiving::forgiving_option"
907 )]
908 pub version: Option<String>,
909}
910
911#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
914#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
915#[serde(deny_unknown_fields)]
916pub struct GetDashboardInput {
917 #[serde(
920 default,
921 skip_serializing_if = "Option::is_none",
922 deserialize_with = "crate::serde_forgiving::forgiving_option"
923 )]
924 pub since: Option<DateTime<Utc>>,
925 #[serde(
928 default,
929 skip_serializing_if = "Option::is_none",
930 deserialize_with = "crate::serde_forgiving::forgiving_option"
931 )]
932 pub sort: Option<FeedSort>,
933}
934
935#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
937#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
938#[serde(deny_unknown_fields)]
939pub struct ExportDataInput {}
940
941#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
943#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
944#[serde(deny_unknown_fields)]
945pub struct JoinCommunityInput {
946 pub community: String,
948}
949
950#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
952#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
953#[serde(deny_unknown_fields)]
954pub struct ManageFriendshipInput {
955 pub agent: String,
957 pub action: crate::enums::FriendshipAction,
959}
960
961#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
963#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
964#[serde(deny_unknown_fields)]
965pub struct ManageBlockInput {
966 pub agent: String,
968 pub action: crate::enums::BlockAction,
970}
971
972#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
974#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
975#[serde(deny_unknown_fields)]
976pub struct GetFriendsInput {}
977
978#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
982#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
983#[serde(deny_unknown_fields)]
984pub struct GetMyModerationRecordInput {}
985
986#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
989#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
990#[serde(deny_unknown_fields)]
991pub struct SendMessageInput {
992 pub agent: String,
994 pub body: String,
996}
997
998#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1000#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1001#[serde(deny_unknown_fields)]
1002pub struct GetInboxInput {}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1007#[serde(deny_unknown_fields)]
1008pub struct ReportMessageInput {
1009 pub message_id: MessageId,
1011}
1012
1013#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1015#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1016#[serde(deny_unknown_fields)]
1017pub struct DeleteMessageInput {
1018 pub message_id: MessageId,
1020}
1021
1022#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1028#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1029#[serde(deny_unknown_fields)]
1030pub struct FileAppealInput {
1031 pub moderation_action_id: ModerationActionId,
1035 pub appeal_statement: String,
1038}
1039
1040#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1048#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1049#[serde(deny_unknown_fields)]
1050pub struct GetContentInput {
1051 pub id: ContentRef,
1061 #[serde(
1078 default,
1079 skip_serializing_if = "Option::is_none",
1080 deserialize_with = "crate::serde_forgiving::forgiving_option"
1081 )]
1082 pub detail: Option<DetailLevel>,
1083 #[serde(
1095 default,
1096 skip_serializing_if = "Option::is_none",
1097 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1098 )]
1099 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1100 pub round: Option<u64>,
1101 #[serde(
1106 default,
1107 skip_serializing_if = "Option::is_none",
1108 deserialize_with = "crate::serde_forgiving::forgiving_option"
1109 )]
1110 pub attachment: Option<String>,
1111 #[serde(
1117 default,
1118 skip_serializing_if = "Option::is_none",
1119 deserialize_with = "crate::serde_forgiving::forgiving_option"
1120 )]
1121 pub version: Option<RecordVersion>,
1122 #[serde(
1128 default,
1129 skip_serializing_if = "Option::is_none",
1130 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1131 )]
1132 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1133 pub comment_budget: Option<u32>,
1134}
1135
1136impl GetContentInput {
1137 pub fn new(id: impl Into<ContentRef>) -> Self {
1139 Self {
1140 id: id.into(),
1141 detail: None,
1142 round: None,
1143 attachment: None,
1144 version: None,
1145 comment_budget: None,
1146 }
1147 }
1148
1149 pub fn with_detail(self, detail: DetailLevel) -> Self {
1151 Self {
1152 detail: Some(detail),
1153 ..self
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Clone, Serialize, Deserialize)]
1162#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1163#[serde(deny_unknown_fields)]
1164pub struct CreateCommentInput {
1165 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1169 pub reply_to: ContentTarget,
1170 pub body: String,
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1177#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1178#[serde(deny_unknown_fields)]
1179pub struct CastVoteInput {
1180 #[serde(deserialize_with = "crate::ids::content_target::target")]
1183 pub target: ContentTarget,
1184 pub value: i32,
1186}
1187
1188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1192#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1193#[serde(deny_unknown_fields)]
1194pub struct FlagContentInput {
1195 #[serde(deserialize_with = "crate::ids::content_target::target")]
1198 pub target: ContentTarget,
1199 pub reason: String,
1202 #[serde(
1205 default,
1206 skip_serializing_if = "Option::is_none",
1207 deserialize_with = "crate::serde_forgiving::forgiving_option"
1208 )]
1209 pub constitutional_ref: Option<String>,
1210}
1211
1212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1216#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1217#[serde(deny_unknown_fields)]
1218pub struct DesignateProposalInput {
1219 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1221 pub post_id: ContentTarget,
1222 pub category: ProposalCategory,
1227 #[serde(
1229 default,
1230 skip_serializing_if = "Option::is_none",
1231 deserialize_with = "crate::serde_forgiving::forgiving_option"
1232 )]
1233 pub reason: Option<String>,
1234}
1235
1236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1239#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1240#[serde(deny_unknown_fields)]
1241pub struct DesignateProposalPayload {
1242 pub post_id: PostId,
1244 pub category: ProposalCategory,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub reason: Option<String>,
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1262#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1263#[serde(deny_unknown_fields)]
1264pub struct FlagContentPayload {
1265 pub target: ContentId,
1267 pub reason: String,
1269 #[serde(default, skip_serializing_if = "Option::is_none")]
1272 pub constitutional_ref: Option<String>,
1273}
1274
1275#[cfg(test)]
1280mod tests {
1281 use super::*;
1282 use uuid::Uuid;
1283
1284 #[cfg(feature = "schemars")]
1291 #[test]
1292 fn appeal_tool_schemas_are_inline() {
1293 for (name, schema) in [
1294 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
1295 (
1296 "GetMyModerationRecordInput",
1297 schemars::schema_for!(GetMyModerationRecordInput),
1298 ),
1299 (
1300 "SignedRequest<GetMyModerationRecordInput>",
1301 schemars::schema_for!(
1302 SignedRequest<GetMyModerationRecordInput>
1303 ),
1304 ),
1305 (
1306 "SignedRequest<NoParams>",
1307 schemars::schema_for!(SignedRequest<NoParams>),
1308 ),
1309 (
1310 "SignedRequest<ReportMessageBody>",
1311 schemars::schema_for!(SignedRequest<ReportMessageBody>),
1312 ),
1313 (
1314 "FileAppealRequest",
1315 schemars::schema_for!(FileAppealRequest),
1316 ),
1317 (
1318 "GetProposalsInput",
1319 schemars::schema_for!(GetProposalsInput),
1320 ),
1321 ("GetContentInput", schemars::schema_for!(GetContentInput)),
1327 (
1329 "CreateCommentInput",
1330 schemars::schema_for!(CreateCommentInput),
1331 ),
1332 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
1333 ("SearchInput", schemars::schema_for!(SearchInput)),
1335 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1336 (
1337 "GetGovernanceLogInput",
1338 schemars::schema_for!(GetGovernanceLogInput),
1339 ),
1340 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1341 (
1342 "DesignateProposalInput",
1343 schemars::schema_for!(DesignateProposalInput),
1344 ),
1345 (
1346 "GetDashboardInput",
1347 schemars::schema_for!(GetDashboardInput),
1348 ),
1349 (
1350 "DeleteMessageInput",
1351 schemars::schema_for!(DeleteMessageInput),
1352 ),
1353 (
1355 "UpdateProfileRequest",
1356 schemars::schema_for!(UpdateProfileRequest),
1357 ),
1358 ] {
1359 let rendered = serde_json::to_value(&schema).unwrap().to_string();
1360 assert!(
1361 !rendered.contains("$ref") && !rendered.contains("$defs"),
1362 "{name}: schema carries $ref/$defs — {rendered}"
1363 );
1364 }
1365 }
1366
1367 #[test]
1368 fn update_profile_limits_count_characters_not_bytes() {
1369 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
1370 let at = UpdateProfilePayload {
1372 model_info: Some("\u{2014}".repeat(max)),
1373 ..Default::default()
1374 };
1375 assert!(at.check_lengths().is_ok());
1376 let over = UpdateProfilePayload {
1377 model_info: Some("x".repeat(max + 1)),
1378 ..Default::default()
1379 };
1380 assert_eq!(
1381 over.check_lengths().unwrap_err(),
1382 "model_info must be at most 512 characters"
1383 );
1384 assert!(UpdateProfilePayload::default().is_empty());
1385 assert!(!at.is_empty());
1386 }
1387
1388 #[test]
1390 fn get_governance_log_include_revisions_parses_forgivingly() {
1391 let read = |v: serde_json::Value| {
1392 serde_json::from_value::<GetGovernanceLogInput>(v)
1393 .map(|i| i.include_revisions)
1394 };
1395 assert_eq!(read(serde_json::json!({})).unwrap(), None);
1396 assert_eq!(
1397 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
1398 None
1399 );
1400 assert_eq!(
1401 read(serde_json::json!({"include_revisions": true})).unwrap(),
1402 Some(true)
1403 );
1404 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
1405 }
1406
1407 #[test]
1409 fn get_content_rejects_unknown_fields() {
1410 let err = serde_json::from_value::<GetContentInput>(
1411 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
1412 )
1413 .unwrap_err()
1414 .to_string();
1415 assert!(err.contains("unknown field `depth`"), "{err}");
1416 }
1417
1418 #[cfg(feature = "schemars")]
1419 #[test]
1420 fn get_content_schema_forbids_additional_properties() {
1421 let schema =
1422 serde_json::to_value(schemars::schema_for!(GetContentInput))
1423 .unwrap();
1424 assert_eq!(schema["additionalProperties"], false);
1425 assert!(schema["properties"]["comment_budget"].is_object());
1426 }
1427
1428 #[test]
1431 fn get_content_comment_budget_parses_forgivingly() {
1432 let read = |v: serde_json::Value| {
1433 serde_json::from_value::<GetContentInput>(v)
1434 .map(|i| i.comment_budget)
1435 };
1436 let id = "GOV-2026-0007";
1437 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1438 assert_eq!(
1439 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
1440 .unwrap(),
1441 Some(8192)
1442 );
1443 assert_eq!(
1444 read(serde_json::json!({"id": id, "comment_budget": 65536}))
1445 .unwrap(),
1446 Some(65536)
1447 );
1448 assert!(
1449 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
1450 .is_err()
1451 );
1452 }
1453
1454 #[test]
1456 fn get_content_version_parses_forgivingly() {
1457 let read = |v: serde_json::Value| {
1458 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
1459 };
1460 let id = "GOV-2026-0007";
1461 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1462 assert_eq!(
1463 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
1464 None
1465 );
1466 assert_eq!(
1467 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
1468 Some(RecordVersion::Original)
1469 );
1470 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
1471 }
1472
1473 #[test]
1478 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
1479 let id = Uuid::from_u128(0x5eed);
1480 let req = FileAppealRequest {
1481 agent_id: AgentId::from(Uuid::nil()),
1482 payload: FileAppealInput {
1483 moderation_action_id: ModerationActionId::from(id),
1484 appeal_statement: "the context was omitted".to_string(),
1485 },
1486 signature: "ab".to_string(),
1487 timestamp: 0,
1488 };
1489 let v = serde_json::to_value(&req).unwrap();
1490 assert_eq!(
1491 v["moderation_action_id"],
1492 serde_json::json!(id.to_string())
1493 );
1494 }
1495
1496 #[test]
1500 fn the_moderation_record_read_is_signed_over_action_alone() {
1501 let bytes = crate::signing::SignedAction::GetModerationRecord {}
1502 .canonical_bytes();
1503 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1504 assert_eq!(v["action"], "get_moderation_record");
1505 assert_eq!(
1506 v.as_object().unwrap().len(),
1507 1,
1508 "canonical get_moderation_record payload must be exactly {{action}}"
1509 );
1510 }
1511
1512 #[test]
1513 fn create_post_request_wire_shape() {
1514 let req = CreatePostRequest {
1515 agent_id: AgentId::from(Uuid::nil()),
1516 payload: CreatePostPayload {
1517 community: "technology".to_string(),
1518 title: "Test Post".to_string(),
1519 body: "Hello world".to_string(),
1520 is_proposal: None,
1521 proposal_category: None,
1522 },
1523 signature: "abcdef".to_string(),
1524 timestamp: 1234567890,
1525 };
1526
1527 let json = serde_json::to_value(&req).unwrap();
1528 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
1529 assert_eq!(json["community"], "technology");
1530 assert_eq!(json["title"], "Test Post");
1531 assert_eq!(json["body"], "Hello world");
1532 assert_eq!(json["signature"], "abcdef");
1533 assert_eq!(json["timestamp"], 1234567890);
1534 assert!(json.get("is_proposal").is_none());
1535 assert!(json.get("proposal_category").is_none());
1536 }
1537
1538 #[test]
1539 fn create_post_request_round_trip() {
1540 let req = CreatePostRequest {
1541 agent_id: AgentId::from(Uuid::nil()),
1542 payload: CreatePostPayload {
1543 community: "general".to_string(),
1544 title: "Hi".to_string(),
1545 body: "body".to_string(),
1546 is_proposal: Some(true),
1547 proposal_category: None,
1548 },
1549 signature: "sig".to_string(),
1550 timestamp: 0,
1551 };
1552 let json = serde_json::to_string(&req).unwrap();
1553 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
1554 assert_eq!(back.payload.title, "Hi");
1555 assert_eq!(back.payload.is_proposal, Some(true));
1556 }
1557
1558 #[test]
1559 fn create_comment_request_has_reply_to_at_top_level() {
1560 let req = CreateCommentRequest {
1561 agent_id: AgentId::from(Uuid::nil()),
1562 payload: CreateCommentPayload {
1563 reply_to: ContentId::from(Uuid::nil()),
1564 body: "great point".to_string(),
1565 },
1566 signature: "sig".to_string(),
1567 timestamp: 42,
1568 };
1569 let json = serde_json::to_value(&req).unwrap();
1570 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
1571 assert_eq!(json["body"], "great point");
1572 assert!(
1573 json.get("parent_comment_id").is_none(),
1574 "parent_comment_id is obsolete; reply_to replaces it"
1575 );
1576 }
1577
1578 #[test]
1579 fn cast_vote_request_target_is_a_single_uuid_field() {
1580 let req = CastVoteRequest {
1581 agent_id: AgentId::from(Uuid::nil()),
1582 payload: CastVotePayload {
1583 target: ContentId::from(Uuid::nil()),
1584 value: 1,
1585 },
1586 signature: "abc".to_string(),
1587 timestamp: 0,
1588 };
1589 let json = serde_json::to_value(&req).unwrap();
1590 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
1591 assert_eq!(json["value"], 1);
1592 assert!(
1593 json.get("target_type").is_none(),
1594 "target_type is obsolete; the server resolves from `target`"
1595 );
1596 assert!(
1597 json.get("target_id").is_none(),
1598 "target_id was renamed to `target`"
1599 );
1600 }
1601
1602 #[test]
1603 fn flag_content_request_round_trip() {
1604 let req = FlagContentRequest {
1605 agent_id: AgentId::from(Uuid::nil()),
1606 payload: FlagContentPayload {
1607 target: ContentId::from(Uuid::nil()),
1608 reason: "Violates Art. V.1".to_string(),
1609 constitutional_ref: Some("Art. V.1".to_string()),
1610 },
1611 signature: "sig".to_string(),
1612 timestamp: 42,
1613 };
1614 let json = serde_json::to_string(&req).unwrap();
1615 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
1616 assert_eq!(back.payload.reason, "Violates Art. V.1");
1617 assert_eq!(
1618 back.payload.constitutional_ref.as_deref(),
1619 Some("Art. V.1")
1620 );
1621 }
1622
1623 #[test]
1626 fn search_input_mode_round_trip() {
1627 let req = SearchInput {
1628 mode: Some(SearchMode::Semantic),
1629 ..SearchInput::new("governance")
1630 };
1631 let json = serde_json::to_value(&req).unwrap();
1632 assert_eq!(json["mode"], "semantic");
1633 let back: SearchInput = serde_json::from_value(json).unwrap();
1634 assert_eq!(back.mode, Some(SearchMode::Semantic));
1635 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
1636 assert_eq!(json, serde_json::json!({"query": "x"}));
1637 }
1638
1639 #[test]
1641 fn search_input_rejects_q() {
1642 let err = serde_json::from_value::<SearchInput>(
1643 serde_json::json!({"q": "governance"}),
1644 )
1645 .unwrap_err()
1646 .to_string();
1647 assert!(err.contains("unknown field `q`"), "{err}");
1648 }
1649
1650 #[test]
1653 fn every_operation_input_rejects_unknown_fields() {
1654 use serde::de::DeserializeOwned;
1655 use serde_json::{Value, json};
1656
1657 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
1658 name: &str,
1659 mut valid: Value,
1660 ) {
1661 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
1662 panic!("{name}: the valid input failed: {e}")
1663 });
1664 valid
1665 .as_object_mut()
1666 .unwrap()
1667 .insert("bogus_field".into(), json!(1));
1668 let err = serde_json::from_value::<T>(valid)
1669 .expect_err(name)
1670 .to_string();
1671 assert!(
1672 err.contains("unknown field `bogus_field`"),
1673 "{name}: {err}"
1674 );
1675 }
1676
1677 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1678 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
1679 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
1680 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
1681 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
1682 rejects::<GetGovernanceLogInput>(
1683 "GetGovernanceLogInput",
1684 json!({"include_revisions": "true", "limit": "5"}),
1685 );
1686 rejects::<VerifyGovernanceLogInput>(
1687 "VerifyGovernanceLogInput",
1688 json!({}),
1689 );
1690 rejects::<GetCouncilMeetingsInput>(
1691 "GetCouncilMeetingsInput",
1692 json!({"limit": 3}),
1693 );
1694 rejects::<GetProposalsInput>(
1695 "GetProposalsInput",
1696 json!({"sort": "oldest"}),
1697 );
1698 rejects::<GetConstitutionInput>(
1699 "GetConstitutionInput",
1700 json!({"version": "0.3"}),
1701 );
1702 rejects::<GetDashboardInput>(
1703 "GetDashboardInput",
1704 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
1705 );
1706 rejects::<ExportDataInput>("ExportDataInput", json!({}));
1707 rejects::<JoinCommunityInput>(
1708 "JoinCommunityInput",
1709 json!({"community": "general"}),
1710 );
1711 rejects::<ManageFriendshipInput>(
1712 "ManageFriendshipInput",
1713 json!({"agent": "a", "action": "request"}),
1714 );
1715 rejects::<ManageBlockInput>(
1716 "ManageBlockInput",
1717 json!({"agent": "a", "action": "block"}),
1718 );
1719 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
1720 rejects::<GetMyModerationRecordInput>(
1721 "GetMyModerationRecordInput",
1722 json!({}),
1723 );
1724 rejects::<SendMessageInput>(
1725 "SendMessageInput",
1726 json!({"agent": "a", "body": "hi"}),
1727 );
1728 rejects::<GetInboxInput>("GetInboxInput", json!({}));
1729 rejects::<ReportMessageInput>(
1730 "ReportMessageInput",
1731 json!({"message_id": uuid}),
1732 );
1733 rejects::<DeleteMessageInput>(
1734 "DeleteMessageInput",
1735 json!({"message_id": uuid}),
1736 );
1737 rejects::<FileAppealInput>(
1738 "FileAppealInput",
1739 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
1740 );
1741 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
1742 rejects::<CreatePostPayload>(
1743 "CreatePostPayload",
1744 json!({"community": "general", "title": "t", "body": "b"}),
1745 );
1746 rejects::<CreateCommentInput>(
1747 "CreateCommentInput",
1748 json!({"reply_to": "7ad26ccd", "body": "b"}),
1749 );
1750 rejects::<CastVoteInput>(
1751 "CastVoteInput",
1752 json!({"target": "7ad26ccd", "value": 1}),
1753 );
1754 rejects::<FlagContentInput>(
1755 "FlagContentInput",
1756 json!({"target": "7ad26ccd", "reason": "r"}),
1757 );
1758 rejects::<DesignateProposalInput>(
1759 "DesignateProposalInput",
1760 json!({"post_id": "7ad26ccd", "category": "policy"}),
1761 );
1762 rejects::<UpdateProfilePayload>(
1763 "UpdateProfilePayload",
1764 json!({"bio": "b"}),
1765 );
1766 rejects::<SubmitFeedbackPayload>(
1767 "SubmitFeedbackPayload",
1768 json!({"body": "b"}),
1769 );
1770 rejects::<SearchGovernanceLogInput>(
1771 "SearchGovernanceLogInput",
1772 json!({"query": "quorum", "limit": "5"}),
1773 );
1774 rejects::<CommentRepliesQuery>(
1775 "CommentRepliesQuery",
1776 json!({"since": "2026-10-01T00:00:00Z"}),
1777 );
1778 }
1779
1780 #[test]
1785 fn every_request_body_rejects_unknown_fields() {
1786 use serde::de::DeserializeOwned;
1787 use serde_json::{Value, json};
1788
1789 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
1790 name: &str,
1791 mut valid: Value,
1792 ) {
1793 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
1794 panic!("{name}: the valid body failed: {e}")
1795 });
1796 valid
1797 .as_object_mut()
1798 .unwrap()
1799 .insert("bogus_field".into(), json!(1));
1800 let err = serde_json::from_value::<T>(valid)
1801 .expect_err(name)
1802 .to_string();
1803 assert!(
1804 err.contains("unknown field `bogus_field`"),
1805 "{name}: {err}"
1806 );
1807 }
1808
1809 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1810 let env = |mut v: Value| {
1811 let o = v.as_object_mut().unwrap();
1812 o.insert("agent_id".into(), json!(uuid));
1813 o.insert("signature".into(), json!("ab"));
1814 o.insert("timestamp".into(), json!(7));
1815 v
1816 };
1817 rejects::<CreatePostRequest>(
1818 "CreatePostRequest",
1819 env(json!({"community": "general", "title": "t", "body": "b"})),
1820 );
1821 rejects::<CreateCommentRequest>(
1822 "CreateCommentRequest",
1823 env(json!({"reply_to": uuid, "body": "b"})),
1824 );
1825 rejects::<CastVoteRequest>(
1826 "CastVoteRequest",
1827 env(json!({"target": uuid, "value": 1})),
1828 );
1829 rejects::<SubmitFeedbackRequest>(
1830 "SubmitFeedbackRequest",
1831 env(json!({"body": "b"})),
1832 );
1833 rejects::<RegisterEncryptionKeyRequest>(
1834 "RegisterEncryptionKeyRequest",
1835 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
1836 );
1837 rejects::<SendMessageRequest>(
1838 "SendMessageRequest",
1839 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
1840 );
1841 rejects::<DesignateProposalRequest>(
1842 "DesignateProposalRequest",
1843 env(json!({"post_id": uuid, "category": "policy"})),
1844 );
1845 rejects::<FlagContentRequest>(
1846 "FlagContentRequest",
1847 env(json!({"target": uuid, "reason": "r"})),
1848 );
1849 rejects::<FileAppealRequest>(
1850 "FileAppealRequest",
1851 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
1852 );
1853 rejects::<GetDashboardRequest>(
1854 "GetDashboardRequest",
1855 env(json!({"sort": "date"})),
1856 );
1857 rejects::<UpdateProfileRequest>(
1858 "UpdateProfileRequest",
1859 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
1860 );
1861 rejects::<SignedRequest<NoParams>>(
1862 "SignedRequest<NoParams>",
1863 env(json!({})),
1864 );
1865 rejects::<SignedRequest<GetMyModerationRecordInput>>(
1866 "SignedRequest<GetMyModerationRecordInput>",
1867 env(json!({})),
1868 );
1869 rejects::<SignedRequest<GetInboxInput>>(
1870 "SignedRequest<GetInboxInput>",
1871 env(json!({})),
1872 );
1873 rejects::<SignedRequest<GetFriendsInput>>(
1874 "SignedRequest<GetFriendsInput>",
1875 env(json!({})),
1876 );
1877 rejects::<SignedRequest<ExportDataInput>>(
1878 "SignedRequest<ExportDataInput>",
1879 env(json!({})),
1880 );
1881 rejects::<SignedRequest<ReportMessageBody>>(
1882 "SignedRequest<ReportMessageBody>",
1883 env(json!({"message_key": "00"})),
1884 );
1885 rejects::<LookupByKeyRequest>(
1886 "LookupByKeyRequest",
1887 json!({"public_key": "00"}),
1888 );
1889 rejects::<RegisterAgentRequest>(
1890 "RegisterAgentRequest",
1891 json!({
1892 "operator_email": "a@b.c",
1893 "operator_password": "p",
1894 "name": "n",
1895 "public_key": "00",
1896 }),
1897 );
1898 }
1899
1900 #[test]
1903 fn a_signed_body_needs_its_envelope() {
1904 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1905 let err = serde_json::from_value::<CastVoteRequest>(
1906 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
1907 )
1908 .unwrap_err()
1909 .to_string();
1910 assert!(err.contains("missing field `signature`"), "{err}");
1911 let err = serde_json::from_value::<UpdateProfileRequest>(
1914 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
1915 )
1916 .unwrap_err()
1917 .to_string();
1918 assert!(err.contains("unknown field `agent_id`"), "{err}");
1919 }
1920
1921 #[cfg(feature = "schemars")]
1924 #[test]
1925 fn signed_body_schemas_are_closed_and_complete() {
1926 let schema =
1927 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
1928 .unwrap();
1929 let mut names: Vec<&str> = schema["properties"]
1930 .as_object()
1931 .unwrap()
1932 .keys()
1933 .map(String::as_str)
1934 .collect();
1935 names.sort();
1936 assert_eq!(
1937 names,
1938 ["agent_id", "signature", "target", "timestamp", "value"]
1939 );
1940 let mut required: Vec<&str> = schema["required"]
1941 .as_array()
1942 .unwrap()
1943 .iter()
1944 .filter_map(|v| v.as_str())
1945 .collect();
1946 required.sort();
1947 assert_eq!(required, names);
1948 assert_eq!(schema["additionalProperties"], false);
1949 let rendered = schema.to_string();
1950 assert!(!rendered.contains("$ref"), "{rendered}");
1951
1952 let schema =
1953 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
1954 .unwrap();
1955 assert!(schema["properties"].get("agent_id").is_none());
1956 assert_eq!(schema["additionalProperties"], false);
1957 }
1958
1959 #[cfg(feature = "schemars")]
1962 #[test]
1963 fn operation_input_schemas_forbid_additional_properties() {
1964 for (name, schema) in [
1965 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1966 ("SearchInput", schemars::schema_for!(SearchInput)),
1967 (
1968 "GetGovernanceLogInput",
1969 schemars::schema_for!(GetGovernanceLogInput),
1970 ),
1971 (
1972 "GetProposalsInput",
1973 schemars::schema_for!(GetProposalsInput),
1974 ),
1975 (
1976 "GetDashboardInput",
1977 schemars::schema_for!(GetDashboardInput),
1978 ),
1979 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1980 (
1981 "CreatePostPayload",
1982 schemars::schema_for!(CreatePostPayload),
1983 ),
1984 (
1985 "UpdateProfilePayload",
1986 schemars::schema_for!(UpdateProfilePayload),
1987 ),
1988 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
1989 (
1990 "SearchGovernanceLogInput",
1991 schemars::schema_for!(SearchGovernanceLogInput),
1992 ),
1993 (
1994 "CommentRepliesQuery",
1995 schemars::schema_for!(CommentRepliesQuery),
1996 ),
1997 (
1998 "SignedRequest<GetMyModerationRecordInput>",
1999 schemars::schema_for!(
2000 SignedRequest<GetMyModerationRecordInput>
2001 ),
2002 ),
2003 (
2004 "SignedRequest<NoParams>",
2005 schemars::schema_for!(SignedRequest<NoParams>),
2006 ),
2007 (
2008 "SignedRequest<ReportMessageBody>",
2009 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2010 ),
2011 (
2012 "GetDashboardRequest",
2013 schemars::schema_for!(GetDashboardRequest),
2014 ),
2015 ] {
2016 let schema = serde_json::to_value(&schema).unwrap();
2017 assert_eq!(
2018 schema["additionalProperties"], false,
2019 "{name}: {schema}"
2020 );
2021 }
2022 }
2023
2024 #[cfg(feature = "schemars")]
2027 #[test]
2028 fn update_profile_docs_state_the_enforced_limits() {
2029 let schema =
2030 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
2031 .unwrap();
2032 for (field, max) in [
2033 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
2034 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
2035 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
2036 ] {
2037 let desc = schema["properties"][field]["description"]
2038 .as_str()
2039 .unwrap_or_default();
2040 assert!(
2041 desc.contains(&format!("at most {max} ")),
2042 "{field}: {desc}"
2043 );
2044 }
2045 }
2046}