From 46eb7fec8f00e4f60c68a52810fd2f9a6bfb9154 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 7 Feb 2025 03:33:49 +0800 Subject: [PATCH] feat: impl record parse --- README.MD | 2 +- mania/src/core/business/messaging_logic.rs | 55 ++++++++++++++ mania/src/core/event/message/mod.rs | 2 + mania/src/core/event/message/push_msg.rs | 20 +++-- .../core/event/message/record_c2c_download.rs | 64 ++++++++++++++++ .../event/message/record_group_download.rs | 63 ++++++++++++++++ mania/src/core/operation/system.rs | 74 +++++++++++++++++++ mania/src/message/entity.rs | 13 +++- mania/src/message/entity/record.rs | 60 +++++++++++++++ 9 files changed, 343 insertions(+), 10 deletions(-) create mode 100644 mania/src/core/event/message/record_c2c_download.rs create mode 100644 mania/src/core/event/message/record_group_download.rs create mode 100644 mania/src/message/entity/record.rs diff --git a/README.MD b/README.MD index b0a7a33..bf47786 100644 --- a/README.MD +++ b/README.MD @@ -23,7 +23,7 @@ |----------|:-------:|----------------------------|:-------:|:--------------|:-------:|:------------------|:-------:|:--------------------|:-------:| | Windows | 🔴 | QrCode | 🟢 | BounceFace | 🔴 | Poke | 🔴 | Captcha | 🔴 | | macOS | 🔴 | ~~Password~~ | 🔴 | Face | 🟡 [^1] | Recall | 🔴 | BotOnline | 🔴 | -| Linux | 🟢 | EasyLogin | 🟢 | File | 🟡[^1] | Leave Group | 🔴 | BotOffline | 🔴 | +| Linux | 🟢 | EasyLogin | 🟡 | File | 🟡[^1] | Leave Group | 🔴 | BotOffline | 🔴 | | | | UnusualDevice
Password | 🔴 | Forward | 🟡[^1] | Set Special Title | 🔴 | Message | 🔴 | | | | UnusualDevice
Easy | 🔴 | ~~GreyTip~~ | 🔴 | Kick Member | 🔴 | Poke | 🔴 | | | | ~~NewDeviceVerify~~ | 🔴 | GroupReaction | 🔴 | Mute Member | 🔴 | MessageRecall | 🔴 | diff --git a/mania/src/core/business/messaging_logic.rs b/mania/src/core/business/messaging_logic.rs index 5c5bdc1..3b0f35e 100644 --- a/mania/src/core/business/messaging_logic.rs +++ b/mania/src/core/business/messaging_logic.rs @@ -121,6 +121,61 @@ async fn messaging_logic_incoming( _ => None, } } + Entity::Record(ref mut record) => { + let node = || -> Option<_> { + record + .msg_info + .as_ref() + .and_then(|info| info.msg_info_body.first()) + .and_then(|node| node.index.clone()) + }; + let url_result = match (&record.msg_info, &record.audio_uuid) { + (Some(_), _) => match &chain.typ { + MessageType::Group(grp) => { + handle + .download_group_record_by_node( + grp.group_uin, + node(), + ) + .await + } + MessageType::Friend(_) | MessageType::Temp => { + handle.download_c2c_record_by_node(node()).await + } + _ => continue, + }, + (None, Some(uuid)) => match &chain.typ { + MessageType::Group(grp) => { + handle + .download_group_record_by_uuid( + grp.group_uin, + Some(uuid.clone()), + ) + .await + } + MessageType::Friend(_) | MessageType::Temp => { + handle + .download_c2c_record_by_uuid(Some(uuid.clone())) + .await + } + _ => continue, + }, + _ => { + tracing::error!( + "{:?} Missing msg_info or audio_uuid!", + record + ); + continue; + } + }; + record.audio_url = match url_result { + Ok(url) => url, + Err(e) => { + tracing::error!("Failed to download record: {:?}", e); + continue; + } + } + } _ => {} } } diff --git a/mania/src/core/event/message/mod.rs b/mania/src/core/event/message/mod.rs index 74d0aa5..acc87a3 100644 --- a/mania/src/core/event/message/mod.rs +++ b/mania/src/core/event/message/mod.rs @@ -4,3 +4,5 @@ pub mod image_c2c_download; pub mod image_group_download; pub mod multi_msg_download; pub mod push_msg; +pub mod record_c2c_download; +pub mod record_group_download; diff --git a/mania/src/core/event/message/push_msg.rs b/mania/src/core/event/message/push_msg.rs index ae98653..03928c1 100644 --- a/mania/src/core/event/message/push_msg.rs +++ b/mania/src/core/event/message/push_msg.rs @@ -76,16 +76,24 @@ impl ClientEvent for PushMessageEvent { PkgType::try_from(typ).map_err(|_| EventError::UnknownOlpushMessageTypeError(typ))?; let mut chain = MessageChain::default(); // FIXME: maybe exist better way to handle this match packet_type { - PkgType::PrivateMessage | PkgType::GroupMessage | PkgType::TempMessage => { - chain = MessagePacker::parse_chain(packet.message.expect("PushMsgBody is None")) + PkgType::PrivateMessage + | PkgType::GroupMessage + | PkgType::TempMessage + | PkgType::PrivateRecordMessage => { + chain = + MessagePacker::parse_chain(packet.message.ok_or_else(|| { + EventError::OtherError("PushMsgBody is None".to_string()) + })?) .map_err(|e| EventError::OtherError(format!("parse_chain failed: {}", e)))?; } PkgType::PrivateFileMessage => { chain = - MessagePacker::parse_private_file(packet.message.expect("PushMsgBody is None")) - .map_err(|e| { - EventError::OtherError(format!("parse_private_file failed: {}", e)) - })?; + MessagePacker::parse_private_file(packet.message.ok_or_else(|| { + EventError::OtherError("PushMsgBody is None".to_string()) + })?) + .map_err(|e| { + EventError::OtherError(format!("parse_file_chain failed: {}", e)) + })?; } // TODO: handle other message types _ => { diff --git a/mania/src/core/event/message/record_c2c_download.rs b/mania/src/core/event/message/record_c2c_download.rs new file mode 100644 index 0000000..0ad9d86 --- /dev/null +++ b/mania/src/core/event/message/record_c2c_download.rs @@ -0,0 +1,64 @@ +use crate::core::event::prelude::*; +use crate::core::protos::service::oidb::{ + C2cUserInfo, ClientMeta, CommonHead, DownloadExt, DownloadReq, IndexNode, MultiMediaReqHead, + Ntv2RichMediaReq, Ntv2RichMediaResp, SceneInfo, VideoDownloadExt, +}; + +#[commend("OidbSvcTrpcTcp.0x126d_200")] +#[derive(Debug, ServerEvent, Default)] +pub struct RecordC2CDownloadEvent { + pub self_uid: String, + pub node: Option, + pub file_uuid: String, + pub audio_url: String, +} + +impl ClientEvent for RecordC2CDownloadEvent { + fn build(&self, _: &Context) -> BinaryPacket { + let packet = dda!(Ntv2RichMediaReq { + req_head: Some(MultiMediaReqHead { + common: Some(CommonHead { + request_id: 1, + command: 200, + }), + scene: Some(dda!(SceneInfo { + request_type: 1, + business_type: 3, + scene_type: 1, + c2c: Some(C2cUserInfo { + account_type: 2, + target_uid: self.self_uid.to_owned(), + }), + })), + client: Some(ClientMeta { agent_type: 2 }), + }), + download: Some(DownloadReq { + node: Some(self.node.clone().unwrap_or(dda!(IndexNode { + file_uuid: self.file_uuid.to_owned(), // TODO: mut? + }),)), + download: Some(dda!(DownloadExt { + video: Some(dda!(VideoDownloadExt { + busi_type: 0, + scene_type: 0, + })) + })), + }), + }); + OidbPacket::new(0x126D, 200, packet.encode_to_vec(), false, true).to_binary() + } + + fn parse(packet: Bytes, _: &Context) -> Result, EventError> { + let packet = OidbPacket::parse_into::(packet)?; + let download = packet.download.ok_or(EventError::OtherError( + "Missing Ntv2RichMediaResp download response".to_string(), + ))?; + let info = download.info.as_ref().ok_or(EventError::OtherError( + "Missing Ntv2RichMediaResp download info".to_string(), + ))?; + let url = format!( + "https://{}{}{}", + info.domain, info.url_path, download.r_key_param + ); + Ok(Box::new(dda!(RecordC2CDownloadEvent { audio_url: url }))) + } +} diff --git a/mania/src/core/event/message/record_group_download.rs b/mania/src/core/event/message/record_group_download.rs new file mode 100644 index 0000000..69d07fa --- /dev/null +++ b/mania/src/core/event/message/record_group_download.rs @@ -0,0 +1,63 @@ +use crate::core::event::prelude::*; +use crate::core::protos::service::oidb::{ + ClientMeta, CommonHead, DownloadExt, DownloadReq, IndexNode, MultiMediaReqHead, NtGroupInfo, + Ntv2RichMediaReq, Ntv2RichMediaResp, SceneInfo, VideoDownloadExt, +}; + +#[commend("OidbSvcTrpcTcp.0x126e_200")] +#[derive(Debug, ServerEvent, Default)] +pub struct RecordGroupDownloadEvent { + pub group_uin: u32, + pub node: Option, + pub file_uuid: String, + pub audio_url: String, +} + +impl ClientEvent for RecordGroupDownloadEvent { + fn build(&self, _: &Context) -> BinaryPacket { + let packet = dda!(Ntv2RichMediaReq { + req_head: Some(MultiMediaReqHead { + common: Some(CommonHead { + request_id: 4, + command: 200, + }), + scene: Some(dda!(SceneInfo { + request_type: 1, + business_type: 3, + scene_type: 2, + group: Some(NtGroupInfo { + group_uin: self.group_uin, + }), + })), + client: Some(ClientMeta { agent_type: 2 }), + }), + download: Some(DownloadReq { + node: Some(self.node.clone().unwrap_or(dda!(IndexNode { + file_uuid: self.file_uuid.to_owned(), // TODO: mut? + }),)), + download: Some(dda!(DownloadExt { + video: Some(dda!(VideoDownloadExt { + busi_type: 0, + scene_type: 0, + })) + })), + }), + }); + OidbPacket::new(0x126E, 200, packet.encode_to_vec(), false, true).to_binary() + } + + fn parse(packet: Bytes, _: &Context) -> Result, EventError> { + let packet = OidbPacket::parse_into::(packet)?; + let download = packet.download.ok_or(EventError::OtherError( + "Missing Ntv2RichMediaResp download response".to_string(), + ))?; + let info = download.info.as_ref().ok_or(EventError::OtherError( + "Missing Ntv2RichMediaResp download info".to_string(), + ))?; + let url = format!( + "https://{}{}{}", + info.domain, info.url_path, download.r_key_param + ); + Ok(Box::new(dda!(RecordGroupDownloadEvent { audio_url: url }))) + } +} diff --git a/mania/src/core/operation/system.rs b/mania/src/core/operation/system.rs index 11e423c..1b876ce 100644 --- a/mania/src/core/operation/system.rs +++ b/mania/src/core/operation/system.rs @@ -4,6 +4,8 @@ use crate::core::event::message::file_group_download::FileGroupDownloadEvent; use crate::core::event::message::image_c2c_download::ImageC2CDownloadEvent; use crate::core::event::message::image_group_download::ImageGroupDownloadEvent; use crate::core::event::message::multi_msg_download::MultiMsgDownloadEvent; +use crate::core::event::message::record_c2c_download::RecordC2CDownloadEvent; +use crate::core::event::message::record_group_download::RecordGroupDownloadEvent; use crate::core::event::system::fetch_rkey::FetchRKeyEvent; use crate::core::event::{downcast_event, downcast_mut_event}; use crate::core::protos::service::oidb::IndexNode; @@ -102,4 +104,76 @@ impl BusinessHandle { let event: &mut FileC2CDownloadEvent = downcast_mut_event(&mut *res).unwrap(); Ok(event.file_url.to_owned()) } + + // TODO: Should parameter requests use an enum? + pub(crate) async fn download_group_record_by_node( + self: &Arc, + group_uin: u32, + node: Option, + ) -> crate::Result { + self.download_group_record_inner(group_uin, node, None) + .await + } + + pub(crate) async fn download_group_record_by_uuid( + self: &Arc, + group_uin: u32, + audio_uuid: Option, + ) -> crate::Result { + self.download_group_record_inner(group_uin, None, audio_uuid) + .await + } + + async fn download_group_record_inner( + self: &Arc, + group_uin: u32, + node: Option, + audio_uuid: Option, + ) -> crate::Result { + let mut event = dda!(RecordGroupDownloadEvent { + group_uin, + node, + file_uuid: audio_uuid.unwrap_or_default(), + }); + let mut res = self.send_event(&mut event).await?; + let event: &mut RecordGroupDownloadEvent = downcast_mut_event(&mut *res).unwrap(); + Ok(event.audio_url.to_owned()) + } + + // TODO: Should parameter requests use an enum? + pub(crate) async fn download_c2c_record_by_node( + self: &Arc, + node: Option, + ) -> crate::Result { + self.download_c2c_record_inner(node, None).await + } + + pub(crate) async fn download_c2c_record_by_uuid( + self: &Arc, + audio_uuid: Option, + ) -> crate::Result { + self.download_c2c_record_inner(None, audio_uuid).await + } + + async fn download_c2c_record_inner( + self: &Arc, + node: Option, + audio_uuid: Option, + ) -> crate::Result { + let self_uid = self + .context + .key_store + .uid + .load() + .as_ref() + .map(|arc| arc.as_ref().clone()); + let mut event = dda!(RecordC2CDownloadEvent { + self_uid: self_uid.expect("Missing self_uid"), + node, + file_uuid: audio_uuid.unwrap_or_default(), + }); + let mut res = self.send_event(&mut event).await?; + let event: &mut RecordC2CDownloadEvent = downcast_mut_event(&mut *res).unwrap(); + Ok(event.audio_url.to_owned()) + } } diff --git a/mania/src/message/entity.rs b/mania/src/message/entity.rs index da92104..3fbea0b 100644 --- a/mania/src/message/entity.rs +++ b/mania/src/message/entity.rs @@ -7,6 +7,7 @@ pub mod light_app; pub mod market_face; pub mod mention; pub mod multi_msg; +pub mod record; pub mod text; pub use face::FaceEntity as Face; @@ -18,6 +19,7 @@ pub use light_app::LightAppEntity as LightApp; pub use market_face::MarketFaceEntity as MarketFace; pub use mention::MentionEntity as Mention; pub use multi_msg::MultiMsgEntity as MultiMsg; +pub use record::RecordEntity as Record; pub use text::TextEntity as Text; use crate::core::protos::message::Elem; @@ -45,6 +47,7 @@ pub enum Entity { MultiMsg(multi_msg::MultiMsgEntity), Mention(mention::MentionEntity), File(file::FileEntity), + Record(record::RecordEntity), } macro_rules! impl_entity_show { @@ -106,10 +109,14 @@ macro_rules! impl_entity_unpack { } } -impl_entity_show!(Text, Json, Image, Face, Forward, MarketFace, LightApp, MultiMsg, Mention, File); -impl_entity_pack!(Text, Json, Image, Face, Forward, MarketFace, LightApp, MultiMsg, Mention, File); +impl_entity_show!( + Text, Json, Image, Face, Forward, MarketFace, LightApp, MultiMsg, Mention, File, Record +); +impl_entity_pack!( + Text, Json, Image, Face, Forward, MarketFace, LightApp, MultiMsg, Mention, File, Record +); impl_entity_unpack!( - Text, Json, Image, Face, Forward, MarketFace, LightApp, MultiMsg, Mention, File + Text, Json, Image, Face, Forward, MarketFace, LightApp, MultiMsg, Mention, File, Record ); impl Entity { diff --git a/mania/src/message/entity/record.rs b/mania/src/message/entity/record.rs new file mode 100644 index 0000000..b910c33 --- /dev/null +++ b/mania/src/message/entity/record.rs @@ -0,0 +1,60 @@ +use super::prelude::*; +use crate::core::protos::service::oidb::MsgInfo; + +#[derive(Default)] +pub struct RecordEntity { + pub audio_length: u32, + pub audio_md5: Bytes, + pub audio_name: String, + pub audio_url: String, + // TODO: stream + pub(crate) audio_uuid: Option, + pub(crate) file_sha1: Option, + pub(crate) msg_info: Option, + pub(crate) compat: Option, +} + +impl Debug for RecordEntity { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "[Record]: {}", self.audio_url) + } +} + +impl Display for RecordEntity { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "[语音]") + } +} + +impl MessageEntity for RecordEntity { + fn pack_element(&self) -> Vec { + todo!() + } + + fn unpack_element(elem: &Elem) -> Option { + let common_elem = elem.common_elem.as_ref()?; + match (common_elem.business_type, common_elem.service_type) { + (22 | 12, 48) => { + let extra = MsgInfo::decode(&*common_elem.pb_elem).ok()?; + let index = &extra.msg_info_body.first()?.index.as_ref()?; + let (uuid, name, sha1) = ( + &index.file_uuid, + &index.info.as_ref()?.file_name, + &index.info.as_ref()?.file_hash, + ); + { + let md5 = Bytes::from(hex::decode(sha1).ok()?); + Some(dda!(Self { + audio_uuid: Some(uuid.to_owned()), + audio_name: name.to_owned(), + audio_md5: md5, + audio_length: index.info.as_ref()?.time, + file_sha1: Some(sha1.to_owned()), + msg_info: Some(extra.to_owned()), + })) + } + } + _ => None, + } + } +}