use serde::{Deserialize, Serialize};
pub const ILINK_BASE_URL: &str = "https://ilinkai.weixin.qq.com";
pub const ILINK_CDN_BASE_URL: &str = "https://novac2c.cdn.weixin.qq.com/c2c";
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BaseInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bot_agent: Option<String>,
}
impl Default for BaseInfo {
fn default() -> Self {
Self {
channel_version: Some(env!("CARGO_PKG_VERSION").to_string()),
bot_agent: Some(format!("ilink-hub/{}", env!("CARGO_PKG_VERSION"))),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetQrcodeResponse {
pub ret: i32,
pub qrcode: Option<String>,
pub qrcode_img_content: Option<String>,
pub errmsg: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct QrcodeStatusResponse {
pub ret: i32,
pub status: Option<String>,
pub bot_token: Option<String>,
pub baseurl: Option<String>,
pub ilink_bot_id: Option<String>,
pub ilink_user_id: Option<String>,
pub errmsg: Option<String>,
}
pub mod msg_type {
pub const TEXT: i32 = 1;
pub const IMAGE: i32 = 2;
pub const VOICE: i32 = 3;
pub const FILE: i32 = 4;
pub const VIDEO: i32 = 5;
}
pub mod message_state {
pub const FINISH: i32 = 2;
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TextItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MessageItem {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub item_type: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_item: Option<TextItem>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WeixinMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub seq: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub create_time_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_time_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_type: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_state: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub item_list: Option<Vec<MessageItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ilink_hub_ext: Option<HubExt>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HubExt {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cli_session_id: Option<String>,
}
impl WeixinMessage {
pub fn text(&self) -> Option<&str> {
self.item_list
.as_ref()?
.iter()
.find_map(|item| item.text_item.as_ref()?.text.as_deref())
}
pub fn build_text_reply(context_token: String, text: String) -> WeixinMessage {
let mut msg = WeixinMessage {
context_token: Some(context_token),
message_type: Some(2), message_state: Some(message_state::FINISH),
from_user_id: Some(String::new()),
client_id: Some(new_client_id()),
item_list: Some(vec![MessageItem {
item_type: Some(msg_type::TEXT),
text_item: Some(TextItem { text: Some(text) }),
extra: serde_json::Value::Object(Default::default()),
}]),
..Default::default()
};
msg.ensure_outbound();
msg
}
pub fn ensure_outbound(&mut self) {
self.from_user_id = Some(String::new());
if self.message_type.is_none() {
self.message_type = Some(2);
}
if self.message_state.is_none() {
self.message_state = Some(message_state::FINISH);
}
if self
.client_id
.as_ref()
.map(|s| s.is_empty())
.unwrap_or(true)
{
self.client_id = Some(new_client_id());
}
}
}
fn new_client_id() -> String {
format!("ilink-hub:{}", uuid::Uuid::new_v4())
}
#[cfg(test)]
mod outbound_tests {
use super::*;
#[test]
fn build_text_reply_sets_outbound_fields() {
let msg = WeixinMessage::build_text_reply("ctx".to_string(), "hi".to_string());
assert_eq!(msg.from_user_id.as_deref(), Some(""));
assert_eq!(msg.message_type, Some(2));
assert_eq!(msg.message_state, Some(message_state::FINISH));
assert!(msg.client_id.as_deref().unwrap().starts_with("ilink-hub:"));
}
#[test]
fn ensure_outbound_assigns_unique_client_id() {
let mut msg1 = WeixinMessage::default();
let mut msg2 = WeixinMessage::default();
msg1.ensure_outbound();
msg2.ensure_outbound();
assert_ne!(msg1.client_id, msg2.client_id);
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetUpdatesRequest {
#[serde(default)]
pub get_updates_buf: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_info: Option<BaseInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetUpdatesResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub ret: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errcode: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errmsg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msgs: Option<Vec<WeixinMessage>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub get_updates_buf: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SendMessageRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub msg: Option<WeixinMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_info: Option<BaseInfo>,
}
impl SendMessageRequest {
pub fn text(context_token: String, text: String) -> Self {
Self {
msg: Some(WeixinMessage::build_text_reply(context_token, text)),
base_info: Some(BaseInfo::default()),
}
}
pub fn reply(context_token: String, text: String, to_user_id: &str) -> Self {
Self::reply_text(context_token, text, to_user_id, None)
}
pub fn reply_text(
context_token: String,
text: String,
to_user_id: &str,
cli_session_id: Option<String>,
) -> Self {
let mut msg = WeixinMessage::build_text_reply(context_token, text);
if !to_user_id.is_empty() {
msg.to_user_id = Some(to_user_id.to_string());
}
if cli_session_id.is_some() {
msg.ilink_hub_ext = Some(HubExt {
cli_session_id,
..Default::default()
});
}
Self {
msg: Some(msg),
base_info: Some(BaseInfo::default()),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct SendMessageResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub ret: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errmsg: Option<String>,
}
impl SendMessageResponse {
pub fn ok() -> Self {
Self {
ret: Some(0),
errmsg: None,
}
}
pub fn err(code: i32, msg: impl Into<String>) -> Self {
Self {
ret: Some(code),
errmsg: Some(msg.into()),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GetConfigRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub ilink_user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_info: Option<BaseInfo>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GetConfigResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub ret: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errmsg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub typing_ticket: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct SendTypingRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub ilink_user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub typing_ticket: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_info: Option<BaseInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetUploadUrlRequest {
pub file_type: String,
pub file_size: u64,
pub file_md5: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetUploadUrlResponse {
pub ret: i32,
pub upload_url: Option<String>,
pub media_id: Option<String>,
pub errmsg: Option<String>,
}