use serde::{Deserialize, Serialize};
use serde_repr::{Serialize_repr, Deserialize_repr};
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum MessageType {
User = 1,
Bot = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum MessageState {
New = 0,
Generating = 1,
Finish = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum MessageItemType {
Text = 1,
Image = 2,
Voice = 3,
File = 4,
Video = 5,
}
#[derive(Debug, Clone, Copy)]
#[repr(i32)]
pub enum MediaType {
Image = 1,
Video = 2,
File = 3,
Voice = 4,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CDNMedia {
pub encrypt_query_param: String,
pub aes_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypt_type: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub full_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextItem {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<CDNMedia>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumb_media: Option<CDNMedia>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aeskey: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mid_size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumb_width: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumb_height: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<CDNMedia>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encode_type: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playtime: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<CDNMedia>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub md5: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub len: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<CDNMedia>,
#[serde(skip_serializing_if = "Option::is_none")]
pub video_size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub play_length: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumb_media: Option<CDNMedia>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_item: Option<Box<WireMessageItem>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireMessageItem {
#[serde(rename = "type")]
pub item_type: MessageItemType,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_item: Option<TextItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_item: Option<ImageItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub voice_item: Option<VoiceItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_item: Option<FileItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub video_item: Option<VideoItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ref_msg: Option<RefMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireMessage {
pub from_user_id: String,
pub to_user_id: String,
pub client_id: String,
pub create_time_ms: i64,
pub message_type: MessageType,
pub message_state: MessageState,
pub context_token: String,
pub item_list: Vec<WireMessageItem>,
}
#[derive(Debug, Clone)]
pub struct IncomingMessage {
pub user_id: String,
pub text: String,
pub content_type: ContentType,
pub timestamp: SystemTime,
pub images: Vec<ImageContent>,
pub voices: Vec<VoiceContent>,
pub files: Vec<FileContent>,
pub videos: Vec<VideoContent>,
pub quoted: Option<QuotedMessage>,
pub raw: WireMessage,
pub(crate) context_token: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentType {
Text,
Image,
Voice,
File,
Video,
}
#[derive(Debug, Clone)]
pub struct ImageContent {
pub media: Option<CDNMedia>,
pub thumb_media: Option<CDNMedia>,
pub aes_key: Option<String>,
pub url: Option<String>,
pub width: Option<i32>,
pub height: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct VoiceContent {
pub media: Option<CDNMedia>,
pub text: Option<String>,
pub duration_ms: Option<i32>,
pub encode_type: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct FileContent {
pub media: Option<CDNMedia>,
pub file_name: Option<String>,
pub md5: Option<String>,
pub size: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct VideoContent {
pub media: Option<CDNMedia>,
pub thumb_media: Option<CDNMedia>,
pub duration_ms: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct QuotedMessage {
pub title: Option<String>,
pub text: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DownloadedMedia {
pub data: Vec<u8>,
pub media_type: String,
pub file_name: Option<String>,
pub format: Option<String>,
}
#[derive(Debug, Clone)]
pub struct UploadResult {
pub media: CDNMedia,
pub aes_key: [u8; 16],
pub encrypted_file_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credentials {
pub token: String,
#[serde(rename = "baseUrl")]
pub base_url: String,
#[serde(rename = "accountId")]
pub account_id: String,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub saved_at: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_type_values() {
assert_eq!(MessageType::User as i32, 1);
assert_eq!(MessageType::Bot as i32, 2);
}
#[test]
fn message_state_values() {
assert_eq!(MessageState::New as i32, 0);
assert_eq!(MessageState::Generating as i32, 1);
assert_eq!(MessageState::Finish as i32, 2);
}
#[test]
fn message_item_type_values() {
assert_eq!(MessageItemType::Text as i32, 1);
assert_eq!(MessageItemType::Image as i32, 2);
assert_eq!(MessageItemType::Voice as i32, 3);
assert_eq!(MessageItemType::File as i32, 4);
assert_eq!(MessageItemType::Video as i32, 5);
}
#[test]
fn wire_message_json_round_trip() {
let wire = WireMessage {
from_user_id: "user1".to_string(),
to_user_id: "bot1".to_string(),
client_id: "c1".to_string(),
create_time_ms: 1700000000000,
message_type: MessageType::User,
message_state: MessageState::Finish,
context_token: "ctx".to_string(),
item_list: vec![WireMessageItem {
item_type: MessageItemType::Text,
text_item: Some(TextItem { text: "hello".to_string() }),
image_item: None, voice_item: None, file_item: None, video_item: None, ref_msg: None,
}],
};
let json = serde_json::to_string(&wire).unwrap();
let decoded: WireMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.from_user_id, "user1");
assert_eq!(decoded.message_type, MessageType::User);
assert_eq!(decoded.item_list.len(), 1);
assert_eq!(decoded.item_list[0].text_item.as_ref().unwrap().text, "hello");
}
#[test]
fn credentials_json_camel_case() {
let creds = Credentials {
token: "tok".to_string(),
base_url: "https://api.example.com".to_string(),
account_id: "acc1".to_string(),
user_id: "uid1".to_string(),
saved_at: Some("2024-01-01T00:00:00Z".to_string()),
};
let json = serde_json::to_string(&creds).unwrap();
assert!(json.contains("\"baseUrl\""), "expected camelCase baseUrl");
assert!(json.contains("\"accountId\""), "expected camelCase accountId");
assert!(json.contains("\"userId\""), "expected camelCase userId");
let decoded: Credentials = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.token, "tok");
assert_eq!(decoded.base_url, "https://api.example.com");
}
#[test]
fn credentials_omits_none_saved_at() {
let creds = Credentials {
token: "tok".to_string(),
base_url: "https://api.example.com".to_string(),
account_id: "acc1".to_string(),
user_id: "uid1".to_string(),
saved_at: None,
};
let json = serde_json::to_string(&creds).unwrap();
assert!(!json.contains("saved_at"), "should omit None saved_at");
}
#[test]
fn cdn_media_json() {
let media = CDNMedia {
encrypt_query_param: "param=abc".to_string(),
aes_key: "key123".to_string(),
encrypt_type: Some(1),
full_url: None,
};
let json = serde_json::to_string(&media).unwrap();
let decoded: CDNMedia = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.encrypt_query_param, "param=abc");
assert_eq!(decoded.aes_key, "key123");
assert_eq!(decoded.encrypt_type, Some(1));
}
#[test]
fn wire_message_with_image() {
let wire = WireMessage {
from_user_id: "user1".to_string(),
to_user_id: "bot1".to_string(),
client_id: "c1".to_string(),
create_time_ms: 1700000000000,
message_type: MessageType::User,
message_state: MessageState::Finish,
context_token: "ctx".to_string(),
item_list: vec![WireMessageItem {
item_type: MessageItemType::Image,
text_item: None,
image_item: Some(ImageItem {
media: None, thumb_media: None,
aeskey: Some("key".to_string()),
url: Some("http://img.jpg".to_string()),
mid_size: Some(1024),
thumb_width: Some(100),
thumb_height: Some(200),
}),
voice_item: None, file_item: None, video_item: None, ref_msg: None,
}],
};
let json = serde_json::to_string(&wire).unwrap();
let decoded: WireMessage = serde_json::from_str(&json).unwrap();
let img = decoded.item_list[0].image_item.as_ref().unwrap();
assert_eq!(img.url, Some("http://img.jpg".to_string()));
assert_eq!(img.thumb_width, Some(100));
}
#[test]
fn content_type_equality() {
assert_eq!(ContentType::Text, ContentType::Text);
assert_ne!(ContentType::Text, ContentType::Image);
}
}