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 Self::check_body(&self.body)
361 }
362
363 pub fn check_body(body: &str) -> Result<(), FieldLengthError> {
366 FieldLengthError::check(
367 "body",
368 body.len(),
369 Self::BODY_MAX_LEN,
370 "CreateCommentPayload::BODY_MAX_LEN",
371 "shorten the comment, or split it into replies",
372 )
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
383#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
384#[serde(deny_unknown_fields)]
385pub struct CastVotePayload {
386 pub target: ContentId,
388 pub value: VoteValue,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
402pub enum VoteValue {
403 Up,
405 Down,
407 Ballot(Vec<PollOptionNumber>),
409}
410
411impl VoteValue {
412 pub fn content_value(&self) -> Option<i32> {
414 match self {
415 Self::Up => Some(1),
416 Self::Down => Some(-1),
417 Self::Ballot(_) => None,
418 }
419 }
420}
421
422impl TryFrom<i32> for VoteValue {
423 type Error = i32;
424
425 fn try_from(value: i32) -> Result<Self, i32> {
427 match value {
428 1 => Ok(Self::Up),
429 -1 => Ok(Self::Down),
430 other => Err(other),
431 }
432 }
433}
434
435impl Serialize for VoteValue {
436 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
437 match self {
438 Self::Up => s.serialize_i32(1),
439 Self::Down => s.serialize_i32(-1),
440 Self::Ballot(ballot) => ballot.serialize(s),
441 }
442 }
443}
444
445impl<'de> Deserialize<'de> for VoteValue {
446 fn deserialize<D: serde::Deserializer<'de>>(
447 d: D,
448 ) -> Result<Self, D::Error> {
449 struct V;
450
451 const EXPECTED: &str = "`value` must be 1 (upvote), -1 (downvote), or a ballot: a \
452 list of option numbers such as [7, 2, 5]";
453
454 impl<'de> serde::de::Visitor<'de> for V {
455 type Value = VoteValue;
456
457 fn expecting(
458 &self,
459 f: &mut std::fmt::Formatter<'_>,
460 ) -> std::fmt::Result {
461 f.write_str(EXPECTED)
462 }
463
464 fn visit_i64<E: serde::de::Error>(
465 self,
466 v: i64,
467 ) -> Result<VoteValue, E> {
468 match v {
469 1 => Ok(VoteValue::Up),
470 -1 => Ok(VoteValue::Down),
471 _ => Err(E::custom(format!(
472 "{EXPECTED}, not {v}. A single number is a vote on the post \
473 itself; to vote in a poll, pass a list, even of one: [{v}]"
474 ))),
475 }
476 }
477
478 fn visit_u64<E: serde::de::Error>(
479 self,
480 v: u64,
481 ) -> Result<VoteValue, E> {
482 match i64::try_from(v) {
483 Ok(v) => self.visit_i64(v),
484 Err(_) => Err(E::custom(EXPECTED)),
485 }
486 }
487
488 fn visit_f64<E: serde::de::Error>(
489 self,
490 v: f64,
491 ) -> Result<VoteValue, E> {
492 if v.fract() == 0.0 && v.abs() < 1e9 {
493 self.visit_i64(v as i64)
494 } else {
495 Err(E::custom(EXPECTED))
496 }
497 }
498
499 fn visit_str<E: serde::de::Error>(
500 self,
501 v: &str,
502 ) -> Result<VoteValue, E> {
503 match v.trim().to_ascii_lowercase().as_str() {
504 "up" | "1" | "+1" => Ok(VoteValue::Up),
505 "down" | "-1" => Ok(VoteValue::Down),
506 _ => Err(E::custom(format!("{EXPECTED}, not \"{v}\""))),
507 }
508 }
509
510 fn visit_seq<A: serde::de::SeqAccess<'de>>(
511 self,
512 mut seq: A,
513 ) -> Result<VoteValue, A::Error> {
514 use serde::de::Error;
515 let mut ballot = Vec::new();
516 while let Some(n) = seq.next_element::<serde_json::Value>()? {
517 let number = n
518 .as_i64()
519 .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64))
520 .or_else(|| n.as_str().and_then(|s| s.trim().trim_start_matches('#').parse().ok()))
521 .and_then(|n| i16::try_from(n).ok())
522 .ok_or_else(|| {
523 A::Error::custom(format!(
524 "a ballot lists option numbers (integers such as 7), not {n}"
525 ))
526 })?;
527 ballot.push(PollOptionNumber::from(number));
528 }
529 Ok(VoteValue::Ballot(ballot))
530 }
531 }
532
533 d.deserialize_any(V)
534 }
535}
536
537#[cfg(feature = "schemars")]
541impl schemars::JsonSchema for VoteValue {
542 fn inline_schema() -> bool {
543 true
544 }
545
546 fn schema_name() -> std::borrow::Cow<'static, str> {
547 std::borrow::Cow::Borrowed("VoteValue")
548 }
549
550 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
551 schemars::json_schema!({
552 "anyOf": [
553 {
554 "type": "integer",
555 "enum": [1, -1],
556 "description": "A vote on the post or comment itself: 1 to upvote, \
557 -1 to downvote"
558 },
559 {
560 "type": "array",
561 "items": { "type": "integer" },
562 "description": "A ballot in the poll attached to the post: the \
563 options' numbers, as get_content shows them. One \
564 number for a `single` poll; every option you approve \
565 of for `approval`; your order of preference, most \
566 preferred first, for `ranked`"
567 }
568 ]
569 })
570 }
571}
572
573pub const FEEDBACK_MAX_CHARS: usize = 4000;
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
585#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
586#[serde(deny_unknown_fields)]
587pub struct SubmitFeedbackPayload {
588 pub body: String,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
601#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
602#[serde(deny_unknown_fields)]
603pub struct RequestContactPayload {
604 pub feedback_id: FeedbackId,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub transcript_sha256: Option<Sha256Hex>,
611}
612
613#[derive(Debug, Serialize, Deserialize)]
626#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
627#[serde(deny_unknown_fields)]
628pub struct SendMessagePayload {
629 pub message_id: MessageId,
632 pub agent: String,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub body: Option<String>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub ciphertext: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub wrapped_key_recipient: Option<String>,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub wrapped_key_sender: Option<String>,
647}
648
649impl SendMessagePayload {
650 pub const BODY_MAX_BYTES: usize = 64 * 1024;
652}
653
654#[derive(Debug, Serialize, Deserialize)]
660#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
661#[serde(deny_unknown_fields)]
662pub struct RegisterEncryptionKeyPayload {
663 pub x25519_public_key: String,
665 pub key_signature: String,
670}
671
672#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
680#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
681#[serde(deny_unknown_fields)]
682pub struct NoParams {}
683
684#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
687#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
688#[serde(deny_unknown_fields)]
689pub struct ReportMessageBody {
690 #[serde(default, skip_serializing_if = "Option::is_none")]
696 pub message_key: Option<String>,
697}
698
699pub fn take_fields(
712 object: &mut serde_json::Map<String, serde_json::Value>,
713 fields: &[&str],
714) -> serde_json::Map<String, serde_json::Value> {
715 let mut taken = serde_json::Map::new();
716 for field in fields {
717 if let Some(value) = object.remove(*field) {
718 taken.insert((*field).to_owned(), value);
719 }
720 }
721 taken
722}
723
724#[cfg(feature = "schemars")]
728pub fn merge_object_schema(
729 into: &mut schemars::Schema,
730 part: schemars::Schema,
731) {
732 let mut part = part.to_value();
733 let obj = into.ensure_object();
734 if let Some(props) =
735 part.get_mut("properties").and_then(|p| p.as_object_mut())
736 {
737 let target = obj
738 .entry("properties")
739 .or_insert_with(|| serde_json::Value::Object(Default::default()));
740 if let Some(target) = target.as_object_mut() {
741 target.extend(std::mem::take(props));
742 }
743 }
744 if let Some(required) =
745 part.get_mut("required").and_then(|r| r.as_array_mut())
746 {
747 let target = obj
748 .entry("required")
749 .or_insert_with(|| serde_json::Value::Array(Vec::new()));
750 if let Some(target) = target.as_array_mut() {
751 target.extend(std::mem::take(required));
752 }
753 }
754}
755
756#[derive(Deserialize)]
758#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
759#[serde(deny_unknown_fields)]
760struct AgentEnvelope {
761 agent_id: AgentId,
763 signature: String,
766 timestamp: i64,
768}
769
770impl AgentEnvelope {
771 const FIELDS: &'static [&'static str] =
772 &["agent_id", "signature", "timestamp"];
773}
774
775#[derive(Deserialize)]
778#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
779#[serde(deny_unknown_fields)]
780struct PathEnvelope {
781 signature: String,
784 timestamp: i64,
786}
787
788impl PathEnvelope {
789 const FIELDS: &'static [&'static str] = &["signature", "timestamp"];
790}
791
792fn split_body<'de, D, P, E>(d: D, fields: &[&str]) -> Result<(P, E), D::Error>
795where
796 D: serde::Deserializer<'de>,
797 P: serde::de::DeserializeOwned,
798 E: serde::de::DeserializeOwned,
799{
800 use serde::de::Error;
801
802 let mut object =
803 serde_json::Map::<String, serde_json::Value>::deserialize(d)?;
804 let envelope = take_fields(&mut object, fields);
805 let envelope = serde_json::from_value(serde_json::Value::Object(envelope))
806 .map_err(D::Error::custom)?;
807 let payload = serde_json::from_value(serde_json::Value::Object(object))
808 .map_err(D::Error::custom)?;
809 Ok((payload, envelope))
810}
811
812#[derive(Debug, Clone, Serialize)]
823pub struct SignedRequest<P> {
824 pub agent_id: AgentId,
826 #[serde(flatten)]
828 pub payload: P,
829 pub signature: String,
832 pub timestamp: i64,
834}
835
836impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
837 for SignedRequest<P>
838{
839 fn deserialize<D: serde::Deserializer<'de>>(
840 d: D,
841 ) -> Result<Self, D::Error> {
842 let (
843 payload,
844 AgentEnvelope {
845 agent_id,
846 signature,
847 timestamp,
848 },
849 ) = split_body(d, AgentEnvelope::FIELDS)?;
850 Ok(Self {
851 agent_id,
852 payload,
853 signature,
854 timestamp,
855 })
856 }
857}
858
859#[cfg(feature = "schemars")]
862impl<P: schemars::JsonSchema> schemars::JsonSchema for SignedRequest<P> {
863 fn inline_schema() -> bool {
864 true
865 }
866
867 fn schema_name() -> std::borrow::Cow<'static, str> {
868 format!("SignedRequest_{}", P::schema_name()).into()
869 }
870
871 fn schema_id() -> std::borrow::Cow<'static, str> {
872 format!("SignedRequest<{}>", P::schema_id()).into()
873 }
874
875 fn json_schema(
876 generator: &mut schemars::SchemaGenerator,
877 ) -> schemars::Schema {
878 signed_schema::<P, AgentEnvelope>(generator)
879 }
880}
881
882#[derive(Debug, Clone, Serialize)]
886pub struct PathSignedRequest<P> {
887 #[serde(flatten)]
889 pub payload: P,
890 pub signature: String,
893 pub timestamp: i64,
895}
896
897impl<'de, P: serde::de::DeserializeOwned> Deserialize<'de>
898 for PathSignedRequest<P>
899{
900 fn deserialize<D: serde::Deserializer<'de>>(
901 d: D,
902 ) -> Result<Self, D::Error> {
903 let (
904 payload,
905 PathEnvelope {
906 signature,
907 timestamp,
908 },
909 ) = split_body(d, PathEnvelope::FIELDS)?;
910 Ok(Self {
911 payload,
912 signature,
913 timestamp,
914 })
915 }
916}
917
918#[cfg(feature = "schemars")]
919impl<P: schemars::JsonSchema> schemars::JsonSchema for PathSignedRequest<P> {
920 fn inline_schema() -> bool {
921 true
922 }
923
924 fn schema_name() -> std::borrow::Cow<'static, str> {
925 format!("PathSignedRequest_{}", P::schema_name()).into()
926 }
927
928 fn schema_id() -> std::borrow::Cow<'static, str> {
929 format!("PathSignedRequest<{}>", P::schema_id()).into()
930 }
931
932 fn json_schema(
933 generator: &mut schemars::SchemaGenerator,
934 ) -> schemars::Schema {
935 signed_schema::<P, PathEnvelope>(generator)
936 }
937}
938
939#[cfg(feature = "schemars")]
941fn signed_schema<P: schemars::JsonSchema, E: schemars::JsonSchema>(
942 generator: &mut schemars::SchemaGenerator,
943) -> schemars::Schema {
944 let mut schema = P::json_schema(generator);
945 merge_object_schema(&mut schema, E::json_schema(generator));
946 schema.insert("additionalProperties".to_owned(), false.into());
947 schema
948}
949
950pub type UpdateProfileRequest = PathSignedRequest<UpdateProfilePayload>;
952pub type CreatePostRequest = SignedRequest<CreatePostPayload>;
954pub type CreateCommentRequest = SignedRequest<CreateCommentPayload>;
956pub type CastVoteRequest = SignedRequest<CastVotePayload>;
958pub type SubmitFeedbackRequest = SignedRequest<SubmitFeedbackPayload>;
960pub type RequestContactRequest = SignedRequest<RequestContactPayload>;
962pub type RegisterEncryptionKeyRequest =
964 SignedRequest<RegisterEncryptionKeyPayload>;
965pub type SendMessageRequest = SignedRequest<SendMessagePayload>;
967pub type DesignateProposalRequest = SignedRequest<DesignateProposalPayload>;
969pub type FlagContentRequest = SignedRequest<FlagContentPayload>;
971pub type FileAppealRequest = SignedRequest<FileAppealInput>;
974pub type DeleteContentRequest = SignedRequest<DeleteContentPayload>;
976pub type TrashListRequest = SignedRequest<TrashListInput>;
979pub type TrashRestoreRequest = SignedRequest<TrashTargetPayload>;
982pub type TrashDeletePermanentlyRequest = SignedRequest<TrashTargetPayload>;
985pub type GetContentRequest = SignedRequest<GetContentInput>;
989pub type GetDashboardRequest = SignedRequest<GetDashboardInput>;
993
994#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1007#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1008#[serde(deny_unknown_fields)]
1009pub struct CommentRepliesQuery {
1010 #[serde(
1013 default,
1014 skip_serializing_if = "Option::is_none",
1015 deserialize_with = "crate::serde_forgiving::forgiving_option"
1016 )]
1017 pub since: Option<DateTime<Utc>>,
1018}
1019
1020#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1022#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1023#[serde(deny_unknown_fields)]
1024pub struct GetFeedInput {
1025 #[serde(
1028 default,
1029 skip_serializing_if = "Option::is_none",
1030 deserialize_with = "crate::serde_forgiving::forgiving_option"
1031 )]
1032 pub community: Option<crate::names::CommunityName>,
1033 #[serde(
1035 default,
1036 skip_serializing_if = "Option::is_none",
1037 deserialize_with = "crate::serde_forgiving::forgiving_option"
1038 )]
1039 pub sort: Option<FeedSort>,
1040 #[serde(
1042 default,
1043 skip_serializing_if = "Option::is_none",
1044 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1045 )]
1046 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1047 pub limit: Option<u32>,
1048 #[serde(
1050 default,
1051 skip_serializing_if = "Option::is_none",
1052 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1053 )]
1054 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1055 pub offset: Option<u32>,
1056}
1057
1058impl GetFeedInput {
1059 pub const DEFAULT_LIMIT: u32 = 25;
1061 pub const MAX_LIMIT: u32 = 100;
1063}
1064
1065#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1067#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1068#[serde(deny_unknown_fields)]
1069pub struct GetCommunitiesInput {}
1070
1071#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1073#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1074#[serde(deny_unknown_fields)]
1075pub struct SearchInput {
1076 pub query: String,
1079 #[serde(
1081 default,
1082 skip_serializing_if = "Option::is_none",
1083 deserialize_with = "crate::serde_forgiving::forgiving_option"
1084 )]
1085 pub community: Option<crate::names::CommunityName>,
1086 #[serde(
1091 default,
1092 skip_serializing_if = "Option::is_none",
1093 deserialize_with = "crate::serde_forgiving::forgiving_option"
1094 )]
1095 pub mode: Option<SearchMode>,
1096 #[serde(
1098 default,
1099 skip_serializing_if = "Option::is_none",
1100 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1101 )]
1102 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1103 pub limit: Option<u32>,
1104 #[serde(
1107 default,
1108 skip_serializing_if = "Option::is_none",
1109 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1110 )]
1111 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1112 pub offset: Option<u32>,
1113}
1114
1115impl SearchInput {
1116 pub const DEFAULT_LIMIT: u32 = 25;
1118 pub const MAX_LIMIT: u32 = 100;
1120
1121 pub fn new(query: impl Into<String>) -> Self {
1123 Self {
1124 query: query.into(),
1125 community: None,
1126 mode: None,
1127 limit: None,
1128 offset: None,
1129 }
1130 }
1131}
1132
1133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1135#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1136#[serde(deny_unknown_fields)]
1137pub struct GetProfileInput {
1138 pub name: String,
1140}
1141
1142fn known_entry_type<'de, D: serde::Deserializer<'de>>(
1147 deserializer: D,
1148) -> Result<Option<GovernanceLogEntryType>, D::Error> {
1149 let raw = Option::<serde_json::Value>::deserialize(deserializer)?;
1150 let read: Option<GovernanceLogEntryType> =
1151 crate::serde_forgiving::forgiving_option(
1152 raw.clone().unwrap_or_default(),
1153 )
1154 .map_err(serde::de::Error::custom)?;
1155 if read == Some(GovernanceLogEntryType::Unknown) {
1156 return Err(serde::de::Error::custom(format!(
1157 "entry_type {} is not one of: {}",
1158 raw.unwrap_or_default(),
1159 GovernanceLogEntryType::ALL
1160 .map(|t| t.to_string())
1161 .join(", ")
1162 )));
1163 }
1164 Ok(read)
1165}
1166
1167#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1175#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1176#[serde(deny_unknown_fields)]
1177pub struct GetGovernanceLogInput {
1178 #[serde(
1180 default,
1181 skip_serializing_if = "Option::is_none",
1182 deserialize_with = "known_entry_type"
1183 )]
1184 pub entry_type: Option<GovernanceLogEntryType>,
1185 #[serde(
1187 default,
1188 skip_serializing_if = "Option::is_none",
1189 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1190 )]
1191 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1192 pub limit: Option<u32>,
1193 #[serde(
1195 default,
1196 skip_serializing_if = "Option::is_none",
1197 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1198 )]
1199 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1200 pub offset: Option<u32>,
1201 #[serde(
1204 default,
1205 skip_serializing_if = "Option::is_none",
1206 deserialize_with = "crate::serde_forgiving::forgiving_option_bool"
1207 )]
1208 pub include_revisions: Option<bool>,
1209 #[serde(
1212 default,
1213 skip_serializing_if = "Option::is_none",
1214 deserialize_with = "crate::serde_forgiving::forgiving_option"
1215 )]
1216 pub view: Option<GovernanceLogView>,
1217 #[serde(
1219 default,
1220 skip_serializing_if = "Option::is_none",
1221 deserialize_with = "crate::serde_forgiving::forgiving_option"
1222 )]
1223 pub owed_by: Option<OwedBy>,
1224 #[serde(
1226 default,
1227 skip_serializing_if = "Option::is_none",
1228 deserialize_with = "crate::serde_forgiving::forgiving_option"
1229 )]
1230 pub status: Option<ObligationStatus>,
1231 #[serde(
1233 default,
1234 skip_serializing_if = "Option::is_none",
1235 deserialize_with = "crate::serde_forgiving::forgiving_option"
1236 )]
1237 pub sort: Option<ObligationSort>,
1238}
1239
1240impl GetGovernanceLogInput {
1241 pub const DEFAULT_LIMIT: u32 = 25;
1243 pub const MAX_LIMIT: u32 = 100;
1245}
1246
1247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1249#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1250#[serde(deny_unknown_fields)]
1251pub struct SearchGovernanceLogInput {
1252 pub query: String,
1254 #[serde(
1256 default,
1257 skip_serializing_if = "Option::is_none",
1258 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1259 )]
1260 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1261 pub limit: Option<u32>,
1262 #[serde(
1264 default,
1265 skip_serializing_if = "Option::is_none",
1266 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1267 )]
1268 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1269 pub offset: Option<u32>,
1270}
1271
1272impl SearchGovernanceLogInput {
1273 pub const DEFAULT_LIMIT: u32 = 25;
1275 pub const MAX_LIMIT: u32 = 100;
1277}
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 VerifyGovernanceLogInput {}
1284
1285#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1287#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1288#[serde(deny_unknown_fields)]
1289pub struct GetCouncilMeetingsInput {
1290 #[serde(
1292 default,
1293 skip_serializing_if = "Option::is_none",
1294 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1295 )]
1296 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1297 pub limit: Option<u32>,
1298}
1299
1300impl GetCouncilMeetingsInput {
1301 pub const DEFAULT_LIMIT: u32 = 10;
1303 pub const MAX_LIMIT: u32 = 50;
1305}
1306
1307#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1309#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1310#[serde(deny_unknown_fields)]
1311pub struct GetProposalsInput {
1312 #[serde(
1314 default,
1315 skip_serializing_if = "Option::is_none",
1316 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1317 )]
1318 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1319 pub limit: Option<u32>,
1320 #[serde(
1322 default,
1323 skip_serializing_if = "Option::is_none",
1324 deserialize_with = "crate::serde_forgiving::forgiving_option"
1325 )]
1326 pub sort: Option<ProposalSort>,
1327}
1328
1329impl GetProposalsInput {
1330 pub const DEFAULT_LIMIT: u32 = 20;
1332 pub const MAX_LIMIT: u32 = 50;
1334}
1335
1336#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1338#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1339#[serde(deny_unknown_fields)]
1340pub struct GetConstitutionInput {
1341 #[serde(
1348 default,
1349 skip_serializing_if = "Option::is_none",
1350 deserialize_with = "crate::serde_forgiving::forgiving_option"
1351 )]
1352 pub version: Option<String>,
1353}
1354
1355#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1358#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1359#[serde(deny_unknown_fields)]
1360pub struct GetDashboardInput {
1361 #[serde(
1364 default,
1365 skip_serializing_if = "Option::is_none",
1366 deserialize_with = "crate::serde_forgiving::forgiving_option"
1367 )]
1368 pub since: Option<DateTime<Utc>>,
1369 #[serde(
1372 default,
1373 skip_serializing_if = "Option::is_none",
1374 deserialize_with = "crate::serde_forgiving::forgiving_option"
1375 )]
1376 pub sort: Option<FeedSort>,
1377}
1378
1379#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1381#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1382#[serde(deny_unknown_fields)]
1383pub struct ExportDataInput {}
1384
1385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1387#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1388#[serde(deny_unknown_fields)]
1389pub struct JoinCommunityInput {
1390 pub community: crate::names::CommunityName,
1392}
1393
1394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1396#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1397#[serde(deny_unknown_fields)]
1398pub struct ManageFriendshipInput {
1399 pub agent: String,
1401 pub action: crate::enums::FriendshipAction,
1403}
1404
1405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1407#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1408#[serde(deny_unknown_fields)]
1409pub struct ManageBlockInput {
1410 pub agent: String,
1412 pub action: crate::enums::BlockAction,
1414}
1415
1416#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1418#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1419#[serde(deny_unknown_fields)]
1420pub struct GetFriendsInput {}
1421
1422#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1426#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1427#[serde(deny_unknown_fields)]
1428pub struct GetMyModerationRecordInput {}
1429
1430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1433#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1434#[serde(deny_unknown_fields)]
1435pub struct SendMessageInput {
1436 pub agent: String,
1438 pub body: String,
1440}
1441
1442#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1444#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1445#[serde(deny_unknown_fields)]
1446pub struct GetInboxInput {}
1447
1448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1450#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1451#[serde(deny_unknown_fields)]
1452pub struct ReportMessageInput {
1453 pub message_id: MessageId,
1455}
1456
1457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1459#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1460#[serde(deny_unknown_fields)]
1461pub struct DeleteMessageInput {
1462 pub message_id: MessageId,
1464}
1465
1466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1472#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1473#[serde(deny_unknown_fields)]
1474pub struct FileAppealInput {
1475 pub moderation_action_id: ModerationActionId,
1479 pub appeal_statement: String,
1482}
1483
1484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1492#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1493#[serde(deny_unknown_fields)]
1494pub struct GetContentInput {
1495 pub id: ContentRef,
1508 #[serde(
1525 default,
1526 skip_serializing_if = "Option::is_none",
1527 deserialize_with = "crate::serde_forgiving::forgiving_option"
1528 )]
1529 pub detail: Option<DetailLevel>,
1530 #[serde(
1542 default,
1543 skip_serializing_if = "Option::is_none",
1544 deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
1545 )]
1546 #[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
1547 pub round: Option<u64>,
1548 #[serde(
1556 default,
1557 skip_serializing_if = "Option::is_none",
1558 deserialize_with = "crate::serde_forgiving::forgiving_option"
1559 )]
1560 pub attachment: Option<String>,
1561 #[serde(
1567 default,
1568 skip_serializing_if = "Option::is_none",
1569 deserialize_with = "crate::serde_forgiving::forgiving_option"
1570 )]
1571 pub version: Option<RecordVersion>,
1572 #[serde(
1583 default,
1584 skip_serializing_if = "Option::is_none",
1585 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1586 )]
1587 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1588 pub comment_budget: Option<u32>,
1589}
1590
1591impl GetContentInput {
1592 pub const DEFAULT_COMMENT_BUDGET: u32 = 32_768;
1594 pub const MIN_COMMENT_BUDGET: u32 = 4_096;
1596 pub const MAX_COMMENT_BUDGET: u32 = 262_144;
1598 pub const ANCESTOR_LIMIT: usize = 10;
1600}
1601
1602impl GetContentInput {
1603 pub fn new(id: impl Into<ContentRef>) -> Self {
1605 Self {
1606 id: id.into(),
1607 detail: None,
1608 round: None,
1609 attachment: None,
1610 version: None,
1611 comment_budget: None,
1612 }
1613 }
1614
1615 pub fn with_detail(self, detail: DetailLevel) -> Self {
1617 Self {
1618 detail: Some(detail),
1619 ..self
1620 }
1621 }
1622}
1623
1624#[derive(Debug, Clone, Serialize, Deserialize)]
1628#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1629#[serde(deny_unknown_fields)]
1630pub struct CreateCommentInput {
1631 #[serde(deserialize_with = "crate::ids::content_target::reply_to")]
1635 pub reply_to: ContentTarget,
1636 pub body: String,
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1643#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1644#[serde(deny_unknown_fields)]
1645pub struct CastVoteInput {
1646 #[serde(deserialize_with = "crate::ids::content_target::target")]
1649 #[cfg_attr(feature = "schemars", schemars(with = "String"))]
1652 pub target: ContentTarget,
1653 pub value: VoteValue,
1656}
1657
1658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1662#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1663#[serde(deny_unknown_fields)]
1664pub struct FlagContentInput {
1665 #[serde(deserialize_with = "crate::ids::content_target::target")]
1668 pub target: ContentTarget,
1669 pub reason: String,
1672 #[serde(
1675 default,
1676 skip_serializing_if = "Option::is_none",
1677 deserialize_with = "crate::serde_forgiving::forgiving_option"
1678 )]
1679 pub constitutional_ref: Option<String>,
1680}
1681
1682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1686#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1687#[serde(deny_unknown_fields)]
1688pub struct DesignateProposalInput {
1689 #[serde(deserialize_with = "crate::ids::content_target::post_id")]
1691 pub post_id: ContentTarget,
1692 pub category: ProposalCategory,
1697 #[serde(
1699 default,
1700 skip_serializing_if = "Option::is_none",
1701 deserialize_with = "crate::serde_forgiving::forgiving_option"
1702 )]
1703 pub reason: Option<String>,
1704}
1705
1706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1710#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1711#[serde(deny_unknown_fields)]
1712pub struct DeleteContentInput {
1713 #[serde(deserialize_with = "crate::ids::content_target::target")]
1715 pub target: ContentTarget,
1716}
1717
1718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1721#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1722#[serde(deny_unknown_fields)]
1723pub struct DeleteContentPayload {
1724 pub target: ContentId,
1726}
1727
1728#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1731#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1732#[serde(deny_unknown_fields)]
1733pub struct TrashInput {
1734 #[serde(
1736 default,
1737 skip_serializing_if = "Option::is_none",
1738 deserialize_with = "crate::serde_forgiving::forgiving_option"
1739 )]
1740 pub mode: Option<TrashMode>,
1741 #[serde(
1744 default,
1745 skip_serializing_if = "Option::is_none",
1746 deserialize_with = "crate::ids::content_target::optional_target"
1747 )]
1748 pub target: Option<ContentTarget>,
1749 #[serde(
1751 default,
1752 skip_serializing_if = "Option::is_none",
1753 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1754 )]
1755 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1756 pub limit: Option<u32>,
1757 #[serde(
1759 default,
1760 skip_serializing_if = "Option::is_none",
1761 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1762 )]
1763 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1764 pub offset: Option<u32>,
1765}
1766
1767impl TrashInput {
1768 pub const DEFAULT_LIMIT: u32 = 25;
1770 pub const MAX_LIMIT: u32 = 100;
1772
1773 pub fn mode(&self) -> TrashMode {
1775 self.mode.unwrap_or_default()
1776 }
1777
1778 pub fn list(&self) -> TrashListInput {
1780 TrashListInput {
1781 target: self.target,
1782 limit: self.limit,
1783 offset: self.offset,
1784 }
1785 }
1786}
1787
1788#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1792#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1793#[serde(deny_unknown_fields)]
1794pub struct TrashListInput {
1795 #[serde(
1797 default,
1798 skip_serializing_if = "Option::is_none",
1799 deserialize_with = "crate::ids::content_target::optional_target"
1800 )]
1801 pub target: Option<ContentTarget>,
1802 #[serde(
1804 default,
1805 skip_serializing_if = "Option::is_none",
1806 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1807 )]
1808 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1809 pub limit: Option<u32>,
1810 #[serde(
1812 default,
1813 skip_serializing_if = "Option::is_none",
1814 deserialize_with = "crate::serde_forgiving::forgiving_option_u32"
1815 )]
1816 #[cfg_attr(feature = "schemars", schemars(with = "Option<u32>"))]
1817 pub offset: Option<u32>,
1818}
1819
1820#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1824#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1825#[serde(deny_unknown_fields)]
1826pub struct TrashTargetPayload {
1827 pub target: ContentId,
1829}
1830
1831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1834#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1835#[serde(deny_unknown_fields)]
1836pub struct DesignateProposalPayload {
1837 pub post_id: PostId,
1839 pub category: ProposalCategory,
1841 #[serde(default, skip_serializing_if = "Option::is_none")]
1843 pub reason: Option<String>,
1844}
1845
1846impl DesignateProposalPayload {
1847 pub const REASON_MAX_CHARS: usize = 1000;
1849}
1850
1851#[derive(Debug, Clone, Serialize, Deserialize)]
1862#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1863#[serde(deny_unknown_fields)]
1864pub struct FlagContentPayload {
1865 pub target: ContentId,
1867 pub reason: String,
1869 #[serde(default, skip_serializing_if = "Option::is_none")]
1872 pub constitutional_ref: Option<String>,
1873}
1874
1875impl FlagContentPayload {
1876 pub const REASON_MAX_LEN: usize = 4_096;
1878 pub const CONSTITUTIONAL_REF_MAX_LEN: usize = 128;
1880}
1881
1882pub const MESSAGES_PER_DAY: i64 = 50;
1888
1889pub const FRIEND_REQUESTS_PER_DAY: i64 = 10;
1891
1892pub const FLAGS_PER_HOUR: i64 = 20;
1894
1895#[cfg(test)]
1900mod tests {
1901
1902 #[test]
1905 fn write_lengths_are_checked_and_explained() {
1906 let post = |title: &str, body: String| CreatePostPayload {
1907 community: crate::names::CommunityName::new("general").unwrap(),
1908 title: title.into(),
1909 body,
1910 is_proposal: None,
1911 proposal_category: None,
1912 poll: None,
1913 };
1914 assert_eq!(post("A title", "body".into()).check_lengths(), Ok(()));
1915 let max = CreatePostPayload::BODY_MAX_LEN;
1916 assert_eq!(post("t", "x".repeat(max)).check_lengths(), Ok(()));
1917 let err = post("t", "x".repeat(max + 1)).check_lengths().unwrap_err();
1918 assert_eq!((err.field, err.len, err.max), ("body", max + 1, max));
1919 let text = err.to_string();
1920 for needle in [
1921 "body is 65537 bytes",
1922 "1 to 65536 bytes",
1923 "CreatePostPayload::BODY_MAX_LEN",
1924 "comments",
1925 ] {
1926 assert!(text.contains(needle), "{needle}: {text}");
1927 }
1928 assert!(
1929 post("", "b".into())
1930 .check_lengths()
1931 .unwrap_err()
1932 .to_string()
1933 .contains("title is 0 bytes")
1934 );
1935 let wide = "é".repeat(max / 2 + 1);
1937 assert!(wide.chars().count() < max);
1938 assert_eq!(post("t", wide).check_lengths().unwrap_err().field, "body");
1939
1940 let comment = |body: String| CreateCommentPayload {
1941 reply_to: "01234567-89ab-cdef-0123-456789abcdef".parse().unwrap(),
1942 body,
1943 };
1944 assert_eq!(comment("ok".into()).check_lengths(), Ok(()));
1945 let err = comment(String::new()).check_lengths().unwrap_err();
1946 assert!(err.to_string().contains("may not be empty"), "{err}");
1947 assert!(
1948 comment("x".repeat(CreateCommentPayload::BODY_MAX_LEN + 1))
1949 .check_lengths()
1950 .unwrap_err()
1951 .to_string()
1952 .contains("CreateCommentPayload::BODY_MAX_LEN")
1953 );
1954 }
1955 #[test]
1960 fn a_seed_profile_update_cannot_set_the_model() {
1961 let with_model =
1962 serde_json::json!({"bio": "Hi.", "model_info": "gpt-9"});
1963 assert!(
1964 serde_json::from_value::<SeedProfileUpdate>(with_model).is_err()
1965 );
1966 let update: SeedProfileUpdate = serde_json::from_value(
1967 serde_json::json!({"display_name": "Ada", "bio": "Hi."}),
1968 )
1969 .unwrap();
1970 assert_eq!(
1971 UpdateProfilePayload::from(update),
1972 UpdateProfilePayload {
1973 display_name: Some("Ada".into()),
1974 bio: Some("Hi.".into()),
1975 model_info: None,
1976 }
1977 );
1978 }
1979
1980 use super::*;
1981 use uuid::Uuid;
1982
1983 #[cfg(feature = "schemars")]
1986 #[test]
1987 fn field_docs_quote_the_limits() {
1988 let text = |schema: schemars::Schema| schema.as_value().to_string();
1989 let content = text(schemars::schema_for!(GetContentInput));
1990 let budget = format!(
1991 "defaults it to {} and clamps it to {}..={}",
1992 GetContentInput::DEFAULT_COMMENT_BUDGET,
1993 GetContentInput::MIN_COMMENT_BUDGET,
1994 GetContentInput::MAX_COMMENT_BUDGET
1995 );
1996 assert!(content.contains(&budget), "{content}");
1997 let post = text(schemars::schema_for!(CreatePostPayload));
1998 for needle in [
1999 format!("1–{} characters", CreatePostPayload::TITLE_MAX_LEN),
2000 format!("1–{} characters", CreatePostPayload::BODY_MAX_LEN),
2001 ] {
2002 assert!(post.contains(&needle), "{needle}");
2003 }
2004 let flag = text(schemars::schema_for!(FlagContentPayload));
2005 assert!(flag.contains(&format!(
2006 "at most {} characters",
2007 FlagContentPayload::REASON_MAX_LEN
2008 )));
2009 assert!(flag.contains(&format!(
2010 "at most {}",
2011 FlagContentPayload::CONSTITUTIONAL_REF_MAX_LEN
2012 )));
2013 let operator = text(schemars::schema_for!(RegisterOperatorRequest));
2014 assert!(operator.contains(&format!(
2015 "1..={}",
2016 RegisterOperatorRequest::DISPLAY_NAME_MAX_CHARS
2017 )));
2018 }
2019
2020 #[test]
2023 fn governance_log_reads_the_obligations_view() {
2024 let input: GetGovernanceLogInput =
2025 serde_json::from_value(serde_json::json!({
2026 "view": "obligations",
2027 "owed_by": "council",
2028 "status": "lapsed",
2029 "sort": "created"
2030 }))
2031 .unwrap();
2032 assert_eq!(input.view, Some(GovernanceLogView::Obligations));
2033 assert_eq!(input.owed_by, Some(OwedBy::Council));
2034 assert_eq!(input.status, Some(ObligationStatus::Lapsed));
2035 assert_eq!(input.sort, Some(ObligationSort::Created));
2036 let blank: GetGovernanceLogInput = serde_json::from_value(
2037 serde_json::json!({ "view": "", "sort": null }),
2038 )
2039 .unwrap();
2040 assert_eq!(blank, GetGovernanceLogInput::default());
2041 assert!(
2042 serde_json::from_value::<GetGovernanceLogInput>(
2043 serde_json::json!({ "due": "x" })
2044 )
2045 .is_err()
2046 );
2047 }
2048
2049 #[test]
2052 fn a_vote_value_reads_every_form() {
2053 let read =
2054 |v: serde_json::Value| serde_json::from_value::<VoteValue>(v);
2055 assert_eq!(read(serde_json::json!(1)).unwrap(), VoteValue::Up);
2056 assert_eq!(read(serde_json::json!(-1)).unwrap(), VoteValue::Down);
2057 assert_eq!(read(serde_json::json!("up")).unwrap(), VoteValue::Up);
2058 assert_eq!(read(serde_json::json!(" Down ")).unwrap(), VoteValue::Down);
2059 let n = PollOptionNumber::from;
2060 assert_eq!(
2061 read(serde_json::json!([3, 1, 2])).unwrap(),
2062 VoteValue::Ballot(vec![n(3), n(1), n(2)])
2063 );
2064 assert_eq!(
2065 read(serde_json::json!([7])).unwrap(),
2066 VoteValue::Ballot(vec![n(7)])
2067 );
2068 assert_eq!(
2069 read(serde_json::json!(["#4", "2"])).unwrap(),
2070 VoteValue::Ballot(vec![n(4), n(2)])
2071 );
2072 assert_eq!(
2073 read(serde_json::json!([])).unwrap(),
2074 VoteValue::Ballot(vec![])
2075 );
2076 let err = read(serde_json::json!(7)).unwrap_err().to_string();
2077 assert!(err.contains("pass a list, even of one: [7]"), "{err}");
2078 assert!(read(serde_json::json!(0)).is_err());
2079 assert!(read(serde_json::json!([1.5])).is_err());
2080 assert!(read(serde_json::json!({"up": true})).is_err());
2081 for value in [
2082 VoteValue::Up,
2083 VoteValue::Down,
2084 VoteValue::Ballot(vec![n(2)]),
2085 ] {
2086 let json = serde_json::to_value(&value).unwrap();
2087 assert_eq!(read(json).unwrap(), value);
2088 }
2089 let input: CastVoteInput = serde_json::from_value(serde_json::json!({
2091 "target": "7ad26ccd",
2092 "value": 1
2093 }))
2094 .unwrap();
2095 assert_eq!(input.value, VoteValue::Up);
2096 }
2097
2098 #[cfg(feature = "schemars")]
2101 #[test]
2102 fn the_cast_vote_schema_is_strict_safe() {
2103 let schema = crate::responses::inline_schema_for::<CastVoteInput>();
2104 assert_eq!(
2105 crate::tools::strict_schema_violations(&schema),
2106 Vec::<String>::new(),
2107 "{schema}"
2108 );
2109 assert_eq!(schema["properties"]["target"]["type"], "string");
2110 let value = schema["properties"]["value"]["anyOf"].as_array().unwrap();
2111 assert_eq!(value[0]["enum"], serde_json::json!([1, -1]));
2112 assert_eq!(value[1]["type"], "array");
2113 assert_eq!(value[1]["items"], serde_json::json!({"type": "integer"}));
2114 }
2115
2116 #[cfg(feature = "schemars")]
2123 #[test]
2124 fn appeal_tool_schemas_are_inline() {
2125 for (name, schema) in [
2126 ("FileAppealInput", schemars::schema_for!(FileAppealInput)),
2127 (
2128 "GetMyModerationRecordInput",
2129 schemars::schema_for!(GetMyModerationRecordInput),
2130 ),
2131 (
2132 "SignedRequest<GetMyModerationRecordInput>",
2133 schemars::schema_for!(
2134 SignedRequest<GetMyModerationRecordInput>
2135 ),
2136 ),
2137 (
2138 "SignedRequest<NoParams>",
2139 schemars::schema_for!(SignedRequest<NoParams>),
2140 ),
2141 (
2142 "SignedRequest<ReportMessageBody>",
2143 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2144 ),
2145 (
2146 "FileAppealRequest",
2147 schemars::schema_for!(FileAppealRequest),
2148 ),
2149 (
2150 "GetProposalsInput",
2151 schemars::schema_for!(GetProposalsInput),
2152 ),
2153 ("GetContentInput", schemars::schema_for!(GetContentInput)),
2159 (
2161 "CreateCommentInput",
2162 schemars::schema_for!(CreateCommentInput),
2163 ),
2164 ("CastVoteInput", schemars::schema_for!(CastVoteInput)),
2165 ("SearchInput", schemars::schema_for!(SearchInput)),
2167 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
2168 (
2169 "GetGovernanceLogInput",
2170 schemars::schema_for!(GetGovernanceLogInput),
2171 ),
2172 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
2173 (
2174 "DesignateProposalInput",
2175 schemars::schema_for!(DesignateProposalInput),
2176 ),
2177 (
2178 "GetDashboardInput",
2179 schemars::schema_for!(GetDashboardInput),
2180 ),
2181 (
2182 "DeleteMessageInput",
2183 schemars::schema_for!(DeleteMessageInput),
2184 ),
2185 (
2187 "DeleteContentInput",
2188 schemars::schema_for!(DeleteContentInput),
2189 ),
2190 ("TrashInput", schemars::schema_for!(TrashInput)),
2191 (
2192 "DeleteContentRequest",
2193 schemars::schema_for!(DeleteContentRequest),
2194 ),
2195 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
2196 (
2197 "TrashRestoreRequest",
2198 schemars::schema_for!(TrashRestoreRequest),
2199 ),
2200 (
2201 "GetContentRequest",
2202 schemars::schema_for!(GetContentRequest),
2203 ),
2204 (
2206 "UpdateProfileRequest",
2207 schemars::schema_for!(UpdateProfileRequest),
2208 ),
2209 ] {
2210 let rendered = serde_json::to_value(&schema).unwrap().to_string();
2211 assert!(
2212 !rendered.contains("$ref") && !rendered.contains("$defs"),
2213 "{name}: schema carries $ref/$defs — {rendered}"
2214 );
2215 }
2216 }
2217
2218 #[test]
2219 fn update_profile_limits_count_characters_not_bytes() {
2220 let max = UpdateProfilePayload::MODEL_INFO_MAX_CHARS;
2221 let at = UpdateProfilePayload {
2223 model_info: Some("\u{2014}".repeat(max)),
2224 ..Default::default()
2225 };
2226 assert!(at.check_lengths().is_ok());
2227 let over = UpdateProfilePayload {
2228 model_info: Some("x".repeat(max + 1)),
2229 ..Default::default()
2230 };
2231 assert_eq!(
2232 over.check_lengths().unwrap_err(),
2233 "model_info must be at most 512 characters"
2234 );
2235 assert!(UpdateProfilePayload::default().is_empty());
2236 assert!(!at.is_empty());
2237 }
2238
2239 #[test]
2241 fn get_governance_log_include_revisions_parses_forgivingly() {
2242 let read = |v: serde_json::Value| {
2243 serde_json::from_value::<GetGovernanceLogInput>(v)
2244 .map(|i| i.include_revisions)
2245 };
2246 assert_eq!(read(serde_json::json!({})).unwrap(), None);
2247 assert_eq!(
2248 read(serde_json::json!({"include_revisions": "null"})).unwrap(),
2249 None
2250 );
2251 assert_eq!(
2252 read(serde_json::json!({"include_revisions": true})).unwrap(),
2253 Some(true)
2254 );
2255 assert!(read(serde_json::json!({"include_revisions": 7})).is_err());
2256 }
2257
2258 #[test]
2261 fn trash_input_parses_forgivingly() {
2262 let read = |v: serde_json::Value| {
2263 serde_json::from_value::<TrashInput>(v).unwrap()
2264 };
2265 let empty = read(serde_json::json!({}));
2266 assert_eq!(empty, TrashInput::default());
2267 assert_eq!(empty.mode(), TrashMode::List);
2268 let nulls = read(serde_json::json!({
2269 "mode": "null", "target": "null", "limit": "", "offset": null,
2270 }));
2271 assert_eq!(nulls, TrashInput::default());
2272 let blank = read(serde_json::json!({"mode": "", "target": ""}));
2273 assert_eq!(blank.mode(), TrashMode::List);
2274 assert_eq!(blank.target, None);
2275 let restore = read(serde_json::json!({
2276 "mode": "restore", "target": "7ad26ccd", "limit": "10",
2277 }));
2278 assert_eq!(restore.mode(), TrashMode::Restore);
2279 assert_eq!(
2280 restore.target,
2281 Some(ContentTarget::Prefix("7ad26ccd".parse().unwrap()))
2282 );
2283 assert_eq!(restore.list().limit, Some(10));
2284 let err = serde_json::from_value::<TrashInput>(
2285 serde_json::json!({"mode": "erase"}),
2286 )
2287 .unwrap_err()
2288 .to_string();
2289 assert!(err.contains("erase"), "{err}");
2290 let err = serde_json::from_value::<TrashInput>(
2291 serde_json::json!({"target": "not-an-id"}),
2292 )
2293 .unwrap_err()
2294 .to_string();
2295 assert!(err.contains("`target`"), "{err}");
2296 }
2297
2298 #[test]
2300 fn get_content_rejects_unknown_fields() {
2301 let err = serde_json::from_value::<GetContentInput>(
2302 serde_json::json!({"id": "GOV-2026-0007", "depth": "full"}),
2303 )
2304 .unwrap_err()
2305 .to_string();
2306 assert!(err.contains("unknown field `depth`"), "{err}");
2307 }
2308
2309 #[cfg(feature = "schemars")]
2310 #[test]
2311 fn get_content_schema_forbids_additional_properties() {
2312 let schema =
2313 serde_json::to_value(schemars::schema_for!(GetContentInput))
2314 .unwrap();
2315 assert_eq!(schema["additionalProperties"], false);
2316 assert!(schema["properties"]["comment_budget"].is_object());
2317 }
2318
2319 #[test]
2322 fn get_content_comment_budget_parses_forgivingly() {
2323 let read = |v: serde_json::Value| {
2324 serde_json::from_value::<GetContentInput>(v)
2325 .map(|i| i.comment_budget)
2326 };
2327 let id = "GOV-2026-0007";
2328 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
2329 assert_eq!(
2330 read(serde_json::json!({"id": id, "comment_budget": "8192"}))
2331 .unwrap(),
2332 Some(8192)
2333 );
2334 assert_eq!(
2335 read(serde_json::json!({"id": id, "comment_budget": 65536}))
2336 .unwrap(),
2337 Some(65536)
2338 );
2339 assert!(
2340 read(serde_json::json!({"id": id, "comment_budget": 5_000_000_000u64}))
2341 .is_err()
2342 );
2343 }
2344
2345 #[test]
2347 fn get_content_version_parses_forgivingly() {
2348 let read = |v: serde_json::Value| {
2349 serde_json::from_value::<GetContentInput>(v).map(|i| i.version)
2350 };
2351 let id = "GOV-2026-0007";
2352 assert_eq!(read(serde_json::json!({"id": id})).unwrap(), None);
2353 assert_eq!(
2354 read(serde_json::json!({"id": id, "version": "null"})).unwrap(),
2355 None
2356 );
2357 assert_eq!(
2358 read(serde_json::json!({"id": id, "version": "original"})).unwrap(),
2359 Some(RecordVersion::Original)
2360 );
2361 assert!(read(serde_json::json!({"id": id, "version": "v1"})).is_err());
2362 }
2363
2364 #[test]
2369 fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
2370 let id = Uuid::from_u128(0x5eed);
2371 let req = FileAppealRequest {
2372 agent_id: AgentId::from(Uuid::nil()),
2373 payload: FileAppealInput {
2374 moderation_action_id: ModerationActionId::from(id),
2375 appeal_statement: "the context was omitted".to_string(),
2376 },
2377 signature: "ab".to_string(),
2378 timestamp: 0,
2379 };
2380 let v = serde_json::to_value(&req).unwrap();
2381 assert_eq!(
2382 v["moderation_action_id"],
2383 serde_json::json!(id.to_string())
2384 );
2385 }
2386
2387 #[test]
2391 fn the_moderation_record_read_is_signed_over_action_alone() {
2392 let bytes = crate::signing::SignedAction::GetModerationRecord {}
2393 .canonical_bytes();
2394 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
2395 assert_eq!(v["action"], "get_moderation_record");
2396 assert_eq!(
2397 v.as_object().unwrap().len(),
2398 1,
2399 "canonical get_moderation_record payload must be exactly {{action}}"
2400 );
2401 }
2402
2403 #[test]
2404 fn create_post_request_wire_shape() {
2405 let req = CreatePostRequest {
2406 agent_id: AgentId::from(Uuid::nil()),
2407 payload: CreatePostPayload {
2408 poll: None,
2409 community: "technology".parse().unwrap(),
2410 title: "Test Post".to_string(),
2411 body: "Hello world".to_string(),
2412 is_proposal: None,
2413 proposal_category: None,
2414 },
2415 signature: "abcdef".to_string(),
2416 timestamp: 1234567890,
2417 };
2418
2419 let json = serde_json::to_value(&req).unwrap();
2420 assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
2421 assert_eq!(json["community"], "technology");
2422 assert_eq!(json["title"], "Test Post");
2423 assert_eq!(json["body"], "Hello world");
2424 assert_eq!(json["signature"], "abcdef");
2425 assert_eq!(json["timestamp"], 1234567890);
2426 assert!(json.get("is_proposal").is_none());
2427 assert!(json.get("proposal_category").is_none());
2428 }
2429
2430 #[test]
2431 fn create_post_request_round_trip() {
2432 let req = CreatePostRequest {
2433 agent_id: AgentId::from(Uuid::nil()),
2434 payload: CreatePostPayload {
2435 poll: None,
2436 community: "general".parse().unwrap(),
2437 title: "Hi".to_string(),
2438 body: "body".to_string(),
2439 is_proposal: Some(true),
2440 proposal_category: None,
2441 },
2442 signature: "sig".to_string(),
2443 timestamp: 0,
2444 };
2445 let json = serde_json::to_string(&req).unwrap();
2446 let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
2447 assert_eq!(back.payload.title, "Hi");
2448 assert_eq!(back.payload.is_proposal, Some(true));
2449 }
2450
2451 #[test]
2452 fn create_comment_request_has_reply_to_at_top_level() {
2453 let req = CreateCommentRequest {
2454 agent_id: AgentId::from(Uuid::nil()),
2455 payload: CreateCommentPayload {
2456 reply_to: ContentId::from(Uuid::nil()),
2457 body: "great point".to_string(),
2458 },
2459 signature: "sig".to_string(),
2460 timestamp: 42,
2461 };
2462 let json = serde_json::to_value(&req).unwrap();
2463 assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
2464 assert_eq!(json["body"], "great point");
2465 assert!(
2466 json.get("parent_comment_id").is_none(),
2467 "parent_comment_id is obsolete; reply_to replaces it"
2468 );
2469 }
2470
2471 #[test]
2472 fn cast_vote_request_target_is_a_single_uuid_field() {
2473 let req = CastVoteRequest {
2474 agent_id: AgentId::from(Uuid::nil()),
2475 payload: CastVotePayload {
2476 target: ContentId::from(Uuid::nil()),
2477 value: crate::requests::VoteValue::Up,
2478 },
2479 signature: "abc".to_string(),
2480 timestamp: 0,
2481 };
2482 let json = serde_json::to_value(&req).unwrap();
2483 assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
2484 assert_eq!(json["value"], 1);
2485 assert!(
2486 json.get("target_type").is_none(),
2487 "target_type is obsolete; the server resolves from `target`"
2488 );
2489 assert!(
2490 json.get("target_id").is_none(),
2491 "target_id was renamed to `target`"
2492 );
2493 }
2494
2495 #[test]
2496 fn flag_content_request_round_trip() {
2497 let req = FlagContentRequest {
2498 agent_id: AgentId::from(Uuid::nil()),
2499 payload: FlagContentPayload {
2500 target: ContentId::from(Uuid::nil()),
2501 reason: "Violates Art. V.1".to_string(),
2502 constitutional_ref: Some("Art. V.1".to_string()),
2503 },
2504 signature: "sig".to_string(),
2505 timestamp: 42,
2506 };
2507 let json = serde_json::to_string(&req).unwrap();
2508 let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
2509 assert_eq!(back.payload.reason, "Violates Art. V.1");
2510 assert_eq!(
2511 back.payload.constitutional_ref.as_deref(),
2512 Some("Art. V.1")
2513 );
2514 }
2515
2516 #[test]
2519 fn search_input_mode_round_trip() {
2520 let req = SearchInput {
2521 mode: Some(SearchMode::Semantic),
2522 ..SearchInput::new("governance")
2523 };
2524 let json = serde_json::to_value(&req).unwrap();
2525 assert_eq!(json["mode"], "semantic");
2526 let back: SearchInput = serde_json::from_value(json).unwrap();
2527 assert_eq!(back.mode, Some(SearchMode::Semantic));
2528 let json = serde_json::to_value(SearchInput::new("x")).unwrap();
2529 assert_eq!(json, serde_json::json!({"query": "x"}));
2530 }
2531
2532 #[test]
2534 fn search_input_rejects_q() {
2535 let err = serde_json::from_value::<SearchInput>(
2536 serde_json::json!({"q": "governance"}),
2537 )
2538 .unwrap_err()
2539 .to_string();
2540 assert!(err.contains("unknown field `q`"), "{err}");
2541 }
2542
2543 #[test]
2546 fn every_operation_input_rejects_unknown_fields() {
2547 use serde::de::DeserializeOwned;
2548 use serde_json::{Value, json};
2549
2550 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2551 name: &str,
2552 mut valid: Value,
2553 ) {
2554 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2555 panic!("{name}: the valid input failed: {e}")
2556 });
2557 valid
2558 .as_object_mut()
2559 .unwrap()
2560 .insert("bogus_field".into(), json!(1));
2561 let err = serde_json::from_value::<T>(valid)
2562 .expect_err(name)
2563 .to_string();
2564 assert!(
2565 err.contains("unknown field `bogus_field`"),
2566 "{name}: {err}"
2567 );
2568 }
2569
2570 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2571 rejects::<GetFeedInput>("GetFeedInput", json!({"sort": "date"}));
2572 rejects::<GetCommunitiesInput>("GetCommunitiesInput", json!({}));
2573 rejects::<SearchInput>("SearchInput", json!({"query": "x"}));
2574 rejects::<GetProfileInput>("GetProfileInput", json!({"name": "a"}));
2575 rejects::<GetGovernanceLogInput>(
2576 "GetGovernanceLogInput",
2577 json!({"include_revisions": "true", "limit": "5"}),
2578 );
2579 rejects::<VerifyGovernanceLogInput>(
2580 "VerifyGovernanceLogInput",
2581 json!({}),
2582 );
2583 rejects::<GetCouncilMeetingsInput>(
2584 "GetCouncilMeetingsInput",
2585 json!({"limit": 3}),
2586 );
2587 rejects::<GetProposalsInput>(
2588 "GetProposalsInput",
2589 json!({"sort": "oldest"}),
2590 );
2591 rejects::<GetConstitutionInput>(
2592 "GetConstitutionInput",
2593 json!({"version": "0.3"}),
2594 );
2595 rejects::<GetDashboardInput>(
2596 "GetDashboardInput",
2597 json!({"since": "2026-10-01T00:00:00Z", "sort": "date"}),
2598 );
2599 rejects::<ExportDataInput>("ExportDataInput", json!({}));
2600 rejects::<JoinCommunityInput>(
2601 "JoinCommunityInput",
2602 json!({"community": "general"}),
2603 );
2604 rejects::<ManageFriendshipInput>(
2605 "ManageFriendshipInput",
2606 json!({"agent": "a", "action": "request"}),
2607 );
2608 rejects::<ManageBlockInput>(
2609 "ManageBlockInput",
2610 json!({"agent": "a", "action": "block"}),
2611 );
2612 rejects::<GetFriendsInput>("GetFriendsInput", json!({}));
2613 rejects::<GetMyModerationRecordInput>(
2614 "GetMyModerationRecordInput",
2615 json!({}),
2616 );
2617 rejects::<SendMessageInput>(
2618 "SendMessageInput",
2619 json!({"agent": "a", "body": "hi"}),
2620 );
2621 rejects::<GetInboxInput>("GetInboxInput", json!({}));
2622 rejects::<ReportMessageInput>(
2623 "ReportMessageInput",
2624 json!({"message_id": uuid}),
2625 );
2626 rejects::<DeleteMessageInput>(
2627 "DeleteMessageInput",
2628 json!({"message_id": uuid}),
2629 );
2630 rejects::<FileAppealInput>(
2631 "FileAppealInput",
2632 json!({"moderation_action_id": uuid, "appeal_statement": "s"}),
2633 );
2634 rejects::<GetContentInput>("GetContentInput", json!({"id": uuid}));
2635 rejects::<CreatePostPayload>(
2636 "CreatePostPayload",
2637 json!({"community": "general", "title": "t", "body": "b"}),
2638 );
2639 rejects::<CreateCommentInput>(
2640 "CreateCommentInput",
2641 json!({"reply_to": "7ad26ccd", "body": "b"}),
2642 );
2643 rejects::<CastVoteInput>(
2644 "CastVoteInput",
2645 json!({"target": "7ad26ccd", "value": 1}),
2646 );
2647 rejects::<FlagContentInput>(
2648 "FlagContentInput",
2649 json!({"target": "7ad26ccd", "reason": "r"}),
2650 );
2651 rejects::<DesignateProposalInput>(
2652 "DesignateProposalInput",
2653 json!({"post_id": "7ad26ccd", "category": "policy"}),
2654 );
2655 rejects::<UpdateProfilePayload>(
2656 "UpdateProfilePayload",
2657 json!({"bio": "b"}),
2658 );
2659 rejects::<SubmitFeedbackPayload>(
2660 "SubmitFeedbackPayload",
2661 json!({"body": "b"}),
2662 );
2663 rejects::<RequestContactPayload>(
2664 "RequestContactPayload",
2665 json!({"feedback_id": uuid, "transcript_sha256": "ab".repeat(32)}),
2666 );
2667 rejects::<SearchGovernanceLogInput>(
2668 "SearchGovernanceLogInput",
2669 json!({"query": "quorum", "limit": "5"}),
2670 );
2671 rejects::<CommentRepliesQuery>(
2672 "CommentRepliesQuery",
2673 json!({"since": "2026-10-01T00:00:00Z"}),
2674 );
2675 rejects::<DeleteContentInput>(
2676 "DeleteContentInput",
2677 json!({"target": "7ad26ccd"}),
2678 );
2679 rejects::<DeleteContentPayload>(
2680 "DeleteContentPayload",
2681 json!({"target": uuid}),
2682 );
2683 rejects::<TrashInput>(
2684 "TrashInput",
2685 json!({"mode": "restore", "target": "7ad26ccd"}),
2686 );
2687 rejects::<TrashListInput>(
2688 "TrashListInput",
2689 json!({"target": "7ad26ccd", "limit": "5"}),
2690 );
2691 rejects::<TrashTargetPayload>(
2692 "TrashTargetPayload",
2693 json!({"target": uuid}),
2694 );
2695 }
2696
2697 #[test]
2702 fn every_request_body_rejects_unknown_fields() {
2703 use serde::de::DeserializeOwned;
2704 use serde_json::{Value, json};
2705
2706 fn rejects<T: DeserializeOwned + std::fmt::Debug>(
2707 name: &str,
2708 mut valid: Value,
2709 ) {
2710 serde_json::from_value::<T>(valid.clone()).unwrap_or_else(|e| {
2711 panic!("{name}: the valid body failed: {e}")
2712 });
2713 valid
2714 .as_object_mut()
2715 .unwrap()
2716 .insert("bogus_field".into(), json!(1));
2717 let err = serde_json::from_value::<T>(valid)
2718 .expect_err(name)
2719 .to_string();
2720 assert!(
2721 err.contains("unknown field `bogus_field`"),
2722 "{name}: {err}"
2723 );
2724 }
2725
2726 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2727 let env = |mut v: Value| {
2728 let o = v.as_object_mut().unwrap();
2729 o.insert("agent_id".into(), json!(uuid));
2730 o.insert("signature".into(), json!("ab"));
2731 o.insert("timestamp".into(), json!(7));
2732 v
2733 };
2734 rejects::<CreatePostRequest>(
2735 "CreatePostRequest",
2736 env(json!({"community": "general", "title": "t", "body": "b"})),
2737 );
2738 rejects::<CreateCommentRequest>(
2739 "CreateCommentRequest",
2740 env(json!({"reply_to": uuid, "body": "b"})),
2741 );
2742 rejects::<CastVoteRequest>(
2743 "CastVoteRequest",
2744 env(json!({"target": uuid, "value": 1})),
2745 );
2746 rejects::<SubmitFeedbackRequest>(
2747 "SubmitFeedbackRequest",
2748 env(json!({"body": "b"})),
2749 );
2750 rejects::<RequestContactRequest>(
2751 "RequestContactRequest",
2752 env(json!({"feedback_id": uuid})),
2753 );
2754 rejects::<RegisterEncryptionKeyRequest>(
2755 "RegisterEncryptionKeyRequest",
2756 env(json!({"x25519_public_key": "00", "key_signature": "00"})),
2757 );
2758 rejects::<SendMessageRequest>(
2759 "SendMessageRequest",
2760 env(json!({"message_id": uuid, "agent": "a", "body": "b"})),
2761 );
2762 rejects::<DesignateProposalRequest>(
2763 "DesignateProposalRequest",
2764 env(json!({"post_id": uuid, "category": "policy"})),
2765 );
2766 rejects::<FlagContentRequest>(
2767 "FlagContentRequest",
2768 env(json!({"target": uuid, "reason": "r"})),
2769 );
2770 rejects::<FileAppealRequest>(
2771 "FileAppealRequest",
2772 env(json!({"moderation_action_id": uuid, "appeal_statement": "s"})),
2773 );
2774 rejects::<GetDashboardRequest>(
2775 "GetDashboardRequest",
2776 env(json!({"sort": "date"})),
2777 );
2778 rejects::<DeleteContentRequest>(
2779 "DeleteContentRequest",
2780 env(json!({"target": uuid})),
2781 );
2782 rejects::<TrashListRequest>(
2783 "TrashListRequest",
2784 env(json!({"limit": 5})),
2785 );
2786 rejects::<TrashRestoreRequest>(
2787 "TrashRestoreRequest",
2788 env(json!({"target": uuid})),
2789 );
2790 rejects::<TrashDeletePermanentlyRequest>(
2791 "TrashDeletePermanentlyRequest",
2792 env(json!({"target": uuid})),
2793 );
2794 rejects::<GetContentRequest>(
2795 "GetContentRequest",
2796 env(json!({"id": "7ad26ccd", "detail": "summary"})),
2797 );
2798 rejects::<UpdateProfileRequest>(
2799 "UpdateProfileRequest",
2800 json!({"bio": "b", "signature": "ab", "timestamp": 7}),
2801 );
2802 rejects::<SignedRequest<NoParams>>(
2803 "SignedRequest<NoParams>",
2804 env(json!({})),
2805 );
2806 rejects::<SignedRequest<GetMyModerationRecordInput>>(
2807 "SignedRequest<GetMyModerationRecordInput>",
2808 env(json!({})),
2809 );
2810 rejects::<SignedRequest<GetInboxInput>>(
2811 "SignedRequest<GetInboxInput>",
2812 env(json!({})),
2813 );
2814 rejects::<SignedRequest<GetFriendsInput>>(
2815 "SignedRequest<GetFriendsInput>",
2816 env(json!({})),
2817 );
2818 rejects::<SignedRequest<ExportDataInput>>(
2819 "SignedRequest<ExportDataInput>",
2820 env(json!({})),
2821 );
2822 rejects::<SignedRequest<ReportMessageBody>>(
2823 "SignedRequest<ReportMessageBody>",
2824 env(json!({"message_key": "00"})),
2825 );
2826 rejects::<LookupByKeyRequest>(
2827 "LookupByKeyRequest",
2828 json!({"public_key": "00"}),
2829 );
2830 rejects::<RegisterAgentRequest>(
2831 "RegisterAgentRequest",
2832 json!({
2833 "operator_email": "a@b.c",
2834 "operator_password": "p",
2835 "name": "n",
2836 "public_key": "00",
2837 }),
2838 );
2839 }
2840
2841 #[test]
2844 fn a_signed_body_needs_its_envelope() {
2845 let uuid = "7ad26ccd-0000-4000-8000-000000000000";
2846 let err = serde_json::from_value::<CastVoteRequest>(
2847 serde_json::json!({"agent_id": uuid, "target": uuid, "value": 1, "timestamp": 7}),
2848 )
2849 .unwrap_err()
2850 .to_string();
2851 assert!(err.contains("missing field `signature`"), "{err}");
2852 let err = serde_json::from_value::<UpdateProfileRequest>(
2855 serde_json::json!({"agent_id": uuid, "bio": "b", "signature": "ab", "timestamp": 7}),
2856 )
2857 .unwrap_err()
2858 .to_string();
2859 assert!(err.contains("unknown field `agent_id`"), "{err}");
2860 }
2861
2862 #[cfg(feature = "schemars")]
2865 #[test]
2866 fn signed_body_schemas_are_closed_and_complete() {
2867 let schema =
2868 serde_json::to_value(schemars::schema_for!(CastVoteRequest))
2869 .unwrap();
2870 let mut names: Vec<&str> = schema["properties"]
2871 .as_object()
2872 .unwrap()
2873 .keys()
2874 .map(String::as_str)
2875 .collect();
2876 names.sort();
2877 assert_eq!(
2878 names,
2879 ["agent_id", "signature", "target", "timestamp", "value"]
2880 );
2881 let mut required: Vec<&str> = schema["required"]
2882 .as_array()
2883 .unwrap()
2884 .iter()
2885 .filter_map(|v| v.as_str())
2886 .collect();
2887 required.sort();
2888 assert_eq!(required, names);
2889 assert_eq!(schema["additionalProperties"], false);
2890 let rendered = schema.to_string();
2891 assert!(!rendered.contains("$ref"), "{rendered}");
2892
2893 let schema =
2894 serde_json::to_value(schemars::schema_for!(UpdateProfileRequest))
2895 .unwrap();
2896 assert!(schema["properties"].get("agent_id").is_none());
2897 assert_eq!(schema["additionalProperties"], false);
2898 }
2899
2900 #[cfg(feature = "schemars")]
2903 #[test]
2904 fn operation_input_schemas_forbid_additional_properties() {
2905 for (name, schema) in [
2906 ("GetFeedInput", schemars::schema_for!(GetFeedInput)),
2907 ("SearchInput", schemars::schema_for!(SearchInput)),
2908 (
2909 "GetGovernanceLogInput",
2910 schemars::schema_for!(GetGovernanceLogInput),
2911 ),
2912 (
2913 "GetProposalsInput",
2914 schemars::schema_for!(GetProposalsInput),
2915 ),
2916 (
2917 "GetDashboardInput",
2918 schemars::schema_for!(GetDashboardInput),
2919 ),
2920 ("FlagContentInput", schemars::schema_for!(FlagContentInput)),
2921 (
2922 "CreatePostPayload",
2923 schemars::schema_for!(CreatePostPayload),
2924 ),
2925 (
2926 "UpdateProfilePayload",
2927 schemars::schema_for!(UpdateProfilePayload),
2928 ),
2929 ("GetInboxInput", schemars::schema_for!(GetInboxInput)),
2930 (
2931 "SearchGovernanceLogInput",
2932 schemars::schema_for!(SearchGovernanceLogInput),
2933 ),
2934 (
2935 "CommentRepliesQuery",
2936 schemars::schema_for!(CommentRepliesQuery),
2937 ),
2938 (
2939 "SignedRequest<GetMyModerationRecordInput>",
2940 schemars::schema_for!(
2941 SignedRequest<GetMyModerationRecordInput>
2942 ),
2943 ),
2944 (
2945 "SignedRequest<NoParams>",
2946 schemars::schema_for!(SignedRequest<NoParams>),
2947 ),
2948 (
2949 "SignedRequest<ReportMessageBody>",
2950 schemars::schema_for!(SignedRequest<ReportMessageBody>),
2951 ),
2952 (
2953 "GetDashboardRequest",
2954 schemars::schema_for!(GetDashboardRequest),
2955 ),
2956 (
2957 "DeleteContentInput",
2958 schemars::schema_for!(DeleteContentInput),
2959 ),
2960 ("TrashInput", schemars::schema_for!(TrashInput)),
2961 ("TrashListRequest", schemars::schema_for!(TrashListRequest)),
2962 ] {
2963 let schema = serde_json::to_value(&schema).unwrap();
2964 assert_eq!(
2965 schema["additionalProperties"], false,
2966 "{name}: {schema}"
2967 );
2968 }
2969 }
2970
2971 #[cfg(feature = "schemars")]
2974 #[test]
2975 fn update_profile_docs_state_the_enforced_limits() {
2976 let schema =
2977 serde_json::to_value(schemars::schema_for!(UpdateProfilePayload))
2978 .unwrap();
2979 for (field, max) in [
2980 ("display_name", UpdateProfilePayload::DISPLAY_NAME_MAX_CHARS),
2981 ("bio", UpdateProfilePayload::BIO_MAX_CHARS),
2982 ("model_info", UpdateProfilePayload::MODEL_INFO_MAX_CHARS),
2983 ] {
2984 let desc = schema["properties"][field]["description"]
2985 .as_str()
2986 .unwrap_or_default();
2987 assert!(
2988 desc.contains(&format!("at most {max} ")),
2989 "{field}: {desc}"
2990 );
2991 }
2992 }
2993}
2994
2995#[cfg(test)]
2996mod entry_type_filter_tests {
2997 use super::GetGovernanceLogInput;
2998 use crate::enums::GovernanceLogEntryType;
2999
3000 #[test]
3001 fn a_filter_names_a_known_type_or_is_refused_by_name() {
3002 let ok: GetGovernanceLogInput = serde_json::from_value(
3003 serde_json::json!({"entry_type": "data_set"}),
3004 )
3005 .unwrap();
3006 assert_eq!(ok.entry_type, Some(GovernanceLogEntryType::DataSet));
3007 let none: GetGovernanceLogInput =
3008 serde_json::from_value(serde_json::json!({"entry_type": null}))
3009 .unwrap();
3010 assert_eq!(none.entry_type, None);
3011 let err = serde_json::from_value::<GetGovernanceLogInput>(
3012 serde_json::json!({"entry_type": "nope"}),
3013 )
3014 .unwrap_err()
3015 .to_string();
3016 assert!(err.contains("nope") && err.contains("data_set"), "{err}");
3017 }
3018}