diff --git a/.gitignore b/.gitignore index 63398e9..584e3a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,35 @@ -.* -!.gitignore -!.editorconfig -!.github/ - -# VS2019 build files -Debug/ -Release/ -x64/ -Out/ - -# Generated files -*.pb.h -*.pb.c -*.pb.cc -*.dll -*.exe -build/ -logs/ -java/wcferry/bin/ - -*_pb2.py -*_pb2_grpc.py -__pycache__ -python/dist/ -python/*.egg-info - -java/target/ -WcfGrpc.java -WcfOuterClass.java +.* +!.gitignore +!.editorconfig +!.github/ + +# VS2019 build files +Debug/ +Release/ +x64/ +Out/ + +# Generated files +*.pb.h +*.pb.c +*.pb.cc +*.dll +*.exe +build/ +logs/ +java/wcferry/bin/ + +*_pb2.py +*_pb2_grpc.py +__pycache__ +python/dist/ +python/*.egg-info + +java/target/ +WcfGrpc.java +WcfOuterClass.java + +rust/target/ +rust/wcferry/Cargo.lock +rust/wcferry/.wcf.lock +/rust/wcferry/target/CACHEDIR.TAG diff --git a/rust/README.MD b/rust/README.MD new file mode 100644 index 0000000..1bbbce5 --- /dev/null +++ b/rust/README.MD @@ -0,0 +1 @@ +测试方法在 wechat.rs 中 diff --git a/rust/wcferry/Cargo.toml b/rust/wcferry/Cargo.toml new file mode 100644 index 0000000..d38aea0 --- /dev/null +++ b/rust/wcferry/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wcferry" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tonic = "0.8.3" +prost = "0.11.5" +nng = "1.0.1" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +log = "0.4.17" + +[build-dependencies] +tonic-build = "0.8.4" diff --git a/rust/wcferry/build.rs b/rust/wcferry/build.rs new file mode 100644 index 0000000..5946ae7 --- /dev/null +++ b/rust/wcferry/build.rs @@ -0,0 +1,9 @@ +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_client(true) + .build_server(false) + .out_dir("src/proto") + .compile(&["proto/wcf.proto"], &["."]) + .expect("failed to compile protos"); + Ok(()) +} diff --git a/rust/wcferry/lib/README.MD b/rust/wcferry/lib/README.MD new file mode 100644 index 0000000..973447c --- /dev/null +++ b/rust/wcferry/lib/README.MD @@ -0,0 +1 @@ +将最新的 dll、exe 放入此文件夹下 diff --git a/rust/wcferry/proto/README.MD b/rust/wcferry/proto/README.MD new file mode 100644 index 0000000..f27403d --- /dev/null +++ b/rust/wcferry/proto/README.MD @@ -0,0 +1 @@ +将最新的 wcf.proto 放入此文件夹下 diff --git a/rust/wcferry/proto/wcf.proto b/rust/wcferry/proto/wcf.proto new file mode 100644 index 0000000..dce93f6 --- /dev/null +++ b/rust/wcferry/proto/wcf.proto @@ -0,0 +1,158 @@ +syntax = "proto3"; + +package wcf; +option java_package = "com.iamteer"; + +/* +service Wcf { + rpc RpcIsLogin(Empty) returns (Response) {} + rpc RpcGetSelfWxid(Empty) returns (String) {} + rpc RpcEnableRecvMsg(Empty) returns (stream WxMsg) {} + rpc RpcDisableRecvMsg(Empty) returns (Response) {} + rpc RpcSendTextMsg(TextMsg) returns (Response) {} + rpc RpcSendPathMsg(PathMsg) returns (Response) {} + rpc RpcGetMsgTypes(Empty) returns (MsgTypes) {} + rpc RpcGetContacts(Empty) returns (Contacts) {} + rpc RpcGetDbNames(Empty) returns (DbNames) {} + rpc RpcGetDbTables(String) returns (DbTables) {} + rpc RpcExecDbQuery(DbQuery) returns (DbRows) {} + rpc RpcAcceptNewFriend(Verification) returns (Response) {} +} +*/ + +enum Functions { + FUNC_RESERVED = 0x00; + FUNC_IS_LOGIN = 0x01; + FUNC_GET_SELF_WXID = 0x10; + FUNC_GET_MSG_TYPES = 0x11; + FUNC_GET_CONTACTS = 0x12; + FUNC_GET_DB_NAMES = 0x13; + FUNC_GET_DB_TABLES = 0x14; + FUNC_SEND_TXT = 0x20; + FUNC_SEND_IMG = 0x21; + FUNC_SEND_FILE = 0x22; + FUNC_SEND_XML = 0x23; + FUNC_SEND_EMOTION = 0x24; + FUNC_ENABLE_RECV_TXT = 0x30; + FUNC_DISABLE_RECV_TXT = 0x40; + FUNC_EXEC_DB_QUERY = 0x50; + FUNC_ACCEPT_FRIEND = 0x51; + FUNC_ADD_ROOM_MEMBERS = 0x52; +} + +message Request +{ + Functions func = 1; + oneof msg + { + Empty empty = 2; + string str = 3; + TextMsg txt = 4; + PathMsg file = 5; + DbQuery query = 6; + Verification v = 7; + AddMembers m = 8; + XmlMsg xml = 9; + } +} + +message Response +{ + Functions func = 1; + oneof msg + { + int32 status = 2; + string str = 3; + WxMsg wxmsg = 4; + MsgTypes types = 5; + RpcContacts contacts = 6; + DbNames dbs = 7; + DbTables tables = 8; + DbRows rows = 9; + }; +} + +message Empty { } + +message WxMsg +{ + bool is_self = 1; // 是否自己发送的 + bool is_group = 2; // 是否群消息 + int32 type = 3; // 消息类型 + string id = 4; // 消息 id + string xml = 5; // 消息 xml + string sender = 6; // 消息发送者 + string roomid = 7; // 群 id(如果是群消息的话) + string content = 8; // 消息内容 +} + +message TextMsg +{ + string msg = 1; // 要发送的消息内容 + string receiver = 2; // 消息接收人,当为群时可@ + string aters = 3; // 要@的人列表,逗号分隔 +} + +message PathMsg +{ + string path = 1; // 要发送的图片的路径 + string receiver = 2; // 消息接收人 +} + +message XmlMsg +{ + string receiver = 1; // 消息接收人 + string content = 2; // xml 内容 + string path = 3; // 图片路径 + int32 type = 4; // 消息类型 +} + +message MsgTypes { map types = 1; } + +message RpcContact +{ + string wxid = 1; // 微信 id + string code = 2; // 微信号 + string name = 3; // 微信昵称 + string country = 4; // 国家 + string province = 5; // 省/州 + string city = 6; // 城市 + int32 gender = 7; // 性别 +} +message RpcContacts { repeated RpcContact contacts = 1; } + +message DbNames { repeated string names = 1; } + +message DbTable +{ + string name = 1; // 表名 + string sql = 2; // 建表 SQL +} +message DbTables { repeated DbTable tables = 1; } + +message DbQuery +{ + string db = 1; // 目标数据库 + string sql = 2; // 查询 SQL +} + +message DbField +{ + int32 type = 1; // 字段类型 + string column = 2; // 字段名称 + bytes content = 3; // 字段内容 +} +message DbRow { repeated DbField fields = 1; } +message DbRows { repeated DbRow rows = 1; } + +message Verification +{ + string v3 = 1; + string v4 = 2; +} + +message AddMembers +{ + string roomid = 1; // 要加的群ID + string wxids = 2; // 要加群的人列表,逗号分隔 +} diff --git a/rust/wcferry/src/main.rs b/rust/wcferry/src/main.rs new file mode 100644 index 0000000..5da9b1f --- /dev/null +++ b/rust/wcferry/src/main.rs @@ -0,0 +1,5 @@ +mod wechat; + +fn main() { + println!("Hello, wcferry!"); +} diff --git a/rust/wcferry/src/proto/wcf.rs b/rust/wcferry/src/proto/wcf.rs new file mode 100644 index 0000000..8d2dda1 --- /dev/null +++ b/rust/wcferry/src/proto/wcf.rs @@ -0,0 +1,315 @@ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Request { + #[prost(enumeration = "Functions", tag = "1")] + pub func: i32, + #[prost(oneof = "request::Msg", tags = "2, 3, 4, 5, 6, 7, 8, 9")] + pub msg: ::core::option::Option, +} +/// Nested message and enum types in `Request`. +pub mod request { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Msg { + #[prost(message, tag = "2")] + Empty(super::Empty), + #[prost(string, tag = "3")] + Str(::prost::alloc::string::String), + #[prost(message, tag = "4")] + Txt(super::TextMsg), + #[prost(message, tag = "5")] + File(super::PathMsg), + #[prost(message, tag = "6")] + Query(super::DbQuery), + #[prost(message, tag = "7")] + V(super::Verification), + #[prost(message, tag = "8")] + M(super::AddMembers), + #[prost(message, tag = "9")] + Xml(super::XmlMsg), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Response { + #[prost(enumeration = "Functions", tag = "1")] + pub func: i32, + #[prost(oneof = "response::Msg", tags = "2, 3, 4, 5, 6, 7, 8, 9")] + pub msg: ::core::option::Option, +} +/// Nested message and enum types in `Response`. +pub mod response { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Msg { + #[prost(int32, tag = "2")] + Status(i32), + #[prost(string, tag = "3")] + Str(::prost::alloc::string::String), + #[prost(message, tag = "4")] + Wxmsg(super::WxMsg), + #[prost(message, tag = "5")] + Types(super::MsgTypes), + #[prost(message, tag = "6")] + Contacts(super::RpcContacts), + #[prost(message, tag = "7")] + Dbs(super::DbNames), + #[prost(message, tag = "8")] + Tables(super::DbTables), + #[prost(message, tag = "9")] + Rows(super::DbRows), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Empty {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct WxMsg { + /// 是否自己发送的 + #[prost(bool, tag = "1")] + pub is_self: bool, + /// 是否群消息 + #[prost(bool, tag = "2")] + pub is_group: bool, + /// 消息类型 + #[prost(int32, tag = "3")] + pub r#type: i32, + /// 消息 id + #[prost(string, tag = "4")] + pub id: ::prost::alloc::string::String, + /// 消息 xml + #[prost(string, tag = "5")] + pub xml: ::prost::alloc::string::String, + /// 消息发送者 + #[prost(string, tag = "6")] + pub sender: ::prost::alloc::string::String, + /// 群 id(如果是群消息的话) + #[prost(string, tag = "7")] + pub roomid: ::prost::alloc::string::String, + /// 消息内容 + #[prost(string, tag = "8")] + pub content: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TextMsg { + /// 要发送的消息内容 + #[prost(string, tag = "1")] + pub msg: ::prost::alloc::string::String, + /// 消息接收人,当为群时可@ + #[prost(string, tag = "2")] + pub receiver: ::prost::alloc::string::String, + /// 要@的人列表,逗号分隔 + #[prost(string, tag = "3")] + pub aters: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PathMsg { + /// 要发送的图片的路径 + #[prost(string, tag = "1")] + pub path: ::prost::alloc::string::String, + /// 消息接收人 + #[prost(string, tag = "2")] + pub receiver: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct XmlMsg { + /// 消息接收人 + #[prost(string, tag = "1")] + pub receiver: ::prost::alloc::string::String, + /// xml 内容 + #[prost(string, tag = "2")] + pub content: ::prost::alloc::string::String, + /// 图片路径 + #[prost(string, tag = "3")] + pub path: ::prost::alloc::string::String, + /// 消息类型 + #[prost(int32, tag = "4")] + pub r#type: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgTypes { + #[prost(map = "int32, string", tag = "1")] + pub types: ::std::collections::HashMap, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RpcContact { + /// 微信 id + #[prost(string, tag = "1")] + pub wxid: ::prost::alloc::string::String, + /// 微信号 + #[prost(string, tag = "2")] + pub code: ::prost::alloc::string::String, + /// 微信昵称 + #[prost(string, tag = "3")] + pub name: ::prost::alloc::string::String, + /// 国家 + #[prost(string, tag = "4")] + pub country: ::prost::alloc::string::String, + /// 省/州 + #[prost(string, tag = "5")] + pub province: ::prost::alloc::string::String, + /// 城市 + #[prost(string, tag = "6")] + pub city: ::prost::alloc::string::String, + /// 性别 + #[prost(int32, tag = "7")] + pub gender: i32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RpcContacts { + #[prost(message, repeated, tag = "1")] + pub contacts: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbNames { + #[prost(string, repeated, tag = "1")] + pub names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbTable { + /// 表名 + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + /// 建表 SQL + #[prost(string, tag = "2")] + pub sql: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbTables { + #[prost(message, repeated, tag = "1")] + pub tables: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbQuery { + /// 目标数据库 + #[prost(string, tag = "1")] + pub db: ::prost::alloc::string::String, + /// 查询 SQL + #[prost(string, tag = "2")] + pub sql: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbField { + /// 字段类型 + #[prost(int32, tag = "1")] + pub r#type: i32, + /// 字段名称 + #[prost(string, tag = "2")] + pub column: ::prost::alloc::string::String, + /// 字段内容 + #[prost(bytes = "vec", tag = "3")] + pub content: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbRow { + #[prost(message, repeated, tag = "1")] + pub fields: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DbRows { + #[prost(message, repeated, tag = "1")] + pub rows: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Verification { + #[prost(string, tag = "1")] + pub v3: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub v4: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddMembers { + /// 要加的群ID + #[prost(string, tag = "1")] + pub roomid: ::prost::alloc::string::String, + /// 要加群的人列表,逗号分隔 + #[prost(string, tag = "2")] + pub wxids: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Functions { + FuncReserved = 0, + FuncIsLogin = 1, + FuncGetSelfWxid = 16, + FuncGetMsgTypes = 17, + FuncGetContacts = 18, + FuncGetDbNames = 19, + FuncGetDbTables = 20, + FuncSendTxt = 32, + FuncSendImg = 33, + FuncSendFile = 34, + FuncSendXml = 35, + FuncSendEmotion = 36, + FuncEnableRecvTxt = 48, + FuncDisableRecvTxt = 64, + FuncExecDbQuery = 80, + FuncAcceptFriend = 81, + FuncAddRoomMembers = 82, +} +impl Functions { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Functions::FuncReserved => "FUNC_RESERVED", + Functions::FuncIsLogin => "FUNC_IS_LOGIN", + Functions::FuncGetSelfWxid => "FUNC_GET_SELF_WXID", + Functions::FuncGetMsgTypes => "FUNC_GET_MSG_TYPES", + Functions::FuncGetContacts => "FUNC_GET_CONTACTS", + Functions::FuncGetDbNames => "FUNC_GET_DB_NAMES", + Functions::FuncGetDbTables => "FUNC_GET_DB_TABLES", + Functions::FuncSendTxt => "FUNC_SEND_TXT", + Functions::FuncSendImg => "FUNC_SEND_IMG", + Functions::FuncSendFile => "FUNC_SEND_FILE", + Functions::FuncSendXml => "FUNC_SEND_XML", + Functions::FuncSendEmotion => "FUNC_SEND_EMOTION", + Functions::FuncEnableRecvTxt => "FUNC_ENABLE_RECV_TXT", + Functions::FuncDisableRecvTxt => "FUNC_DISABLE_RECV_TXT", + Functions::FuncExecDbQuery => "FUNC_EXEC_DB_QUERY", + Functions::FuncAcceptFriend => "FUNC_ACCEPT_FRIEND", + Functions::FuncAddRoomMembers => "FUNC_ADD_ROOM_MEMBERS", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "FUNC_RESERVED" => Some(Self::FuncReserved), + "FUNC_IS_LOGIN" => Some(Self::FuncIsLogin), + "FUNC_GET_SELF_WXID" => Some(Self::FuncGetSelfWxid), + "FUNC_GET_MSG_TYPES" => Some(Self::FuncGetMsgTypes), + "FUNC_GET_CONTACTS" => Some(Self::FuncGetContacts), + "FUNC_GET_DB_NAMES" => Some(Self::FuncGetDbNames), + "FUNC_GET_DB_TABLES" => Some(Self::FuncGetDbTables), + "FUNC_SEND_TXT" => Some(Self::FuncSendTxt), + "FUNC_SEND_IMG" => Some(Self::FuncSendImg), + "FUNC_SEND_FILE" => Some(Self::FuncSendFile), + "FUNC_SEND_XML" => Some(Self::FuncSendXml), + "FUNC_SEND_EMOTION" => Some(Self::FuncSendEmotion), + "FUNC_ENABLE_RECV_TXT" => Some(Self::FuncEnableRecvTxt), + "FUNC_DISABLE_RECV_TXT" => Some(Self::FuncDisableRecvTxt), + "FUNC_EXEC_DB_QUERY" => Some(Self::FuncExecDbQuery), + "FUNC_ACCEPT_FRIEND" => Some(Self::FuncAcceptFriend), + "FUNC_ADD_ROOM_MEMBERS" => Some(Self::FuncAddRoomMembers), + _ => None, + } + } +} diff --git a/rust/wcferry/src/wechat.rs b/rust/wcferry/src/wechat.rs new file mode 100644 index 0000000..abfb1a0 --- /dev/null +++ b/rust/wcferry/src/wechat.rs @@ -0,0 +1,761 @@ +use std::{env, path::PathBuf, process::Command, time::Duration, vec}; + +use log::{error, info, warn}; +use nng::options::{Options, RecvTimeout}; +use prost::Message; +use std::collections::HashMap; + +const DEFAULT_URL: &'static str = "tcp://127.0.0.1:10086"; +const LISTEN_URL: &'static str = "tcp://127.0.0.1:10087"; + +pub mod wcf { + include!("proto/wcf.rs"); +} + +#[derive(Clone, Debug)] +pub struct WeChat { + pub url: String, + pub wcf_path: PathBuf, + pub debug: bool, + pub socket: nng::Socket, + pub listening: bool, + pub enable_accept_firend: bool, +} + +impl Default for WeChat { + fn default() -> Self { + WeChat::new(false) + } +} + +impl WeChat { + pub fn new(debug: bool) -> Self { + let path = env::current_dir().unwrap().join("lib").join("wcf.exe"); + let _ = start(path.clone(), debug); + let socket = connect(&DEFAULT_URL).unwrap(); + WeChat { + url: String::from(DEFAULT_URL), + wcf_path: path, + debug, + socket, + listening: false, + enable_accept_firend: false, + } + } +} + +fn start(wcf_path: PathBuf, debug: bool) -> Result<(), Box> { + let mut args = vec!["start"]; + if debug { + args.push("debug"); + } + info!( + "wcf_path: {}, debug: {}", + wcf_path.clone().to_str().unwrap(), + debug + ); + let _ = match Command::new(wcf_path.to_str().unwrap()).args(args).output() { + Ok(output) => output, + Err(e) => { + error!("命令行启动失败: {}", e); + return Err("服务启动失败".into()); + } + }; + Ok(()) +} + +pub fn stop(wechat: &mut WeChat) -> Result<(), Box> { + let _ = disable_listen(wechat); + wechat.socket.close(); + let output = Command::new(wechat.wcf_path.to_str().unwrap()) + .args(["stop"]) + .output(); + let _output = match output { + Ok(output) => output, + Err(e) => { + error!("服务停止失败: {}", e); + return Err("服务停止失败".into()); + } + }; + info!("服务已停止: {}", wechat.url); + Ok(()) +} + +fn connect(url: &str) -> Result> { + let client = match nng::Socket::new(nng::Protocol::Pair1) { + Ok(client) => client, + Err(e) => { + error!("Socket创建失败: {}", e); + return Err("连接服务失败".into()); + } + }; + match client.set_opt::(Some(Duration::from_millis(5000))) { + Ok(()) => (), + Err(e) => { + error!("连接参数设置失败: {}", e); + return Err("连接参数设置失败".into()); + } + }; + match client.set_opt::(Some(Duration::from_millis(5000))) { + Ok(()) => (), + Err(e) => { + error!("连接参数设置失败: {}", e); + return Err("连接参数设置失败".into()); + } + }; + match client.dial(url) { + Ok(()) => (), + Err(e) => { + error!("连接服务失败: {}", e); + return Err("连接服务失败".into()); + } + }; + Ok(client) +} + +fn send_cmd( + wechat: &WeChat, + req: wcf::Request, +) -> Result, Box> { + let mut buf = Vec::with_capacity(req.encoded_len()); + match req.encode(&mut buf) { + Ok(()) => (), + Err(e) => { + error!("序列化失败: {}", e); + return Err("通信失败".into()); + } + }; + let msg = nng::Message::from(&buf[..]); + let _ = match wechat.socket.send(msg) { + Ok(()) => {} + Err(e) => { + error!("Socket发送失败: {:?}, {}", e.0, e.1); + return Err("通信失败".into()); + } + }; + let mut msg = match wechat.socket.recv() { + Ok(msg) => msg, + Err(e) => { + error!("Socket接收失败: {}", e); + return Err("通信失败".into()); + } + }; + // 反序列化为prost消息 + let response = match wcf::Response::decode(msg.as_slice()) { + Ok(res) => res, + Err(e) => { + error!("反序列化失败: {}", e); + return Err("通信失败".into()); + } + }; + msg.clear(); + Ok(response.msg) +} + +pub fn is_login(wechat: &WeChat) -> Result> { + let req = wcf::Request { + func: wcf::Functions::FuncIsLogin.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("登录状态检查失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(1 == status); + } + _ => { + return Ok(false); + } + }; +} + +pub fn get_self_wx_id(wechat: &mut WeChat) -> Result, Box> { + let req = wcf::Request { + func: wcf::Functions::FuncGetSelfWxid.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("获取微信ID失败".into()); + } + }; + if response.is_none() { + return Ok(None); + } + match response.unwrap() { + wcf::response::Msg::Str(wx_id) => { + return Ok(Some(wx_id)); + } + _ => { + return Ok(None); + } + }; +} + +pub fn get_contacts( + wechat: &mut WeChat, +) -> Result, Box> { + info!("获取联系人"); + let req = wcf::Request { + func: wcf::Functions::FuncGetContacts.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("获取微信联系人失败".into()); + } + }; + if response.is_none() { + return Ok(None); + } + match response.unwrap() { + wcf::response::Msg::Contacts(contact) => { + return Ok(Some(contact)); + } + _ => { + return Ok(None); + } + }; +} + +pub fn get_db_names(wechat: &mut WeChat) -> Result, Box> { + let req = wcf::Request { + func: wcf::Functions::FuncGetDbNames.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("获取微信数据库失败".into()); + } + }; + if response.is_none() { + return Ok(vec![]); + } + match response.unwrap() { + wcf::response::Msg::Dbs(dbs) => { + return Ok(dbs.names); + } + _ => { + return Ok(vec![]); + } + }; +} + +pub fn get_db_tables( + wechat: &mut WeChat, + db: String, +) -> Result, Box> { + let req = wcf::Request { + func: wcf::Functions::FuncGetDbTables.into(), + msg: Some(wcf::request::Msg::Str(db)), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("获取数据库表失败".into()); + } + }; + if response.is_none() { + return Ok(vec![]); + } + match response.unwrap() { + wcf::response::Msg::Tables(tables) => { + let tables = tables.tables; + return Ok(tables); + } + _ => { + return Ok(vec![]); + } + }; +} + +pub fn exec_db_query( + wechat: &mut WeChat, + db: String, + sql: String, +) -> Result, Box> { + let req = wcf::Request { + func: wcf::Functions::FuncExecDbQuery.into(), + msg: Some(wcf::request::Msg::Query(wcf::DbQuery { db, sql })), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("执行SQL失败".into()); + } + }; + if response.is_none() { + return Ok(vec![]); + } + match response.unwrap() { + wcf::response::Msg::Rows(rows) => { + let rows = rows.rows; + return Ok(rows); + } + _ => { + return Ok(vec![]); + } + }; +} + +/** + * @param msg: 消息内容(如果是 @ 消息则需要有跟 @ 的人数量相同的 @) + * @param receiver: 消息接收人,私聊为 wxid(wxid_xxxxxxxxxxxxxx),群聊为 + * roomid(xxxxxxxxxx@chatroom) + * @param aters: 群聊时要 @ 的人(私聊时为空字符串),多个用逗号分隔。@所有人 用 + * notify@all(必须是群主或者管理员才有权限) + * @return int + * @Description 发送文本消息 + * @author Changhua + * @example sendText(" Hello @ 某人1 @ 某人2 ", " xxxxxxxx @ chatroom ", + * "wxid_xxxxxxxxxxxxx1,wxid_xxxxxxxxxxxxx2"); + */ +pub fn send_text( + wechat: &mut WeChat, + msg: String, + receiver: String, + aters: String, +) -> Result> { + let text_msg = wcf::TextMsg { + msg, + receiver, + aters, + }; + let req = wcf::Request { + func: wcf::Functions::FuncSendTxt.into(), + msg: Some(wcf::request::Msg::Txt(text_msg)), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("微信消息发送失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(1 == status); + } + _ => { + return Ok(false); + } + }; +} + +pub fn send_image( + wechat: &mut WeChat, + path: PathBuf, + receiver: String, +) -> Result> { + let image_msg = wcf::PathMsg { + path: String::from(path.to_str().unwrap()), + receiver, + }; + let req = wcf::Request { + func: wcf::Functions::FuncSendImg.into(), + msg: Some(wcf::request::Msg::File(image_msg)), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("图片发送失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(1 == status); + } + _ => { + return Ok(false); + } + }; +} + +pub fn send_file( + wechat: &mut WeChat, + path: PathBuf, + receiver: String, +) -> Result> { + let image_msg = wcf::PathMsg { + path: String::from(path.to_str().unwrap()), + receiver, + }; + let req = wcf::Request { + func: wcf::Functions::FuncSendFile.into(), + msg: Some(wcf::request::Msg::File(image_msg)), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("文件发送失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(1 == status); + } + _ => { + return Ok(false); + } + }; +} + +pub fn send_xml( + wechat: &mut WeChat, + xml: String, + path: PathBuf, + receiver: String, + xml_type: i32, +) -> Result> { + let xml_msg = wcf::XmlMsg { + content: xml, + path: String::from(path.to_str().unwrap()), + receiver, + r#type: xml_type, + }; + let req = wcf::Request { + func: wcf::Functions::FuncSendXml.into(), + msg: Some(wcf::request::Msg::Xml(xml_msg)), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("微信XML消息发送失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(1 == status); + } + _ => { + return Ok(false); + } + }; +} + +pub fn send_emotion( + wechat: &mut WeChat, + path: PathBuf, + receiver: String, +) -> Result> { + let image_msg = wcf::PathMsg { + path: String::from(path.to_str().unwrap()), + receiver, + }; + let req = wcf::Request { + func: wcf::Functions::FuncSendEmotion.into(), + msg: Some(wcf::request::Msg::File(image_msg)), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("微信表情发送失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(1 == status); + } + _ => { + return Ok(false); + } + }; +} + +pub fn enable_listen(wechat: &mut WeChat) -> Result> { + if wechat.listening { + return Err("消息接收服务已开启".into()); + } + let req = wcf::Request { + func: wcf::Functions::FuncEnableRecvTxt.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("消息接收服务启动失败".into()); + } + }; + if response.is_none() { + return Err("消息接收服务启动失败".into()); + } + let client = connect(LISTEN_URL).unwrap(); + wechat.listening = true; + Ok(client) +} + +pub fn disable_listen(wechat: &mut WeChat) -> Result> { + if !wechat.listening { + return Ok(true); + } + let req = wcf::Request { + func: wcf::Functions::FuncDisableRecvTxt.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("消息接收服务停止失败".into()); + } + }; + if response.is_none() { + return Err("消息接收服务停止失败".into()); + } + wechat.listening = false; + return Ok(true); +} + +pub fn recv_msg(client: &nng::Socket) -> Result, Box> { + let mut msg = match client.recv() { + Ok(msg) => msg, + Err(e) => { + warn!("Socket消息接收失败: {}", e); + return Ok(None); + } + }; + // 反序列化为prost消息 + let res = match wcf::Response::decode(msg.as_slice()) { + Ok(res) => res, + Err(e) => { + error!("反序列化失败: {}", e); + return Err("微信消息接收失败".into()); + } + }; + msg.clear(); + let res_msg = res.msg; + match res_msg { + Some(wcf::response::Msg::Wxmsg(msg)) => { + return Ok(Some(msg)); + } + _ => { + return Ok(None); + } + } +} + +/** + * 获取消息类型 + * {"47": "石头剪刀布 | 表情图片", "62": "小视频", "43": "视频", "1": "文字", "10002": "撤回消息", "40": "POSSIBLEFRIEND_MSG", "10000": "红包、系统消息", "37": "好友确认", "48": "位置", "42": "名片", "49": "共享实时位置、文件、转账、链接", "3": "图片", "34": "语音", "9999": "SYSNOTICE", "52": "VOIPNOTIFY", "53": "VOIPINVITE", "51": "微信初始化", "50": "VOIPMSG"} + */ +pub fn get_msg_types( + wechat: &mut WeChat, +) -> Result, Box> { + let req = wcf::Request { + func: wcf::Functions::FuncGetMsgTypes.into(), + msg: None, + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("获取消息类型失败".into()); + } + }; + if response.is_none() { + return Ok(HashMap::default()); + } + match response.unwrap() { + wcf::response::Msg::Types(msg_types) => { + let types = msg_types.types; + return Ok(types); + } + _ => { + return Ok(HashMap::default()); + } + }; +} + +pub fn accept_new_friend( + v3: String, + v4: String, + wechat: &mut WeChat, +) -> Result> { + let req = wcf::Request { + func: wcf::Functions::FuncAcceptFriend.into(), + msg: Some(wcf::request::Msg::V(wcf::Verification { v3, v4 })), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("同意加好友请求失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(status == 1); + } + _ => { + return Ok(false); + } + }; +} + +pub fn add_chatroom_members( + roomid: String, + wxids: String, + wechat: &mut WeChat, +) -> Result> { + let req = wcf::Request { + func: wcf::Functions::FuncAddRoomMembers.into(), + msg: Some(wcf::request::Msg::M(wcf::AddMembers { roomid, wxids })), + }; + let response = match send_cmd(wechat, req) { + Ok(res) => res, + Err(e) => { + error!("命令发送失败: {}", e); + return Err("微信群加人失败".into()); + } + }; + if response.is_none() { + return Ok(false); + } + match response.unwrap() { + wcf::response::Msg::Status(status) => { + return Ok(status == 1); + } + _ => { + return Ok(false); + } + }; +} + +mod test { + + #[test] + fn test_start_stop() { + use std::thread; + use std::time::Duration; + + let mut wechat = crate::wechat::WeChat::new(false); + thread::sleep(Duration::from_secs(20)); + let _ = crate::wechat::stop(&mut wechat); + } + + #[test] + fn test_is_login() { + let mut wechat = crate::wechat::WeChat::default(); + let is_login = crate::wechat::is_login(&mut wechat).unwrap(); + println!("IsLogin: {}", is_login); + } + + #[test] + fn test_get_self_wx_id() { + let mut wechat = crate::wechat::WeChat::default(); + let wx_id = crate::wechat::get_self_wx_id(&mut wechat).unwrap().unwrap(); + println!("WxId: {}", wx_id); + } + + #[test] + fn test_get_contacts() { + let mut wechat = crate::wechat::WeChat::default(); + let contacts = crate::wechat::get_contacts(&mut wechat).unwrap().unwrap(); + println!("WxId: {:?}", contacts); + } + + #[test] + fn test_send_text() { + let mut wechat = crate::wechat::WeChat::default(); + let status = crate::wechat::send_text( + &mut wechat, + String::from("Hello, wcferry!"), + String::from("******@chatroom"), + String::from(""), + ) + .unwrap(); + println!("Success: {}", status); + } + + #[test] + fn test_send_image() { + use std::path::PathBuf; + + let mut wechat = crate::wechat::WeChat::default(); + let status = crate::wechat::send_image( + &mut wechat, + PathBuf::from("C:\\Users\\Administrator\\Pictures\\1.jpg"), + String::from("****@chatroom"), + ) + .unwrap(); + println!("Success: {}", status); + } + + #[test] + fn test_recv_msg() { + let mut wechat = crate::wechat::WeChat::default(); + let mut socket = crate::wechat::enable_listen(&mut wechat).unwrap(); + for _ in 1..100 { + let msg = crate::wechat::recv_msg(&mut socket).unwrap(); + println!("WxMsg: {:?}", msg); + } + let _ = crate::wechat::disable_listen(&mut wechat); + } + + #[test] + fn test_get_msg_types() { + let mut wechat = crate::wechat::WeChat::default(); + let types = crate::wechat::get_msg_types(&mut wechat); + println!("{:?}", types); + } + + #[test] + fn test_accept_new_friend() { + let mut wechat = crate::wechat::WeChat::default(); + let v3 = String::from("v3_020b3826fd03010000000000d65613e9435fd2000000501ea9a3dba12f95f6b60a0536a1adb6b4e20a513856625d11892e0635fe745d9c7ee96937f341a860c34107c6417414e5b41e427fc3d26a6af2590a1f@stranger"); + let v4 = String::from("v4_000b708f0b0400000100000000003c3767b326120d5b5795b98031641000000050ded0b020927e3c97896a09d47e6e9eac7eea28e4a39b49644b3b702b82268c1d40370261e3ae6eb543d231fbd29ee7a326598ba810316c10171871103ad967ca4d147d9f6dd8fa5ccd4986042520a1173c8138e5afe21f795ee50fecf58b4ac5269acd80028627dbf65fd17ca57c0e479fbe0392288a6f42@stranger"); + let status = crate::wechat::accept_new_friend(v3, v4, &mut wechat).unwrap(); + println!("Status: {}", status); + } + + #[test] + fn test_add_chatroom_members() { + let mut wechat = crate::wechat::WeChat::default(); + let status = crate::wechat::add_chatroom_members( + String::from("*****@chatroom"), + String::from("****"), + &mut wechat, + ) + .unwrap(); + println!("Status: {}", status); + } +}