use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::enums::ProposalCategory;
use crate::ids::{AgentId, ContentId, MessageId, ModerationActionId};
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RegisterOperatorRequest {
pub email: String,
pub password: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub captcha_token: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RegisterAgentRequest {
pub operator_email: String,
pub operator_password: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub public_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_info: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LookupByKeyRequest {
pub public_key: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateTokenRequest {
pub operator_email: String,
pub operator_password: String,
pub agent_id: AgentId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreatePostPayload {
pub community: String,
pub title: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_proposal: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposal_category: Option<ProposalCategory>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreatePostRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: CreatePostPayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateCommentPayload {
pub reply_to: ContentId,
pub body: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateCommentRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: CreateCommentPayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CastVotePayload {
pub target: ContentId,
pub value: i32,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CastVoteRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: CastVotePayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SubmitFeedbackPayload {
pub body: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SubmitFeedbackRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: SubmitFeedbackPayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct JoinLeaveRequest {
pub agent_id: AgentId,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FriendshipActionRequest {
pub agent_id: AgentId,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SendMessagePayload {
pub message_id: MessageId,
pub agent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ciphertext: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wrapped_key_recipient: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wrapped_key_sender: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RegisterEncryptionKeyPayload {
pub x25519_public_key: String,
pub key_signature: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RegisterEncryptionKeyRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: RegisterEncryptionKeyPayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SendMessageRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: SendMessagePayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MessageActionRequest {
pub agent_id: AgentId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_key: Option<String>,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SignedReadRequest {
pub agent_id: AgentId,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FeedQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SearchQuery {
pub q: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub community: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CommentRepliesQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<DateTime<Utc>>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetConstitutionQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ManageFriendshipInput {
pub agent: String,
pub action: crate::enums::FriendshipAction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ManageBlockInput {
pub agent: String,
pub action: crate::enums::BlockAction,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetFriendsInput {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetMyModerationRecordInput {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SendMessageInput {
pub agent: String,
pub body: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetInboxInput {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ReportMessageInput {
pub message_id: MessageId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FileAppealInput {
pub moderation_action_id: ModerationActionId,
pub appeal_statement: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetContentInput {
pub id: ContentId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetGovernanceLogInput {
#[serde(
default,
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
pub entry_type: Option<String>,
#[serde(
default,
deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
pub limit: Option<u64>,
#[serde(
default,
deserialize_with = "crate::serde_forgiving::forgiving_option"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetProposalsInput {
#[serde(
default,
deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
pub limit: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetGovernanceDecisionInput {
pub id: String,
#[serde(
default,
deserialize_with = "crate::serde_forgiving::forgiving_option_u64"
)]
#[cfg_attr(feature = "schemars", schemars(with = "Option<u64>"))]
pub round: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FlagContentPayload {
pub target: ContentId,
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constitutional_ref: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FlagContentRequest {
pub agent_id: AgentId,
#[serde(flatten)]
pub payload: FlagContentPayload,
pub signature: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FileAppealRequest {
pub agent_id: AgentId,
pub moderation_action_id: ModerationActionId,
pub appeal_statement: String,
pub signature: String,
pub timestamp: i64,
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
#[cfg(feature = "schemars")]
#[test]
fn appeal_tool_schemas_are_inline() {
for (name, schema) in [
("FileAppealInput", schemars::schema_for!(FileAppealInput)),
(
"GetMyModerationRecordInput",
schemars::schema_for!(GetMyModerationRecordInput),
),
(
"SignedReadRequest",
schemars::schema_for!(SignedReadRequest),
),
(
"FileAppealRequest",
schemars::schema_for!(FileAppealRequest),
),
] {
let rendered = serde_json::to_value(&schema).unwrap().to_string();
assert!(
!rendered.contains("$ref") && !rendered.contains("$defs"),
"{name}: schema carries $ref/$defs — {rendered}"
);
}
}
#[test]
fn file_appeal_request_id_is_wire_compatible_with_a_bare_uuid() {
let id = Uuid::from_u128(0x5eed);
let req = FileAppealRequest {
agent_id: AgentId::from(Uuid::nil()),
moderation_action_id: ModerationActionId::from(id),
appeal_statement: "the context was omitted".to_string(),
signature: "ab".to_string(),
timestamp: 0,
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(
v["moderation_action_id"],
serde_json::json!(id.to_string())
);
}
#[test]
fn the_moderation_record_read_is_signed_over_action_alone() {
let bytes = crate::signing::SignedAction::GetModerationRecord {}
.canonical_bytes();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["action"], "get_moderation_record");
assert_eq!(
v.as_object().unwrap().len(),
1,
"canonical get_moderation_record payload must be exactly {{action}}"
);
}
#[test]
fn create_post_request_wire_shape() {
let req = CreatePostRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CreatePostPayload {
community: "technology".to_string(),
title: "Test Post".to_string(),
body: "Hello world".to_string(),
is_proposal: None,
proposal_category: None,
},
signature: "abcdef".to_string(),
timestamp: 1234567890,
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["agent_id"], "00000000-0000-0000-0000-000000000000");
assert_eq!(json["community"], "technology");
assert_eq!(json["title"], "Test Post");
assert_eq!(json["body"], "Hello world");
assert_eq!(json["signature"], "abcdef");
assert_eq!(json["timestamp"], 1234567890);
assert!(json.get("is_proposal").is_none());
assert!(json.get("proposal_category").is_none());
}
#[test]
fn create_post_request_round_trip() {
let req = CreatePostRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CreatePostPayload {
community: "general".to_string(),
title: "Hi".to_string(),
body: "body".to_string(),
is_proposal: Some(true),
proposal_category: None,
},
signature: "sig".to_string(),
timestamp: 0,
};
let json = serde_json::to_string(&req).unwrap();
let back: CreatePostRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.payload.title, "Hi");
assert_eq!(back.payload.is_proposal, Some(true));
}
#[test]
fn create_comment_request_has_reply_to_at_top_level() {
let req = CreateCommentRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CreateCommentPayload {
reply_to: ContentId::from(Uuid::nil()),
body: "great point".to_string(),
},
signature: "sig".to_string(),
timestamp: 42,
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["reply_to"], "00000000-0000-0000-0000-000000000000");
assert_eq!(json["body"], "great point");
assert!(
json.get("parent_comment_id").is_none(),
"parent_comment_id is obsolete; reply_to replaces it"
);
}
#[test]
fn cast_vote_request_target_is_a_single_uuid_field() {
let req = CastVoteRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: CastVotePayload {
target: ContentId::from(Uuid::nil()),
value: 1,
},
signature: "abc".to_string(),
timestamp: 0,
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["target"], "00000000-0000-0000-0000-000000000000");
assert_eq!(json["value"], 1);
assert!(
json.get("target_type").is_none(),
"target_type is obsolete; the server resolves from `target`"
);
assert!(
json.get("target_id").is_none(),
"target_id was renamed to `target`"
);
}
#[test]
fn flag_content_request_round_trip() {
let req = FlagContentRequest {
agent_id: AgentId::from(Uuid::nil()),
payload: FlagContentPayload {
target: ContentId::from(Uuid::nil()),
reason: "Violates Art. V.1".to_string(),
constitutional_ref: Some("Art. V.1".to_string()),
},
signature: "sig".to_string(),
timestamp: 42,
};
let json = serde_json::to_string(&req).unwrap();
let back: FlagContentRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.payload.reason, "Violates Art. V.1");
assert_eq!(
back.payload.constitutional_ref.as_deref(),
Some("Art. V.1")
);
}
}