use std::{
collections::HashMap,
io::{BufRead, Write},
sync::Arc,
};
use cookie_store::{
RawCookie,
serde::json::{load_all, save_incl_expired_and_nonpersistent},
};
use reqwest::{Client, Url};
use reqwest_cookie_store::CookieStoreMutex;
use serde::{Deserialize, Serialize};
use serde_json::json;
use time::OffsetDateTime;
use tracing::trace;
use crate::{XiaoaiResponse, conversation, login::Login, util::random_id};
const API_SERVER: &str = "https://api2.mina.mi.com/";
const API_UA: &str = "MiHome/6.0.103 (com.xiaomi.mihome; build:6.0.103.1; iOS 14.4.0) Alamofire/6.0.103 MICO/iOSApp/appStore/6.0.103";
#[derive(Clone, Debug)]
pub struct Xiaoai {
client: Client,
cookie_store: Arc<CookieStoreMutex>,
server: Url,
}
impl Xiaoai {
pub async fn login(username: &str, password: &str) -> crate::Result<Self> {
let login = Login::new(username, password)?;
let login_response = login.login().await?;
let auth_response = login.auth(login_response).await?;
login.get_token(auth_response).await?;
Self::from_login(login)
}
pub fn from_login(login: Login) -> crate::Result<Self> {
let cookie_store = login.into_cookie_store();
let client = Client::builder()
.user_agent(API_UA)
.cookie_provider(cookie_store.clone())
.build()?;
Ok(Self {
client,
cookie_store,
server: Url::parse(API_SERVER)?,
})
}
pub async fn device_info(&self) -> crate::Result<Vec<DeviceInfo>> {
self.raw_device_info().await?.extract_data()
}
pub async fn raw_device_info(&self) -> crate::Result<XiaoaiResponse> {
let response = self.get("admin/v2/device_list?master=0").await?;
trace!("获取到设备列表: {}", response.data);
Ok(response)
}
pub fn client(&self) -> &Client {
&self.client
}
pub async fn get(&self, uri: &str) -> crate::Result<XiaoaiResponse> {
let request_id = random_request_id();
let url =
Url::parse_with_params(self.server.join(uri)?.as_str(), [("requestId", request_id)])?;
let response = self
.client
.get(url)
.send()
.await?
.error_for_status()?
.json::<XiaoaiResponse>()
.await?
.error_for_code()?;
Ok(response)
}
pub async fn post(
&self,
uri: &str,
mut form: HashMap<&str, &str>,
) -> crate::Result<XiaoaiResponse> {
let request_id = random_request_id();
form.insert("requestId", &request_id);
let url = self.server.join(uri)?;
let response = self
.client
.post(url)
.form(&form)
.send()
.await?
.error_for_status()?
.json::<XiaoaiResponse>()
.await?
.error_for_code()?;
Ok(response)
}
pub fn save<W: Write>(&self, writer: &mut W) -> cookie_store::Result<()> {
save_incl_expired_and_nonpersistent(&self.cookie_store.lock().unwrap(), writer)
}
pub fn load<R: BufRead>(reader: R) -> cookie_store::Result<Self> {
let cookie_store = Arc::new(CookieStoreMutex::new(load_all(reader)?));
let client = Client::builder()
.user_agent(API_UA)
.cookie_provider(Arc::clone(&cookie_store))
.build()?;
Ok(Self {
client,
cookie_store,
server: Url::parse(API_SERVER)?,
})
}
pub async fn ubus_call(
&self,
device_id: &str,
path: &str,
method: &str,
message: &str,
) -> crate::Result<XiaoaiResponse> {
let form = HashMap::from([
("deviceId", device_id),
("method", method),
("path", path),
("message", message),
]);
self.post("remote/ubus", form).await
}
pub async fn tts(&self, device_id: &str, text: &str) -> crate::Result<XiaoaiResponse> {
let message = json!({"text": text}).to_string();
self.ubus_call(device_id, "mibrain", "text_to_speech", &message)
.await
}
pub async fn play_url(&self, device_id: &str, url: &str) -> crate::Result<XiaoaiResponse> {
let message = json!({
"url": url,
"type": 3,
"media": "app_ios"
})
.to_string();
self.ubus_call(device_id, "mediaplayer", "player_play_url", &message)
.await
}
pub async fn play_music(&self, device_id: &str, url: &str) -> crate::Result<XiaoaiResponse> {
const AUDIO_ID: &str = "1582971365183456177";
const ID: &str = "355454500";
let message = json!({
"startaudioid": AUDIO_ID,
"music": {
"payload": {
"audio_items": [
{
"item_id": {
"audio_id": AUDIO_ID,
"cp": {
"album_id": "-1",
"episode_index": 0,
"id": ID,
"name": "xiaowei",
},
},
"stream": {"url": url},
}
],
"list_params": {
"listId": "-1",
"loadmore_offset": 0,
"origin": "xiaowei",
"type": "MUSIC",
},
},
"play_behavior": "REPLACE_ALL",
}
})
.to_string();
self.ubus_call(device_id, "mediaplayer", "player_play_music", &message)
.await
}
pub async fn set_volume(&self, device_id: &str, volume: u32) -> crate::Result<XiaoaiResponse> {
let message = json!({
"volume": volume,
"media": "app_ios"
})
.to_string();
self.ubus_call(device_id, "mediaplayer", "player_set_volume", &message)
.await
}
pub async fn nlp(&self, device_id: &str, text: &str) -> crate::Result<XiaoaiResponse> {
let message = json!({
"tts": 1,
"nlp": 1,
"nlp_text": text
})
.to_string();
self.ubus_call(device_id, "mibrain", "ai_service", &message)
.await
}
pub async fn player_status(&self, device_id: &str) -> crate::Result<XiaoaiResponse> {
let message = json!({"media": "app_ios"}).to_string();
self.ubus_call(device_id, "mediaplayer", "player_get_play_status", &message)
.await
}
pub async fn set_play_state(
&self,
device_id: &str,
state: PlayState,
) -> crate::Result<XiaoaiResponse> {
let action = match state {
PlayState::Play => "play",
PlayState::Pause => "pause",
PlayState::Stop => "stop",
PlayState::Toggle => "toggle",
};
let message = json!({"action": action, "media": "app_ios"}).to_string();
self.ubus_call(device_id, "mediaplayer", "player_play_operation", &message)
.await
}
pub async fn conversations(
&self,
device_id: &str,
hardware: &str,
until: OffsetDateTime,
limit: u32,
) -> crate::Result<conversation::Data> {
let data_string: String = self
.raw_conversations(device_id, hardware, until, limit)
.await?
.extract_data()?;
let data = serde_json::from_str(&data_string)?;
Ok(data)
}
pub async fn raw_conversations(
&self,
device_id: &str,
hardware: &str,
until: OffsetDateTime,
limit: u32,
) -> crate::Result<XiaoaiResponse> {
let url = Url::parse_with_params(
"https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu",
&[
("hardware", hardware),
("timestamp", &(until.unix_timestamp() * 1000).to_string()),
("limit", &limit.to_string()),
],
)?;
let cookie = RawCookie::build(("deviceId", device_id))
.domain(url.domain().unwrap_or_default())
.build();
self.cookie_store
.lock()
.unwrap()
.insert_raw(&cookie, &url)?;
let response: XiaoaiResponse = self.client.get(url).send().await?.json().await?;
trace!("获取到对话记录: {}", response.data);
Ok(response)
}
}
#[derive(Clone, Debug)]
pub enum PlayState {
Play,
Pause,
Stop,
Toggle,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
#[serde(rename = "deviceID")]
pub device_id: String,
pub name: String,
pub hardware: String,
}
fn random_request_id() -> String {
let mut request_id = random_id(30);
request_id.insert_str(0, "app_ios_");
request_id
}