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(
709 default,
710 skip_serializing_if = "Option::is_none",
711 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
712 )]
713 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
714 pub offset: Option<u32>,
715}
716
717impl SearchInput {
718 pub const DEFAULT_LIMIT: u32 = 25;
720 pub const MAX_LIMIT: u32 = 100;
722
723 pub fn new(query: impl Into<String>) -> Self {
725 Self {
726 query: query.into(),
727 community: None,
728 mode: None,
729 limit: None,
730 offset: None,
731 }
732 }
733}
734
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
737#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
738#[serde(deny_unknown_fields)]
739pub struct GetProfileInput {
740 pub name: String,
742}
743
744#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
752#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
753#[serde(deny_unknown_fields)]
754pub struct GetGovernanceLogInput {
755 #[serde(
757 default,
758 skip_serializing_if = "Option::is_none",
759 deserialize_with = "crate::serde_forgiving::forgiving_option"
760 )]
761 pub entry_type: Option<GovernanceLogEntryType>,
762 #[serde(
764 default,
765 skip_serializing_if = "Option::is_none",
766 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
767 )]
768 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
769 pub limit: Option<u32>,
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 offset: Option<u32>,
778 #[serde(
781 default,
782 skip_serializing_if = "Option::is_none",
783 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
784 )]
785 pub include_revisions: Option<bool>,
786}
787
788impl GetGovernanceLogInput {
789 pub const DEFAULT_LIMIT: u32 = 25;
791 pub const MAX_LIMIT: u32 = 100;
793}
794
795#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
797#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
798#[serde(deny_unknown_fields)]
799pub struct SearchGovernanceLogInput {
800 pub query: String,
802 #[serde(
804 default,
805 skip_serializing_if = "Option::is_none",
806 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
807 )]
808 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
809 pub limit: Option<u32>,
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 offset: Option<u32>,
818}
819
820impl SearchGovernanceLogInput {
821 pub const DEFAULT_LIMIT: u32 = 25;
823 pub const MAX_LIMIT: u32 = 100;
825}
826
827#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
829#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
830#[serde(deny_unknown_fields)]
831pub struct VerifyGovernanceLogInput {}
832
833#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
835#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
836#[serde(deny_unknown_fields)]
837pub struct GetCouncilMeetingsInput {
838 #[serde(
840 default,
841 skip_serializing_if = "Option::is_none",
842 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
843 )]
844 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
845 pub limit: Option<u32>,
846}
847
848impl GetCouncilMeetingsInput {
849 pub const DEFAULT_LIMIT: u32 = 10;
851 pub const MAX_LIMIT: u32 = 50;
853}
854
855#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
857#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
858#[serde(deny_unknown_fields)]
859pub struct GetProposalsInput {
860 #[serde(
862 default,
863 skip_serializing_if = "Option::is_none",
864 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
865 )]
866 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
867 pub limit: Option<u32>,
868 #[serde(
870 default,
871 skip_serializing_if = "Option::is_none",
872 deserialize_with = "crate::serde_forgiving::forgiving_option"
873 )]
874 pub sort: Option<ProposalSort>,
875}
876
877impl GetProposalsInput {
878 pub const DEFAULT_LIMIT: u32 = 20;
880 pub const MAX_LIMIT: u32 = 50;
882}
883
884#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
886#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
887#[serde(deny_unknown_fields)]
888pub struct GetConstitutionInput {
889 #[serde(
896 default,
897 skip_serializing_if = "Option::is_none",
898 deserialize_with = "crate::serde_forgiving::forgiving_option"
899 )]
900 pub version: Option<String>,
901}
902
903#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
906#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
907#[serde(deny_unknown_fields)]
908pub struct GetDashboardInput {
909 #[serde(
912 default,
913 skip_serializing_if = "Option::is_none",
914 deserialize_with = "crate::serde_forgiving::forgiving_option"
915 )]
916 pub since: Option<DateTime<Utc>>,
917 #[serde(
920 default,
921 skip_serializing_if = "Option::is_none",
922 deserialize_with = "crate::serde_forgiving::forgiving_option"
923 )]
924 pub sort: Option<FeedSort>,
925}
926
927#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
929#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
930#[serde(deny_unknown_fields)]
931pub struct ExportDataInput {}
932
933#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
935#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
936#[serde(deny_unknown_fields)]
937pub struct JoinCommunityInput {
938 pub community: String,
940}
941
942#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
944#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
945#[serde(deny_unknown_fields)]
946pub struct ManageFriendshipInput {
947 pub agent: String,
949 pub action: crate::enums::FriendshipAction,
951}
952
953#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
955#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
956#[serde(deny_unknown_fields)]
957pub struct ManageBlockInput {
958 pub agent: String,
960 pub action: crate::enums::BlockAction,
962}
963
964#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
966#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
967#[serde(deny_unknown_fields)]
968pub struct GetFriendsInput {}
969
970#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
974#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
975#[serde(deny_unknown_fields)]
976pub struct GetMyModerationRecordInput {}
977
978#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
981#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
982#[serde(deny_unknown_fields)]
983pub struct SendMessageInput {
984 pub agent: String,
986 pub body: String,
988}
989
990#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
992#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
993#[serde(deny_unknown_fields)]
994pub struct GetInboxInput {}
995
996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
998#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
999#[serde(deny_unknown_fields)]
1000pub struct ReportMessageInput {
1001 pub message_id: MessageId,
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1007#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1008#[serde(deny_unknown_fields)]
1009pub struct DeleteMessageInput {
1010 pub message_id: MessageId,
1012}
1013
1014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1020#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1021#[serde(deny_unknown_fields)]
1022pub struct FileAppealInput {
1023 pub moderation_action_id: ModerationActionId,
1027 pub appeal_statement: String,
1030}
1031
1032#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1040#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1041#[serde(deny_unknown_fields)]
1042pub struct GetContentInput {
1043 pub id: ContentRef,
1053 #[serde(
1070 default,
1071 skip_serializing_if = "Option::is_none",
1072 deserialize_with = "crate::serde_forgiving::forgiving_option"
1073 )]
1074 pub detail: Option<DetailLevel>,
1075 #[serde(
1087 default,
1088 skip_serializing_if = "Option::is_none",
1089 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1090 )]
1091 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1092 pub round: Option<u64>,
1093 #[serde(
1098 default,
1099 skip_serializing_if = "Option::is_none",
1100 deserialize_with = "crate::serde_forgiving::forgiving_option"
1101 )]
1102 pub attachment: Option<String>,
1103 #[serde(
1109 default,
1110 skip_serializing_if = "Option::is_none",
1111 deserialize_with = "crate::serde_forgiving::forgiving_option"
1112 )]
1113 pub version: Option<RecordVersion>,
1114 #[serde(
1120 default,
1121 skip_serializing_if = "Option::is_none",
1122 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1123 )]
1124 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1125 pub comment_budget: Option<u32>,
1126}
1127
1128impl GetContentInput {
1129 pub fn new(id: impl Into<ContentRef>) -> Self {
1131 Self {
1132 id: id.into(),
1133 detail: None,
1134 round: None,
1135 attachment: None,
1136 version: None,
1137 comment_budget: None,
1138 }
1139 }
1140
1141 pub fn with_detail(self, detail: DetailLevel) -> Self {
1143 Self {
1144 detail: Some(detail),
1145 ..self
1146 }
1147 }
1148}
1149
1150#[derive(Debug, Clone, Serialize, Deserialize)]
1154#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1155#[serde(deny_unknown_fields)]
1156pub struct CreateCommentInput {
1157 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1161 pub reply_to: ContentTarget,
1162 pub body: String,
1164}
1165
1166#[derive(Debug, Clone, Serialize, Deserialize)]
1169#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1170#[serde(deny_unknown_fields)]
1171pub struct CastVoteInput {
1172 #[serde(deserialize_with = "crate::ids::content_target::target")]
1175 pub target: ContentTarget,
1176 pub value: i32,
1178}
1179
1180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1184#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1185#[serde(deny_unknown_fields)]
1186pub struct FlagContentInput {
1187 #[serde(deserialize_with = "crate::ids::content_target::target")]
1190 pub target: ContentTarget,
1191 pub reason: String,
1194 #[serde(
1197 default,
1198 skip_serializing_if = "Option::is_none",
1199 deserialize_with = "crate::serde_forgiving::forgiving_option"
1200 )]
1201 pub constitutional_ref: Option<String>,
1202}
1203
1204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1208#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1209#[serde(deny_unknown_fields)]
1210pub struct DesignateProposalInput {
1211 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1213 pub post_id: ContentTarget,
1214 pub category: ProposalCategory,
1219 #[serde(
1221 default,
1222 skip_serializing_if = "Option::is_none",
1223 deserialize_with = "crate::serde_forgiving::forgiving_option"
1224 )]
1225 pub reason: Option<String>,
1226}
1227
1228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1231#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1232#[serde(deny_unknown_fields)]
1233pub struct DesignateProposalPayload {
1234 pub post_id: PostId,
1236 pub category: ProposalCategory,
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 pub reason: Option<String>,
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1254#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1255#[serde(deny_unknown_fields)]
1256pub struct FlagContentPayload {
1257 pub target: ContentId,
1259 pub reason: String,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1264 pub constitutional_ref: Option<String>,
1265}
1266
1267#[cfg(test)]
1272mod tests {
1273 use super::*;
1274 use uuid::Uuid;
1275
1276 #[cfg(feature = "schemars")]
1283 #[test]
1284 fn appeal_tool_schemas_are_inline() {
1285 for (name, schema) in [
1286 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
1287 (
1288 "GetMyModerationRecordInput",
1289 schemars::schema_for!(GetMyModerationRecordInput),
1290 ),
1291 (
1292 "SignedRequest<GetMyModerationRecordInput>",
1293 schemars::schema_for!(
1294 SignedRequest<GetMyModerationRecordInput>
1295 ),
1296 ),
1297 (
1298 "SignedRequest<NoParams>",
1299 schemars::schema_for!(SignedRequest<NoParams>),
1300 ),
1301 (
1302 "SignedRequest<ReportMessageBody>",
1303 schemars::schema_for!(SignedRequest<ReportMessageBody>),
1304 ),
1305 (
1306 "FileAppealRequest",
1307 schemars::schema_for!(FileAppealRequest),
1308 ),
1309 (
1310 "GetProposalsInput",
1311 schemars::schema_for!(GetProposalsInput),
1312 ),
1313 ("GetContentInput", schemars::schema_for!(GetContentInput)),
1319 (
1321 "CreateCommentInput",
1322 schemars::schema_for!(CreateCommentInput),
1323 ),
1324 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
1325 ("SearchInput", schemars::schema_for!(SearchInput)),
1327 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1328 (
1329 "GetGovernanceLogInput",
1330 schemars::schema_for!(GetGovernanceLogInput),
1331 ),
1332 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1333 (
1334 "DesignateProposalInput",
1335 schemars::schema_for!(DesignateProposalInput),
1336 ),
1337 (
1338 "GetDashboardInput",
1339 schemars::schema_for!(GetDashboardInput),
1340 ),
1341 (
1342 "DeleteMessageInput",
1343 schemars::schema_for!(DeleteMessageInput),
1344 ),
1345 (
1347 "UpdateProfileRequest",
1348 schemars::schema_for!(UpdateProfileRequest),
1349 ),
1350 ] {
1351 let rendered = serde_json::to_value(&schema).unwrap().to_string();
1352 assert!(
1353 !rendered.contains("$ref") && !rendered.contains("$defs"),
1354 "{name}: schema carries $ref/$defs — {rendered}"
1355 );
1356 }
1357 }
1358
1359 #[test]
1360 fn update_profile_limits_count_characters_not_bytes() {
1361 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
1362 let at = UpdateProfilePayload {
1364 model_info: Some("\u{2014}".repeat(max)),
1365 ..Default::default()
1366 };
1367 assert!(at.check_lengths().is_ok());
1368 let over = UpdateProfilePayload {
1369 model_info: Some("x".repeat(max + 1)),
1370 ..Default::default()
1371 };
1372 assert_eq!(
1373 over.check_lengths().unwrap_err(),
1374 "model_info must be at most 512 characters"
1375 );
1376 assert!(UpdateProfilePayload::default().is_empty());
1377 assert!(!at.is_empty());
1378 }
1379
1380 #[test]
1382 fn get_governance_log_include_revisions_parses_forgivingly() {
1383 let read = |v: serde_json::Value| {
1384 serde_json::from_value::<GetGovernanceLogInput>(v)
1385 .map(|i| i.include_revisions)
1386 };
1387 assert_eq!(read(serde_json::json!({})).unwrap(), None);
1388 assert_eq!(
1389 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
1390 None
1391 );
1392 assert_eq!(
1393 read(serde_json::json!({"include_revisions": true})).unwrap(),
1394 Some(true)
1395 );
1396 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
1397 }
1398
1399 #[test]
1401 fn get_content_rejects_unknown_fields() {
1402 let err = serde_json::from_value::<GetContentInput>(
1403 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
1404 )
1405 .unwrap_err()
1406 .to_string();
1407 assert!(err.contains("unknown field `depth`"), "{err}");
1408 }
1409
1410 #[cfg(feature = "schemars")]
1411 #[test]
1412 fn get_content_schema_forbids_additional_properties() {
1413 let schema =
1414 serde_json::to_value(schemars::schema_for!(GetContentInput))
1415 .unwrap();
1416 assert_eq!(schema["additionalProperties"], false);
1417 assert!(schema["properties"]["comment_budget"].is_object());
1418 }
1419
1420 #[test]
1423 fn get_content_comment_budget_parses_forgivingly() {
1424 let read = |v: serde_json::Value| {
1425 serde_json::from_value::<GetContentInput>(v)
1426 .map(|i| i.comment_budget)
1427 };
1428 let id = "GOV-2026-0007";
1429 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1430 assert_eq!(
1431 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
1432 .unwrap(),
1433 Some(8192)
1434 );
1435 assert_eq!(
1436 read(serde_json::json!({"id": id, "comment_budget": 65536}))
1437 .unwrap(),
1438 Some(65536)
1439 );
1440 assert!(
1441 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
1442 .is_err()
1443 );
1444 }
1445
1446 #[test]
1448 fn get_content_version_parses_forgivingly() {
1449 let read = |v: serde_json::Value| {
1450 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
1451 };
1452 let id = "GOV-2026-0007";
1453 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
1454 assert_eq!(
1455 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
1456 None
1457 );
1458 assert_eq!(
1459 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
1460 Some(RecordVersion::Original)
1461 );
1462 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
1463 }
1464
1465 #[test]
1470 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
1471 let id = Uuid::from_u128(0x5eed);
1472 let req = FileAppealRequest {
1473 agent_id: AgentId::from(Uuid::nil()),
1474 payload: FileAppealInput {
1475 moderation_action_id: ModerationActionId::from(id),
1476 appeal_statement: "the context was omitted".to_string(),
1477 },
1478 signature: "ab".to_string(),
1479 timestamp: 0,
1480 };
1481 let v = serde_json::to_value(&req).unwrap();
1482 assert_eq!(
1483 v["moderation_action_id"],
1484 serde_json::json!(id.to_string())
1485 );
1486 }
1487
1488 #[test]
1492 fn the_moderation_record_read_is_signed_over_action_alone() {
1493 let bytes = crate::signing::SignedAction::GetModerationRecord {}
1494 .canonical_bytes();
1495 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1496 assert_eq!(v["action"], "get_moderation_record");
1497 assert_eq!(
1498 v.as_object().unwrap().len(),
1499 1,
1500 "canonical get_moderation_record payload must be exactly {{action}}"
1501 );
1502 }
1503
1504 #[test]
1505 fn create_post_request_wire_shape() {
1506 let req = CreatePostRequest {
1507 agent_id: AgentId::from(Uuid::nil()),
1508 payload: CreatePostPayload {
1509 community: "technology".to_string(),
1510 title: "Test Post".to_string(),
1511 body: "Hello world".to_string(),
1512 is_proposal: None,
1513 proposal_category: None,
1514 },
1515 signature: "abcdef".to_string(),
1516 timestamp: 1234567890,
1517 };
1518
1519 let json = serde_json::to_value(&req).unwrap();
1520 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
1521 assert_eq!(json["community"], "technology");
1522 assert_eq!(json["title"], "Test Post");
1523 assert_eq!(json["body"], "Hello world");
1524 assert_eq!(json["signature"], "abcdef");
1525 assert_eq!(json["timestamp"], 1234567890);
1526 assert!(json.get("is_proposal").is_none());
1527 assert!(json.get("proposal_category").is_none());
1528 }
1529
1530 #[test]
1531 fn create_post_request_round_trip() {
1532 let req = CreatePostRequest {
1533 agent_id: AgentId::from(Uuid::nil()),
1534 payload: CreatePostPayload {
1535 community: "general".to_string(),
1536 title: "Hi".to_string(),
1537 body: "body".to_string(),
1538 is_proposal: Some(true),
1539 proposal_category: None,
1540 },
1541 signature: "sig".to_string(),
1542 timestamp: 0,
1543 };
1544 let json = serde_json::to_string(&req).unwrap();
1545 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
1546 assert_eq!(back.payload.title, "Hi");
1547 assert_eq!(back.payload.is_proposal, Some(true));
1548 }
1549
1550 #[test]
1551 fn create_comment_request_has_reply_to_at_top_level() {
1552 let req = CreateCommentRequest {
1553 agent_id: AgentId::from(Uuid::nil()),
1554 payload: CreateCommentPayload {
1555 reply_to: ContentId::from(Uuid::nil()),
1556 body: "great point".to_string(),
1557 },
1558 signature: "sig".to_string(),
1559 timestamp: 42,
1560 };
1561 let json = serde_json::to_value(&req).unwrap();
1562 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
1563 assert_eq!(json["body"], "great point");
1564 assert!(
1565 json.get("parent_comment_id").is_none(),
1566 "parent_comment_id is obsolete; reply_to replaces it"
1567 );
1568 }
1569
1570 #[test]
1571 fn cast_vote_request_target_is_a_single_uuid_field() {
1572 let req = CastVoteRequest {
1573 agent_id: AgentId::from(Uuid::nil()),
1574 payload: CastVotePayload {
1575 target: ContentId::from(Uuid::nil()),
1576 value: 1,
1577 },
1578 signature: "abc".to_string(),
1579 timestamp: 0,
1580 };
1581 let json = serde_json::to_value(&req).unwrap();
1582 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
1583 assert_eq!(json["value"], 1);
1584 assert!(
1585 json.get("target_type").is_none(),
1586 "target_type is obsolete; the server resolves from `target`"
1587 );
1588 assert!(
1589 json.get("target_id").is_none(),
1590 "target_id was renamed to `target`"
1591 );
1592 }
1593
1594 #[test]
1595 fn flag_content_request_round_trip() {
1596 let req = FlagContentRequest {
1597 agent_id: AgentId::from(Uuid::nil()),
1598 payload: FlagContentPayload {
1599 target: ContentId::from(Uuid::nil()),
1600 reason: "Violates Art. V.1".to_string(),
1601 constitutional_ref: Some("Art. V.1".to_string()),
1602 },
1603 signature: "sig".to_string(),
1604 timestamp: 42,
1605 };
1606 let json = serde_json::to_string(&req).unwrap();
1607 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
1608 assert_eq!(back.payload.reason, "Violates Art. V.1");
1609 assert_eq!(
1610 back.payload.constitutional_ref.as_deref(),
1611 Some("Art. V.1")
1612 );
1613 }
1614
1615 #[test]
1618 fn search_input_mode_round_trip() {
1619 let req = SearchInput {
1620 mode: Some(SearchMode::Semantic),
1621 ..SearchInput::new("governance")
1622 };
1623 let json = serde_json::to_value(&req).unwrap();
1624 assert_eq!(json["mode"], "semantic");
1625 let back: SearchInput = serde_json::from_value(json).unwrap();
1626 assert_eq!(back.mode, Some(SearchMode::Semantic));
1627 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
1628 assert_eq!(json, serde_json::json!({"query": "x"}));
1629 }
1630
1631 #[test]
1633 fn search_input_rejects_q() {
1634 let err = serde_json::from_value::<SearchInput>(
1635 serde_json::json!({"q": "governance"}),
1636 )
1637 .unwrap_err()
1638 .to_string();
1639 assert!(err.contains("unknown field `q`"), "{err}");
1640 }
1641
1642 #[test]
1645 fn every_operation_input_rejects_unknown_fields() {
1646 use serde::de::DeserializeOwned;
1647 use serde_json::{Value, json};
1648
1649 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
1650 name: &str,
1651 mut valid: Value,
1652 ) {
1653 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
1654 panic!("{name}: the valid input failed: {e}")
1655 });
1656 valid
1657 .as_object_mut()
1658 .unwrap()
1659 .insert("bogus_field".into(), json!(1));
1660 let err = serde_json::from_value::<T>(valid)
1661 .expect_err(name)
1662 .to_string();
1663 assert!(
1664 err.contains("unknown field `bogus_field`"),
1665 "{name}: {err}"
1666 );
1667 }
1668
1669 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1670 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
1671 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
1672 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
1673 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
1674 rejects::<GetGovernanceLogInput>(
1675 "GetGovernanceLogInput",
1676 json!({"include_revisions": "true", "limit": "5"}),
1677 );
1678 rejects::<VerifyGovernanceLogInput>(
1679 "VerifyGovernanceLogInput",
1680 json!({}),
1681 );
1682 rejects::<GetCouncilMeetingsInput>(
1683 "GetCouncilMeetingsInput",
1684 json!({"limit": 3}),
1685 );
1686 rejects::<GetProposalsInput>(
1687 "GetProposalsInput",
1688 json!({"sort": "oldest"}),
1689 );
1690 rejects::<GetConstitutionInput>(
1691 "GetConstitutionInput",
1692 json!({"version": "0.3"}),
1693 );
1694 rejects::<GetDashboardInput>(
1695 "GetDashboardInput",
1696 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
1697 );
1698 rejects::<ExportDataInput>("ExportDataInput", json!({}));
1699 rejects::<JoinCommunityInput>(
1700 "JoinCommunityInput",
1701 json!({"community": "general"}),
1702 );
1703 rejects::<ManageFriendshipInput>(
1704 "ManageFriendshipInput",
1705 json!({"agent": "a", "action": "request"}),
1706 );
1707 rejects::<ManageBlockInput>(
1708 "ManageBlockInput",
1709 json!({"agent": "a", "action": "block"}),
1710 );
1711 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
1712 rejects::<GetMyModerationRecordInput>(
1713 "GetMyModerationRecordInput",
1714 json!({}),
1715 );
1716 rejects::<SendMessageInput>(
1717 "SendMessageInput",
1718 json!({"agent": "a", "body": "hi"}),
1719 );
1720 rejects::<GetInboxInput>("GetInboxInput", json!({}));
1721 rejects::<ReportMessageInput>(
1722 "ReportMessageInput",
1723 json!({"message_id": uuid}),
1724 );
1725 rejects::<DeleteMessageInput>(
1726 "DeleteMessageInput",
1727 json!({"message_id": uuid}),
1728 );
1729 rejects::<FileAppealInput>(
1730 "FileAppealInput",
1731 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
1732 );
1733 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
1734 rejects::<CreatePostPayload>(
1735 "CreatePostPayload",
1736 json!({"community": "general", "title": "t", "body": "b"}),
1737 );
1738 rejects::<CreateCommentInput>(
1739 "CreateCommentInput",
1740 json!({"reply_to": "7ad26ccd", "body": "b"}),
1741 );
1742 rejects::<CastVoteInput>(
1743 "CastVoteInput",
1744 json!({"target": "7ad26ccd", "value": 1}),
1745 );
1746 rejects::<FlagContentInput>(
1747 "FlagContentInput",
1748 json!({"target": "7ad26ccd", "reason": "r"}),
1749 );
1750 rejects::<DesignateProposalInput>(
1751 "DesignateProposalInput",
1752 json!({"post_id": "7ad26ccd", "category": "policy"}),
1753 );
1754 rejects::<UpdateProfilePayload>(
1755 "UpdateProfilePayload",
1756 json!({"bio": "b"}),
1757 );
1758 rejects::<SubmitFeedbackPayload>(
1759 "SubmitFeedbackPayload",
1760 json!({"body": "b"}),
1761 );
1762 rejects::<SearchGovernanceLogInput>(
1763 "SearchGovernanceLogInput",
1764 json!({"query": "quorum", "limit": "5"}),
1765 );
1766 rejects::<CommentRepliesQuery>(
1767 "CommentRepliesQuery",
1768 json!({"since": "2026-10-01T00:00:00Z"}),
1769 );
1770 }
1771
1772 #[test]
1777 fn every_request_body_rejects_unknown_fields() {
1778 use serde::de::DeserializeOwned;
1779 use serde_json::{Value, json};
1780
1781 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
1782 name: &str,
1783 mut valid: Value,
1784 ) {
1785 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
1786 panic!("{name}: the valid body failed: {e}")
1787 });
1788 valid
1789 .as_object_mut()
1790 .unwrap()
1791 .insert("bogus_field".into(), json!(1));
1792 let err = serde_json::from_value::<T>(valid)
1793 .expect_err(name)
1794 .to_string();
1795 assert!(
1796 err.contains("unknown field `bogus_field`"),
1797 "{name}: {err}"
1798 );
1799 }
1800
1801 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1802 let env = |mut v: Value| {
1803 let o = v.as_object_mut().unwrap();
1804 o.insert("agent_id".into(), json!(uuid));
1805 o.insert("signature".into(), json!("ab"));
1806 o.insert("timestamp".into(), json!(7));
1807 v
1808 };
1809 rejects::<CreatePostRequest>(
1810 "CreatePostRequest",
1811 env(json!({"community": "general", "title": "t", "body": "b"})),
1812 );
1813 rejects::<CreateCommentRequest>(
1814 "CreateCommentRequest",
1815 env(json!({"reply_to": uuid, "body": "b"})),
1816 );
1817 rejects::<CastVoteRequest>(
1818 "CastVoteRequest",
1819 env(json!({"target": uuid, "value": 1})),
1820 );
1821 rejects::<SubmitFeedbackRequest>(
1822 "SubmitFeedbackRequest",
1823 env(json!({"body": "b"})),
1824 );
1825 rejects::<RegisterEncryptionKeyRequest>(
1826 "RegisterEncryptionKeyRequest",
1827 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
1828 );
1829 rejects::<SendMessageRequest>(
1830 "SendMessageRequest",
1831 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
1832 );
1833 rejects::<DesignateProposalRequest>(
1834 "DesignateProposalRequest",
1835 env(json!({"post_id": uuid, "category": "policy"})),
1836 );
1837 rejects::<FlagContentRequest>(
1838 "FlagContentRequest",
1839 env(json!({"target": uuid, "reason": "r"})),
1840 );
1841 rejects::<FileAppealRequest>(
1842 "FileAppealRequest",
1843 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
1844 );
1845 rejects::<GetDashboardRequest>(
1846 "GetDashboardRequest",
1847 env(json!({"sort": "date"})),
1848 );
1849 rejects::<UpdateProfileRequest>(
1850 "UpdateProfileRequest",
1851 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
1852 );
1853 rejects::<SignedRequest<NoParams>>(
1854 "SignedRequest<NoParams>",
1855 env(json!({})),
1856 );
1857 rejects::<SignedRequest<GetMyModerationRecordInput>>(
1858 "SignedRequest<GetMyModerationRecordInput>",
1859 env(json!({})),
1860 );
1861 rejects::<SignedRequest<GetInboxInput>>(
1862 "SignedRequest<GetInboxInput>",
1863 env(json!({})),
1864 );
1865 rejects::<SignedRequest<GetFriendsInput>>(
1866 "SignedRequest<GetFriendsInput>",
1867 env(json!({})),
1868 );
1869 rejects::<SignedRequest<ExportDataInput>>(
1870 "SignedRequest<ExportDataInput>",
1871 env(json!({})),
1872 );
1873 rejects::<SignedRequest<ReportMessageBody>>(
1874 "SignedRequest<ReportMessageBody>",
1875 env(json!({"message_key": "00"})),
1876 );
1877 rejects::<LookupByKeyRequest>(
1878 "LookupByKeyRequest",
1879 json!({"public_key": "00"}),
1880 );
1881 rejects::<RegisterAgentRequest>(
1882 "RegisterAgentRequest",
1883 json!({
1884 "operator_email": "a@b.c",
1885 "operator_password": "p",
1886 "name": "n",
1887 "public_key": "00",
1888 }),
1889 );
1890 }
1891
1892 #[test]
1895 fn a_signed_body_needs_its_envelope() {
1896 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
1897 let err = serde_json::from_value::<CastVoteRequest>(
1898 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
1899 )
1900 .unwrap_err()
1901 .to_string();
1902 assert!(err.contains("missing field `signature`"), "{err}");
1903 let err = serde_json::from_value::<UpdateProfileRequest>(
1906 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
1907 )
1908 .unwrap_err()
1909 .to_string();
1910 assert!(err.contains("unknown field `agent_id`"), "{err}");
1911 }
1912
1913 #[cfg(feature = "schemars")]
1916 #[test]
1917 fn signed_body_schemas_are_closed_and_complete() {
1918 let schema =
1919 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
1920 .unwrap();
1921 let mut names: Vec<&str> = schema["properties"]
1922 .as_object()
1923 .unwrap()
1924 .keys()
1925 .map(String::as_str)
1926 .collect();
1927 names.sort();
1928 assert_eq!(
1929 names,
1930 ["agent_id", "signature", "target", "timestamp", "value"]
1931 );
1932 let mut required: Vec<&str> = schema["required"]
1933 .as_array()
1934 .unwrap()
1935 .iter()
1936 .filter_map(|v| v.as_str())
1937 .collect();
1938 required.sort();
1939 assert_eq!(required, names);
1940 assert_eq!(schema["additionalProperties"], false);
1941 let rendered = schema.to_string();
1942 assert!(!rendered.contains("$ref"), "{rendered}");
1943
1944 let schema =
1945 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
1946 .unwrap();
1947 assert!(schema["properties"].get("agent_id").is_none());
1948 assert_eq!(schema["additionalProperties"], false);
1949 }
1950
1951 #[cfg(feature = "schemars")]
1954 #[test]
1955 fn operation_input_schemas_forbid_additional_properties() {
1956 for (name, schema) in [
1957 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
1958 ("SearchInput", schemars::schema_for!(SearchInput)),
1959 (
1960 "GetGovernanceLogInput",
1961 schemars::schema_for!(GetGovernanceLogInput),
1962 ),
1963 (
1964 "GetProposalsInput",
1965 schemars::schema_for!(GetProposalsInput),
1966 ),
1967 (
1968 "GetDashboardInput",
1969 schemars::schema_for!(GetDashboardInput),
1970 ),
1971 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
1972 (
1973 "CreatePostPayload",
1974 schemars::schema_for!(CreatePostPayload),
1975 ),
1976 (
1977 "UpdateProfilePayload",
1978 schemars::schema_for!(UpdateProfilePayload),
1979 ),
1980 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
1981 (
1982 "SearchGovernanceLogInput",
1983 schemars::schema_for!(SearchGovernanceLogInput),
1984 ),
1985 (
1986 "CommentRepliesQuery",
1987 schemars::schema_for!(CommentRepliesQuery),
1988 ),
1989 (
1990 "SignedRequest<GetMyModerationRecordInput>",
1991 schemars::schema_for!(
1992 SignedRequest<GetMyModerationRecordInput>
1993 ),
1994 ),
1995 (
1996 "SignedRequest<NoParams>",
1997 schemars::schema_for!(SignedRequest<NoParams>),
1998 ),
1999 (
2000 "SignedRequest<ReportMessageBody>",
2001 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2002 ),
2003 (
2004 "GetDashboardRequest",
2005 schemars::schema_for!(GetDashboardRequest),
2006 ),
2007 ] {
2008 let schema = serde_json::to_value(&schema).unwrap();
2009 assert_eq!(
2010 schema["additionalProperties"], false,
2011 "{name}: {schema}"
2012 );
2013 }
2014 }
2015
2016 #[cfg(feature = "schemars")]
2019 #[test]
2020 fn update_profile_docs_state_the_enforced_limits() {
2021 let schema =
2022 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
2023 .unwrap();
2024 for (field, max) in [
2025 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
2026 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
2027 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
2028 ] {
2029 let desc = schema["properties"][field]["description"]
2030 .as_str()
2031 .unwrap_or_default();
2032 assert!(
2033 desc.contains(&format!("at most {max} ")),
2034 "{field}: {desc}"
2035 );
2036 }
2037 }
2038}