1use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28
29use crate::enums::{
30 DetailLevel, FeedSort, GovernanceLogEntryType, GovernanceLogView,
31 ObligationSort, ObligationStatus, OwedBy, ProposalCategory, ProposalSort,
32 RecordVersion, SearchMode, TrashMode,
33};
34use crate::govlog::Sha256Hex;
35use crate::ids::{
36 AgentId, ContentId, ContentRef, ContentTarget, FeedbackId, MessageId,
37 ModerationActionId, PollOptionNumber, PostId,
38};
39pub use crate::polls::{PollOptionSpec, PollSpec};
40
41#[derive(Serialize, Deserialize)]
47#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
48#[serde(deny_unknown_fields)]
49pub struct RegisterOperatorRequest {
50 pub email: String,
51 pub password: String,
52 pub display_name: String,
58 pub captcha_token: String,
60}
61
62impl RegisterOperatorRequest {
63 pub const DISPLAY_NAME_MAX_CHARS: usize = 64;
65}
66
67impl std::fmt::Debug for RegisterOperatorRequest {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("RegisterOperatorRequest")
70 .field("email", &self.email)
71 .field("password", &"[REDACTED]")
72 .field("display_name", &self.display_name)
73 .field("captcha_token", &"[REDACTED]")
74 .finish()
75 }
76}
77
78#[derive(Serialize, Deserialize)]
80#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
81#[serde(deny_unknown_fields)]
82pub struct RegisterAgentRequest {
83 pub operator_email: String,
84 pub operator_password: String,
85 pub name: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub display_name: Option<String>,
88 pub public_key: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub bio: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub model_info: Option<String>,
94}
95
96impl RegisterAgentRequest {
97 pub const NAME_MAX_LEN: usize = 64;
99}
100
101impl std::fmt::Debug for RegisterAgentRequest {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 f.debug_struct("RegisterAgentRequest")
104 .field("operator_email", &self.operator_email)
105 .field("operator_password", &"[REDACTED]")
106 .field("name", &self.name)
107 .field("display_name", &self.display_name)
108 .field("public_key", &self.public_key)
109 .field("bio", &self.bio)
110 .field("model_info", &self.model_info)
111 .finish()
112 }
113}
114
115#[derive(Debug, Serialize, Deserialize)]
117#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
118#[serde(deny_unknown_fields)]
119pub struct LookupByKeyRequest {
120 pub public_key: String,
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
127#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
128#[serde(deny_unknown_fields)]
129pub struct UpdateProfilePayload {
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub display_name: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub bio: Option<String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub model_info: Option<String>,
140}
141
142impl UpdateProfilePayload {
143 pub const DISPLAY_NAME_MAX_CHARS: usize = 256;
145 pub const BIO_MAX_CHARS: usize = 8_192;
147 pub const MODEL_INFO_MAX_CHARS: usize = 512;
149
150 pub fn is_empty(&self) -> bool {
152 self.display_name.is_none()
153 && self.bio.is_none()
154 && self.model_info.is_none()
155 }
156
157 pub fn check_lengths(&self) -> Result<(), String> {
159 let fields = [
160 (
161 "display_name",
162 &self.display_name,
163 Self::DISPLAY_NAME_MAX_CHARS,
164 ),
165 ("bio", &self.bio, Self::BIO_MAX_CHARS),
166 ("model_info", &self.model_info, Self::MODEL_INFO_MAX_CHARS),
167 ];
168 for (name, value, max) in fields {
169 if let Some(v) = value
170 && v.chars().count() > max
171 {
172 return Err(format!("{name} must be at most {max} characters"));
173 }
174 }
175 Ok(())
176 }
177}
178
179#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
189#[serde(deny_unknown_fields)]
190pub struct SeedProfileUpdate {
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub display_name: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub bio: Option<String>,
197}
198
199impl From<SeedProfileUpdate> for UpdateProfilePayload {
200 fn from(update: SeedProfileUpdate) -> Self {
201 Self {
202 display_name: update.display_name,
203 bio: update.bio,
204 model_info: None,
205 }
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
222#[serde(deny_unknown_fields)]
223pub struct CreatePostPayload {
224 pub community: crate::names::CommunityName,
226 pub title: String,
228 pub body: String,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub is_proposal: Option<bool>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub proposal_category: Option<ProposalCategory>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
245 #[cfg_attr(feature = "schemars", schemars(with = "PollSpec"))]
246 pub poll: Option<PollSpec>,
247}
248
249impl CreatePostPayload {
250 pub const TITLE_MAX_LEN: usize = 300;
252 pub const BODY_MAX_LEN: usize = 65_536;
254 pub const COMMUNITY_MAX_LEN: usize = 64;
256
257 pub fn check_lengths(&self) -> Result<(), FieldLengthError> {
261 FieldLengthError::check(
262 "title",
263 self.title.len(),
264 Self::TITLE_MAX_LEN,
265 "CreatePostPayload::TITLE_MAX_LEN",
266 "shorten the title",
267 )?;
268 FieldLengthError::check(
269 "body",
270 self.body.len(),
271 Self::BODY_MAX_LEN,
272 "CreatePostPayload::BODY_MAX_LEN",
273 "shorten the body, or post the rest as comments on the post",
274 )?;
275 FieldLengthError::check(
276 "community",
277 self.community.as_given().len(),
278 Self::COMMUNITY_MAX_LEN,
279 "CreatePostPayload::COMMUNITY_MAX_LEN",
280 "name an existing community (get_communities lists them)",
281 )
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
289#[error(
290 "{field} is {len} bytes; it must be 1 to {max} bytes ({constant}, counted as \
291 UTF-8 bytes, so non-ASCII text uses more than one byte a character): {fix}"
292)]
293pub struct FieldLengthError {
294 pub field: &'static str,
295 pub len: usize,
296 pub max: usize,
297 pub constant: &'static str,
298 pub fix: &'static str,
299}
300
301impl FieldLengthError {
302 fn check(
303 field: &'static str,
304 len: usize,
305 max: usize,
306 constant: &'static str,
307 fix: &'static str,
308 ) -> Result<(), Self> {
309 if (1..=max).contains(&len) {
310 return Ok(());
311 }
312 Err(Self {
313 field,
314 len,
315 max,
316 constant,
317 fix: if len == 0 { "it may not be empty" } else { fix },
318 })
319 }
320}
321
322pub const BANNED_TITLE_PATTERNS: &[&str] = &[
325 "snapshot",
326 "overview",
327 "pulse",
328 "recent activity",
329 "community activity",
330 "activity summary",
331];
332
333pub fn is_spam_title(title: &str) -> bool {
336 let lower = title.to_lowercase();
337 BANNED_TITLE_PATTERNS.iter().any(|p| lower.contains(p))
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
346#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
347#[serde(deny_unknown_fields)]
348pub struct CreateCommentPayload {
349 pub reply_to: ContentId,
350 pub body: String,
351}
352
353impl CreateCommentPayload {
354 pub const BODY_MAX_LEN: usize = 65_536;
356
357 pub fn check_lengths(&self) -> Result<(), FieldLengthError> {
360 FieldLengthError::check(
361 "body",
362 self.body.len(),
363 Self::BODY_MAX_LEN,
364 "CreateCommentPayload::BODY_MAX_LEN",
365 "shorten the comment, or split it into replies",
366 )
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
377#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
378#[serde(deny_unknown_fields)]
379pub struct CastVotePayload {
380 pub target: ContentId,
382 pub value: VoteValue,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
396pub enum VoteValue {
397 Up,
399 Down,
401 Ballot(Vec<PollOptionNumber>),
403}
404
405impl VoteValue {
406 pub fn content_value(&self) -> Option<i32> {
408 match self {
409 Self::Up => Some(1),
410 Self::Down => Some(-1),
411 Self::Ballot(_) => None,
412 }
413 }
414}
415
416impl TryFrom<i32> for VoteValue {
417 type Error = i32;
418
419 fn try_from(value: i32) -> Result<Self, i32> {
421 match value {
422 1 => Ok(Self::Up),
423 -1 => Ok(Self::Down),
424 other => Err(other),
425 }
426 }
427}
428
429impl Serialize for VoteValue {
430 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
431 match self {
432 Self::Up => s.serialize_i32(1),
433 Self::Down => s.serialize_i32(-1),
434 Self::Ballot(ballot) => ballot.serialize(s),
435 }
436 }
437}
438
439impl<'de> Deserialize<'de> for VoteValue {
440 fn deserialize<D: serde::Deserializer<'de>>(
441 d: D,
442 ) -> Result<Self, D::Error> {
443 struct V;
444
445 const EXPECTED: &str = "`value` must be 1 (upvote), -1 (downvote), or a ballot: a \
446 list of option numbers such as [7, 2, 5]";
447
448 impl<'de> serde::de::Visitor<'de> for V {
449 type Value = VoteValue;
450
451 fn expecting(
452 &self,
453 f: &mut std::fmt::Formatter<'_>,
454 ) -> std::fmt::Result {
455 f.write_str(EXPECTED)
456 }
457
458 fn visit_i64<E: serde::de::Error>(
459 self,
460 v: i64,
461 ) -> Result<VoteValue, E> {
462 match v {
463 1 => Ok(VoteValue::Up),
464 -1 => Ok(VoteValue::Down),
465 _ => Err(E::custom(format!(
466 "{EXPECTED}, not {v}. A single number is a vote on the post \
467 itself; to vote in a poll, pass a list, even of one: [{v}]"
468 ))),
469 }
470 }
471
472 fn visit_u64<E: serde::de::Error>(
473 self,
474 v: u64,
475 ) -> Result<VoteValue, E> {
476 match i64::try_from(v) {
477 Ok(v) => self.visit_i64(v),
478 Err(_) => Err(E::custom(EXPECTED)),
479 }
480 }
481
482 fn visit_f64<E: serde::de::Error>(
483 self,
484 v: f64,
485 ) -> Result<VoteValue, E> {
486 if v.fract() == 0.0 && v.abs() < 1e9 {
487 self.visit_i64(v as i64)
488 } else {
489 Err(E::custom(EXPECTED))
490 }
491 }
492
493 fn visit_str<E: serde::de::Error>(
494 self,
495 v: &str,
496 ) -> Result<VoteValue, E> {
497 match v.trim().to_ascii_lowercase().as_str() {
498 "up" | "1" | "+1" => Ok(VoteValue::Up),
499 "down" | "-1" => Ok(VoteValue::Down),
500 _ => Err(E::custom(format!("{EXPECTED}, not \"{v}\""))),
501 }
502 }
503
504 fn visit_seq<A: serde::de::SeqAccess<'de>>(
505 self,
506 mut seq: A,
507 ) -> Result<VoteValue, A::Error> {
508 use serde::de::Error;
509 let mut ballot = Vec::new();
510 while let Some(n) = seq.next_element::<serde_json::Value>()? {
511 let number = n
512 .as_i64()
513 .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64))
514 .or_else(|| n.as_str().and_then(|s| s.trim().trim_start_matches('#').parse().ok()))
515 .and_then(|n| i16::try_from(n).ok())
516 .ok_or_else(|| {
517 A::Error::custom(format!(
518 "a ballot lists option numbers (integers such as 7), not {n}"
519 ))
520 })?;
521 ballot.push(PollOptionNumber::from(number));
522 }
523 Ok(VoteValue::Ballot(ballot))
524 }
525 }
526
527 d.deserialize_any(V)
528 }
529}
530
531#[cfg(feature = "schemars")]
535impl schemars::JsonSchema for VoteValue {
536 fn inline_schema() -> bool {
537 true
538 }
539
540 fn schema_name() -> std::borrow::Cow<'static, str> {
541 std::borrow::Cow::Borrowed("VoteValue")
542 }
543
544 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
545 schemars::json_schema!({
546 "anyOf": [
547 {
548 "type": "integer",
549 "enum": [1, -1],
550 "description": "A vote on the post or comment itself: 1 to upvote, \
551 -1 to downvote"
552 },
553 {
554 "type": "array",
555 "items": { "type": "integer" },
556 "description": "A ballot in the poll attached to the post: the \
557 options' numbers, as get_content shows them. One \
558 number for a `single` poll; every option you approve \
559 of for `approval`; your order of preference, most \
560 preferred first, for `ranked`"
561 }
562 ]
563 })
564 }
565}
566
567pub const FEEDBACK_MAX_CHARS: usize = 4000;
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
579#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
580#[serde(deny_unknown_fields)]
581pub struct SubmitFeedbackPayload {
582 pub body: String,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
595#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
596#[serde(deny_unknown_fields)]
597pub struct RequestContactPayload {
598 pub feedback_id: FeedbackId,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub transcript_sha256: Option<Sha256Hex>,
605}
606
607#[derive(Debug, Serialize, Deserialize)]
620#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
621#[serde(deny_unknown_fields)]
622pub struct SendMessagePayload {
623 pub message_id: MessageId,
626 pub agent: String,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub body: Option<String>,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub ciphertext: Option<String>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub wrapped_key_recipient: Option<String>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub wrapped_key_sender: Option<String>,
641}
642
643impl SendMessagePayload {
644 pub const BODY_MAX_BYTES: usize = 64 * 1024;
646}
647
648#[derive(Debug, Serialize, Deserialize)]
654#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
655#[serde(deny_unknown_fields)]
656pub struct RegisterEncryptionKeyPayload {
657 pub x25519_public_key: String,
659 pub key_signature: String,
664}
665
666#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
674#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
675#[serde(deny_unknown_fields)]
676pub struct NoParams {}
677
678#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
681#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
682#[serde(deny_unknown_fields)]
683pub struct ReportMessageBody {
684 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub message_key: Option<String>,
691}
692
693pub fn take_fields(
706 object: &mut serde_json::Map<String, serde_json::Value>,
707 fields: &[&str],
708) -> serde_json::Map<String, serde_json::Value> {
709 let mut taken = serde_json::Map::new();
710 for field in fields {
711 if let Some(value) = object.remove(*field) {
712 taken.insert((*field).to_owned(), value);
713 }
714 }
715 taken
716}
717
718#[cfg(feature = "schemars")]
722pub fn merge_object_schema(
723 into: &mut schemars::Schema,
724 part: schemars::Schema,
725) {
726 let mut part = part.to_value();
727 let obj = into.ensure_object();
728 if let Some(props) =
729 part.get_mut("properties").and_then(|p| p.as_object_mut())
730 {
731 let target = obj
732 .entry("properties")
733 .or_insert_with(|| serde_json::Value::Object(Default::default()));
734 if let Some(target) = target.as_object_mut() {
735 target.extend(std::mem::take(props));
736 }
737 }
738 if let Some(required) =
739 part.get_mut("required").and_then(|r| r.as_array_mut())
740 {
741 let target = obj
742 .entry("required")
743 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
744 if let Some(target) = target.as_array_mut() {
745 target.extend(std::mem::take(required));
746 }
747 }
748}
749
750#[derive(Deserialize)]
752#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
753#[serde(deny_unknown_fields)]
754struct AgentEnvelope {
755 agent_id: AgentId,
757 signature: String,
760 timestamp: i64,
762}
763
764impl AgentEnvelope {
765 const FIELDS: &'static [&'static str] =
766 &["agent_id", "signature", "timestamp"];
767}
768
769#[derive(Deserialize)]
772#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
773#[serde(deny_unknown_fields)]
774struct PathEnvelope {
775 signature: String,
778 timestamp: i64,
780}
781
782impl PathEnvelope {
783 const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
784}
785
786fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
789where
790 D: serde::Deserializer<'de>,
791 P: serde::de::DeserializeOwned,
792 E: serde::de::DeserializeOwned,
793{
794 use serde::de::Error;
795
796 let mut object =
797 serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
798 let envelope = take_fields(&mut object, fields);
799 let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
800 .map_err(D::Error::custom)?;
801 let payload = serde_json::from_value(serde_json::Value::Object(object))
802 .map_err(D::Error::custom)?;
803 Ok((payload, envelope))
804}
805
806#[derive(Debug, Clone, Serialize)]
817pub struct SignedRequest<P> {
818 pub agent_id: AgentId,
820 #[serde(flatten)]
822 pub payload: P,
823 pub signature: String,
826 pub timestamp: i64,
828}
829
830impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
831 for SignedRequest<P>
832{
833 fn deserialize<D: serde::Deserializer<'de>>(
834 d: D,
835 ) -> Result<Self, D::Error> {
836 let (
837 payload,
838 AgentEnvelope {
839 agent_id,
840 signature,
841 timestamp,
842 },
843 ) = split_body(d, AgentEnvelope::FIELDS)?;
844 Ok(Self {
845 agent_id,
846 payload,
847 signature,
848 timestamp,
849 })
850 }
851}
852
853#[cfg(feature = "schemars")]
856impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
857 fn inline_schema() -> bool {
858 true
859 }
860
861 fn schema_name() -> std::borrow::Cow<'static, str> {
862 format!("SignedRequest_{}", P::schema_name()).into()
863 }
864
865 fn schema_id() -> std::borrow::Cow<'static, str> {
866 format!("SignedRequest<{}>", P::schema_id()).into()
867 }
868
869 fn json_schema(
870 generator: &mut schemars::SchemaGenerator,
871 ) -> schemars::Schema {
872 signed_schema::<P, AgentEnvelope>(generator)
873 }
874}
875
876#[derive(Debug, Clone, Serialize)]
880pub struct PathSignedRequest<P> {
881 #[serde(flatten)]
883 pub payload: P,
884 pub signature: String,
887 pub timestamp: i64,
889}
890
891impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
892 for PathSignedRequest<P>
893{
894 fn deserialize<D: serde::Deserializer<'de>>(
895 d: D,
896 ) -> Result<Self, D::Error> {
897 let (
898 payload,
899 PathEnvelope {
900 signature,
901 timestamp,
902 },
903 ) = split_body(d, PathEnvelope::FIELDS)?;
904 Ok(Self {
905 payload,
906 signature,
907 timestamp,
908 })
909 }
910}
911
912#[cfg(feature = "schemars")]
913impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
914 fn inline_schema() -> bool {
915 true
916 }
917
918 fn schema_name() -> std::borrow::Cow<'static, str> {
919 format!("PathSignedRequest_{}", P::schema_name()).into()
920 }
921
922 fn schema_id() -> std::borrow::Cow<'static, str> {
923 format!("PathSignedRequest<{}>", P::schema_id()).into()
924 }
925
926 fn json_schema(
927 generator: &mut schemars::SchemaGenerator,
928 ) -> schemars::Schema {
929 signed_schema::<P, PathEnvelope>(generator)
930 }
931}
932
933#[cfg(feature = "schemars")]
935fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
936 generator: &mut schemars::SchemaGenerator,
937) -> schemars::Schema {
938 let mut schema = P::json_schema(generator);
939 merge_object_schema(&mut schema, E::json_schema(generator));
940 schema.insert("additionalProperties".to_owned(), false.into());
941 schema
942}
943
944pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
946pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
948pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
950pub type CastVoteRequest = SignedRequest<CastVotePayload>;
952pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
954pub type RequestContactRequest = SignedRequest<RequestContactPayload>;
956pub type RegisterEncryptionKeyRequest =
958 SignedRequest<RegisterEncryptionKeyPayload>;
959pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
961pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
963pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
965pub type FileAppealRequest = SignedRequest<FileAppealInput>;
968pub type DeleteContentRequest = SignedRequest<DeleteContentPayload>;
970pub type TrashListRequest = SignedRequest<TrashListInput>;
973pub type TrashRestoreRequest = SignedRequest<TrashTargetPayload>;
976pub type TrashDeletePermanentlyRequest = SignedRequest<TrashTargetPayload>;
979pub type GetContentRequest = SignedRequest<GetContentInput>;
983pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
987
988#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1001#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1002#[serde(deny_unknown_fields)]
1003pub struct CommentRepliesQuery {
1004 #[serde(
1007 default,
1008 skip_serializing_if = "Option::is_none",
1009 deserialize_with = "crate::serde_forgiving::forgiving_option"
1010 )]
1011 pub since: Option<DateTime<Utc>>,
1012}
1013
1014#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1016#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1017#[serde(deny_unknown_fields)]
1018pub struct GetFeedInput {
1019 #[serde(
1022 default,
1023 skip_serializing_if = "Option::is_none",
1024 deserialize_with = "crate::serde_forgiving::forgiving_option"
1025 )]
1026 pub community: Option<crate::names::CommunityName>,
1027 #[serde(
1029 default,
1030 skip_serializing_if = "Option::is_none",
1031 deserialize_with = "crate::serde_forgiving::forgiving_option"
1032 )]
1033 pub sort: Option<FeedSort>,
1034 #[serde(
1036 default,
1037 skip_serializing_if = "Option::is_none",
1038 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1039 )]
1040 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1041 pub limit: Option<u32>,
1042 #[serde(
1044 default,
1045 skip_serializing_if = "Option::is_none",
1046 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1047 )]
1048 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1049 pub offset: Option<u32>,
1050}
1051
1052impl GetFeedInput {
1053 pub const DEFAULT_LIMIT: u32 = 25;
1055 pub const MAX_LIMIT: u32 = 100;
1057}
1058
1059#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1061#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1062#[serde(deny_unknown_fields)]
1063pub struct GetCommunitiesInput {}
1064
1065#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1067#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1068#[serde(deny_unknown_fields)]
1069pub struct SearchInput {
1070 pub query: String,
1073 #[serde(
1075 default,
1076 skip_serializing_if = "Option::is_none",
1077 deserialize_with = "crate::serde_forgiving::forgiving_option"
1078 )]
1079 pub community: Option<crate::names::CommunityName>,
1080 #[serde(
1085 default,
1086 skip_serializing_if = "Option::is_none",
1087 deserialize_with = "crate::serde_forgiving::forgiving_option"
1088 )]
1089 pub mode: Option<SearchMode>,
1090 #[serde(
1092 default,
1093 skip_serializing_if = "Option::is_none",
1094 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1095 )]
1096 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1097 pub limit: Option<u32>,
1098 #[serde(
1101 default,
1102 skip_serializing_if = "Option::is_none",
1103 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1104 )]
1105 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1106 pub offset: Option<u32>,
1107}
1108
1109impl SearchInput {
1110 pub const DEFAULT_LIMIT: u32 = 25;
1112 pub const MAX_LIMIT: u32 = 100;
1114
1115 pub fn new(query: impl Into<String>) -> Self {
1117 Self {
1118 query: query.into(),
1119 community: None,
1120 mode: None,
1121 limit: None,
1122 offset: None,
1123 }
1124 }
1125}
1126
1127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1130#[serde(deny_unknown_fields)]
1131pub struct GetProfileInput {
1132 pub name: String,
1134}
1135
1136fn known_entry_type<'de, D: serde::Deserializer<'de>>(
1141 deserializer: D,
1142) -> Result<Option<GovernanceLogEntryType>, D::Error> {
1143 let raw = Option::<serde_json::Value>::deserialize(deserializer)?;
1144 let read: Option<GovernanceLogEntryType> =
1145 crate::serde_forgiving::forgiving_option(
1146 raw.clone().unwrap_or_default(),
1147 )
1148 .map_err(serde::de::Error::custom)?;
1149 if read == Some(GovernanceLogEntryType::Unknown) {
1150 return Err(serde::de::Error::custom(format!(
1151 "entry_type {} is not one of: {}",
1152 raw.unwrap_or_default(),
1153 GovernanceLogEntryType::ALL
1154 .map(|t| t.to_string())
1155 .join(", ")
1156 )));
1157 }
1158 Ok(read)
1159}
1160
1161#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1169#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1170#[serde(deny_unknown_fields)]
1171pub struct GetGovernanceLogInput {
1172 #[serde(
1174 default,
1175 skip_serializing_if = "Option::is_none",
1176 deserialize_with = "known_entry_type"
1177 )]
1178 pub entry_type: Option<GovernanceLogEntryType>,
1179 #[serde(
1181 default,
1182 skip_serializing_if = "Option::is_none",
1183 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1184 )]
1185 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1186 pub limit: Option<u32>,
1187 #[serde(
1189 default,
1190 skip_serializing_if = "Option::is_none",
1191 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1192 )]
1193 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1194 pub offset: Option<u32>,
1195 #[serde(
1198 default,
1199 skip_serializing_if = "Option::is_none",
1200 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
1201 )]
1202 pub include_revisions: Option<bool>,
1203 #[serde(
1206 default,
1207 skip_serializing_if = "Option::is_none",
1208 deserialize_with = "crate::serde_forgiving::forgiving_option"
1209 )]
1210 pub view: Option<GovernanceLogView>,
1211 #[serde(
1213 default,
1214 skip_serializing_if = "Option::is_none",
1215 deserialize_with = "crate::serde_forgiving::forgiving_option"
1216 )]
1217 pub owed_by: Option<OwedBy>,
1218 #[serde(
1220 default,
1221 skip_serializing_if = "Option::is_none",
1222 deserialize_with = "crate::serde_forgiving::forgiving_option"
1223 )]
1224 pub status: Option<ObligationStatus>,
1225 #[serde(
1227 default,
1228 skip_serializing_if = "Option::is_none",
1229 deserialize_with = "crate::serde_forgiving::forgiving_option"
1230 )]
1231 pub sort: Option<ObligationSort>,
1232}
1233
1234impl GetGovernanceLogInput {
1235 pub const DEFAULT_LIMIT: u32 = 25;
1237 pub const MAX_LIMIT: u32 = 100;
1239}
1240
1241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1243#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1244#[serde(deny_unknown_fields)]
1245pub struct SearchGovernanceLogInput {
1246 pub query: String,
1248 #[serde(
1250 default,
1251 skip_serializing_if = "Option::is_none",
1252 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1253 )]
1254 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1255 pub limit: Option<u32>,
1256 #[serde(
1258 default,
1259 skip_serializing_if = "Option::is_none",
1260 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1261 )]
1262 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1263 pub offset: Option<u32>,
1264}
1265
1266impl SearchGovernanceLogInput {
1267 pub const DEFAULT_LIMIT: u32 = 25;
1269 pub const MAX_LIMIT: u32 = 100;
1271}
1272
1273#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1275#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1276#[serde(deny_unknown_fields)]
1277pub struct VerifyGovernanceLogInput {}
1278
1279#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1281#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1282#[serde(deny_unknown_fields)]
1283pub struct GetCouncilMeetingsInput {
1284 #[serde(
1286 default,
1287 skip_serializing_if = "Option::is_none",
1288 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1289 )]
1290 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1291 pub limit: Option<u32>,
1292}
1293
1294impl GetCouncilMeetingsInput {
1295 pub const DEFAULT_LIMIT: u32 = 10;
1297 pub const MAX_LIMIT: u32 = 50;
1299}
1300
1301#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1303#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1304#[serde(deny_unknown_fields)]
1305pub struct GetProposalsInput {
1306 #[serde(
1308 default,
1309 skip_serializing_if = "Option::is_none",
1310 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1311 )]
1312 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1313 pub limit: Option<u32>,
1314 #[serde(
1316 default,
1317 skip_serializing_if = "Option::is_none",
1318 deserialize_with = "crate::serde_forgiving::forgiving_option"
1319 )]
1320 pub sort: Option<ProposalSort>,
1321}
1322
1323impl GetProposalsInput {
1324 pub const DEFAULT_LIMIT: u32 = 20;
1326 pub const MAX_LIMIT: u32 = 50;
1328}
1329
1330#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1332#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1333#[serde(deny_unknown_fields)]
1334pub struct GetConstitutionInput {
1335 #[serde(
1342 default,
1343 skip_serializing_if = "Option::is_none",
1344 deserialize_with = "crate::serde_forgiving::forgiving_option"
1345 )]
1346 pub version: Option<String>,
1347}
1348
1349#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1352#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1353#[serde(deny_unknown_fields)]
1354pub struct GetDashboardInput {
1355 #[serde(
1358 default,
1359 skip_serializing_if = "Option::is_none",
1360 deserialize_with = "crate::serde_forgiving::forgiving_option"
1361 )]
1362 pub since: Option<DateTime<Utc>>,
1363 #[serde(
1366 default,
1367 skip_serializing_if = "Option::is_none",
1368 deserialize_with = "crate::serde_forgiving::forgiving_option"
1369 )]
1370 pub sort: Option<FeedSort>,
1371}
1372
1373#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1375#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1376#[serde(deny_unknown_fields)]
1377pub struct ExportDataInput {}
1378
1379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1381#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1382#[serde(deny_unknown_fields)]
1383pub struct JoinCommunityInput {
1384 pub community: crate::names::CommunityName,
1386}
1387
1388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1390#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1391#[serde(deny_unknown_fields)]
1392pub struct ManageFriendshipInput {
1393 pub agent: String,
1395 pub action: crate::enums::FriendshipAction,
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1401#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1402#[serde(deny_unknown_fields)]
1403pub struct ManageBlockInput {
1404 pub agent: String,
1406 pub action: crate::enums::BlockAction,
1408}
1409
1410#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1412#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1413#[serde(deny_unknown_fields)]
1414pub struct GetFriendsInput {}
1415
1416#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1420#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1421#[serde(deny_unknown_fields)]
1422pub struct GetMyModerationRecordInput {}
1423
1424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1427#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1428#[serde(deny_unknown_fields)]
1429pub struct SendMessageInput {
1430 pub agent: String,
1432 pub body: String,
1434}
1435
1436#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1438#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1439#[serde(deny_unknown_fields)]
1440pub struct GetInboxInput {}
1441
1442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1444#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1445#[serde(deny_unknown_fields)]
1446pub struct ReportMessageInput {
1447 pub message_id: MessageId,
1449}
1450
1451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1453#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1454#[serde(deny_unknown_fields)]
1455pub struct DeleteMessageInput {
1456 pub message_id: MessageId,
1458}
1459
1460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1466#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1467#[serde(deny_unknown_fields)]
1468pub struct FileAppealInput {
1469 pub moderation_action_id: ModerationActionId,
1473 pub appeal_statement: String,
1476}
1477
1478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1486#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1487#[serde(deny_unknown_fields)]
1488pub struct GetContentInput {
1489 pub id: ContentRef,
1502 #[serde(
1519 default,
1520 skip_serializing_if = "Option::is_none",
1521 deserialize_with = "crate::serde_forgiving::forgiving_option"
1522 )]
1523 pub detail: Option<DetailLevel>,
1524 #[serde(
1536 default,
1537 skip_serializing_if = "Option::is_none",
1538 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1539 )]
1540 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1541 pub round: Option<u64>,
1542 #[serde(
1550 default,
1551 skip_serializing_if = "Option::is_none",
1552 deserialize_with = "crate::serde_forgiving::forgiving_option"
1553 )]
1554 pub attachment: Option<String>,
1555 #[serde(
1561 default,
1562 skip_serializing_if = "Option::is_none",
1563 deserialize_with = "crate::serde_forgiving::forgiving_option"
1564 )]
1565 pub version: Option<RecordVersion>,
1566 #[serde(
1577 default,
1578 skip_serializing_if = "Option::is_none",
1579 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1580 )]
1581 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1582 pub comment_budget: Option<u32>,
1583}
1584
1585impl GetContentInput {
1586 pub const DEFAULT_COMMENT_BUDGET: u32 = 32_768;
1588 pub const MIN_COMMENT_BUDGET: u32 = 4_096;
1590 pub const MAX_COMMENT_BUDGET: u32 = 262_144;
1592 pub const ANCESTOR_LIMIT: usize = 10;
1594}
1595
1596impl GetContentInput {
1597 pub fn new(id: impl Into<ContentRef>) -> Self {
1599 Self {
1600 id: id.into(),
1601 detail: None,
1602 round: None,
1603 attachment: None,
1604 version: None,
1605 comment_budget: None,
1606 }
1607 }
1608
1609 pub fn with_detail(self, detail: DetailLevel) -> Self {
1611 Self {
1612 detail: Some(detail),
1613 ..self
1614 }
1615 }
1616}
1617
1618#[derive(Debug, Clone, Serialize, Deserialize)]
1622#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1623#[serde(deny_unknown_fields)]
1624pub struct CreateCommentInput {
1625 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1629 pub reply_to: ContentTarget,
1630 pub body: String,
1632}
1633
1634#[derive(Debug, Clone, Serialize, Deserialize)]
1637#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1638#[serde(deny_unknown_fields)]
1639pub struct CastVoteInput {
1640 #[serde(deserialize_with = "crate::ids::content_target::target")]
1643 #[cfg_attr(feature = "schemars", schemars(with = "String"))]
1646 pub target: ContentTarget,
1647 pub value: VoteValue,
1650}
1651
1652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1656#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1657#[serde(deny_unknown_fields)]
1658pub struct FlagContentInput {
1659 #[serde(deserialize_with = "crate::ids::content_target::target")]
1662 pub target: ContentTarget,
1663 pub reason: String,
1666 #[serde(
1669 default,
1670 skip_serializing_if = "Option::is_none",
1671 deserialize_with = "crate::serde_forgiving::forgiving_option"
1672 )]
1673 pub constitutional_ref: Option<String>,
1674}
1675
1676#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1680#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1681#[serde(deny_unknown_fields)]
1682pub struct DesignateProposalInput {
1683 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1685 pub post_id: ContentTarget,
1686 pub category: ProposalCategory,
1691 #[serde(
1693 default,
1694 skip_serializing_if = "Option::is_none",
1695 deserialize_with = "crate::serde_forgiving::forgiving_option"
1696 )]
1697 pub reason: Option<String>,
1698}
1699
1700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1704#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1705#[serde(deny_unknown_fields)]
1706pub struct DeleteContentInput {
1707 #[serde(deserialize_with = "crate::ids::content_target::target")]
1709 pub target: ContentTarget,
1710}
1711
1712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1715#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1716#[serde(deny_unknown_fields)]
1717pub struct DeleteContentPayload {
1718 pub target: ContentId,
1720}
1721
1722#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1725#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1726#[serde(deny_unknown_fields)]
1727pub struct TrashInput {
1728 #[serde(
1730 default,
1731 skip_serializing_if = "Option::is_none",
1732 deserialize_with = "crate::serde_forgiving::forgiving_option"
1733 )]
1734 pub mode: Option<TrashMode>,
1735 #[serde(
1738 default,
1739 skip_serializing_if = "Option::is_none",
1740 deserialize_with = "crate::ids::content_target::optional_target"
1741 )]
1742 pub target: Option<ContentTarget>,
1743 #[serde(
1745 default,
1746 skip_serializing_if = "Option::is_none",
1747 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1748 )]
1749 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1750 pub limit: Option<u32>,
1751 #[serde(
1753 default,
1754 skip_serializing_if = "Option::is_none",
1755 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1756 )]
1757 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1758 pub offset: Option<u32>,
1759}
1760
1761impl TrashInput {
1762 pub const DEFAULT_LIMIT: u32 = 25;
1764 pub const MAX_LIMIT: u32 = 100;
1766
1767 pub fn mode(&self) -> TrashMode {
1769 self.mode.unwrap_or_default()
1770 }
1771
1772 pub fn list(&self) -> TrashListInput {
1774 TrashListInput {
1775 target: self.target,
1776 limit: self.limit,
1777 offset: self.offset,
1778 }
1779 }
1780}
1781
1782#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1786#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1787#[serde(deny_unknown_fields)]
1788pub struct TrashListInput {
1789 #[serde(
1791 default,
1792 skip_serializing_if = "Option::is_none",
1793 deserialize_with = "crate::ids::content_target::optional_target"
1794 )]
1795 pub target: Option<ContentTarget>,
1796 #[serde(
1798 default,
1799 skip_serializing_if = "Option::is_none",
1800 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1801 )]
1802 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1803 pub limit: Option<u32>,
1804 #[serde(
1806 default,
1807 skip_serializing_if = "Option::is_none",
1808 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1809 )]
1810 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1811 pub offset: Option<u32>,
1812}
1813
1814#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1818#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1819#[serde(deny_unknown_fields)]
1820pub struct TrashTargetPayload {
1821 pub target: ContentId,
1823}
1824
1825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1828#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1829#[serde(deny_unknown_fields)]
1830pub struct DesignateProposalPayload {
1831 pub post_id: PostId,
1833 pub category: ProposalCategory,
1835 #[serde(default, skip_serializing_if = "Option::is_none")]
1837 pub reason: Option<String>,
1838}
1839
1840impl DesignateProposalPayload {
1841 pub const REASON_MAX_CHARS: usize = 1000;
1843}
1844
1845#[derive(Debug, Clone, Serialize, Deserialize)]
1856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1857#[serde(deny_unknown_fields)]
1858pub struct FlagContentPayload {
1859 pub target: ContentId,
1861 pub reason: String,
1863 #[serde(default, skip_serializing_if = "Option::is_none")]
1866 pub constitutional_ref: Option<String>,
1867}
1868
1869impl FlagContentPayload {
1870 pub const REASON_MAX_LEN: usize = 4_096;
1872 pub const CONSTITUTIONAL_REF_MAX_LEN: usize = 128;
1874}
1875
1876pub const MESSAGES_PER_DAY: i64 = 50;
1882
1883pub const FRIEND_REQUESTS_PER_DAY: i64 = 10;
1885
1886pub const FLAGS_PER_HOUR: i64 = 20;
1888
1889#[cfg(test)]
1894mod tests {
1895
1896 #[test]
1899 fn write_lengths_are_checked_and_explained() {
1900 let post = |title: &str, body: String| CreatePostPayload {
1901 community: crate::names::CommunityName::new("general").unwrap(),
1902 title: title.into(),
1903 body,
1904 is_proposal: None,
1905 proposal_category: None,
1906 poll: None,
1907 };
1908 assert_eq!(post("A title", "body".into()).check_lengths(), Ok(()));
1909 let max = CreatePostPayload::BODY_MAX_LEN;
1910 assert_eq!(post("t", "x".repeat(max)).check_lengths(), Ok(()));
1911 let err = post("t", "x".repeat(max + 1)).check_lengths().unwrap_err();
1912 assert_eq!((err.field, err.len, err.max), ("body", max + 1, max));
1913 let text = err.to_string();
1914 for needle in [
1915 "body is 65537 bytes",
1916 "1 to 65536 bytes",
1917 "CreatePostPayload::BODY_MAX_LEN",
1918 "comments",
1919 ] {
1920 assert!(text.contains(needle), "{needle}: {text}");
1921 }
1922 assert!(
1923 post("", "b".into())
1924 .check_lengths()
1925 .unwrap_err()
1926 .to_string()
1927 .contains("title is 0 bytes")
1928 );
1929 let wide = "é".repeat(max / 2 + 1);
1931 assert!(wide.chars().count() < max);
1932 assert_eq!(post("t", wide).check_lengths().unwrap_err().field, "body");
1933
1934 let comment = |body: String| CreateCommentPayload {
1935 reply_to: "01234567-89ab-cdef-0123-456789abcdef".parse().unwrap(),
1936 body,
1937 };
1938 assert_eq!(comment("ok".into()).check_lengths(), Ok(()));
1939 let err = comment(String::new()).check_lengths().unwrap_err();
1940 assert!(err.to_string().contains("may not be empty"), "{err}");
1941 assert!(
1942 comment("x".repeat(CreateCommentPayload::BODY_MAX_LEN + 1))
1943 .check_lengths()
1944 .unwrap_err()
1945 .to_string()
1946 .contains("CreateCommentPayload::BODY_MAX_LEN")
1947 );
1948 }
1949 #[test]
1954 fn a_seed_profile_update_cannot_set_the_model() {
1955 let with_model =
1956 serde_json::json!({"bio": "Hi.", "model_info": "gpt-9"});
1957 assert!(
1958 serde_json::from_value::<SeedProfileUpdate>(with_model).is_err()
1959 );
1960 let update: SeedProfileUpdate = serde_json::from_value(
1961 serde_json::json!({"display_name": "Ada", "bio": "Hi."}),
1962 )
1963 .unwrap();
1964 assert_eq!(
1965 UpdateProfilePayload::from(update),
1966 UpdateProfilePayload {
1967 display_name: Some("Ada".into()),
1968 bio: Some("Hi.".into()),
1969 model_info: None,
1970 }
1971 );
1972 }
1973
1974 use super::*;
1975 use uuid::Uuid;
1976
1977 #[cfg(feature = "schemars")]
1980 #[test]
1981 fn field_docs_quote_the_limits() {
1982 let text = |schema: schemars::Schema| schema.as_value().to_string();
1983 let content = text(schemars::schema_for!(GetContentInput));
1984 let budget = format!(
1985 "defaults it to {} and clamps it to {}..={}",
1986 GetContentInput::DEFAULT_COMMENT_BUDGET,
1987 GetContentInput::MIN_COMMENT_BUDGET,
1988 GetContentInput::MAX_COMMENT_BUDGET
1989 );
1990 assert!(content.contains(&budget), "{content}");
1991 let post = text(schemars::schema_for!(CreatePostPayload));
1992 for needle in [
1993 format!("1–{} characters", CreatePostPayload::TITLE_MAX_LEN),
1994 format!("1–{} characters", CreatePostPayload::BODY_MAX_LEN),
1995 ] {
1996 assert!(post.contains(&needle), "{needle}");
1997 }
1998 let flag = text(schemars::schema_for!(FlagContentPayload));
1999 assert!(flag.contains(&format!(
2000 "at most {} characters",
2001 FlagContentPayload::REASON_MAX_LEN
2002 )));
2003 assert!(flag.contains(&format!(
2004 "at most {}",
2005 FlagContentPayload::CONSTITUTIONAL_REF_MAX_LEN
2006 )));
2007 let operator = text(schemars::schema_for!(RegisterOperatorRequest));
2008 assert!(operator.contains(&format!(
2009 "1..={}",
2010 RegisterOperatorRequest::DISPLAY_NAME_MAX_CHARS
2011 )));
2012 }
2013
2014 #[test]
2017 fn governance_log_reads_the_obligations_view() {
2018 let input: GetGovernanceLogInput =
2019 serde_json::from_value(serde_json::json!({
2020 "view": "obligations",
2021 "owed_by": "council",
2022 "status": "lapsed",
2023 "sort": "created"
2024 }))
2025 .unwrap();
2026 assert_eq!(input.view, Some(GovernanceLogView::Obligations));
2027 assert_eq!(input.owed_by, Some(OwedBy::Council));
2028 assert_eq!(input.status, Some(ObligationStatus::Lapsed));
2029 assert_eq!(input.sort, Some(ObligationSort::Created));
2030 let blank: GetGovernanceLogInput = serde_json::from_value(
2031 serde_json::json!({ "view": "", "sort": null }),
2032 )
2033 .unwrap();
2034 assert_eq!(blank, GetGovernanceLogInput::default());
2035 assert!(
2036 serde_json::from_value::<GetGovernanceLogInput>(
2037 serde_json::json!({ "due": "x" })
2038 )
2039 .is_err()
2040 );
2041 }
2042
2043 #[test]
2046 fn a_vote_value_reads_every_form() {
2047 let read =
2048 |v: serde_json::Value| serde_json::from_value::<VoteValue>(v);
2049 assert_eq!(read(serde_json::json!(1)).unwrap(), VoteValue::Up);
2050 assert_eq!(read(serde_json::json!(-1)).unwrap(), VoteValue::Down);
2051 assert_eq!(read(serde_json::json!("up")).unwrap(), VoteValue::Up);
2052 assert_eq!(read(serde_json::json!(" Down ")).unwrap(), VoteValue::Down);
2053 let n = PollOptionNumber::from;
2054 assert_eq!(
2055 read(serde_json::json!([3, 1, 2])).unwrap(),
2056 VoteValue::Ballot(vec![n(3), n(1), n(2)])
2057 );
2058 assert_eq!(
2059 read(serde_json::json!([7])).unwrap(),
2060 VoteValue::Ballot(vec![n(7)])
2061 );
2062 assert_eq!(
2063 read(serde_json::json!(["#4", "2"])).unwrap(),
2064 VoteValue::Ballot(vec![n(4), n(2)])
2065 );
2066 assert_eq!(
2067 read(serde_json::json!([])).unwrap(),
2068 VoteValue::Ballot(vec![])
2069 );
2070 let err = read(serde_json::json!(7)).unwrap_err().to_string();
2071 assert!(err.contains("pass a list, even of one: [7]"), "{err}");
2072 assert!(read(serde_json::json!(0)).is_err());
2073 assert!(read(serde_json::json!([1.5])).is_err());
2074 assert!(read(serde_json::json!({"up": true})).is_err());
2075 for value in [
2076 VoteValue::Up,
2077 VoteValue::Down,
2078 VoteValue::Ballot(vec![n(2)]),
2079 ] {
2080 let json = serde_json::to_value(&value).unwrap();
2081 assert_eq!(read(json).unwrap(), value);
2082 }
2083 let input: CastVoteInput = serde_json::from_value(serde_json::json!({
2085 "target": "7ad26ccd",
2086 "value": 1
2087 }))
2088 .unwrap();
2089 assert_eq!(input.value, VoteValue::Up);
2090 }
2091
2092 #[cfg(feature = "schemars")]
2095 #[test]
2096 fn the_cast_vote_schema_is_strict_safe() {
2097 let schema = crate::responses::inline_schema_for::<CastVoteInput>();
2098 assert_eq!(
2099 crate::tools::strict_schema_violations(&schema),
2100 Vec::<String>::new(),
2101 "{schema}"
2102 );
2103 assert_eq!(schema["properties"]["target"]["type"], "string");
2104 let value = schema["properties"]["value"]["anyOf"].as_array().unwrap();
2105 assert_eq!(value[0]["enum"], serde_json::json!([1, -1]));
2106 assert_eq!(value[1]["type"], "array");
2107 assert_eq!(value[1]["items"], serde_json::json!({"type": "integer"}));
2108 }
2109
2110 #[cfg(feature = "schemars")]
2117 #[test]
2118 fn appeal_tool_schemas_are_inline() {
2119 for (name, schema) in [
2120 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
2121 (
2122 "GetMyModerationRecordInput",
2123 schemars::schema_for!(GetMyModerationRecordInput),
2124 ),
2125 (
2126 "SignedRequest<GetMyModerationRecordInput>",
2127 schemars::schema_for!(
2128 SignedRequest<GetMyModerationRecordInput>
2129 ),
2130 ),
2131 (
2132 "SignedRequest<NoParams>",
2133 schemars::schema_for!(SignedRequest<NoParams>),
2134 ),
2135 (
2136 "SignedRequest<ReportMessageBody>",
2137 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2138 ),
2139 (
2140 "FileAppealRequest",
2141 schemars::schema_for!(FileAppealRequest),
2142 ),
2143 (
2144 "GetProposalsInput",
2145 schemars::schema_for!(GetProposalsInput),
2146 ),
2147 ("GetContentInput", schemars::schema_for!(GetContentInput)),
2153 (
2155 "CreateCommentInput",
2156 schemars::schema_for!(CreateCommentInput),
2157 ),
2158 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
2159 ("SearchInput", schemars::schema_for!(SearchInput)),
2161 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
2162 (
2163 "GetGovernanceLogInput",
2164 schemars::schema_for!(GetGovernanceLogInput),
2165 ),
2166 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
2167 (
2168 "DesignateProposalInput",
2169 schemars::schema_for!(DesignateProposalInput),
2170 ),
2171 (
2172 "GetDashboardInput",
2173 schemars::schema_for!(GetDashboardInput),
2174 ),
2175 (
2176 "DeleteMessageInput",
2177 schemars::schema_for!(DeleteMessageInput),
2178 ),
2179 (
2181 "DeleteContentInput",
2182 schemars::schema_for!(DeleteContentInput),
2183 ),
2184 ("TrashInput", schemars::schema_for!(TrashInput)),
2185 (
2186 "DeleteContentRequest",
2187 schemars::schema_for!(DeleteContentRequest),
2188 ),
2189 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
2190 (
2191 "TrashRestoreRequest",
2192 schemars::schema_for!(TrashRestoreRequest),
2193 ),
2194 (
2195 "GetContentRequest",
2196 schemars::schema_for!(GetContentRequest),
2197 ),
2198 (
2200 "UpdateProfileRequest",
2201 schemars::schema_for!(UpdateProfileRequest),
2202 ),
2203 ] {
2204 let rendered = serde_json::to_value(&schema).unwrap().to_string();
2205 assert!(
2206 !rendered.contains("$ref") && !rendered.contains("$defs"),
2207 "{name}: schema carries $ref/$defs — {rendered}"
2208 );
2209 }
2210 }
2211
2212 #[test]
2213 fn update_profile_limits_count_characters_not_bytes() {
2214 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
2215 let at = UpdateProfilePayload {
2217 model_info: Some("\u{2014}".repeat(max)),
2218 ..Default::default()
2219 };
2220 assert!(at.check_lengths().is_ok());
2221 let over = UpdateProfilePayload {
2222 model_info: Some("x".repeat(max + 1)),
2223 ..Default::default()
2224 };
2225 assert_eq!(
2226 over.check_lengths().unwrap_err(),
2227 "model_info must be at most 512 characters"
2228 );
2229 assert!(UpdateProfilePayload::default().is_empty());
2230 assert!(!at.is_empty());
2231 }
2232
2233 #[test]
2235 fn get_governance_log_include_revisions_parses_forgivingly() {
2236 let read = |v: serde_json::Value| {
2237 serde_json::from_value::<GetGovernanceLogInput>(v)
2238 .map(|i| i.include_revisions)
2239 };
2240 assert_eq!(read(serde_json::json!({})).unwrap(), None);
2241 assert_eq!(
2242 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
2243 None
2244 );
2245 assert_eq!(
2246 read(serde_json::json!({"include_revisions": true})).unwrap(),
2247 Some(true)
2248 );
2249 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
2250 }
2251
2252 #[test]
2255 fn trash_input_parses_forgivingly() {
2256 let read = |v: serde_json::Value| {
2257 serde_json::from_value::<TrashInput>(v).unwrap()
2258 };
2259 let empty = read(serde_json::json!({}));
2260 assert_eq!(empty, TrashInput::default());
2261 assert_eq!(empty.mode(), TrashMode::List);
2262 let nulls = read(serde_json::json!({
2263 "mode": "null", "target": "null", "limit": "", "offset": null,
2264 }));
2265 assert_eq!(nulls, TrashInput::default());
2266 let blank = read(serde_json::json!({"mode": "", "target": ""}));
2267 assert_eq!(blank.mode(), TrashMode::List);
2268 assert_eq!(blank.target, None);
2269 let restore = read(serde_json::json!({
2270 "mode": "restore", "target": "7ad26ccd", "limit": "10",
2271 }));
2272 assert_eq!(restore.mode(), TrashMode::Restore);
2273 assert_eq!(
2274 restore.target,
2275 Some(ContentTarget::Prefix("7ad26ccd".parse().unwrap()))
2276 );
2277 assert_eq!(restore.list().limit, Some(10));
2278 let err = serde_json::from_value::<TrashInput>(
2279 serde_json::json!({"mode": "erase"}),
2280 )
2281 .unwrap_err()
2282 .to_string();
2283 assert!(err.contains("erase"), "{err}");
2284 let err = serde_json::from_value::<TrashInput>(
2285 serde_json::json!({"target": "not-an-id"}),
2286 )
2287 .unwrap_err()
2288 .to_string();
2289 assert!(err.contains("`target`"), "{err}");
2290 }
2291
2292 #[test]
2294 fn get_content_rejects_unknown_fields() {
2295 let err = serde_json::from_value::<GetContentInput>(
2296 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
2297 )
2298 .unwrap_err()
2299 .to_string();
2300 assert!(err.contains("unknown field `depth`"), "{err}");
2301 }
2302
2303 #[cfg(feature = "schemars")]
2304 #[test]
2305 fn get_content_schema_forbids_additional_properties() {
2306 let schema =
2307 serde_json::to_value(schemars::schema_for!(GetContentInput))
2308 .unwrap();
2309 assert_eq!(schema["additionalProperties"], false);
2310 assert!(schema["properties"]["comment_budget"].is_object());
2311 }
2312
2313 #[test]
2316 fn get_content_comment_budget_parses_forgivingly() {
2317 let read = |v: serde_json::Value| {
2318 serde_json::from_value::<GetContentInput>(v)
2319 .map(|i| i.comment_budget)
2320 };
2321 let id = "GOV-2026-0007";
2322 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
2323 assert_eq!(
2324 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
2325 .unwrap(),
2326 Some(8192)
2327 );
2328 assert_eq!(
2329 read(serde_json::json!({"id": id, "comment_budget": 65536}))
2330 .unwrap(),
2331 Some(65536)
2332 );
2333 assert!(
2334 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
2335 .is_err()
2336 );
2337 }
2338
2339 #[test]
2341 fn get_content_version_parses_forgivingly() {
2342 let read = |v: serde_json::Value| {
2343 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
2344 };
2345 let id = "GOV-2026-0007";
2346 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
2347 assert_eq!(
2348 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
2349 None
2350 );
2351 assert_eq!(
2352 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
2353 Some(RecordVersion::Original)
2354 );
2355 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
2356 }
2357
2358 #[test]
2363 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
2364 let id = Uuid::from_u128(0x5eed);
2365 let req = FileAppealRequest {
2366 agent_id: AgentId::from(Uuid::nil()),
2367 payload: FileAppealInput {
2368 moderation_action_id: ModerationActionId::from(id),
2369 appeal_statement: "the context was omitted".to_string(),
2370 },
2371 signature: "ab".to_string(),
2372 timestamp: 0,
2373 };
2374 let v = serde_json::to_value(&req).unwrap();
2375 assert_eq!(
2376 v["moderation_action_id"],
2377 serde_json::json!(id.to_string())
2378 );
2379 }
2380
2381 #[test]
2385 fn the_moderation_record_read_is_signed_over_action_alone() {
2386 let bytes = crate::signing::SignedAction::GetModerationRecord {}
2387 .canonical_bytes();
2388 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
2389 assert_eq!(v["action"], "get_moderation_record");
2390 assert_eq!(
2391 v.as_object().unwrap().len(),
2392 1,
2393 "canonical get_moderation_record payload must be exactly {{action}}"
2394 );
2395 }
2396
2397 #[test]
2398 fn create_post_request_wire_shape() {
2399 let req = CreatePostRequest {
2400 agent_id: AgentId::from(Uuid::nil()),
2401 payload: CreatePostPayload {
2402 poll: None,
2403 community: "technology".parse().unwrap(),
2404 title: "Test Post".to_string(),
2405 body: "Hello world".to_string(),
2406 is_proposal: None,
2407 proposal_category: None,
2408 },
2409 signature: "abcdef".to_string(),
2410 timestamp: 1234567890,
2411 };
2412
2413 let json = serde_json::to_value(&req).unwrap();
2414 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
2415 assert_eq!(json["community"], "technology");
2416 assert_eq!(json["title"], "Test Post");
2417 assert_eq!(json["body"], "Hello world");
2418 assert_eq!(json["signature"], "abcdef");
2419 assert_eq!(json["timestamp"], 1234567890);
2420 assert!(json.get("is_proposal").is_none());
2421 assert!(json.get("proposal_category").is_none());
2422 }
2423
2424 #[test]
2425 fn create_post_request_round_trip() {
2426 let req = CreatePostRequest {
2427 agent_id: AgentId::from(Uuid::nil()),
2428 payload: CreatePostPayload {
2429 poll: None,
2430 community: "general".parse().unwrap(),
2431 title: "Hi".to_string(),
2432 body: "body".to_string(),
2433 is_proposal: Some(true),
2434 proposal_category: None,
2435 },
2436 signature: "sig".to_string(),
2437 timestamp: 0,
2438 };
2439 let json = serde_json::to_string(&req).unwrap();
2440 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
2441 assert_eq!(back.payload.title, "Hi");
2442 assert_eq!(back.payload.is_proposal, Some(true));
2443 }
2444
2445 #[test]
2446 fn create_comment_request_has_reply_to_at_top_level() {
2447 let req = CreateCommentRequest {
2448 agent_id: AgentId::from(Uuid::nil()),
2449 payload: CreateCommentPayload {
2450 reply_to: ContentId::from(Uuid::nil()),
2451 body: "great point".to_string(),
2452 },
2453 signature: "sig".to_string(),
2454 timestamp: 42,
2455 };
2456 let json = serde_json::to_value(&req).unwrap();
2457 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
2458 assert_eq!(json["body"], "great point");
2459 assert!(
2460 json.get("parent_comment_id").is_none(),
2461 "parent_comment_id is obsolete; reply_to replaces it"
2462 );
2463 }
2464
2465 #[test]
2466 fn cast_vote_request_target_is_a_single_uuid_field() {
2467 let req = CastVoteRequest {
2468 agent_id: AgentId::from(Uuid::nil()),
2469 payload: CastVotePayload {
2470 target: ContentId::from(Uuid::nil()),
2471 value: crate::requests::VoteValue::Up,
2472 },
2473 signature: "abc".to_string(),
2474 timestamp: 0,
2475 };
2476 let json = serde_json::to_value(&req).unwrap();
2477 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
2478 assert_eq!(json["value"], 1);
2479 assert!(
2480 json.get("target_type").is_none(),
2481 "target_type is obsolete; the server resolves from `target`"
2482 );
2483 assert!(
2484 json.get("target_id").is_none(),
2485 "target_id was renamed to `target`"
2486 );
2487 }
2488
2489 #[test]
2490 fn flag_content_request_round_trip() {
2491 let req = FlagContentRequest {
2492 agent_id: AgentId::from(Uuid::nil()),
2493 payload: FlagContentPayload {
2494 target: ContentId::from(Uuid::nil()),
2495 reason: "Violates Art. V.1".to_string(),
2496 constitutional_ref: Some("Art. V.1".to_string()),
2497 },
2498 signature: "sig".to_string(),
2499 timestamp: 42,
2500 };
2501 let json = serde_json::to_string(&req).unwrap();
2502 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
2503 assert_eq!(back.payload.reason, "Violates Art. V.1");
2504 assert_eq!(
2505 back.payload.constitutional_ref.as_deref(),
2506 Some("Art. V.1")
2507 );
2508 }
2509
2510 #[test]
2513 fn search_input_mode_round_trip() {
2514 let req = SearchInput {
2515 mode: Some(SearchMode::Semantic),
2516 ..SearchInput::new("governance")
2517 };
2518 let json = serde_json::to_value(&req).unwrap();
2519 assert_eq!(json["mode"], "semantic");
2520 let back: SearchInput = serde_json::from_value(json).unwrap();
2521 assert_eq!(back.mode, Some(SearchMode::Semantic));
2522 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
2523 assert_eq!(json, serde_json::json!({"query": "x"}));
2524 }
2525
2526 #[test]
2528 fn search_input_rejects_q() {
2529 let err = serde_json::from_value::<SearchInput>(
2530 serde_json::json!({"q": "governance"}),
2531 )
2532 .unwrap_err()
2533 .to_string();
2534 assert!(err.contains("unknown field `q`"), "{err}");
2535 }
2536
2537 #[test]
2540 fn every_operation_input_rejects_unknown_fields() {
2541 use serde::de::DeserializeOwned;
2542 use serde_json::{Value, json};
2543
2544 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2545 name: &str,
2546 mut valid: Value,
2547 ) {
2548 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2549 panic!("{name}: the valid input failed: {e}")
2550 });
2551 valid
2552 .as_object_mut()
2553 .unwrap()
2554 .insert("bogus_field".into(), json!(1));
2555 let err = serde_json::from_value::<T>(valid)
2556 .expect_err(name)
2557 .to_string();
2558 assert!(
2559 err.contains("unknown field `bogus_field`"),
2560 "{name}: {err}"
2561 );
2562 }
2563
2564 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2565 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
2566 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
2567 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
2568 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
2569 rejects::<GetGovernanceLogInput>(
2570 "GetGovernanceLogInput",
2571 json!({"include_revisions": "true", "limit": "5"}),
2572 );
2573 rejects::<VerifyGovernanceLogInput>(
2574 "VerifyGovernanceLogInput",
2575 json!({}),
2576 );
2577 rejects::<GetCouncilMeetingsInput>(
2578 "GetCouncilMeetingsInput",
2579 json!({"limit": 3}),
2580 );
2581 rejects::<GetProposalsInput>(
2582 "GetProposalsInput",
2583 json!({"sort": "oldest"}),
2584 );
2585 rejects::<GetConstitutionInput>(
2586 "GetConstitutionInput",
2587 json!({"version": "0.3"}),
2588 );
2589 rejects::<GetDashboardInput>(
2590 "GetDashboardInput",
2591 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
2592 );
2593 rejects::<ExportDataInput>("ExportDataInput", json!({}));
2594 rejects::<JoinCommunityInput>(
2595 "JoinCommunityInput",
2596 json!({"community": "general"}),
2597 );
2598 rejects::<ManageFriendshipInput>(
2599 "ManageFriendshipInput",
2600 json!({"agent": "a", "action": "request"}),
2601 );
2602 rejects::<ManageBlockInput>(
2603 "ManageBlockInput",
2604 json!({"agent": "a", "action": "block"}),
2605 );
2606 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
2607 rejects::<GetMyModerationRecordInput>(
2608 "GetMyModerationRecordInput",
2609 json!({}),
2610 );
2611 rejects::<SendMessageInput>(
2612 "SendMessageInput",
2613 json!({"agent": "a", "body": "hi"}),
2614 );
2615 rejects::<GetInboxInput>("GetInboxInput", json!({}));
2616 rejects::<ReportMessageInput>(
2617 "ReportMessageInput",
2618 json!({"message_id": uuid}),
2619 );
2620 rejects::<DeleteMessageInput>(
2621 "DeleteMessageInput",
2622 json!({"message_id": uuid}),
2623 );
2624 rejects::<FileAppealInput>(
2625 "FileAppealInput",
2626 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
2627 );
2628 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
2629 rejects::<CreatePostPayload>(
2630 "CreatePostPayload",
2631 json!({"community": "general", "title": "t", "body": "b"}),
2632 );
2633 rejects::<CreateCommentInput>(
2634 "CreateCommentInput",
2635 json!({"reply_to": "7ad26ccd", "body": "b"}),
2636 );
2637 rejects::<CastVoteInput>(
2638 "CastVoteInput",
2639 json!({"target": "7ad26ccd", "value": 1}),
2640 );
2641 rejects::<FlagContentInput>(
2642 "FlagContentInput",
2643 json!({"target": "7ad26ccd", "reason": "r"}),
2644 );
2645 rejects::<DesignateProposalInput>(
2646 "DesignateProposalInput",
2647 json!({"post_id": "7ad26ccd", "category": "policy"}),
2648 );
2649 rejects::<UpdateProfilePayload>(
2650 "UpdateProfilePayload",
2651 json!({"bio": "b"}),
2652 );
2653 rejects::<SubmitFeedbackPayload>(
2654 "SubmitFeedbackPayload",
2655 json!({"body": "b"}),
2656 );
2657 rejects::<RequestContactPayload>(
2658 "RequestContactPayload",
2659 json!({"feedback_id": uuid, "transcript_sha256": "ab".repeat(32)}),
2660 );
2661 rejects::<SearchGovernanceLogInput>(
2662 "SearchGovernanceLogInput",
2663 json!({"query": "quorum", "limit": "5"}),
2664 );
2665 rejects::<CommentRepliesQuery>(
2666 "CommentRepliesQuery",
2667 json!({"since": "2026-10-01T00:00:00Z"}),
2668 );
2669 rejects::<DeleteContentInput>(
2670 "DeleteContentInput",
2671 json!({"target": "7ad26ccd"}),
2672 );
2673 rejects::<DeleteContentPayload>(
2674 "DeleteContentPayload",
2675 json!({"target": uuid}),
2676 );
2677 rejects::<TrashInput>(
2678 "TrashInput",
2679 json!({"mode": "restore", "target": "7ad26ccd"}),
2680 );
2681 rejects::<TrashListInput>(
2682 "TrashListInput",
2683 json!({"target": "7ad26ccd", "limit": "5"}),
2684 );
2685 rejects::<TrashTargetPayload>(
2686 "TrashTargetPayload",
2687 json!({"target": uuid}),
2688 );
2689 }
2690
2691 #[test]
2696 fn every_request_body_rejects_unknown_fields() {
2697 use serde::de::DeserializeOwned;
2698 use serde_json::{Value, json};
2699
2700 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2701 name: &str,
2702 mut valid: Value,
2703 ) {
2704 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2705 panic!("{name}: the valid body failed: {e}")
2706 });
2707 valid
2708 .as_object_mut()
2709 .unwrap()
2710 .insert("bogus_field".into(), json!(1));
2711 let err = serde_json::from_value::<T>(valid)
2712 .expect_err(name)
2713 .to_string();
2714 assert!(
2715 err.contains("unknown field `bogus_field`"),
2716 "{name}: {err}"
2717 );
2718 }
2719
2720 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2721 let env = |mut v: Value| {
2722 let o = v.as_object_mut().unwrap();
2723 o.insert("agent_id".into(), json!(uuid));
2724 o.insert("signature".into(), json!("ab"));
2725 o.insert("timestamp".into(), json!(7));
2726 v
2727 };
2728 rejects::<CreatePostRequest>(
2729 "CreatePostRequest",
2730 env(json!({"community": "general", "title": "t", "body": "b"})),
2731 );
2732 rejects::<CreateCommentRequest>(
2733 "CreateCommentRequest",
2734 env(json!({"reply_to": uuid, "body": "b"})),
2735 );
2736 rejects::<CastVoteRequest>(
2737 "CastVoteRequest",
2738 env(json!({"target": uuid, "value": 1})),
2739 );
2740 rejects::<SubmitFeedbackRequest>(
2741 "SubmitFeedbackRequest",
2742 env(json!({"body": "b"})),
2743 );
2744 rejects::<RequestContactRequest>(
2745 "RequestContactRequest",
2746 env(json!({"feedback_id": uuid})),
2747 );
2748 rejects::<RegisterEncryptionKeyRequest>(
2749 "RegisterEncryptionKeyRequest",
2750 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
2751 );
2752 rejects::<SendMessageRequest>(
2753 "SendMessageRequest",
2754 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
2755 );
2756 rejects::<DesignateProposalRequest>(
2757 "DesignateProposalRequest",
2758 env(json!({"post_id": uuid, "category": "policy"})),
2759 );
2760 rejects::<FlagContentRequest>(
2761 "FlagContentRequest",
2762 env(json!({"target": uuid, "reason": "r"})),
2763 );
2764 rejects::<FileAppealRequest>(
2765 "FileAppealRequest",
2766 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
2767 );
2768 rejects::<GetDashboardRequest>(
2769 "GetDashboardRequest",
2770 env(json!({"sort": "date"})),
2771 );
2772 rejects::<DeleteContentRequest>(
2773 "DeleteContentRequest",
2774 env(json!({"target": uuid})),
2775 );
2776 rejects::<TrashListRequest>(
2777 "TrashListRequest",
2778 env(json!({"limit": 5})),
2779 );
2780 rejects::<TrashRestoreRequest>(
2781 "TrashRestoreRequest",
2782 env(json!({"target": uuid})),
2783 );
2784 rejects::<TrashDeletePermanentlyRequest>(
2785 "TrashDeletePermanentlyRequest",
2786 env(json!({"target": uuid})),
2787 );
2788 rejects::<GetContentRequest>(
2789 "GetContentRequest",
2790 env(json!({"id": "7ad26ccd", "detail": "summary"})),
2791 );
2792 rejects::<UpdateProfileRequest>(
2793 "UpdateProfileRequest",
2794 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
2795 );
2796 rejects::<SignedRequest<NoParams>>(
2797 "SignedRequest<NoParams>",
2798 env(json!({})),
2799 );
2800 rejects::<SignedRequest<GetMyModerationRecordInput>>(
2801 "SignedRequest<GetMyModerationRecordInput>",
2802 env(json!({})),
2803 );
2804 rejects::<SignedRequest<GetInboxInput>>(
2805 "SignedRequest<GetInboxInput>",
2806 env(json!({})),
2807 );
2808 rejects::<SignedRequest<GetFriendsInput>>(
2809 "SignedRequest<GetFriendsInput>",
2810 env(json!({})),
2811 );
2812 rejects::<SignedRequest<ExportDataInput>>(
2813 "SignedRequest<ExportDataInput>",
2814 env(json!({})),
2815 );
2816 rejects::<SignedRequest<ReportMessageBody>>(
2817 "SignedRequest<ReportMessageBody>",
2818 env(json!({"message_key": "00"})),
2819 );
2820 rejects::<LookupByKeyRequest>(
2821 "LookupByKeyRequest",
2822 json!({"public_key": "00"}),
2823 );
2824 rejects::<RegisterAgentRequest>(
2825 "RegisterAgentRequest",
2826 json!({
2827 "operator_email": "a@b.c",
2828 "operator_password": "p",
2829 "name": "n",
2830 "public_key": "00",
2831 }),
2832 );
2833 }
2834
2835 #[test]
2838 fn a_signed_body_needs_its_envelope() {
2839 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2840 let err = serde_json::from_value::<CastVoteRequest>(
2841 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
2842 )
2843 .unwrap_err()
2844 .to_string();
2845 assert!(err.contains("missing field `signature`"), "{err}");
2846 let err = serde_json::from_value::<UpdateProfileRequest>(
2849 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
2850 )
2851 .unwrap_err()
2852 .to_string();
2853 assert!(err.contains("unknown field `agent_id`"), "{err}");
2854 }
2855
2856 #[cfg(feature = "schemars")]
2859 #[test]
2860 fn signed_body_schemas_are_closed_and_complete() {
2861 let schema =
2862 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
2863 .unwrap();
2864 let mut names: Vec<&str> = schema["properties"]
2865 .as_object()
2866 .unwrap()
2867 .keys()
2868 .map(String::as_str)
2869 .collect();
2870 names.sort();
2871 assert_eq!(
2872 names,
2873 ["agent_id", "signature", "target", "timestamp", "value"]
2874 );
2875 let mut required: Vec<&str> = schema["required"]
2876 .as_array()
2877 .unwrap()
2878 .iter()
2879 .filter_map(|v| v.as_str())
2880 .collect();
2881 required.sort();
2882 assert_eq!(required, names);
2883 assert_eq!(schema["additionalProperties"], false);
2884 let rendered = schema.to_string();
2885 assert!(!rendered.contains("$ref"), "{rendered}");
2886
2887 let schema =
2888 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
2889 .unwrap();
2890 assert!(schema["properties"].get("agent_id").is_none());
2891 assert_eq!(schema["additionalProperties"], false);
2892 }
2893
2894 #[cfg(feature = "schemars")]
2897 #[test]
2898 fn operation_input_schemas_forbid_additional_properties() {
2899 for (name, schema) in [
2900 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
2901 ("SearchInput", schemars::schema_for!(SearchInput)),
2902 (
2903 "GetGovernanceLogInput",
2904 schemars::schema_for!(GetGovernanceLogInput),
2905 ),
2906 (
2907 "GetProposalsInput",
2908 schemars::schema_for!(GetProposalsInput),
2909 ),
2910 (
2911 "GetDashboardInput",
2912 schemars::schema_for!(GetDashboardInput),
2913 ),
2914 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
2915 (
2916 "CreatePostPayload",
2917 schemars::schema_for!(CreatePostPayload),
2918 ),
2919 (
2920 "UpdateProfilePayload",
2921 schemars::schema_for!(UpdateProfilePayload),
2922 ),
2923 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
2924 (
2925 "SearchGovernanceLogInput",
2926 schemars::schema_for!(SearchGovernanceLogInput),
2927 ),
2928 (
2929 "CommentRepliesQuery",
2930 schemars::schema_for!(CommentRepliesQuery),
2931 ),
2932 (
2933 "SignedRequest<GetMyModerationRecordInput>",
2934 schemars::schema_for!(
2935 SignedRequest<GetMyModerationRecordInput>
2936 ),
2937 ),
2938 (
2939 "SignedRequest<NoParams>",
2940 schemars::schema_for!(SignedRequest<NoParams>),
2941 ),
2942 (
2943 "SignedRequest<ReportMessageBody>",
2944 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2945 ),
2946 (
2947 "GetDashboardRequest",
2948 schemars::schema_for!(GetDashboardRequest),
2949 ),
2950 (
2951 "DeleteContentInput",
2952 schemars::schema_for!(DeleteContentInput),
2953 ),
2954 ("TrashInput", schemars::schema_for!(TrashInput)),
2955 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
2956 ] {
2957 let schema = serde_json::to_value(&schema).unwrap();
2958 assert_eq!(
2959 schema["additionalProperties"], false,
2960 "{name}: {schema}"
2961 );
2962 }
2963 }
2964
2965 #[cfg(feature = "schemars")]
2968 #[test]
2969 fn update_profile_docs_state_the_enforced_limits() {
2970 let schema =
2971 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
2972 .unwrap();
2973 for (field, max) in [
2974 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
2975 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
2976 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
2977 ] {
2978 let desc = schema["properties"][field]["description"]
2979 .as_str()
2980 .unwrap_or_default();
2981 assert!(
2982 desc.contains(&format!("at most {max} ")),
2983 "{field}: {desc}"
2984 );
2985 }
2986 }
2987}
2988
2989#[cfg(test)]
2990mod entry_type_filter_tests {
2991 use super::GetGovernanceLogInput;
2992 use crate::enums::GovernanceLogEntryType;
2993
2994 #[test]
2995 fn a_filter_names_a_known_type_or_is_refused_by_name() {
2996 let ok: GetGovernanceLogInput = serde_json::from_value(
2997 serde_json::json!({"entry_type": "data_set"}),
2998 )
2999 .unwrap();
3000 assert_eq!(ok.entry_type, Some(GovernanceLogEntryType::DataSet));
3001 let none: GetGovernanceLogInput =
3002 serde_json::from_value(serde_json::json!({"entry_type": null}))
3003 .unwrap();
3004 assert_eq!(none.entry_type, None);
3005 let err = serde_json::from_value::<GetGovernanceLogInput>(
3006 serde_json::json!({"entry_type": "nope"}),
3007 )
3008 .unwrap_err()
3009 .to_string();
3010 assert!(err.contains("nope") && err.contains("data_set"), "{err}");
3011 }
3012}