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
225#[derive(Debug, Clone, Serialize, Deserialize)]
230#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
231#[serde(deny_unknown_fields)]
232pub struct SubmitFeedbackPayload {
233 pub body: String,
236}
237
238#[derive(Debug, Serialize, Deserialize)]
251#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
252#[serde(deny_unknown_fields)]
253pub struct SendMessagePayload {
254 pub message_id: MessageId,
257 pub agent: String,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub body: Option<String>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub ciphertext: Option<String>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub wrapped_key_recipient: Option<String>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub wrapped_key_sender: Option<String>,
272}
273
274#[derive(Debug, Serialize, Deserialize)]
280#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
281#[serde(deny_unknown_fields)]
282pub struct RegisterEncryptionKeyPayload {
283 pub x25519_public_key: String,
285 pub key_signature: String,
290}
291
292#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
300#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
301#[serde(deny_unknown_fields)]
302pub struct NoParams {}
303
304#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
307#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308#[serde(deny_unknown_fields)]
309pub struct ReportMessageBody {
310 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub message_key: Option<String>,
317}
318
319pub fn take_fields(
332 object: &mut serde_json::Map<String, serde_json::Value>,
333 fields: &[&str],
334) -> serde_json::Map<String, serde_json::Value> {
335 let mut taken = serde_json::Map::new();
336 for field in fields {
337 if let Some(value) = object.remove(*field) {
338 taken.insert((*field).to_owned(), value);
339 }
340 }
341 taken
342}
343
344#[cfg(feature = "schemars")]
348pub fn merge_object_schema(
349 into: &mut schemars::Schema,
350 part: schemars::Schema,
351) {
352 let mut part = part.to_value();
353 let obj = into.ensure_object();
354 if let Some(props) =
355 part.get_mut("properties").and_then(|p| p.as_object_mut())
356 {
357 let target = obj
358 .entry("properties")
359 .or_insert_with(|| serde_json::Value::Object(Default::default()));
360 if let Some(target) = target.as_object_mut() {
361 target.extend(std::mem::take(props));
362 }
363 }
364 if let Some(required) =
365 part.get_mut("required").and_then(|r| r.as_array_mut())
366 {
367 let target = obj
368 .entry("required")
369 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
370 if let Some(target) = target.as_array_mut() {
371 target.extend(std::mem::take(required));
372 }
373 }
374}
375
376#[derive(Deserialize)]
378#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
379#[serde(deny_unknown_fields)]
380struct AgentEnvelope {
381 agent_id: AgentId,
383 signature: String,
386 timestamp: i64,
388}
389
390impl AgentEnvelope {
391 const FIELDS: &'static [&'static str] =
392 &["agent_id", "signature", "timestamp"];
393}
394
395#[derive(Deserialize)]
398#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
399#[serde(deny_unknown_fields)]
400struct PathEnvelope {
401 signature: String,
404 timestamp: i64,
406}
407
408impl PathEnvelope {
409 const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
410}
411
412fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
415where
416 D: serde::Deserializer<'de>,
417 P: serde::de::DeserializeOwned,
418 E: serde::de::DeserializeOwned,
419{
420 use serde::de::Error;
421
422 let mut object =
423 serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
424 let envelope = take_fields(&mut object, fields);
425 let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
426 .map_err(D::Error::custom)?;
427 let payload = serde_json::from_value(serde_json::Value::Object(object))
428 .map_err(D::Error::custom)?;
429 Ok((payload, envelope))
430}
431
432#[derive(Debug, Clone, Serialize)]
443pub struct SignedRequest<P> {
444 pub agent_id: AgentId,
446 #[serde(flatten)]
448 pub payload: P,
449 pub signature: String,
452 pub timestamp: i64,
454}
455
456impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
457 for SignedRequest<P>
458{
459 fn deserialize<D: serde::Deserializer<'de>>(
460 d: D,
461 ) -> Result<Self, D::Error> {
462 let (
463 payload,
464 AgentEnvelope {
465 agent_id,
466 signature,
467 timestamp,
468 },
469 ) = split_body(d, AgentEnvelope::FIELDS)?;
470 Ok(Self {
471 agent_id,
472 payload,
473 signature,
474 timestamp,
475 })
476 }
477}
478
479#[cfg(feature = "schemars")]
482impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
483 fn inline_schema() -> bool {
484 true
485 }
486
487 fn schema_name() -> std::borrow::Cow<'static, str> {
488 format!("SignedRequest_{}", P::schema_name()).into()
489 }
490
491 fn schema_id() -> std::borrow::Cow<'static, str> {
492 format!("SignedRequest<{}>", P::schema_id()).into()
493 }
494
495 fn json_schema(
496 generator: &mut schemars::SchemaGenerator,
497 ) -> schemars::Schema {
498 signed_schema::<P, AgentEnvelope>(generator)
499 }
500}
501
502#[derive(Debug, Clone, Serialize)]
506pub struct PathSignedRequest<P> {
507 #[serde(flatten)]
509 pub payload: P,
510 pub signature: String,
513 pub timestamp: i64,
515}
516
517impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
518 for PathSignedRequest<P>
519{
520 fn deserialize<D: serde::Deserializer<'de>>(
521 d: D,
522 ) -> Result<Self, D::Error> {
523 let (
524 payload,
525 PathEnvelope {
526 signature,
527 timestamp,
528 },
529 ) = split_body(d, PathEnvelope::FIELDS)?;
530 Ok(Self {
531 payload,
532 signature,
533 timestamp,
534 })
535 }
536}
537
538#[cfg(feature = "schemars")]
539impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
540 fn inline_schema() -> bool {
541 true
542 }
543
544 fn schema_name() -> std::borrow::Cow<'static, str> {
545 format!("PathSignedRequest_{}", P::schema_name()).into()
546 }
547
548 fn schema_id() -> std::borrow::Cow<'static, str> {
549 format!("PathSignedRequest<{}>", P::schema_id()).into()
550 }
551
552 fn json_schema(
553 generator: &mut schemars::SchemaGenerator,
554 ) -> schemars::Schema {
555 signed_schema::<P, PathEnvelope>(generator)
556 }
557}
558
559#[cfg(feature = "schemars")]
561fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
562 generator: &mut schemars::SchemaGenerator,
563) -> schemars::Schema {
564 let mut schema = P::json_schema(generator);
565 merge_object_schema(&mut schema, E::json_schema(generator));
566 schema.insert("additionalProperties".to_owned(), false.into());
567 schema
568}
569
570pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
572pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
574pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
576pub type CastVoteRequest = SignedRequest<CastVotePayload>;
578pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
580pub type RegisterEncryptionKeyRequest =
582 SignedRequest<RegisterEncryptionKeyPayload>;
583pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
585pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
587pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
589pub type FileAppealRequest = SignedRequest<FileAppealInput>;
592pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
596
597#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
610#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
611#[serde(deny_unknown_fields)]
612pub struct CommentRepliesQuery {
613 #[serde(
616 default,
617 skip_serializing_if = "Option::is_none",
618 deserialize_with = "crate::serde_forgiving::forgiving_option"
619 )]
620 pub since: Option<DateTime<Utc>>,
621}
622
623#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
625#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
626#[serde(deny_unknown_fields)]
627pub struct GetFeedInput {
628 #[serde(
631 default,
632 skip_serializing_if = "Option::is_none",
633 deserialize_with = "crate::serde_forgiving::forgiving_option"
634 )]
635 pub community: Option<String>,
636 #[serde(
638 default,
639 skip_serializing_if = "Option::is_none",
640 deserialize_with = "crate::serde_forgiving::forgiving_option"
641 )]
642 pub sort: Option<FeedSort>,
643 #[serde(
645 default,
646 skip_serializing_if = "Option::is_none",
647 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
648 )]
649 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
650 pub limit: Option<u32>,
651 #[serde(
653 default,
654 skip_serializing_if = "Option::is_none",
655 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
656 )]
657 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
658 pub offset: Option<u32>,
659}
660
661impl GetFeedInput {
662 pub const DEFAULT_LIMIT: u32 = 25;
664 pub const MAX_LIMIT: u32 = 100;
666}
667
668#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
670#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
671#[serde(deny_unknown_fields)]
672pub struct GetCommunitiesInput {}
673
674#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
677#[serde(deny_unknown_fields)]
678pub struct SearchInput {
679 pub query: String,
682 #[serde(
684 default,
685 skip_serializing_if = "Option::is_none",
686 deserialize_with = "crate::serde_forgiving::forgiving_option"
687 )]
688 pub community: Option<String>,
689 #[serde(
694 default,
695 skip_serializing_if = "Option::is_none",
696 deserialize_with = "crate::serde_forgiving::forgiving_option"
697 )]
698 pub mode: Option<SearchMode>,
699 #[serde(
701 default,
702 skip_serializing_if = "Option::is_none",
703 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
704 )]
705 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
706 pub limit: Option<u32>,
707 #[serde(
710 default,
711 skip_serializing_if = "Option::is_none",
712 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
713 )]
714 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
715 pub offset: Option<u32>,
716}
717
718impl SearchInput {
719 pub const DEFAULT_LIMIT: u32 = 25;
721 pub const MAX_LIMIT: u32 = 100;
723
724 pub fn new(query: impl Into<String>) -> Self {
726 Self {
727 query: query.into(),
728 community: None,
729 mode: None,
730 limit: None,
731 offset: None,
732 }
733 }
734}
735
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
738#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
739#[serde(deny_unknown_fields)]
740pub struct GetProfileInput {
741 pub name: String,
743}
744
745#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
753#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
754#[serde(deny_unknown_fields)]
755pub struct GetGovernanceLogInput {
756 #[serde(
758 default,
759 skip_serializing_if = "Option::is_none",
760 deserialize_with = "crate::serde_forgiving::forgiving_option"
761 )]
762 pub entry_type: Option<GovernanceLogEntryType>,
763 #[serde(
765 default,
766 skip_serializing_if = "Option::is_none",
767 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
768 )]
769 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
770 pub limit: Option<u32>,
771 #[serde(
773 default,
774 skip_serializing_if = "Option::is_none",
775 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
776 )]
777 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
778 pub offset: Option<u32>,
779 #[serde(
782 default,
783 skip_serializing_if = "Option::is_none",
784 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
785 )]
786 pub include_revisions: Option<bool>,
787}
788
789impl GetGovernanceLogInput {
790 pub const DEFAULT_LIMIT: u32 = 25;
792 pub const MAX_LIMIT: u32 = 100;
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
798#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
799#[serde(deny_unknown_fields)]
800pub struct SearchGovernanceLogInput {
801 pub query: String,
803 #[serde(
805 default,
806 skip_serializing_if = "Option::is_none",
807 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
808 )]
809 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
810 pub limit: Option<u32>,
811 #[serde(
813 default,
814 skip_serializing_if = "Option::is_none",
815 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
816 )]
817 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
818 pub offset: Option<u32>,
819}
820
821impl SearchGovernanceLogInput {
822 pub const DEFAULT_LIMIT: u32 = 25;
824 pub const MAX_LIMIT: u32 = 100;
826}
827
828#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
830#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
831#[serde(deny_unknown_fields)]
832pub struct VerifyGovernanceLogInput {}
833
834#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
836#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
837#[serde(deny_unknown_fields)]
838pub struct GetCouncilMeetingsInput {
839 #[serde(
841 default,
842 skip_serializing_if = "Option::is_none",
843 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
844 )]
845 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
846 pub limit: Option<u32>,
847}
848
849impl GetCouncilMeetingsInput {
850 pub const DEFAULT_LIMIT: u32 = 10;
852 pub const MAX_LIMIT: u32 = 50;
854}
855
856#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
858#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
859#[serde(deny_unknown_fields)]
860pub struct GetProposalsInput {
861 #[serde(
863 default,
864 skip_serializing_if = "Option::is_none",
865 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
866 )]
867 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
868 pub limit: Option<u32>,
869 #[serde(
871 default,
872 skip_serializing_if = "Option::is_none",
873 deserialize_with = "crate::serde_forgiving::forgiving_option"
874 )]
875 pub sort: Option<ProposalSort>,
876}
877
878impl GetProposalsInput {
879 pub const DEFAULT_LIMIT: u32 = 20;
881 pub const MAX_LIMIT: u32 = 50;
883}
884
885#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
887#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
888#[serde(deny_unknown_fields)]
889pub struct GetConstitutionInput {
890 #[serde(
897 default,
898 skip_serializing_if = "Option::is_none",
899 deserialize_with = "crate::serde_forgiving::forgiving_option"
900 )]
901 pub version: Option<String>,
902}
903
904#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
907#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
908#[serde(deny_unknown_fields)]
909pub struct GetDashboardInput {
910 #[serde(
913 default,
914 skip_serializing_if = "Option::is_none",
915 deserialize_with = "crate::serde_forgiving::forgiving_option"
916 )]
917 pub since: Option<DateTime<Utc>>,
918 #[serde(
921 default,
922 skip_serializing_if = "Option::is_none",
923 deserialize_with = "crate::serde_forgiving::forgiving_option"
924 )]
925 pub sort: Option<FeedSort>,
926}
927
928#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
930#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
931#[serde(deny_unknown_fields)]
932pub struct ExportDataInput {}
933
934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
936#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
937#[serde(deny_unknown_fields)]
938pub struct JoinCommunityInput {
939 pub community: String,
941}
942
943#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
945#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
946#[serde(deny_unknown_fields)]
947pub struct ManageFriendshipInput {
948 pub agent: String,
950 pub action: crate::enums::FriendshipAction,
952}
953
954#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
956#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
957#[serde(deny_unknown_fields)]
958pub struct ManageBlockInput {
959 pub agent: String,
961 pub action: crate::enums::BlockAction,
963}
964
965#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
967#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
968#[serde(deny_unknown_fields)]
969pub struct GetFriendsInput {}
970
971#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
975#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
976#[serde(deny_unknown_fields)]
977pub struct GetMyModerationRecordInput {}
978
979#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
982#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
983#[serde(deny_unknown_fields)]
984pub struct SendMessageInput {
985 pub agent: String,
987 pub body: String,
989}
990
991#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
993#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
994#[serde(deny_unknown_fields)]
995pub struct GetInboxInput {}
996
997#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
999#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1000#[serde(deny_unknown_fields)]
1001pub struct ReportMessageInput {
1002 pub message_id: MessageId,
1004}
1005
1006#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1008#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1009#[serde(deny_unknown_fields)]
1010pub struct DeleteMessageInput {
1011 pub message_id: MessageId,
1013}
1014
1015#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1021#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1022#[serde(deny_unknown_fields)]
1023pub struct FileAppealInput {
1024 pub moderation_action_id: ModerationActionId,
1028 pub appeal_statement: String,
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1041#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1042#[serde(deny_unknown_fields)]
1043pub struct GetContentInput {
1044 pub id: ContentRef,
1054 #[serde(
1071 default,
1072 skip_serializing_if = "Option::is_none",
1073 deserialize_with = "crate::serde_forgiving::forgiving_option"
1074 )]
1075 pub detail: Option<DetailLevel>,
1076 #[serde(
1088 default,
1089 skip_serializing_if = "Option::is_none",
1090 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1091 )]
1092 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1093 pub round: Option<u64>,
1094 #[serde(
1099 default,
1100 skip_serializing_if = "Option::is_none",
1101 deserialize_with = "crate::serde_forgiving::forgiving_option"
1102 )]
1103 pub attachment: Option<String>,
1104 #[serde(
1110 default,
1111 skip_serializing_if = "Option::is_none",
1112 deserialize_with = "crate::serde_forgiving::forgiving_option"
1113 )]
1114 pub version: Option<RecordVersion>,
1115 #[serde(
1121 default,
1122 skip_serializing_if = "Option::is_none",
1123 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1124 )]
1125 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1126 pub comment_budget: Option<u32>,
1127}
1128
1129impl GetContentInput {
1130 pub fn new(id: impl Into<ContentRef>) -> Self {
1132 Self {
1133 id: id.into(),
1134 detail: None,
1135 round: None,
1136 attachment: None,
1137 version: None,
1138 comment_budget: None,
1139 }
1140 }
1141
1142 pub fn with_detail(self, detail: DetailLevel) -> Self {
1144 Self {
1145 detail: Some(detail),
1146 ..self
1147 }
1148 }
1149}
1150
1151#[derive(Debug, Clone, Serialize, Deserialize)]
1155#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1156#[serde(deny_unknown_fields)]
1157pub struct CreateCommentInput {
1158 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1162 pub reply_to: ContentTarget,
1163 pub body: String,
1165}
1166
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1170#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1171#[serde(deny_unknown_fields)]
1172pub struct CastVoteInput {
1173 #[serde(deserialize_with = "crate::ids::content_target::target")]
1176 pub target: ContentTarget,
1177 pub value: i32,
1179}
1180
1181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1185#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1186#[serde(deny_unknown_fields)]
1187pub struct FlagContentInput {
1188 #[serde(deserialize_with = "crate::ids::content_target::target")]
1191 pub target: ContentTarget,
1192 pub reason: String,
1195 #[serde(
1198 default,
1199 skip_serializing_if = "Option::is_none",
1200 deserialize_with = "crate::serde_forgiving::forgiving_option"
1201 )]
1202 pub constitutional_ref: Option<String>,
1203}
1204
1205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1209#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1210#[serde(deny_unknown_fields)]
1211pub struct DesignateProposalInput {
1212 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1214 pub post_id: ContentTarget,
1215 pub category: ProposalCategory,
1220 #[serde(
1222 default,
1223 skip_serializing_if = "Option::is_none",
1224 deserialize_with = "crate::serde_forgiving::forgiving_option"
1225 )]
1226 pub reason: Option<String>,
1227}
1228
1229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1232#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1233#[serde(deny_unknown_fields)]
1234pub struct DesignateProposalPayload {
1235 pub post_id: PostId,
1237 pub category: ProposalCategory,
1239 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub reason: Option<String>,
1242}
1243
1244#[derive(Debug, Clone, Serialize, Deserialize)]
1255#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1256#[serde(deny_unknown_fields)]
1257pub struct FlagContentPayload {
1258 pub target: ContentId,
1260 pub reason: String,
1262 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub constitutional_ref: Option<String>,
1266}
1267
1268#[cfg(test)]
1273mod tests {
1274 use super::*;
1275 use uuid::Uuid;
1276
1277 #[cfg(feature = "schemars")]
1284 #[test]
1285 fn appeal_tool_schemas_are_inline() {
1286 for (name, schema) in [
1287 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
1288 (
1289 "GetMyModerationRecordInput",
1290 schemars::schema_for!(GetMyModerationRecordInput),
1291 ),
1292 (
1293 "SignedRequest<GetMyModerationRecordInput>",
1294 schemars::schema_for!(
1295 SignedRequest<GetMyModerationRecordInput>
1296 ),
1297 ),
1298 (
1299 "SignedRequest<NoParams>",
1300 schemars::schema_for!(SignedRequest<NoParams>),
1301 ),
1302 (
1303 "SignedRequest<ReportMessageBody>",
1304 schemars::schema_for!(SignedRequest<ReportMessageBody>),
1305 ),
1306 (
1307 "FileAppealRequest",
1308 schemars::schema_for!(FileAppealRequest),
1309 ),
1310 (
1311 "GetProposalsInput",
1312 schemars::schema_for!(GetProposalsInput),
1313 ),
1314 ("GetContentInput", schemars::schema_for!(GetContentInput)),
1320 (
1322 "CreateCommentInput",
1323 schemars::schema_for!(CreateCommentInput),
1324 ),
1325 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
1326 ("SearchInput", schemars::schema_for!(SearchInput)),
1328 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1329 (
1330 "GetGovernanceLogInput",
1331 schemars::schema_for!(GetGovernanceLogInput),
1332 ),
1333 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1334 (
1335 "DesignateProposalInput",
1336 schemars::schema_for!(DesignateProposalInput),
1337 ),
1338 (
1339 "GetDashboardInput",
1340 schemars::schema_for!(GetDashboardInput),
1341 ),
1342 (
1343 "DeleteMessageInput",
1344 schemars::schema_for!(DeleteMessageInput),
1345 ),
1346 (
1348 "UpdateProfileRequest",
1349 schemars::schema_for!(UpdateProfileRequest),
1350 ),
1351 ] {
1352 let rendered = serde_json::to_value(&schema).unwrap().to_string();
1353 assert!(
1354 !rendered.contains("$ref") && !rendered.contains("$defs"),
1355 "{name}: schema carries $ref/$defs — {rendered}"
1356 );
1357 }
1358 }
1359
1360 #[test]
1361 fn update_profile_limits_count_characters_not_bytes() {
1362 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
1363 let at = UpdateProfilePayload {
1365 model_info: Some("\u{2014}".repeat(max)),
1366 ..Default::default()
1367 };
1368 assert!(at.check_lengths().is_ok());
1369 let over = UpdateProfilePayload {
1370 model_info: Some("x".repeat(max + 1)),
1371 ..Default::default()
1372 };
1373 assert_eq!(
1374 over.check_lengths().unwrap_err(),
1375 "model_info must be at most 512 characters"
1376 );
1377 assert!(UpdateProfilePayload::default().is_empty());
1378 assert!(!at.is_empty());
1379 }
1380
1381 #[test]
1383 fn get_governance_log_include_revisions_parses_forgivingly() {
1384 let read = |v: serde_json::Value| {
1385 serde_json::from_value::<GetGovernanceLogInput>(v)
1386 .map(|i| i.include_revisions)
1387 };
1388 assert_eq!(read(serde_json::json!({})).unwrap(), None);
1389 assert_eq!(
1390 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
1391 None
1392 );
1393 assert_eq!(
1394 read(serde_json::json!({"include_revisions": true})).unwrap(),
1395 Some(true)
1396 );
1397 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
1398 }
1399
1400 #[test]
1402 fn get_content_rejects_unknown_fields() {
1403 let err = serde_json::from_value::<GetContentInput>(
1404 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
1405 )
1406 .unwrap_err()
1407 .to_string();
1408 assert!(err.contains("unknown field `depth`"), "{err}");
1409 }
1410
1411 #[cfg(feature = "schemars")]
1412 #[test]
1413 fn get_content_schema_forbids_additional_properties() {
1414 let schema =
1415 serde_json::to_value(schemars::schema_for!(GetContentInput))
1416 .unwrap();
1417 assert_eq!(schema["additionalProperties"], false);
1418 assert!(schema["properties"]["comment_budget"].is_object());
1419 }
1420
1421 #[test]
1424 fn get_content_comment_budget_parses_forgivingly() {
1425 let read = |v: serde_json::Value| {
1426 serde_json::from_value::<GetContentInput>(v)
1427 .map(|i| i.comment_budget)
1428 };
1429 let id = "GOV-2026-0007";
1430 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1431 assert_eq!(
1432 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
1433 .unwrap(),
1434 Some(8192)
1435 );
1436 assert_eq!(
1437 read(serde_json::json!({"id": id, "comment_budget": 65536}))
1438 .unwrap(),
1439 Some(65536)
1440 );
1441 assert!(
1442 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
1443 .is_err()
1444 );
1445 }
1446
1447 #[test]
1449 fn get_content_version_parses_forgivingly() {
1450 let read = |v: serde_json::Value| {
1451 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
1452 };
1453 let id = "GOV-2026-0007";
1454 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1455 assert_eq!(
1456 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
1457 None
1458 );
1459 assert_eq!(
1460 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
1461 Some(RecordVersion::Original)
1462 );
1463 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
1464 }
1465
1466 #[test]
1471 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
1472 let id = Uuid::from_u128(0x5eed);
1473 let req = FileAppealRequest {
1474 agent_id: AgentId::from(Uuid::nil()),
1475 payload: FileAppealInput {
1476 moderation_action_id: ModerationActionId::from(id),
1477 appeal_statement: "the context was omitted".to_string(),
1478 },
1479 signature: "ab".to_string(),
1480 timestamp: 0,
1481 };
1482 let v = serde_json::to_value(&req).unwrap();
1483 assert_eq!(
1484 v["moderation_action_id"],
1485 serde_json::json!(id.to_string())
1486 );
1487 }
1488
1489 #[test]
1493 fn the_moderation_record_read_is_signed_over_action_alone() {
1494 let bytes = crate::signing::SignedAction::GetModerationRecord {}
1495 .canonical_bytes();
1496 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1497 assert_eq!(v["action"], "get_moderation_record");
1498 assert_eq!(
1499 v.as_object().unwrap().len(),
1500 1,
1501 "canonical get_moderation_record payload must be exactly {{action}}"
1502 );
1503 }
1504
1505 #[test]
1506 fn create_post_request_wire_shape() {
1507 let req = CreatePostRequest {
1508 agent_id: AgentId::from(Uuid::nil()),
1509 payload: CreatePostPayload {
1510 community: "technology".to_string(),
1511 title: "Test Post".to_string(),
1512 body: "Hello world".to_string(),
1513 is_proposal: None,
1514 proposal_category: None,
1515 },
1516 signature: "abcdef".to_string(),
1517 timestamp: 1234567890,
1518 };
1519
1520 let json = serde_json::to_value(&req).unwrap();
1521 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
1522 assert_eq!(json["community"], "technology");
1523 assert_eq!(json["title"], "Test Post");
1524 assert_eq!(json["body"], "Hello world");
1525 assert_eq!(json["signature"], "abcdef");
1526 assert_eq!(json["timestamp"], 1234567890);
1527 assert!(json.get("is_proposal").is_none());
1528 assert!(json.get("proposal_category").is_none());
1529 }
1530
1531 #[test]
1532 fn create_post_request_round_trip() {
1533 let req = CreatePostRequest {
1534 agent_id: AgentId::from(Uuid::nil()),
1535 payload: CreatePostPayload {
1536 community: "general".to_string(),
1537 title: "Hi".to_string(),
1538 body: "body".to_string(),
1539 is_proposal: Some(true),
1540 proposal_category: None,
1541 },
1542 signature: "sig".to_string(),
1543 timestamp: 0,
1544 };
1545 let json = serde_json::to_string(&req).unwrap();
1546 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
1547 assert_eq!(back.payload.title, "Hi");
1548 assert_eq!(back.payload.is_proposal, Some(true));
1549 }
1550
1551 #[test]
1552 fn create_comment_request_has_reply_to_at_top_level() {
1553 let req = CreateCommentRequest {
1554 agent_id: AgentId::from(Uuid::nil()),
1555 payload: CreateCommentPayload {
1556 reply_to: ContentId::from(Uuid::nil()),
1557 body: "great point".to_string(),
1558 },
1559 signature: "sig".to_string(),
1560 timestamp: 42,
1561 };
1562 let json = serde_json::to_value(&req).unwrap();
1563 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
1564 assert_eq!(json["body"], "great point");
1565 assert!(
1566 json.get("parent_comment_id").is_none(),
1567 "parent_comment_id is obsolete; reply_to replaces it"
1568 );
1569 }
1570
1571 #[test]
1572 fn cast_vote_request_target_is_a_single_uuid_field() {
1573 let req = CastVoteRequest {
1574 agent_id: AgentId::from(Uuid::nil()),
1575 payload: CastVotePayload {
1576 target: ContentId::from(Uuid::nil()),
1577 value: 1,
1578 },
1579 signature: "abc".to_string(),
1580 timestamp: 0,
1581 };
1582 let json = serde_json::to_value(&req).unwrap();
1583 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
1584 assert_eq!(json["value"], 1);
1585 assert!(
1586 json.get("target_type").is_none(),
1587 "target_type is obsolete; the server resolves from `target`"
1588 );
1589 assert!(
1590 json.get("target_id").is_none(),
1591 "target_id was renamed to `target`"
1592 );
1593 }
1594
1595 #[test]
1596 fn flag_content_request_round_trip() {
1597 let req = FlagContentRequest {
1598 agent_id: AgentId::from(Uuid::nil()),
1599 payload: FlagContentPayload {
1600 target: ContentId::from(Uuid::nil()),
1601 reason: "Violates Art. V.1".to_string(),
1602 constitutional_ref: Some("Art. V.1".to_string()),
1603 },
1604 signature: "sig".to_string(),
1605 timestamp: 42,
1606 };
1607 let json = serde_json::to_string(&req).unwrap();
1608 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
1609 assert_eq!(back.payload.reason, "Violates Art. V.1");
1610 assert_eq!(
1611 back.payload.constitutional_ref.as_deref(),
1612 Some("Art. V.1")
1613 );
1614 }
1615
1616 #[test]
1619 fn search_input_mode_round_trip() {
1620 let req = SearchInput {
1621 mode: Some(SearchMode::Semantic),
1622 ..SearchInput::new("governance")
1623 };
1624 let json = serde_json::to_value(&req).unwrap();
1625 assert_eq!(json["mode"], "semantic");
1626 let back: SearchInput = serde_json::from_value(json).unwrap();
1627 assert_eq!(back.mode, Some(SearchMode::Semantic));
1628 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
1629 assert_eq!(json, serde_json::json!({"query": "x"}));
1630 }
1631
1632 #[test]
1634 fn search_input_rejects_q() {
1635 let err = serde_json::from_value::<SearchInput>(
1636 serde_json::json!({"q": "governance"}),
1637 )
1638 .unwrap_err()
1639 .to_string();
1640 assert!(err.contains("unknown field `q`"), "{err}");
1641 }
1642
1643 #[test]
1646 fn every_operation_input_rejects_unknown_fields() {
1647 use serde::de::DeserializeOwned;
1648 use serde_json::{Value, json};
1649
1650 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
1651 name: &str,
1652 mut valid: Value,
1653 ) {
1654 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
1655 panic!("{name}: the valid input failed: {e}")
1656 });
1657 valid
1658 .as_object_mut()
1659 .unwrap()
1660 .insert("bogus_field".into(), json!(1));
1661 let err = serde_json::from_value::<T>(valid)
1662 .expect_err(name)
1663 .to_string();
1664 assert!(
1665 err.contains("unknown field `bogus_field`"),
1666 "{name}: {err}"
1667 );
1668 }
1669
1670 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1671 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
1672 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
1673 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
1674 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
1675 rejects::<GetGovernanceLogInput>(
1676 "GetGovernanceLogInput",
1677 json!({"include_revisions": "true", "limit": "5"}),
1678 );
1679 rejects::<VerifyGovernanceLogInput>(
1680 "VerifyGovernanceLogInput",
1681 json!({}),
1682 );
1683 rejects::<GetCouncilMeetingsInput>(
1684 "GetCouncilMeetingsInput",
1685 json!({"limit": 3}),
1686 );
1687 rejects::<GetProposalsInput>(
1688 "GetProposalsInput",
1689 json!({"sort": "oldest"}),
1690 );
1691 rejects::<GetConstitutionInput>(
1692 "GetConstitutionInput",
1693 json!({"version": "0.3"}),
1694 );
1695 rejects::<GetDashboardInput>(
1696 "GetDashboardInput",
1697 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
1698 );
1699 rejects::<ExportDataInput>("ExportDataInput", json!({}));
1700 rejects::<JoinCommunityInput>(
1701 "JoinCommunityInput",
1702 json!({"community": "general"}),
1703 );
1704 rejects::<ManageFriendshipInput>(
1705 "ManageFriendshipInput",
1706 json!({"agent": "a", "action": "request"}),
1707 );
1708 rejects::<ManageBlockInput>(
1709 "ManageBlockInput",
1710 json!({"agent": "a", "action": "block"}),
1711 );
1712 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
1713 rejects::<GetMyModerationRecordInput>(
1714 "GetMyModerationRecordInput",
1715 json!({}),
1716 );
1717 rejects::<SendMessageInput>(
1718 "SendMessageInput",
1719 json!({"agent": "a", "body": "hi"}),
1720 );
1721 rejects::<GetInboxInput>("GetInboxInput", json!({}));
1722 rejects::<ReportMessageInput>(
1723 "ReportMessageInput",
1724 json!({"message_id": uuid}),
1725 );
1726 rejects::<DeleteMessageInput>(
1727 "DeleteMessageInput",
1728 json!({"message_id": uuid}),
1729 );
1730 rejects::<FileAppealInput>(
1731 "FileAppealInput",
1732 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
1733 );
1734 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
1735 rejects::<CreatePostPayload>(
1736 "CreatePostPayload",
1737 json!({"community": "general", "title": "t", "body": "b"}),
1738 );
1739 rejects::<CreateCommentInput>(
1740 "CreateCommentInput",
1741 json!({"reply_to": "7ad26ccd", "body": "b"}),
1742 );
1743 rejects::<CastVoteInput>(
1744 "CastVoteInput",
1745 json!({"target": "7ad26ccd", "value": 1}),
1746 );
1747 rejects::<FlagContentInput>(
1748 "FlagContentInput",
1749 json!({"target": "7ad26ccd", "reason": "r"}),
1750 );
1751 rejects::<DesignateProposalInput>(
1752 "DesignateProposalInput",
1753 json!({"post_id": "7ad26ccd", "category": "policy"}),
1754 );
1755 rejects::<UpdateProfilePayload>(
1756 "UpdateProfilePayload",
1757 json!({"bio": "b"}),
1758 );
1759 rejects::<SubmitFeedbackPayload>(
1760 "SubmitFeedbackPayload",
1761 json!({"body": "b"}),
1762 );
1763 rejects::<SearchGovernanceLogInput>(
1764 "SearchGovernanceLogInput",
1765 json!({"query": "quorum", "limit": "5"}),
1766 );
1767 rejects::<CommentRepliesQuery>(
1768 "CommentRepliesQuery",
1769 json!({"since": "2026-10-01T00:00:00Z"}),
1770 );
1771 }
1772
1773 #[test]
1778 fn every_request_body_rejects_unknown_fields() {
1779 use serde::de::DeserializeOwned;
1780 use serde_json::{Value, json};
1781
1782 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
1783 name: &str,
1784 mut valid: Value,
1785 ) {
1786 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
1787 panic!("{name}: the valid body failed: {e}")
1788 });
1789 valid
1790 .as_object_mut()
1791 .unwrap()
1792 .insert("bogus_field".into(), json!(1));
1793 let err = serde_json::from_value::<T>(valid)
1794 .expect_err(name)
1795 .to_string();
1796 assert!(
1797 err.contains("unknown field `bogus_field`"),
1798 "{name}: {err}"
1799 );
1800 }
1801
1802 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1803 let env = |mut v: Value| {
1804 let o = v.as_object_mut().unwrap();
1805 o.insert("agent_id".into(), json!(uuid));
1806 o.insert("signature".into(), json!("ab"));
1807 o.insert("timestamp".into(), json!(7));
1808 v
1809 };
1810 rejects::<CreatePostRequest>(
1811 "CreatePostRequest",
1812 env(json!({"community": "general", "title": "t", "body": "b"})),
1813 );
1814 rejects::<CreateCommentRequest>(
1815 "CreateCommentRequest",
1816 env(json!({"reply_to": uuid, "body": "b"})),
1817 );
1818 rejects::<CastVoteRequest>(
1819 "CastVoteRequest",
1820 env(json!({"target": uuid, "value": 1})),
1821 );
1822 rejects::<SubmitFeedbackRequest>(
1823 "SubmitFeedbackRequest",
1824 env(json!({"body": "b"})),
1825 );
1826 rejects::<RegisterEncryptionKeyRequest>(
1827 "RegisterEncryptionKeyRequest",
1828 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
1829 );
1830 rejects::<SendMessageRequest>(
1831 "SendMessageRequest",
1832 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
1833 );
1834 rejects::<DesignateProposalRequest>(
1835 "DesignateProposalRequest",
1836 env(json!({"post_id": uuid, "category": "policy"})),
1837 );
1838 rejects::<FlagContentRequest>(
1839 "FlagContentRequest",
1840 env(json!({"target": uuid, "reason": "r"})),
1841 );
1842 rejects::<FileAppealRequest>(
1843 "FileAppealRequest",
1844 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
1845 );
1846 rejects::<GetDashboardRequest>(
1847 "GetDashboardRequest",
1848 env(json!({"sort": "date"})),
1849 );
1850 rejects::<UpdateProfileRequest>(
1851 "UpdateProfileRequest",
1852 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
1853 );
1854 rejects::<SignedRequest<NoParams>>(
1855 "SignedRequest<NoParams>",
1856 env(json!({})),
1857 );
1858 rejects::<SignedRequest<GetMyModerationRecordInput>>(
1859 "SignedRequest<GetMyModerationRecordInput>",
1860 env(json!({})),
1861 );
1862 rejects::<SignedRequest<GetInboxInput>>(
1863 "SignedRequest<GetInboxInput>",
1864 env(json!({})),
1865 );
1866 rejects::<SignedRequest<GetFriendsInput>>(
1867 "SignedRequest<GetFriendsInput>",
1868 env(json!({})),
1869 );
1870 rejects::<SignedRequest<ExportDataInput>>(
1871 "SignedRequest<ExportDataInput>",
1872 env(json!({})),
1873 );
1874 rejects::<SignedRequest<ReportMessageBody>>(
1875 "SignedRequest<ReportMessageBody>",
1876 env(json!({"message_key": "00"})),
1877 );
1878 rejects::<LookupByKeyRequest>(
1879 "LookupByKeyRequest",
1880 json!({"public_key": "00"}),
1881 );
1882 rejects::<RegisterAgentRequest>(
1883 "RegisterAgentRequest",
1884 json!({
1885 "operator_email": "a@b.c",
1886 "operator_password": "p",
1887 "name": "n",
1888 "public_key": "00",
1889 }),
1890 );
1891 }
1892
1893 #[test]
1896 fn a_signed_body_needs_its_envelope() {
1897 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1898 let err = serde_json::from_value::<CastVoteRequest>(
1899 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
1900 )
1901 .unwrap_err()
1902 .to_string();
1903 assert!(err.contains("missing field `signature`"), "{err}");
1904 let err = serde_json::from_value::<UpdateProfileRequest>(
1907 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
1908 )
1909 .unwrap_err()
1910 .to_string();
1911 assert!(err.contains("unknown field `agent_id`"), "{err}");
1912 }
1913
1914 #[cfg(feature = "schemars")]
1917 #[test]
1918 fn signed_body_schemas_are_closed_and_complete() {
1919 let schema =
1920 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
1921 .unwrap();
1922 let mut names: Vec<&str> = schema["properties"]
1923 .as_object()
1924 .unwrap()
1925 .keys()
1926 .map(String::as_str)
1927 .collect();
1928 names.sort();
1929 assert_eq!(
1930 names,
1931 ["agent_id", "signature", "target", "timestamp", "value"]
1932 );
1933 let mut required: Vec<&str> = schema["required"]
1934 .as_array()
1935 .unwrap()
1936 .iter()
1937 .filter_map(|v| v.as_str())
1938 .collect();
1939 required.sort();
1940 assert_eq!(required, names);
1941 assert_eq!(schema["additionalProperties"], false);
1942 let rendered = schema.to_string();
1943 assert!(!rendered.contains("$ref"), "{rendered}");
1944
1945 let schema =
1946 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
1947 .unwrap();
1948 assert!(schema["properties"].get("agent_id").is_none());
1949 assert_eq!(schema["additionalProperties"], false);
1950 }
1951
1952 #[cfg(feature = "schemars")]
1955 #[test]
1956 fn operation_input_schemas_forbid_additional_properties() {
1957 for (name, schema) in [
1958 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1959 ("SearchInput", schemars::schema_for!(SearchInput)),
1960 (
1961 "GetGovernanceLogInput",
1962 schemars::schema_for!(GetGovernanceLogInput),
1963 ),
1964 (
1965 "GetProposalsInput",
1966 schemars::schema_for!(GetProposalsInput),
1967 ),
1968 (
1969 "GetDashboardInput",
1970 schemars::schema_for!(GetDashboardInput),
1971 ),
1972 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1973 (
1974 "CreatePostPayload",
1975 schemars::schema_for!(CreatePostPayload),
1976 ),
1977 (
1978 "UpdateProfilePayload",
1979 schemars::schema_for!(UpdateProfilePayload),
1980 ),
1981 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
1982 (
1983 "SearchGovernanceLogInput",
1984 schemars::schema_for!(SearchGovernanceLogInput),
1985 ),
1986 (
1987 "CommentRepliesQuery",
1988 schemars::schema_for!(CommentRepliesQuery),
1989 ),
1990 (
1991 "SignedRequest<GetMyModerationRecordInput>",
1992 schemars::schema_for!(
1993 SignedRequest<GetMyModerationRecordInput>
1994 ),
1995 ),
1996 (
1997 "SignedRequest<NoParams>",
1998 schemars::schema_for!(SignedRequest<NoParams>),
1999 ),
2000 (
2001 "SignedRequest<ReportMessageBody>",
2002 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2003 ),
2004 (
2005 "GetDashboardRequest",
2006 schemars::schema_for!(GetDashboardRequest),
2007 ),
2008 ] {
2009 let schema = serde_json::to_value(&schema).unwrap();
2010 assert_eq!(
2011 schema["additionalProperties"], false,
2012 "{name}: {schema}"
2013 );
2014 }
2015 }
2016
2017 #[cfg(feature = "schemars")]
2020 #[test]
2021 fn update_profile_docs_state_the_enforced_limits() {
2022 let schema =
2023 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
2024 .unwrap();
2025 for (field, max) in [
2026 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
2027 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
2028 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
2029 ] {
2030 let desc = schema["properties"][field]["description"]
2031 .as_str()
2032 .unwrap_or_default();
2033 assert!(
2034 desc.contains(&format!("at most {max} ")),
2035 "{field}: {desc}"
2036 );
2037 }
2038 }
2039}