├── .gitignore ├── Cargo.toml ├── README.md ├── docs └── images │ ├── rust-wechaty-logo.png │ └── rust-wechaty-logo.svg ├── examples └── ding_dong_bot.rs ├── file-box ├── Cargo.toml └── src │ └── lib.rs ├── rustfmt.toml ├── wechaty-puppet-mock ├── Cargo.toml └── src │ ├── lib.rs │ └── puppet_mock.rs ├── wechaty-puppet-service ├── Cargo.toml └── src │ ├── from_payload_response.rs │ ├── lib.rs │ ├── puppet_service.rs │ └── service_endpoint.rs ├── wechaty-puppet ├── Cargo.toml └── src │ ├── error.rs │ ├── events.rs │ ├── lib.rs │ ├── puppet.rs │ ├── schemas │ ├── contact.rs │ ├── event.rs │ ├── friendship.rs │ ├── image.rs │ ├── message.rs │ ├── mini_program.rs │ ├── mod.rs │ ├── payload.rs │ ├── puppet.rs │ ├── room.rs │ ├── room_invitation.rs │ └── url_link.rs │ └── types.rs └── wechaty ├── Cargo.toml └── src ├── context.rs ├── error.rs ├── lib.rs ├── payload.rs ├── traits ├── contact.rs ├── event_listener.rs ├── mod.rs └── talkable.rs ├── user ├── contact.rs ├── contact_self.rs ├── entity.rs ├── favorite.rs ├── friendship.rs ├── image.rs ├── location.rs ├── message.rs ├── mini_program.rs ├── mod.rs ├── moment.rs ├── money.rs ├── room.rs ├── room_invitation.rs ├── tag.rs └── url_link.rs └── wechaty.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,rust 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Rust ### 49 | # Generated by Cargo 50 | # will have compiled files and executables 51 | /target/ 52 | 53 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 54 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 55 | Cargo.lock 56 | 57 | ### Windows ### 58 | # Windows thumbnail cache files 59 | Thumbs.db 60 | Thumbs.db:encryptable 61 | ehthumbs.db 62 | ehthumbs_vista.db 63 | 64 | # Dump file 65 | *.stackdump 66 | 67 | # Folder config file 68 | [Dd]esktop.ini 69 | 70 | # Recycle Bin used on file shares 71 | $RECYCLE.BIN/ 72 | 73 | # Windows Installer files 74 | *.cab 75 | *.msi 76 | *.msix 77 | *.msm 78 | *.msp 79 | 80 | # Windows shortcuts 81 | *.lnk 82 | 83 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust 84 | 85 | # IDE 86 | .idea 87 | .vscode -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "file-box", 5 | "wechaty", 6 | "wechaty-puppet", 7 | "wechaty-puppet-service", 8 | "wechaty-puppet-mock", 9 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-wechaty 2 | 3 | ![Rust Wechaty](docs/images/rust-wechaty-logo.png) 4 | 5 | ## Connecting Chatbots 6 | 7 | [![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-brightgreen.svg)](https://github.com/Wechaty/wechaty) 8 | 9 | Wechaty is a Conversational RPA SDK for Chatbot Makers that can help you create a bot in Rust quickly. 10 | 11 | ## Voice of the Developers 12 | 13 | > "Wechaty is a great solution, I believe there would be much more users recognize it." [link](https://github.com/Wechaty/wechaty/pull/310#issuecomment-285574472) 14 | > — @Gcaufy, Tencent Engineer, Author of [WePY](https://github.com/Tencent/wepy) 15 | > 16 | > "太好用,好用的想哭" 17 | > — @xinbenlv, Google Engineer, Founder of HaoShiYou.org 18 | > 19 | > "最好的微信开发库" [link](http://weibo.com/3296245513/Ec4iNp9Ld?type=comment) 20 | > — @Jarvis, Baidu Engineer 21 | > 22 | > "Wechaty让运营人员更多的时间思考如何进行活动策划、留存用户,商业变现" [link](http://mp.weixin.qq.com/s/dWHAj8XtiKG-1fIS5Og79g) 23 | > — @lijiarui, Founder & CEO of Juzi.BOT. 24 | > 25 | > "If you know js ... try Wechaty, it's easy to use." 26 | > — @Urinx Uri Lee, Author of [WeixinBot(Python)](https://github.com/Urinx/WeixinBot) 27 | 28 | See more at [Wiki:Voice Of Developer](https://github.com/Wechaty/wechaty/wiki/Voice%20Of%20Developer) 29 | 30 | ## Join Us 31 | 32 | Wechaty is used in many ChatBot projects by thousands of developers. If you want to talk with other developers, just scan the following QR Code in WeChat with secret code _rust wechaty_, join our **Wechaty Rust Developers' Home**. 33 | 34 | ![Wechaty Friday.BOT QR Code](https://wechaty.js.org/img/friday-qrcode.svg) 35 | 36 | Scan now, because other Wechaty Rust developers want to talk to you too! (secret code: _rust wechaty_) 37 | 38 | ## Examples 39 | 40 | ### Ding-dong bot 41 | 42 | ```shell 43 | export WECHATY_PUPPET_SERVICE_TOKEN= 44 | export RUST_LOG= 45 | cargo run --example ding-dong-bot 46 | ``` 47 | 48 | ## Related Projects 49 | 50 | - [Wechaty](https://github.com/wechaty/wechaty) - Conversatioanl AI Chatot SDK for Wechaty Individual Accounts (TypeScript) 51 | - [Rust Wechaty](https://github.com/wechaty/rust-wechaty) - Rust WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Rust) 52 | - [Python Wechaty](https://github.com/wechaty/python-wechaty) - Python WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Python) 53 | - [Go Wechaty](https://github.com/wechaty/go-wechaty) - Go WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Go) 54 | - [Java Wechaty](https://github.com/wechaty/java-wechaty) - Java WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Java) 55 | - [Scala Wechaty](https://github.com/wechaty/scala-wechaty) - Scala WeChaty Conversational AI Chatbot SDK for WechatyIndividual Accounts (Scala) 56 | 57 | ## Badge 58 | 59 | [![Wechaty in Rust](https://img.shields.io/badge/Wechaty-Rust-f42)](https://github.com/wechaty/rust-wechaty) 60 | 61 | ```md 62 | [![Wechaty in Rust](https://img.shields.io/badge/Wechaty-Rust-f42)](https://github.com/wechaty/rust-wechaty) 63 | ``` 64 | 65 | ## Stargazers over time 66 | 67 | [![Stargazers over time](https://starchart.cc/wechaty/rust-wechaty.svg)](https://starchart.cc/wechaty/rust-wechaty) 68 | 69 | ## Creators 70 | 71 | - [@lucifer1004](https://github.com/lucifer1004) - (吴自华) wuzihua@pku.edu.cn 72 | - [@huan](https://github.com/huan) - ([李卓桓](http://linkedin.com/in/zixia)) zixia@zixia.net 73 | 74 | ## Copyright & License 75 | 76 | - Code & Docs © 2021--Now Wechaty Contributors 77 | - Code released under the Apache-2.0 License 78 | - Docs released under Creative Commons 79 | -------------------------------------------------------------------------------- /docs/images/rust-wechaty-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/rust-wechaty/2cf5545fb677a9da2e983892b8320ef0294dc62d/docs/images/rust-wechaty-logo.png -------------------------------------------------------------------------------- /docs/images/rust-wechaty-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/ding_dong_bot.rs: -------------------------------------------------------------------------------- 1 | #![feature(async_closure)] 2 | use std::env; 3 | 4 | use wechaty::prelude::*; 5 | use wechaty_puppet_service::PuppetService; 6 | 7 | #[wechaty_rt::main] 8 | async fn main() { 9 | env_logger::init(); 10 | let options = PuppetOptions { 11 | endpoint: match env::var("WECHATY_PUPPET_SERVICE_ENDPOINT") { 12 | Ok(endpoint) => Some(endpoint), 13 | Err(_) => None, 14 | }, 15 | timeout: None, 16 | token: match env::var("WECHATY_PUPPET_SERVICE_TOKEN") { 17 | Ok(endpoint) => Some(endpoint), 18 | Err(_) => None, 19 | }, 20 | }; 21 | let mut bot = Wechaty::new(PuppetService::new(options).await.unwrap()); 22 | 23 | bot.on_scan(async move |payload: ScanPayload, _ctx| { 24 | if let Some(qrcode) = payload.qrcode { 25 | println!( 26 | "Visit {} to log in", 27 | format!("https://wechaty.js.org/qrcode/{}", qrcode) 28 | ); 29 | } 30 | }) 31 | .on_login( 32 | async move |payload: LoginPayload, ctx: WechatyContext| { 33 | println!("User {} has logged in", payload.contact); 34 | println!("Contact list: {:?}", ctx.contact_find_all(None).await); 35 | }, 36 | ) 37 | .on_logout(async move |payload: LogoutPayload, _ctx| { 38 | println!("User {} has logged out", payload.contact); 39 | }) 40 | .on_message( 41 | async move |payload: MessagePayload, ctx: WechatyContext| { 42 | let mut message = payload.message; 43 | let mentioned = message.mention_list().await; 44 | println!( 45 | "Got message: {}, mentioned: {:?}, age: {}", 46 | message, 47 | mentioned, 48 | message.age() 49 | ); 50 | if message.is_self() { 51 | println!("Message discarded because it's outgoing"); 52 | return; 53 | } 54 | if message.is_in_room() { 55 | println!("Message discarded because it's from a room"); 56 | return; 57 | } 58 | if let Some(message_type) = message.message_type() { 59 | if message_type != MessageType::Text { 60 | println!("Message discarded because it is not a text"); 61 | } else { 62 | let text = message.text().unwrap_or_default(); 63 | if text == "bye" { 64 | println!("Good bye!"); 65 | ctx.logout().await.unwrap_or_default(); 66 | return; 67 | } 68 | if text == "ding" { 69 | if let Err(e) = message.reply_text("dong".to_owned()).await { 70 | println!("Failed to send message, reason: {}", e); 71 | } else { 72 | println!("REPLY: dong"); 73 | } 74 | return; 75 | } 76 | println!("Message discarded because it does not match any keyword"); 77 | } 78 | } 79 | }, 80 | ) 81 | .start() 82 | .await; 83 | } 84 | -------------------------------------------------------------------------------- /file-box/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "file-box" 3 | version = "0.1.0-beta.0" 4 | authors = ["Gabriel Wu "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Unified file (local, remote url, or cloud storage) box for easy easier management and manipulation." 8 | keywords = ["file", "cloud-storage", "wechaty"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | -------------------------------------------------------------------------------- /file-box/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | // TODO: FileBox Implementation 4 | pub struct FileBox {} 5 | 6 | impl fmt::Display for FileBox { 7 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 8 | write!(fmt, "") 9 | } 10 | } 11 | 12 | impl From for FileBox { 13 | fn from(_: String) -> Self { 14 | Self {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | max_width = 120 3 | reorder_imports = true 4 | reorder_impl_items = true -------------------------------------------------------------------------------- /wechaty-puppet-mock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wechaty-puppet-mock" 3 | version = "0.1.0-beta.1" 4 | authors = ["Gabriel Wu "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Rust implementation of wechaty-puppet-mock" 8 | keywords = ["chatbot", "wechaty"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | actix = "0.12" 14 | async-trait = "0.1" 15 | wechaty_puppet = { version = "0.1.0-beta.1", path = "../wechaty-puppet" } -------------------------------------------------------------------------------- /wechaty-puppet-mock/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod puppet_mock; 2 | 3 | pub use puppet_mock::PuppetMock; 4 | -------------------------------------------------------------------------------- /wechaty-puppet-mock/src/puppet_mock.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use wechaty_puppet::*; 3 | 4 | #[derive(Debug)] 5 | pub struct PuppetMock {} 6 | 7 | #[allow(dead_code)] 8 | #[allow(unused_variables)] 9 | #[async_trait] 10 | impl PuppetImpl for PuppetMock { 11 | async fn contact_self_name_set(&self, name: String) -> Result<(), PuppetError> { 12 | unimplemented!() 13 | } 14 | 15 | async fn contact_self_qr_code(&self) -> Result { 16 | unimplemented!() 17 | } 18 | 19 | async fn contact_self_signature_set(&self, signature: String) -> Result<(), PuppetError> { 20 | unimplemented!() 21 | } 22 | 23 | async fn tag_contact_add(&self, tag_id: String, contact_id: String) -> Result<(), PuppetError> { 24 | unimplemented!() 25 | } 26 | 27 | async fn tag_contact_remove(&self, tag_id: String, contact_id: String) -> Result<(), PuppetError> { 28 | unimplemented!() 29 | } 30 | 31 | async fn tag_contact_delete(&self, tag_id: String) -> Result<(), PuppetError> { 32 | unimplemented!() 33 | } 34 | 35 | async fn tag_contact_list(&self, contact_id: String) -> Result, PuppetError> { 36 | unimplemented!() 37 | } 38 | 39 | async fn tag_list(&self) -> Result, PuppetError> { 40 | unimplemented!() 41 | } 42 | 43 | async fn contact_alias(&self, contact_id: String) -> Result { 44 | unimplemented!() 45 | } 46 | 47 | async fn contact_alias_set(&self, contact_id: String, alias: String) -> Result<(), PuppetError> { 48 | unimplemented!() 49 | } 50 | 51 | async fn contact_avatar(&self, contact_id: String) -> Result { 52 | unimplemented!() 53 | } 54 | 55 | async fn contact_avatar_set(&self, contact_id: String, file: FileBox) -> Result<(), PuppetError> { 56 | unimplemented!() 57 | } 58 | 59 | async fn contact_phone_set(&self, contact_id: String, phone_list: Vec) -> Result<(), PuppetError> { 60 | unimplemented!() 61 | } 62 | 63 | async fn contact_corporation_remark_set( 64 | &self, 65 | contact_id: String, 66 | corporation_remark: Option, 67 | ) -> Result<(), PuppetError> { 68 | unimplemented!() 69 | } 70 | 71 | async fn contact_description_set( 72 | &self, 73 | contact_id: String, 74 | description: Option, 75 | ) -> Result<(), PuppetError> { 76 | unimplemented!() 77 | } 78 | 79 | async fn contact_list(&self) -> Result, PuppetError> { 80 | unimplemented!() 81 | } 82 | 83 | async fn contact_raw_payload(&self, contact_id: String) -> Result { 84 | unimplemented!() 85 | } 86 | 87 | async fn message_contact(&self, message_id: String) -> Result { 88 | unimplemented!() 89 | } 90 | 91 | async fn message_file(&self, message_id: String) -> Result { 92 | unimplemented!() 93 | } 94 | 95 | async fn message_image(&self, message_id: String, image_type: ImageType) -> Result { 96 | unimplemented!() 97 | } 98 | 99 | async fn message_mini_program(&self, message_id: String) -> Result { 100 | unimplemented!() 101 | } 102 | 103 | async fn message_url(&self, message_id: String) -> Result { 104 | unimplemented!() 105 | } 106 | 107 | async fn message_send_contact( 108 | &self, 109 | conversation_id: String, 110 | contact_id: String, 111 | ) -> Result, PuppetError> { 112 | unimplemented!() 113 | } 114 | 115 | async fn message_send_file(&self, conversation_id: String, file: FileBox) -> Result, PuppetError> { 116 | unimplemented!() 117 | } 118 | 119 | async fn message_send_mini_program( 120 | &self, 121 | conversation_id: String, 122 | mini_program_payload: MiniProgramPayload, 123 | ) -> Result, PuppetError> { 124 | unimplemented!() 125 | } 126 | 127 | async fn message_send_text( 128 | &self, 129 | conversation_id: String, 130 | text: String, 131 | mention_id_list: Vec, 132 | ) -> Result, PuppetError> { 133 | unimplemented!() 134 | } 135 | 136 | async fn message_send_url( 137 | &self, 138 | conversation_id: String, 139 | url_link_payload: UrlLinkPayload, 140 | ) -> Result, PuppetError> { 141 | unimplemented!() 142 | } 143 | 144 | async fn message_raw_payload(&self, message_id: String) -> Result { 145 | unimplemented!() 146 | } 147 | 148 | async fn friendship_accept(&self, friendship_id: String) -> Result<(), PuppetError> { 149 | unimplemented!() 150 | } 151 | 152 | async fn friendship_add(&self, contact_id: String, hello: Option) -> Result<(), PuppetError> { 153 | unimplemented!() 154 | } 155 | 156 | async fn friendship_search_phone(&self, phone: String) -> Result, PuppetError> { 157 | unimplemented!() 158 | } 159 | 160 | async fn friendship_search_weixin(&self, weixin: String) -> Result, PuppetError> { 161 | unimplemented!() 162 | } 163 | 164 | async fn friendship_raw_payload(&self, friendship_id: String) -> Result { 165 | unimplemented!() 166 | } 167 | 168 | async fn room_invitation_accept(&self, room_invitation_id: String) -> Result<(), PuppetError> { 169 | unimplemented!() 170 | } 171 | 172 | async fn room_invitation_raw_payload( 173 | &self, 174 | room_invitation_id: String, 175 | ) -> Result { 176 | unimplemented!() 177 | } 178 | 179 | async fn room_add(&self, room_id: String, contact_id: String) -> Result<(), PuppetError> { 180 | unimplemented!() 181 | } 182 | 183 | async fn room_avatar(&self, room_id: String) -> Result { 184 | unimplemented!() 185 | } 186 | 187 | async fn room_create(&self, contact_id_list: Vec, topic: Option) -> Result { 188 | unimplemented!() 189 | } 190 | 191 | async fn room_del(&self, room_id: String, contact_id: String) -> Result<(), PuppetError> { 192 | unimplemented!() 193 | } 194 | 195 | async fn room_qr_code(&self, room_id: String) -> Result { 196 | unimplemented!() 197 | } 198 | 199 | async fn room_quit(&self, room_id: String) -> Result<(), PuppetError> { 200 | unimplemented!() 201 | } 202 | 203 | async fn room_topic(&self, room_id: String) -> Result { 204 | unimplemented!() 205 | } 206 | 207 | async fn room_topic_set(&self, room_id: String, topic: String) -> Result<(), PuppetError> { 208 | unimplemented!() 209 | } 210 | 211 | async fn room_list(&self) -> Result, PuppetError> { 212 | unimplemented!() 213 | } 214 | 215 | async fn room_raw_payload(&self, room_id: String) -> Result { 216 | unimplemented!() 217 | } 218 | 219 | async fn room_announce(&self, room_id: String) -> Result { 220 | unimplemented!() 221 | } 222 | 223 | async fn room_announce_set(&self, room_id: String, text: String) -> Result<(), PuppetError> { 224 | unimplemented!() 225 | } 226 | 227 | async fn room_member_list(&self, room_id: String) -> Result, PuppetError> { 228 | unimplemented!() 229 | } 230 | 231 | async fn room_member_raw_payload( 232 | &self, 233 | room_id: String, 234 | contact_id: String, 235 | ) -> Result { 236 | unimplemented!() 237 | } 238 | 239 | async fn start(&self) -> Result<(), PuppetError> { 240 | unimplemented!() 241 | } 242 | 243 | async fn stop(&self) -> Result<(), PuppetError> { 244 | unimplemented!() 245 | } 246 | 247 | async fn ding(&self, data: String) -> Result<(), PuppetError> { 248 | unimplemented!() 249 | } 250 | 251 | async fn version(&self) -> Result { 252 | unimplemented!() 253 | } 254 | 255 | async fn logout(&self) -> Result<(), PuppetError> { 256 | unimplemented!() 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /wechaty-puppet-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wechaty-puppet-service" 3 | version = "0.1.0-beta.1" 4 | authors = ["Gabriel Wu "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Rust implementation of wechaty-puppet-service" 8 | keywords = ["chatbot", "wechaty"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | actix = "0.12" 14 | actix-rt = "2" 15 | async-trait = "0.1" 16 | log = "0.4" 17 | num-traits = "0.2" 18 | reqwest = { version = "0.11", features = ["json"] } 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | tokio = "1" 22 | tonic = "0.4" 23 | uuid = { version = "0.8", features = ["v4"] } 24 | wechaty_puppet = { version = "0.1.0-beta.1", path = "../wechaty-puppet" } 25 | wechaty-grpc = "0.1" -------------------------------------------------------------------------------- /wechaty-puppet-service/src/from_payload_response.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | use num_traits::FromPrimitive; 4 | use wechaty_grpc::puppet::{ 5 | ContactPayloadResponse, FriendshipPayloadResponse, MessagePayloadResponse, RoomInvitationPayloadResponse, 6 | RoomMemberPayloadResponse, RoomPayloadResponse, 7 | }; 8 | use wechaty_puppet::schemas::contact::ContactPayload; 9 | use wechaty_puppet::schemas::friendship::FriendshipPayload; 10 | use wechaty_puppet::schemas::message::MessagePayload; 11 | use wechaty_puppet::schemas::room::{RoomMemberPayload, RoomPayload}; 12 | use wechaty_puppet::schemas::room_invitation::RoomInvitationPayload; 13 | 14 | pub trait FromPayloadResponse { 15 | fn from_payload_response(payload_response: T) -> Self; 16 | } 17 | 18 | impl FromPayloadResponse for ContactPayload { 19 | fn from_payload_response(response: ContactPayloadResponse) -> Self { 20 | Self { 21 | id: response.id, 22 | gender: FromPrimitive::from_i32(response.gender).unwrap(), 23 | contact_type: FromPrimitive::from_i32(response.r#type).unwrap(), 24 | name: response.name, 25 | avatar: response.avatar, 26 | address: response.address, 27 | alias: response.alias, 28 | city: response.city, 29 | friend: response.friend, 30 | province: response.province, 31 | signature: response.signature, 32 | star: response.star, 33 | weixin: response.weixin, 34 | corporation: response.corporation, 35 | title: response.title, 36 | description: response.description, 37 | coworker: response.coworker, 38 | phone: response.phone, 39 | } 40 | } 41 | } 42 | 43 | impl FromPayloadResponse for FriendshipPayload { 44 | fn from_payload_response(response: FriendshipPayloadResponse) -> Self { 45 | Self { 46 | id: response.id, 47 | contact_id: response.contact_id, 48 | hello: response.hello, 49 | timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), 50 | scene: FromPrimitive::from_i32(response.scene).unwrap(), 51 | stranger: response.stranger, 52 | ticket: response.ticket, 53 | friendship_type: FromPrimitive::from_i32(response.r#type).unwrap(), 54 | } 55 | } 56 | } 57 | 58 | impl FromPayloadResponse for MessagePayload { 59 | fn from_payload_response(response: MessagePayloadResponse) -> Self { 60 | Self { 61 | id: response.id, 62 | from_id: response.from_id, 63 | to_id: response.to_id, 64 | room_id: response.room_id, 65 | filename: response.filename, 66 | text: response.text, 67 | timestamp: response.timestamp, 68 | message_type: FromPrimitive::from_i32(response.r#type).unwrap(), 69 | mention_id_list: response.mention_ids, 70 | } 71 | } 72 | } 73 | 74 | impl FromPayloadResponse for RoomPayload { 75 | fn from_payload_response(response: RoomPayloadResponse) -> Self { 76 | Self { 77 | id: response.id, 78 | topic: response.topic, 79 | avatar: response.avatar, 80 | member_id_list: response.member_ids, 81 | owner_id: response.owner_id, 82 | admin_id_list: response.admin_ids, 83 | } 84 | } 85 | } 86 | 87 | impl FromPayloadResponse for RoomMemberPayload { 88 | fn from_payload_response(response: RoomMemberPayloadResponse) -> Self { 89 | Self { 90 | id: response.id, 91 | room_alias: response.room_alias, 92 | avatar: response.avatar, 93 | inviter_id: response.inviter_id, 94 | name: response.name, 95 | } 96 | } 97 | } 98 | 99 | impl FromPayloadResponse for RoomInvitationPayload { 100 | fn from_payload_response(response: RoomInvitationPayloadResponse) -> Self { 101 | Self { 102 | id: response.id, 103 | inviter_id: response.inviter_id, 104 | topic: response.topic, 105 | avatar: response.avatar, 106 | invitation: response.invitation, 107 | member_count: response.member_count, 108 | member_id_list: response.member_ids, 109 | timestamp: response.timestamp, 110 | receiver_id: response.receiver_id, 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /wechaty-puppet-service/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod from_payload_response; 2 | mod puppet_service; 3 | mod service_endpoint; 4 | 5 | pub use puppet_service::PuppetService; 6 | -------------------------------------------------------------------------------- /wechaty-puppet-service/src/puppet_service.rs: -------------------------------------------------------------------------------- 1 | use actix::{Actor, Addr, AsyncContext, Context, Handler, Message, Recipient, StreamHandler}; 2 | use async_trait::async_trait; 3 | use log::{debug, error, info}; 4 | use num_traits::cast::ToPrimitive; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::{from_str, to_string}; 7 | use tonic::{transport::Channel, Status, Streaming}; 8 | use wechaty_grpc::puppet::*; 9 | use wechaty_grpc::puppet_client::PuppetClient; 10 | use wechaty_puppet::*; 11 | use wechaty_puppet::{ImageType, PayloadType}; 12 | 13 | use crate::from_payload_response::FromPayloadResponse; 14 | use crate::service_endpoint::discover; 15 | 16 | #[derive(Clone)] 17 | pub struct PuppetService { 18 | client_: PuppetClient, 19 | addr: Addr, 20 | } 21 | 22 | impl PuppetService { 23 | /// Create puppet instance from puppet options. 24 | /// 25 | /// First use endpoint, if endpoint is not given, try token instead. 26 | pub async fn new(options: PuppetOptions) -> Result, PuppetError> { 27 | let endpoint = if let Some(endpoint) = options.endpoint { 28 | endpoint 29 | } else if let Some(token) = options.token { 30 | match discover(token).await { 31 | Ok(endpoint) => endpoint, 32 | Err(e) => return Err(e), 33 | } 34 | } else { 35 | return Err(PuppetError::InvalidToken); 36 | }; 37 | 38 | match PuppetClient::connect(endpoint.clone()).await { 39 | Ok(mut client) => { 40 | info!("Connected to endpoint {}", endpoint); 41 | let response = client.event(EventRequest {}).await; 42 | match response { 43 | Ok(response) => { 44 | info!("Subscribed to event stream"); 45 | let addr = PuppetServiceInner::new().start(); 46 | let puppet_service = Self { 47 | client_: client, 48 | addr: addr.clone(), 49 | }; 50 | let puppet = Puppet::new(puppet_service); 51 | let callback_addr = puppet.self_addr(); 52 | addr.do_send(PuppetServiceInternalMessage::SetupCallback(callback_addr)); 53 | addr.do_send(PuppetServiceInternalMessage::SetupStream(response.into_inner())); 54 | Ok(puppet) 55 | } 56 | Err(e) => Err(PuppetError::Network(format!( 57 | "Failed to establish event stream, reason: {}", 58 | e 59 | ))), 60 | } 61 | } 62 | Err(e) => Err(PuppetError::Network(format!( 63 | "Failed to establish RPC connection, reason: {}", 64 | e 65 | ))), 66 | } 67 | } 68 | 69 | fn client(&self) -> PuppetClient { 70 | self.client_.clone() 71 | } 72 | } 73 | 74 | #[derive(Message)] 75 | #[rtype("()")] 76 | enum PuppetServiceInternalMessage { 77 | SetupCallback(Recipient), 78 | SetupStream(Streaming), 79 | } 80 | 81 | #[derive(Clone, Debug)] 82 | struct PuppetServiceInner { 83 | callback_addr: Option>, 84 | } 85 | 86 | impl PuppetServiceInner { 87 | fn new() -> Self { 88 | Self { callback_addr: None } 89 | } 90 | 91 | fn emit(&self, msg: PuppetEvent) { 92 | if let Err(e) = self.callback_addr.as_ref().unwrap().do_send(msg) { 93 | error!("Internal error: {}", e) 94 | } 95 | } 96 | } 97 | 98 | impl Actor for PuppetServiceInner { 99 | type Context = Context; 100 | 101 | fn started(&mut self, _ctx: &mut Self::Context) { 102 | info!("Puppet service started"); 103 | } 104 | 105 | fn stopped(&mut self, _ctx: &mut Self::Context) { 106 | info!("Puppet service stopped"); 107 | } 108 | } 109 | 110 | impl Handler for PuppetServiceInner { 111 | type Result = (); 112 | 113 | fn handle(&mut self, msg: PuppetServiceInternalMessage, ctx: &mut Self::Context) -> Self::Result { 114 | match msg { 115 | PuppetServiceInternalMessage::SetupCallback(callback_addr) => { 116 | self.callback_addr = Some(callback_addr); 117 | } 118 | PuppetServiceInternalMessage::SetupStream(stream) => { 119 | ctx.add_stream(stream); 120 | } 121 | } 122 | } 123 | } 124 | 125 | #[derive(Debug, Serialize, Deserialize, Clone)] 126 | #[serde(rename_all = "camelCase")] 127 | struct EventPayload { 128 | pub data: Option, 129 | pub contact_id: Option, 130 | pub message_id: Option, 131 | pub room_invitation_id: Option, 132 | pub friendship_id: Option, 133 | pub qrcode: Option, 134 | pub status: Option, 135 | pub timestamp: Option, 136 | pub changer_id: Option, 137 | pub new_topic: Option, 138 | pub old_topic: Option, 139 | pub room_id: Option, 140 | pub removee_id_list: Option>, 141 | pub remover_id: Option, 142 | pub invitee_id_list: Option>, 143 | pub inviter_id: Option, 144 | pub payload_type: Option, 145 | pub payload_id: Option, 146 | } 147 | 148 | impl StreamHandler> for PuppetServiceInner { 149 | fn handle(&mut self, item: Result, _ctx: &mut Self::Context) { 150 | match item { 151 | Ok(response) => { 152 | info!("Receive event response, {:?}", response); 153 | let payload: EventPayload = from_str(&response.payload).unwrap(); 154 | 155 | match response.r#type { 156 | 0 => { 157 | // Unspecified 158 | } 159 | 1 => { 160 | // Heartbeat 161 | if payload.data == None { 162 | error!("Heartbeat payload should have data"); 163 | } else { 164 | let data = match payload.data.unwrap() { 165 | serde_json::Value::String(data) => data, 166 | object @ serde_json::Value::Object(_) => object.to_string(), 167 | _ => { 168 | error!("Heartbeat payload should have string or object data"); 169 | return; 170 | } 171 | }; 172 | self.emit(PuppetEvent::Heartbeat(EventHeartbeatPayload { data })) 173 | } 174 | } 175 | 2 => { 176 | // Message 177 | if payload.message_id == None { 178 | error!("Message payload should have message id"); 179 | } else { 180 | self.emit(PuppetEvent::Message(EventMessagePayload { 181 | message_id: payload.message_id.unwrap(), 182 | })); 183 | } 184 | } 185 | 3 => { 186 | // Dong 187 | if payload.data == None { 188 | error!("Dong payload should have data"); 189 | } else if let serde_json::Value::String(data) = payload.data.unwrap() { 190 | self.emit(PuppetEvent::Dong(EventDongPayload { data })); 191 | } else { 192 | error!("Dong payload should have string data"); 193 | } 194 | } 195 | 16 => { 196 | // Error 197 | if payload.data == None { 198 | error!("Error payload should have data"); 199 | } else if let serde_json::Value::String(data) = payload.data.unwrap() { 200 | self.emit(PuppetEvent::Error(EventErrorPayload { data })); 201 | } else { 202 | error!("Error payload should have string data"); 203 | } 204 | } 205 | 17 => { 206 | // Friendship 207 | if payload.friendship_id == None { 208 | error!("Friendship payload should have friendship id"); 209 | } else { 210 | self.emit(PuppetEvent::Friendship(EventFriendshipPayload { 211 | friendship_id: payload.friendship_id.unwrap(), 212 | })); 213 | } 214 | } 215 | 18 => { 216 | // Room invite 217 | if payload.room_invitation_id == None { 218 | error!("Room invite payload should have room invitation id"); 219 | } else { 220 | self.emit(PuppetEvent::RoomInvite(EventRoomInvitePayload { 221 | room_invitation_id: payload.room_invitation_id.unwrap(), 222 | })); 223 | } 224 | } 225 | 19 => { 226 | // Room join 227 | if payload.room_id == None 228 | || payload.invitee_id_list == None 229 | || payload.inviter_id == None 230 | || payload.timestamp == None 231 | { 232 | error!("Room join payload should have room id, inviter id, invitee id list and timestamp"); 233 | } else { 234 | self.emit(PuppetEvent::RoomJoin(EventRoomJoinPayload { 235 | room_id: payload.room_id.unwrap(), 236 | inviter_id: payload.inviter_id.unwrap(), 237 | invitee_id_list: payload.invitee_id_list.unwrap(), 238 | timestamp: payload.timestamp.unwrap(), 239 | })); 240 | } 241 | } 242 | 20 => { 243 | // Room leave 244 | if payload.room_id == None 245 | || payload.removee_id_list == None 246 | || payload.remover_id == None 247 | || payload.timestamp == None 248 | { 249 | error!("Room leave payload should have room id, remover id, removee id list and timestamp"); 250 | } else { 251 | self.emit(PuppetEvent::RoomLeave(EventRoomLeavePayload { 252 | room_id: payload.room_id.unwrap(), 253 | remover_id: payload.remover_id.unwrap(), 254 | removee_id_list: payload.removee_id_list.unwrap(), 255 | timestamp: payload.timestamp.unwrap(), 256 | })); 257 | } 258 | } 259 | 21 => { 260 | // Room topic 261 | if payload.room_id == None 262 | || payload.changer_id == None 263 | || payload.old_topic == None 264 | || payload.new_topic == None 265 | || payload.timestamp == None 266 | { 267 | error!("Room topic payload should have room id, changer id, old topic, new topic and timestamp"); 268 | } else { 269 | self.emit(PuppetEvent::RoomTopic(EventRoomTopicPayload { 270 | room_id: payload.room_id.unwrap(), 271 | changer_id: payload.changer_id.unwrap(), 272 | old_topic: payload.old_topic.unwrap(), 273 | new_topic: payload.new_topic.unwrap(), 274 | timestamp: payload.timestamp.unwrap(), 275 | })); 276 | } 277 | } 278 | 22 => { 279 | // Scan 280 | if payload.status == None { 281 | error!("Scan payload should have scan status"); 282 | } else { 283 | self.emit(PuppetEvent::Scan(EventScanPayload { 284 | status: payload.status.unwrap(), 285 | qrcode: payload.qrcode, 286 | data: payload 287 | .data 288 | .map(|value| value.as_str().map(|s| s.to_string())) 289 | .flatten(), 290 | })); 291 | } 292 | } 293 | 23 => { 294 | // Ready 295 | if payload.data == None { 296 | error!("Ready payload should have data"); 297 | } else if let serde_json::Value::String(data) = payload.data.unwrap() { 298 | self.emit(PuppetEvent::Ready(EventReadyPayload { data })); 299 | } else { 300 | error!("Ready payload should have string data"); 301 | } 302 | } 303 | 24 => { 304 | // Reset 305 | if payload.data == None { 306 | error!("Reset payload should have data"); 307 | } else if let serde_json::Value::String(data) = payload.data.unwrap() { 308 | self.emit(PuppetEvent::Reset(EventResetPayload { data })); 309 | } else { 310 | error!("Reset payload should have string data"); 311 | } 312 | } 313 | 25 => { 314 | // Log in 315 | if payload.contact_id == None { 316 | error!("Login payload should have contact id"); 317 | } else { 318 | self.emit(PuppetEvent::Login(EventLoginPayload { 319 | contact_id: payload.contact_id.unwrap(), 320 | })); 321 | } 322 | } 323 | 26 => { 324 | // Log out 325 | if payload.contact_id == None || payload.data == None { 326 | error!("Logout payload should have contact id and data"); 327 | } else if let serde_json::Value::String(data) = payload.data.unwrap() { 328 | self.emit(PuppetEvent::Logout(EventLogoutPayload { 329 | contact_id: payload.contact_id.unwrap(), 330 | data, 331 | })); 332 | } else { 333 | error!("Logout payload should have string data"); 334 | } 335 | } 336 | 27 => { 337 | // Dirty 338 | if payload.payload_type == None || payload.payload_id == None { 339 | error!("Dirty payload should have payload type and payload id"); 340 | } else { 341 | self.emit(PuppetEvent::Dirty(EventDirtyPayload { 342 | payload_type: payload.payload_type.unwrap(), 343 | payload_id: payload.payload_id.unwrap(), 344 | })); 345 | } 346 | } 347 | _ => { 348 | error!("Invalid event type: {}", response.r#type); 349 | } 350 | } 351 | } 352 | Err(e) => { 353 | error!("Network error: {}", e); 354 | } 355 | } 356 | } 357 | 358 | fn finished(&mut self, _ctx: &mut Self::Context) { 359 | info!("Stream finished"); 360 | } 361 | } 362 | 363 | #[async_trait] 364 | impl PuppetImpl for PuppetService { 365 | async fn contact_self_name_set(&self, name: String) -> Result<(), PuppetError> { 366 | debug!("contact_self_name_set(name = {})", name); 367 | match self.client().contact_self_name(ContactSelfNameRequest { name }).await { 368 | Ok(_) => Ok(()), 369 | Err(_) => Err(PuppetError::Network("Failed to set contact self name".to_owned())), 370 | } 371 | } 372 | 373 | async fn contact_self_qr_code(&self) -> Result { 374 | debug!("contact_self_qr_code()"); 375 | match self.client().contact_self_qr_code(ContactSelfQrCodeRequest {}).await { 376 | Ok(response) => Ok(response.into_inner().qrcode), 377 | Err(_) => Err(PuppetError::Network("Failed to get contact self qrcode".to_owned())), 378 | } 379 | } 380 | 381 | async fn contact_self_signature_set(&self, signature: String) -> Result<(), PuppetError> { 382 | debug!("contact_self_signature_set(signature = {})", signature); 383 | match self 384 | .client() 385 | .contact_self_signature(ContactSelfSignatureRequest { signature }) 386 | .await 387 | { 388 | Ok(_) => Ok(()), 389 | Err(_) => Err(PuppetError::Network("Failed to set contact self signature".to_owned())), 390 | } 391 | } 392 | 393 | async fn tag_contact_add(&self, tag_id: String, contact_id: String) -> Result<(), PuppetError> { 394 | debug!("tag_contact_add(tag_id = {}, contact_id = {})", tag_id, contact_id); 395 | match self 396 | .client() 397 | .tag_contact_add(TagContactAddRequest { 398 | id: tag_id.clone(), 399 | contact_id: contact_id.clone(), 400 | }) 401 | .await 402 | { 403 | Ok(_) => Ok(()), 404 | Err(_) => Err(PuppetError::Network(format!( 405 | "Failed to add tag {} for contact {}", 406 | tag_id, contact_id 407 | ))), 408 | } 409 | } 410 | 411 | async fn tag_contact_remove(&self, tag_id: String, contact_id: String) -> Result<(), PuppetError> { 412 | debug!("tag_contact_remove(tag_id = {}, contact_id = {})", tag_id, contact_id); 413 | match self 414 | .client() 415 | .tag_contact_remove(TagContactRemoveRequest { 416 | id: tag_id.clone(), 417 | contact_id: contact_id.clone(), 418 | }) 419 | .await 420 | { 421 | Ok(_) => Ok(()), 422 | Err(_) => Err(PuppetError::Network(format!( 423 | "Failed to remove tag {} for contact {}", 424 | tag_id, contact_id 425 | ))), 426 | } 427 | } 428 | 429 | async fn tag_contact_delete(&self, tag_id: String) -> Result<(), PuppetError> { 430 | debug!("tag_contact_delete(tag_id = {})", tag_id); 431 | match self 432 | .client() 433 | .tag_contact_delete(TagContactDeleteRequest { id: tag_id.clone() }) 434 | .await 435 | { 436 | Ok(_) => Ok(()), 437 | Err(_) => Err(PuppetError::Network(format!("Failed to remove tag {}", tag_id))), 438 | } 439 | } 440 | 441 | async fn tag_contact_list(&self, contact_id: String) -> Result, PuppetError> { 442 | debug!("tag_contact_list(contact_id = {})", contact_id); 443 | match self 444 | .client() 445 | .tag_contact_list(TagContactListRequest { 446 | contact_id: Some(contact_id.clone()), 447 | }) 448 | .await 449 | { 450 | Ok(response) => Ok(response.into_inner().ids), 451 | Err(_) => Err(PuppetError::Network(format!( 452 | "Failed to get tags for contact {}", 453 | contact_id 454 | ))), 455 | } 456 | } 457 | 458 | async fn tag_list(&self) -> Result, PuppetError> { 459 | debug!("tag_list()"); 460 | match self 461 | .client() 462 | .tag_contact_list(TagContactListRequest { contact_id: None }) 463 | .await 464 | { 465 | Ok(response) => Ok(response.into_inner().ids), 466 | Err(_) => Err(PuppetError::Network("Failed to get tags".to_owned())), 467 | } 468 | } 469 | 470 | async fn contact_alias(&self, contact_id: String) -> Result { 471 | debug!("contact_alias(contact_id = {})", contact_id); 472 | match self 473 | .client() 474 | .contact_alias(ContactAliasRequest { 475 | id: contact_id.clone(), 476 | alias: None, 477 | }) 478 | .await 479 | { 480 | Ok(response) => Ok(response.into_inner().alias.unwrap()), 481 | Err(_) => Err(PuppetError::Network(format!( 482 | "Failed to get alias of contact {}", 483 | contact_id 484 | ))), 485 | } 486 | } 487 | 488 | async fn contact_alias_set(&self, contact_id: String, alias: String) -> Result<(), PuppetError> { 489 | debug!("contact_alias_set(contact_id = {}, alias = {})", contact_id, alias); 490 | match self 491 | .client() 492 | .contact_alias(ContactAliasRequest { 493 | id: contact_id.clone(), 494 | alias: Some(alias.clone()), 495 | }) 496 | .await 497 | { 498 | Ok(_) => Ok(()), 499 | Err(_) => Err(PuppetError::Network(format!( 500 | "Failed to set alias for contact {}", 501 | contact_id 502 | ))), 503 | } 504 | } 505 | 506 | async fn contact_avatar(&self, contact_id: String) -> Result { 507 | debug!("contact_avatar(contact_id = {})", contact_id); 508 | match self 509 | .client() 510 | .contact_avatar(ContactAvatarRequest { 511 | id: contact_id.clone(), 512 | filebox: None, 513 | }) 514 | .await 515 | { 516 | Ok(response) => Ok(FileBox::from(response.into_inner().filebox.unwrap())), 517 | Err(_) => Err(PuppetError::Network(format!( 518 | "Failed to get avatar of contact {}", 519 | contact_id 520 | ))), 521 | } 522 | } 523 | 524 | async fn contact_avatar_set(&self, contact_id: String, file: FileBox) -> Result<(), PuppetError> { 525 | debug!("contact_avatar_set(contact_id = {}, file = {})", contact_id, file); 526 | match self 527 | .client() 528 | .contact_avatar(ContactAvatarRequest { 529 | id: contact_id.clone(), 530 | filebox: Some(file.to_string()), 531 | }) 532 | .await 533 | { 534 | Ok(_) => Ok(()), 535 | Err(_) => Err(PuppetError::Network(format!( 536 | "Failed to set avatar for contact {}", 537 | contact_id 538 | ))), 539 | } 540 | } 541 | 542 | async fn contact_phone_set(&self, contact_id: String, phone_list: Vec) -> Result<(), PuppetError> { 543 | debug!( 544 | "contact_phone_set(contact_id = {}, phone_list = {:?})", 545 | contact_id, phone_list 546 | ); 547 | match self 548 | .client() 549 | .contact_phone(ContactPhoneRequest { 550 | contact_id: contact_id.clone(), 551 | phone_list, 552 | }) 553 | .await 554 | { 555 | Ok(_) => Ok(()), 556 | Err(_) => Err(PuppetError::Network(format!( 557 | "Failed to set phone for contact {}", 558 | contact_id 559 | ))), 560 | } 561 | } 562 | 563 | async fn contact_corporation_remark_set( 564 | &self, 565 | contact_id: String, 566 | corporation_remark: Option, 567 | ) -> Result<(), PuppetError> { 568 | debug!( 569 | "contact_corporation_remark_set(contact_id = {}, corporation_remark = {:?})", 570 | contact_id, corporation_remark 571 | ); 572 | match self 573 | .client() 574 | .contact_corporation_remark(ContactCorporationRemarkRequest { 575 | contact_id: contact_id.clone(), 576 | corporation_remark, 577 | }) 578 | .await 579 | { 580 | Ok(_) => Ok(()), 581 | Err(_) => Err(PuppetError::Network(format!( 582 | "Failed to set corporation remark for contact {}", 583 | contact_id 584 | ))), 585 | } 586 | } 587 | 588 | async fn contact_description_set( 589 | &self, 590 | contact_id: String, 591 | description: Option, 592 | ) -> Result<(), PuppetError> { 593 | debug!( 594 | "contact_description_set(contact_id = {}, description = {:?})", 595 | contact_id, description 596 | ); 597 | match self 598 | .client() 599 | .contact_description(ContactDescriptionRequest { 600 | contact_id: contact_id.clone(), 601 | description, 602 | }) 603 | .await 604 | { 605 | Ok(_) => Ok(()), 606 | Err(_) => Err(PuppetError::Network(format!( 607 | "Failed to set description for contact {}", 608 | contact_id 609 | ))), 610 | } 611 | } 612 | 613 | async fn contact_list(&self) -> Result, PuppetError> { 614 | debug!("contact_list()"); 615 | match self.client().contact_list(ContactListRequest {}).await { 616 | Ok(response) => Ok(response.into_inner().ids), 617 | Err(_) => Err(PuppetError::Network("Failed to get contacts".to_owned())), 618 | } 619 | } 620 | 621 | async fn contact_raw_payload(&self, contact_id: String) -> Result { 622 | debug!("contact_raw_payload(contact_id = {})", contact_id); 623 | match self 624 | .client() 625 | .contact_payload(ContactPayloadRequest { id: contact_id.clone() }) 626 | .await 627 | { 628 | Ok(response) => Ok(ContactPayload::from_payload_response(response.into_inner())), 629 | Err(_) => Err(PuppetError::Network(format!( 630 | "Failed to get raw payload for contact {}", 631 | contact_id 632 | ))), 633 | } 634 | } 635 | 636 | async fn message_contact(&self, message_id: String) -> Result { 637 | debug!("message_contact(message_id = {})", message_id); 638 | match self 639 | .client() 640 | .message_contact(MessageContactRequest { id: message_id.clone() }) 641 | .await 642 | { 643 | Ok(response) => Ok(response.into_inner().id), 644 | Err(_) => Err(PuppetError::Network(format!( 645 | "Failed to get contact of message {}", 646 | message_id 647 | ))), 648 | } 649 | } 650 | 651 | async fn message_file(&self, message_id: String) -> Result { 652 | debug!("message_file(message_id = {})", message_id); 653 | match self 654 | .client() 655 | .message_file(MessageFileRequest { id: message_id.clone() }) 656 | .await 657 | { 658 | Ok(response) => Ok(FileBox::from(response.into_inner().filebox)), 659 | Err(_) => Err(PuppetError::Network(format!( 660 | "Failed to get file of message {}", 661 | message_id 662 | ))), 663 | } 664 | } 665 | 666 | async fn message_image(&self, message_id: String, image_type: ImageType) -> Result { 667 | debug!("message_image(message_id = {})", message_id); 668 | match self 669 | .client() 670 | .message_image(MessageImageRequest { 671 | id: message_id.clone(), 672 | r#type: image_type.to_i32().unwrap(), 673 | }) 674 | .await 675 | { 676 | Ok(response) => Ok(FileBox::from(response.into_inner().filebox)), 677 | Err(_) => Err(PuppetError::Network(format!( 678 | "Failed to get image of message {}", 679 | message_id 680 | ))), 681 | } 682 | } 683 | 684 | async fn message_mini_program(&self, message_id: String) -> Result { 685 | debug!("message_mini_program(message_id = {})", message_id); 686 | match self 687 | .client() 688 | .message_mini_program(MessageMiniProgramRequest { id: message_id.clone() }) 689 | .await 690 | { 691 | Ok(response) => Ok(from_str(&response.into_inner().mini_program).unwrap()), 692 | Err(_) => Err(PuppetError::Network(format!( 693 | "Failed to get mini_program of message {}", 694 | message_id 695 | ))), 696 | } 697 | } 698 | 699 | async fn message_url(&self, message_id: String) -> Result { 700 | debug!("message_url(message_id = {})", message_id); 701 | match self 702 | .client() 703 | .message_url(MessageUrlRequest { id: message_id.clone() }) 704 | .await 705 | { 706 | Ok(response) => Ok(from_str(&response.into_inner().url_link).unwrap()), 707 | Err(_) => Err(PuppetError::Network(format!( 708 | "Failed to get url link of message {}", 709 | message_id 710 | ))), 711 | } 712 | } 713 | 714 | async fn message_send_contact( 715 | &self, 716 | conversation_id: String, 717 | contact_id: String, 718 | ) -> Result, PuppetError> { 719 | debug!( 720 | "message_send_contact(conversation_id = {}, contact_id = {})", 721 | conversation_id, contact_id 722 | ); 723 | match self 724 | .client() 725 | .message_send_contact(MessageSendContactRequest { 726 | conversation_id: conversation_id.clone(), 727 | contact_id: contact_id.clone(), 728 | }) 729 | .await 730 | { 731 | Ok(response) => Ok(response.into_inner().id), 732 | Err(_) => Err(PuppetError::Network(format!( 733 | "Failed to send contact {} in conversation {}", 734 | contact_id, conversation_id 735 | ))), 736 | } 737 | } 738 | 739 | async fn message_send_file(&self, conversation_id: String, file: FileBox) -> Result, PuppetError> { 740 | debug!( 741 | "message_send_file(conversation_id = {}, file = {})", 742 | conversation_id, file 743 | ); 744 | match self 745 | .client() 746 | .message_send_file(MessageSendFileRequest { 747 | conversation_id: conversation_id.clone(), 748 | filebox: file.to_string(), 749 | }) 750 | .await 751 | { 752 | Ok(response) => Ok(response.into_inner().id), 753 | Err(_) => Err(PuppetError::Network(format!( 754 | "Failed to send file in conversation {}", 755 | conversation_id 756 | ))), 757 | } 758 | } 759 | 760 | async fn message_send_mini_program( 761 | &self, 762 | conversation_id: String, 763 | mini_program_payload: MiniProgramPayload, 764 | ) -> Result, PuppetError> { 765 | debug!( 766 | "message_send_file(conversation_id = {}, mini_program_payload = {:?})", 767 | conversation_id, mini_program_payload 768 | ); 769 | match self 770 | .client() 771 | .message_send_mini_program(MessageSendMiniProgramRequest { 772 | conversation_id: conversation_id.clone(), 773 | mini_program: to_string::(&mini_program_payload).unwrap(), 774 | }) 775 | .await 776 | { 777 | Ok(response) => Ok(response.into_inner().id), 778 | Err(_) => Err(PuppetError::Network(format!( 779 | "Failed to send mini program in conversation {}", 780 | conversation_id 781 | ))), 782 | } 783 | } 784 | 785 | async fn message_send_text( 786 | &self, 787 | conversation_id: String, 788 | text: String, 789 | mention_id_list: Vec, 790 | ) -> Result, PuppetError> { 791 | debug!( 792 | "message_send_text(conversation_id = {}, text = {}, mention_id_list = {:?})", 793 | conversation_id, text, mention_id_list 794 | ); 795 | match self 796 | .client() 797 | .message_send_text(MessageSendTextRequest { 798 | conversation_id: conversation_id.clone(), 799 | text, 800 | mentonal_ids: mention_id_list, 801 | }) 802 | .await 803 | { 804 | Ok(response) => Ok(response.into_inner().id), 805 | Err(_) => Err(PuppetError::Network(format!( 806 | "Failed to send text in conversation {}", 807 | conversation_id 808 | ))), 809 | } 810 | } 811 | 812 | async fn message_send_url( 813 | &self, 814 | conversation_id: String, 815 | url_link_payload: UrlLinkPayload, 816 | ) -> Result, PuppetError> { 817 | debug!( 818 | "message_send_url(conversation_id = {}, url_link_payload = {:?})", 819 | conversation_id, url_link_payload 820 | ); 821 | match self 822 | .client() 823 | .message_send_url(MessageSendUrlRequest { 824 | conversation_id: conversation_id.clone(), 825 | url_link: to_string::(&url_link_payload).unwrap(), 826 | }) 827 | .await 828 | { 829 | Ok(response) => Ok(response.into_inner().id), 830 | Err(_) => Err(PuppetError::Network(format!( 831 | "Failed to send url link in conversation {}", 832 | conversation_id 833 | ))), 834 | } 835 | } 836 | 837 | async fn message_raw_payload(&self, message_id: String) -> Result { 838 | debug!("message_raw_payload(message_id = {})", message_id); 839 | match self 840 | .client() 841 | .message_payload(MessagePayloadRequest { id: message_id.clone() }) 842 | .await 843 | { 844 | Ok(response) => Ok(MessagePayload::from_payload_response(response.into_inner())), 845 | Err(_) => Err(PuppetError::Network(format!( 846 | "Failed to get raw payload for message {}", 847 | message_id 848 | ))), 849 | } 850 | } 851 | 852 | async fn friendship_accept(&self, friendship_id: String) -> Result<(), PuppetError> { 853 | debug!("friendship_accept(friendship_id = {})", friendship_id); 854 | match self 855 | .client() 856 | .friendship_accept(FriendshipAcceptRequest { 857 | id: friendship_id.clone(), 858 | }) 859 | .await 860 | { 861 | Ok(_) => Ok(()), 862 | Err(_) => Err(PuppetError::Network(format!( 863 | "Failed to accept friendship {}", 864 | friendship_id 865 | ))), 866 | } 867 | } 868 | 869 | async fn friendship_add(&self, contact_id: String, hello: Option) -> Result<(), PuppetError> { 870 | debug!("friendship_add(contact_id = {}, hello = {:?})", contact_id, hello); 871 | match self 872 | .client() 873 | .friendship_add(FriendshipAddRequest { 874 | contact_id: contact_id.clone(), 875 | hello: if let Some(hello) = hello { hello } else { String::new() }, 876 | }) 877 | .await 878 | { 879 | Ok(_) => Ok(()), 880 | Err(_) => Err(PuppetError::Network(format!("Failed to add contact {}", contact_id))), 881 | } 882 | } 883 | 884 | async fn friendship_search_phone(&self, phone: String) -> Result, PuppetError> { 885 | debug!("friendship_search_phone(phone = {})", phone); 886 | match self 887 | .client() 888 | .friendship_search_phone(FriendshipSearchPhoneRequest { phone: phone.clone() }) 889 | .await 890 | { 891 | Ok(response) => Ok(response.into_inner().contact_id), 892 | Err(_) => Err(PuppetError::Network(format!("Failed to search phone {}", phone))), 893 | } 894 | } 895 | 896 | async fn friendship_search_weixin(&self, weixin: String) -> Result, PuppetError> { 897 | debug!("friendship_search_weixin(weixin = {})", weixin); 898 | match self 899 | .client() 900 | .friendship_search_weixin(FriendshipSearchWeixinRequest { weixin: weixin.clone() }) 901 | .await 902 | { 903 | Ok(response) => Ok(response.into_inner().contact_id), 904 | Err(_) => Err(PuppetError::Network(format!("Failed to search weixin {}", weixin))), 905 | } 906 | } 907 | 908 | async fn friendship_raw_payload(&self, friendship_id: String) -> Result { 909 | debug!("friendship_raw_payload(friendship_id = {})", friendship_id); 910 | match self 911 | .client() 912 | .friendship_payload(FriendshipPayloadRequest { 913 | id: friendship_id.clone(), 914 | payload: None, 915 | }) 916 | .await 917 | { 918 | Ok(response) => Ok(FriendshipPayload::from_payload_response(response.into_inner())), 919 | Err(_) => Err(PuppetError::Network(format!( 920 | "Failed to get raw payload for friendship {}", 921 | friendship_id 922 | ))), 923 | } 924 | } 925 | 926 | async fn room_invitation_accept(&self, room_invitation_id: String) -> Result<(), PuppetError> { 927 | debug!("room_invitation_accept(room_invitation_id = {})", room_invitation_id); 928 | match self 929 | .client() 930 | .room_invitation_accept(RoomInvitationAcceptRequest { 931 | id: room_invitation_id.clone(), 932 | }) 933 | .await 934 | { 935 | Ok(_) => Ok(()), 936 | Err(_) => Err(PuppetError::Network(format!( 937 | "Failed to accept room invitation {}", 938 | room_invitation_id 939 | ))), 940 | } 941 | } 942 | 943 | async fn room_invitation_raw_payload( 944 | &self, 945 | room_invitation_id: String, 946 | ) -> Result { 947 | debug!( 948 | "room_invitation_raw_payload(room_invitation_id = {})", 949 | room_invitation_id 950 | ); 951 | match self 952 | .client() 953 | .room_invitation_payload(RoomInvitationPayloadRequest { 954 | id: room_invitation_id.clone(), 955 | payload: None, 956 | }) 957 | .await 958 | { 959 | Ok(response) => Ok(RoomInvitationPayload::from_payload_response(response.into_inner())), 960 | Err(_) => Err(PuppetError::Network(format!( 961 | "Failed to get raw payload for room invitation {}", 962 | room_invitation_id 963 | ))), 964 | } 965 | } 966 | 967 | async fn room_add(&self, room_id: String, contact_id: String) -> Result<(), PuppetError> { 968 | debug!("room_add(room_id = {}, contact_id = {})", room_id, contact_id); 969 | match self 970 | .client() 971 | .room_add(RoomAddRequest { 972 | id: room_id.clone(), 973 | contact_id: contact_id.clone(), 974 | }) 975 | .await 976 | { 977 | Ok(_) => Ok(()), 978 | Err(_) => Err(PuppetError::Network(format!( 979 | "Failed to add contact {} into room {}", 980 | contact_id, room_id 981 | ))), 982 | } 983 | } 984 | 985 | async fn room_avatar(&self, room_id: String) -> Result { 986 | debug!("room_avatar(room_id = {})", room_id); 987 | match self 988 | .client() 989 | .room_avatar(RoomAvatarRequest { id: room_id.clone() }) 990 | .await 991 | { 992 | Ok(response) => Ok(FileBox::from(response.into_inner().filebox)), 993 | Err(_) => Err(PuppetError::Network(format!( 994 | "Failed to get avatar of room {}", 995 | room_id 996 | ))), 997 | } 998 | } 999 | 1000 | async fn room_create(&self, contact_id_list: Vec, topic: Option) -> Result { 1001 | debug!( 1002 | "room_create(contact_id_list = {:?}, topic = {:?})", 1003 | contact_id_list, topic 1004 | ); 1005 | match self 1006 | .client() 1007 | .room_create(RoomCreateRequest { 1008 | contact_ids: contact_id_list, 1009 | topic: if let Some(topic) = topic { topic } else { String::new() }, 1010 | }) 1011 | .await 1012 | { 1013 | Ok(response) => Ok(response.into_inner().id), 1014 | Err(_) => Err(PuppetError::Network("Failed to create room".to_owned())), 1015 | } 1016 | } 1017 | 1018 | async fn room_del(&self, room_id: String, contact_id: String) -> Result<(), PuppetError> { 1019 | debug!("room_del(room_id = {}, contact_id = {})", room_id, contact_id); 1020 | match self 1021 | .client() 1022 | .room_del(RoomDelRequest { 1023 | id: room_id.clone(), 1024 | contact_id: contact_id.clone(), 1025 | }) 1026 | .await 1027 | { 1028 | Ok(_) => Ok(()), 1029 | Err(_) => Err(PuppetError::Network(format!( 1030 | "Failed to remove contact {} from room {}", 1031 | contact_id, room_id 1032 | ))), 1033 | } 1034 | } 1035 | 1036 | async fn room_qr_code(&self, room_id: String) -> Result { 1037 | debug!("room_qr_code(room_id = {})", room_id); 1038 | match self 1039 | .client() 1040 | .room_qr_code(RoomQrCodeRequest { id: room_id.clone() }) 1041 | .await 1042 | { 1043 | Ok(response) => Ok(response.into_inner().qrcode), 1044 | Err(_) => Err(PuppetError::Network(format!( 1045 | "Failed to get qrcode of room {}", 1046 | room_id 1047 | ))), 1048 | } 1049 | } 1050 | 1051 | async fn room_quit(&self, room_id: String) -> Result<(), PuppetError> { 1052 | debug!("room_quit(room_id = {})", room_id); 1053 | match self.client().room_quit(RoomQuitRequest { id: room_id.clone() }).await { 1054 | Ok(_) => Ok(()), 1055 | Err(_) => Err(PuppetError::Network(format!("Failed to quit room {}", room_id))), 1056 | } 1057 | } 1058 | 1059 | async fn room_topic(&self, room_id: String) -> Result { 1060 | debug!("room_topic(room_id = {})", room_id); 1061 | match self 1062 | .client() 1063 | .room_topic(RoomTopicRequest { 1064 | id: room_id.clone(), 1065 | topic: None, 1066 | }) 1067 | .await 1068 | { 1069 | Ok(response) => Ok(response.into_inner().topic.unwrap()), 1070 | Err(_) => Err(PuppetError::Network(format!("Failed to get topic of room {}", room_id))), 1071 | } 1072 | } 1073 | 1074 | async fn room_topic_set(&self, room_id: String, topic: String) -> Result<(), PuppetError> { 1075 | debug!("room_topic_set(room_id = {}, topic = {})", room_id, topic); 1076 | match self 1077 | .client() 1078 | .room_topic(RoomTopicRequest { 1079 | id: room_id.clone(), 1080 | topic: Some(topic), 1081 | }) 1082 | .await 1083 | { 1084 | Ok(_) => Ok(()), 1085 | Err(_) => Err(PuppetError::Network(format!( 1086 | "Failed to set topic for room {}", 1087 | room_id 1088 | ))), 1089 | } 1090 | } 1091 | 1092 | async fn room_list(&self) -> Result, PuppetError> { 1093 | debug!("room_list()"); 1094 | match self.client().room_list(RoomListRequest {}).await { 1095 | Ok(response) => Ok(response.into_inner().ids), 1096 | Err(_) => Err(PuppetError::Network("Failed to get rooms".to_owned())), 1097 | } 1098 | } 1099 | 1100 | async fn room_raw_payload(&self, room_id: String) -> Result { 1101 | debug!("room_raw_payload(room_id = {})", room_id); 1102 | match self 1103 | .client() 1104 | .room_payload(RoomPayloadRequest { id: room_id.clone() }) 1105 | .await 1106 | { 1107 | Ok(response) => Ok(RoomPayload::from_payload_response(response.into_inner())), 1108 | Err(_) => Err(PuppetError::Network(format!( 1109 | "Failed to get raw payload for room {}", 1110 | room_id 1111 | ))), 1112 | } 1113 | } 1114 | 1115 | async fn room_announce(&self, room_id: String) -> Result { 1116 | debug!("room_announce(room_id = {})", room_id); 1117 | match self 1118 | .client() 1119 | .room_announce(RoomAnnounceRequest { 1120 | id: room_id.clone(), 1121 | text: None, 1122 | }) 1123 | .await 1124 | { 1125 | Ok(response) => Ok(response.into_inner().text.unwrap()), 1126 | Err(_) => Err(PuppetError::Network(format!( 1127 | "Failed to get announce of room {}", 1128 | room_id 1129 | ))), 1130 | } 1131 | } 1132 | 1133 | async fn room_announce_set(&self, room_id: String, text: String) -> Result<(), PuppetError> { 1134 | debug!("room_announce(room_id = {}, text = {})", room_id, text); 1135 | match self 1136 | .client() 1137 | .room_announce(RoomAnnounceRequest { 1138 | id: room_id.clone(), 1139 | text: Some(text), 1140 | }) 1141 | .await 1142 | { 1143 | Ok(_) => Ok(()), 1144 | Err(_) => Err(PuppetError::Network(format!( 1145 | "Failed to set announce for room {}", 1146 | room_id 1147 | ))), 1148 | } 1149 | } 1150 | 1151 | async fn room_member_list(&self, room_id: String) -> Result, PuppetError> { 1152 | debug!("room_member_list(room_id = {})", room_id); 1153 | match self 1154 | .client() 1155 | .room_member_list(RoomMemberListRequest { id: room_id.clone() }) 1156 | .await 1157 | { 1158 | Ok(response) => Ok(response.into_inner().member_ids), 1159 | Err(_) => Err(PuppetError::Network(format!( 1160 | "Failed to get members of room {}", 1161 | room_id 1162 | ))), 1163 | } 1164 | } 1165 | 1166 | async fn room_member_raw_payload( 1167 | &self, 1168 | room_id: String, 1169 | contact_id: String, 1170 | ) -> Result { 1171 | debug!( 1172 | "room_member_raw_payload(room_id = {}, contact_id = {})", 1173 | room_id, contact_id 1174 | ); 1175 | match self 1176 | .client() 1177 | .room_member_payload(RoomMemberPayloadRequest { 1178 | id: room_id.clone(), 1179 | member_id: contact_id.clone(), 1180 | }) 1181 | .await 1182 | { 1183 | Ok(response) => Ok(RoomMemberPayload::from_payload_response(response.into_inner())), 1184 | Err(_) => Err(PuppetError::Network(format!( 1185 | "Failed to get raw payload for member {} of room {}", 1186 | contact_id, room_id 1187 | ))), 1188 | } 1189 | } 1190 | 1191 | async fn start(&self) -> Result<(), PuppetError> { 1192 | debug!("start()"); 1193 | match self.client().start(StartRequest {}).await { 1194 | Ok(_) => Ok(()), 1195 | Err(_) => Err(PuppetError::Network("Failed to start puppet".to_owned())), 1196 | } 1197 | } 1198 | 1199 | async fn stop(&self) -> Result<(), PuppetError> { 1200 | debug!("stop()"); 1201 | match self.client().stop(StopRequest {}).await { 1202 | Ok(_) => Ok(()), 1203 | Err(_) => Err(PuppetError::Network("Failed to stop puppet".to_owned())), 1204 | } 1205 | } 1206 | 1207 | async fn ding(&self, data: String) -> Result<(), PuppetError> { 1208 | debug!("ding(data = {})", data); 1209 | match self.client().ding(DingRequest { data }).await { 1210 | Ok(_) => Ok(()), 1211 | Err(_) => Err(PuppetError::Network("Failed to ding".to_owned())), 1212 | } 1213 | } 1214 | 1215 | async fn version(&self) -> Result { 1216 | debug!("version()"); 1217 | match self.client().version(VersionRequest {}).await { 1218 | Ok(response) => Ok(response.into_inner().version), 1219 | Err(_) => Err(PuppetError::Network("Failed to get puppet version".to_owned())), 1220 | } 1221 | } 1222 | 1223 | async fn logout(&self) -> Result<(), PuppetError> { 1224 | debug!("logout()"); 1225 | match self.client().logout(LogoutRequest {}).await { 1226 | Ok(_) => Ok(()), 1227 | Err(_) => Err(PuppetError::Network("Failed to logout".to_owned())), 1228 | } 1229 | } 1230 | } 1231 | 1232 | #[cfg(test)] 1233 | mod tests { 1234 | use super::*; 1235 | 1236 | #[actix_rt::test] 1237 | async fn cannot_create_puppet_service_with_invalid_token() { 1238 | let invalid_token = uuid::Uuid::new_v4().to_string(); 1239 | 1240 | match PuppetService::new(PuppetOptions { 1241 | endpoint: None, 1242 | timeout: None, 1243 | token: Some(invalid_token), 1244 | }) 1245 | .await 1246 | { 1247 | Err(e) => println!("Failed to create puppet service: {}", e), 1248 | Ok(_) => println!("Create puppet service successfully"), 1249 | } 1250 | } 1251 | } 1252 | -------------------------------------------------------------------------------- /wechaty-puppet-service/src/service_endpoint.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use wechaty_puppet::error::PuppetError; 3 | 4 | #[derive(Debug, Deserialize)] 5 | struct Endpoint { 6 | ip: String, 7 | port: usize, 8 | } 9 | 10 | const WECHATY_ENDPOINT_RESOLUTION_SERVICE_URI: &str = "https://api.chatie.io/v0/hosties/"; 11 | const ENDPOINT_SERVICE_ERROR: &str = "Endpoint service error"; 12 | 13 | pub async fn discover(token: String) -> Result { 14 | match reqwest::get(&format!("{}{}", WECHATY_ENDPOINT_RESOLUTION_SERVICE_URI, token)).await { 15 | Ok(res) => match res.json::().await { 16 | Ok(endpoint) => { 17 | if endpoint.port == 0 { 18 | Err(PuppetError::InvalidToken) 19 | } else { 20 | Ok(format!("grpc://{}:{}", endpoint.ip, endpoint.port)) 21 | } 22 | } 23 | Err(_) => Err(PuppetError::Network(ENDPOINT_SERVICE_ERROR.to_owned())), 24 | }, 25 | Err(_) => Err(PuppetError::Network(ENDPOINT_SERVICE_ERROR.to_owned())), 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | 33 | #[actix_rt::test] 34 | async fn can_discover() { 35 | println!("{:?}", discover("123".to_owned()).await); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /wechaty-puppet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wechaty_puppet" 3 | version = "0.1.0-beta.1" 4 | authors = ["Gabriel Wu "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Rust implementation of wechaty-puppet" 8 | keywords = ["chatbot", "wechaty"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | actix = "0.12" 14 | async-trait = "0.1" 15 | file-box = { version = "0.1.0-beta.0", path = "../file-box" } 16 | futures = "0.3" 17 | log = "0.4" 18 | lru = "0.6" 19 | num-derive = "0.3" 20 | num-traits = "0.2" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_repr = "0.1" 23 | tokio-stream = "0.1" 24 | regex = "1" -------------------------------------------------------------------------------- /wechaty-puppet/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | /// The errors that can occur during the communication with the puppet. 4 | pub enum PuppetError { 5 | InvalidToken, 6 | Network(String), 7 | Unsupported(String), 8 | UnknownPayloadType, 9 | UnknownMessageType, 10 | } 11 | 12 | impl fmt::Debug for PuppetError { 13 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | write!(fmt, "PuppetError({})", self) 15 | } 16 | } 17 | 18 | impl fmt::Display for PuppetError { 19 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match self { 21 | PuppetError::InvalidToken => write!(fmt, "Invalid token"), 22 | PuppetError::Network(reason) => write!(fmt, "Network failure, reason: {}", reason), 23 | PuppetError::Unsupported(function) => write!(fmt, "Unsupported function: {}", function), 24 | PuppetError::UnknownPayloadType => write!(fmt, "Unknown payload type"), 25 | PuppetError::UnknownMessageType => write!(fmt, "Unknown message type"), 26 | } 27 | } 28 | } 29 | 30 | impl error::Error for PuppetError {} 31 | -------------------------------------------------------------------------------- /wechaty-puppet/src/events.rs: -------------------------------------------------------------------------------- 1 | use actix::Message; 2 | 3 | use crate::schemas::event::*; 4 | // use crate::types::AsyncFnPtr; 5 | 6 | // pub type PuppetDirtyListener = AsyncFnPtr; 7 | // pub type PuppetDongListener = AsyncFnPtr; 8 | // pub type PuppetErrorListener = AsyncFnPtr; 9 | // pub type PuppetFriendshipListener = AsyncFnPtr; 10 | // pub type PuppetHeartbeatListener = AsyncFnPtr; 11 | // pub type PuppetLoginListener = AsyncFnPtr; 12 | // pub type PuppetLogoutListener = AsyncFnPtr; 13 | // pub type PuppetMessageListener = AsyncFnPtr; 14 | // pub type PuppetReadyListener = AsyncFnPtr; 15 | // pub type PuppetResetListener = AsyncFnPtr; 16 | // pub type PuppetRoomInviteListener = AsyncFnPtr; 17 | // pub type PuppetRoomJoinListener = AsyncFnPtr; 18 | // pub type PuppetRoomLeaveListener = AsyncFnPtr; 19 | // pub type PuppetRoomTopicListener = AsyncFnPtr; 20 | // pub type PuppetScanListener = AsyncFnPtr; 21 | 22 | #[derive(Debug, Clone, Message)] 23 | #[rtype("()")] 24 | pub enum PuppetEvent { 25 | Dirty(EventDirtyPayload), 26 | Dong(EventDongPayload), 27 | Error(EventErrorPayload), 28 | Friendship(EventFriendshipPayload), 29 | Heartbeat(EventHeartbeatPayload), 30 | Login(EventLoginPayload), 31 | Logout(EventLogoutPayload), 32 | Message(EventMessagePayload), 33 | Ready(EventReadyPayload), 34 | Reset(EventResetPayload), 35 | RoomInvite(EventRoomInvitePayload), 36 | RoomJoin(EventRoomJoinPayload), 37 | RoomLeave(EventRoomLeavePayload), 38 | RoomTopic(EventRoomTopicPayload), 39 | Scan(EventScanPayload), 40 | } 41 | -------------------------------------------------------------------------------- /wechaty-puppet/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate num_derive; 3 | 4 | pub mod error; 5 | pub mod events; 6 | pub mod puppet; 7 | pub mod schemas; 8 | pub mod types; 9 | 10 | pub use error::PuppetError; 11 | pub use events::PuppetEvent; 12 | pub use file_box::FileBox; 13 | pub use puppet::{Puppet, PuppetImpl, Subscribe, UnSubscribe}; 14 | pub use schemas::contact::*; 15 | pub use schemas::event::*; 16 | pub use schemas::friendship::*; 17 | pub use schemas::image::ImageType; 18 | pub use schemas::message::*; 19 | pub use schemas::mini_program::MiniProgramPayload; 20 | pub use schemas::payload::PayloadType; 21 | pub use schemas::puppet::PuppetOptions; 22 | pub use schemas::room::*; 23 | pub use schemas::room_invitation::RoomInvitationPayload; 24 | pub use schemas::url_link::UrlLinkPayload; 25 | pub use types::{AsyncFnPtr, IntoAsyncFnPtr}; 26 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/contact.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use serde_repr::{Deserialize_repr, Serialize_repr}; 3 | 4 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 5 | #[repr(i32)] 6 | pub enum ContactGender { 7 | Unknown, 8 | Male, 9 | Female, 10 | } 11 | 12 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 13 | #[repr(i32)] 14 | pub enum ContactType { 15 | Unknown, 16 | Individual, 17 | Official, 18 | Corporation, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct ContactPayload { 23 | pub id: String, 24 | pub gender: ContactGender, 25 | pub contact_type: ContactType, 26 | pub name: String, 27 | pub avatar: String, 28 | pub address: String, 29 | pub alias: String, 30 | pub city: String, 31 | pub friend: bool, 32 | pub province: String, 33 | pub signature: String, 34 | pub star: bool, 35 | pub weixin: String, 36 | pub corporation: String, 37 | pub title: String, 38 | pub description: String, 39 | pub coworker: bool, 40 | pub phone: Vec, 41 | } 42 | 43 | #[derive(Default, Debug, Clone)] 44 | pub struct ContactQueryFilter { 45 | pub alias: Option, 46 | pub alias_regex: Option, 47 | pub id: Option, 48 | pub name: Option, 49 | pub name_regex: Option, 50 | pub weixin: Option, 51 | } 52 | 53 | // FIXME: trait aliases are experimental, see issue #41517 54 | // pub trait ContactPayloadFilterFunction = Fn(ContactPayload) -> bool; 55 | // 56 | // pub trait ContactPayloadFilterFactory = Fn(ContactQueryFilter) -> 57 | // ContactPayloadFilterFunction; 58 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/event.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | use crate::schemas::payload::PayloadType; 4 | 5 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 6 | #[repr(i32)] 7 | pub enum ScanStatus { 8 | Unknown, 9 | Cancel, 10 | Waiting, 11 | Scanned, 12 | Confirmed, 13 | Timeout, 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct EventFriendshipPayload { 18 | pub friendship_id: String, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct EventLoginPayload { 23 | pub contact_id: String, 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct EventLogoutPayload { 28 | pub contact_id: String, 29 | pub data: String, 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | pub struct EventMessagePayload { 34 | pub message_id: String, 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct EventRoomInvitePayload { 39 | pub room_invitation_id: String, 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct EventRoomJoinPayload { 44 | pub invitee_id_list: Vec, 45 | pub inviter_id: String, 46 | pub room_id: String, 47 | pub timestamp: u64, 48 | } 49 | 50 | #[derive(Debug, Clone)] 51 | pub struct EventRoomLeavePayload { 52 | pub removee_id_list: Vec, 53 | pub remover_id: String, 54 | pub room_id: String, 55 | pub timestamp: u64, 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | pub struct EventRoomTopicPayload { 60 | pub changer_id: String, 61 | pub new_topic: String, 62 | pub old_topic: String, 63 | pub room_id: String, 64 | pub timestamp: u64, 65 | } 66 | 67 | #[derive(Debug, Clone)] 68 | pub struct EventScanPayload { 69 | pub status: ScanStatus, 70 | pub qrcode: Option, 71 | pub data: Option, 72 | } 73 | 74 | #[derive(Debug, Clone)] 75 | pub struct EventDongPayload { 76 | pub data: String, 77 | } 78 | 79 | #[derive(Debug, Clone)] 80 | pub struct EventErrorPayload { 81 | pub data: String, 82 | } 83 | 84 | #[derive(Debug, Clone)] 85 | pub struct EventReadyPayload { 86 | pub data: String, 87 | } 88 | 89 | #[derive(Debug, Clone)] 90 | pub struct EventResetPayload { 91 | pub data: String, 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct EventHeartbeatPayload { 96 | pub data: String, 97 | } 98 | 99 | #[derive(Debug, Clone)] 100 | pub struct EventDirtyPayload { 101 | pub payload_type: PayloadType, 102 | pub payload_id: String, 103 | } 104 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/friendship.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 4 | #[repr(i32)] 5 | pub enum FriendshipType { 6 | Unknown, 7 | Confirm, 8 | Receive, 9 | Verify, 10 | } 11 | 12 | #[allow(clippy::upper_case_acronyms)] 13 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 14 | #[repr(i32)] 15 | pub enum FriendshipSceneType { 16 | Unknown = 0, 17 | QQ = 1, 18 | Email = 2, 19 | Weixin = 3, 20 | QQtbd = 12, 21 | Room = 14, 22 | Phone = 15, 23 | Card = 17, 24 | Location = 18, 25 | Bottle = 25, 26 | Shaking = 29, 27 | QRCode = 30, 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct FriendshipPayload { 32 | pub id: String, 33 | pub contact_id: String, 34 | pub hello: String, 35 | pub timestamp: u64, 36 | pub scene: FriendshipSceneType, 37 | pub stranger: String, 38 | pub ticket: String, 39 | pub friendship_type: FriendshipType, 40 | } 41 | 42 | #[derive(Default, Debug, Clone)] 43 | pub struct FriendshipSearchQueryFilter { 44 | pub phone: Option, 45 | pub weixin: Option, 46 | } 47 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/image.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | #[allow(clippy::upper_case_acronyms)] 4 | #[derive(Debug, Clone, PartialEq, FromPrimitive, ToPrimitive, Deserialize_repr, Serialize_repr)] 5 | #[repr(i32)] 6 | pub enum ImageType { 7 | Unknown, 8 | Thumbnail, 9 | HD, 10 | Artwork, 11 | } 12 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/message.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use serde_repr::{Deserialize_repr, Serialize_repr}; 3 | 4 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 5 | #[repr(i32)] 6 | pub enum MessageType { 7 | Unknown, 8 | Attachment, 9 | Audio, 10 | Contact, 11 | ChatHistory, 12 | Emoticon, 13 | Image, 14 | Text, 15 | Location, 16 | MiniProgram, 17 | GroupNote, 18 | Transfer, 19 | RedEnvelope, 20 | Recalled, 21 | Url, 22 | Video, 23 | } 24 | 25 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 26 | #[repr(i32)] 27 | pub enum WechatAppMessageType { 28 | Text = 1, 29 | Img = 2, 30 | Audio = 3, 31 | Video = 4, 32 | Url = 5, 33 | Attach = 6, 34 | Open = 7, 35 | Emoji = 8, 36 | VoiceRemind = 9, 37 | ScanGood = 10, 38 | Good = 13, 39 | Emotion = 15, 40 | CardTicket = 16, 41 | RealtimeShareLocation = 17, 42 | ChatHistory = 19, 43 | MiniProgram = 33, 44 | Transfers = 2000, 45 | RedEnvelopes = 2001, 46 | ReaderType = 100001, 47 | } 48 | 49 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 50 | #[repr(i32)] 51 | pub enum WechatMessageType { 52 | Text = 1, 53 | Image = 3, 54 | Voice = 34, 55 | VerifyMsg = 37, 56 | PossibleFriendMsg = 40, 57 | ShareCard = 42, 58 | Video = 43, 59 | Emoticon = 47, 60 | Location = 48, 61 | App = 49, 62 | VoipMsg = 50, 63 | StatusNotify = 51, 64 | VoipNotify = 52, 65 | VoipInvite = 53, 66 | MicroVideo = 62, 67 | Transfer = 2000, 68 | RedEnvelope = 2001, 69 | MiniProgram = 2002, 70 | GroupInvite = 2003, 71 | File = 2004, 72 | SysNotice = 9999, 73 | Sys = 10000, 74 | Recalled = 10002, 75 | } 76 | 77 | #[derive(Debug, Clone)] 78 | pub struct MessagePayload { 79 | pub id: String, 80 | pub filename: String, 81 | pub text: String, 82 | pub timestamp: u64, 83 | pub message_type: MessageType, 84 | pub from_id: String, 85 | pub mention_id_list: Vec, 86 | pub room_id: String, 87 | pub to_id: String, 88 | } 89 | 90 | #[derive(Default, Debug, Clone)] 91 | pub struct MessageQueryFilter { 92 | pub from_id: Option, 93 | pub id: Option, 94 | pub room_id: Option, 95 | pub text: Option, 96 | pub text_regex: Option, 97 | pub to_id: Option, 98 | pub message_type: Option, 99 | } 100 | 101 | // FIXME: trait aliases are experimental, see issue #41517 102 | // pub trait MessagePayloadFilterFunction = Fn(MessagePayload) -> bool; 103 | // 104 | // pub trait MessagePayloadFilterFactory = Fn(MessageQueryFilter) -> 105 | // MessagePayloadFilterFunction; 106 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/mini_program.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub struct MiniProgramPayload { 5 | appid: Option, 6 | description: Option, 7 | page_path: Option, 8 | icon_url: Option, 9 | share_id: Option, 10 | thumb_url: Option, 11 | title: Option, 12 | username: Option, 13 | thumb_key: Option, 14 | } 15 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod contact; 2 | pub mod event; 3 | pub mod friendship; 4 | pub mod image; 5 | pub mod message; 6 | pub mod mini_program; 7 | pub mod payload; 8 | pub mod puppet; 9 | pub mod room; 10 | pub mod room_invitation; 11 | pub mod url_link; 12 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/payload.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | #[derive(Debug, Clone, PartialEq, FromPrimitive, Deserialize_repr, Serialize_repr)] 4 | #[repr(i32)] 5 | pub enum PayloadType { 6 | Unknown, 7 | Message, 8 | Contact, 9 | Room, 10 | RoomMember, 11 | Friendship, 12 | } 13 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/puppet.rs: -------------------------------------------------------------------------------- 1 | pub struct PuppetOptions { 2 | pub endpoint: Option, 3 | pub timeout: Option, 4 | pub token: Option, 5 | } 6 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/room.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | #[derive(Default, Debug, Clone)] 4 | pub struct RoomMemberQueryFilter { 5 | pub name: Option, 6 | pub room_alias: Option, 7 | pub name_regex: Option, 8 | pub room_alias_regex: Option, 9 | } 10 | 11 | #[derive(Default, Debug, Clone)] 12 | pub struct RoomQueryFilter { 13 | pub id: Option, 14 | pub topic: Option, 15 | pub topic_regex: Option, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct RoomPayload { 20 | pub id: String, 21 | pub topic: String, 22 | pub avatar: String, 23 | pub member_id_list: Vec, 24 | pub owner_id: String, 25 | pub admin_id_list: Vec, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct RoomMemberPayload { 30 | pub id: String, 31 | pub room_alias: String, 32 | pub inviter_id: String, 33 | pub avatar: String, 34 | pub name: String, 35 | } 36 | 37 | // FIXME: trait aliases are experimental, see issue #41517 38 | // pub trait RoomPayloadFilterFunction = Fn(RoomPayload) -> bool; 39 | // 40 | // pub trait RoomPayloadFilterFactory = Fn(RoomQueryFilter) -> 41 | // RoomPayloadFilterFunction; 42 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/room_invitation.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct RoomInvitationPayload { 3 | pub id: String, 4 | pub inviter_id: String, 5 | pub topic: String, 6 | pub avatar: String, 7 | pub invitation: String, 8 | pub member_count: u32, 9 | pub member_id_list: Vec, 10 | pub timestamp: u64, 11 | pub receiver_id: String, 12 | } 13 | -------------------------------------------------------------------------------- /wechaty-puppet/src/schemas/url_link.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Deserialize, Serialize)] 4 | pub struct UrlLinkPayload { 5 | pub description: Option, 6 | pub thumbnail_url: Option, 7 | pub title: String, 8 | pub url: String, 9 | } 10 | -------------------------------------------------------------------------------- /wechaty-puppet/src/types.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{BoxFuture, Future}; 2 | 3 | pub struct AsyncFnPtr { 4 | func: Box BoxFuture<'static, Result> + Send + 'static>, 5 | } 6 | 7 | #[allow(clippy::new_ret_no_self)] 8 | impl AsyncFnPtr 9 | where 10 | Payload: 'static, 11 | { 12 | fn new(f: F) -> AsyncFnPtr 13 | where 14 | F: Fn(Payload, Context) -> Fut + Send + 'static, 15 | Fut: Future + Send + 'static, 16 | { 17 | AsyncFnPtr { 18 | func: Box::new(move |t: Payload, ctx: Context| Box::pin(f(t, ctx))), 19 | } 20 | } 21 | 22 | pub async fn run(&self, t: Payload, ctx: Context) -> Result { 23 | (self.func)(t, ctx).await 24 | } 25 | } 26 | 27 | pub trait IntoAsyncFnPtr 28 | where 29 | Payload: 'static, 30 | { 31 | fn into(self) -> AsyncFnPtr; 32 | } 33 | 34 | impl IntoAsyncFnPtr for F 35 | where 36 | F: Fn(Payload, Context) -> Fut + Send + 'static, 37 | Payload: 'static, 38 | Fut: Future + Send + 'static, 39 | { 40 | fn into(self) -> AsyncFnPtr { 41 | AsyncFnPtr::new(self) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /wechaty/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wechaty" 3 | version = "0.1.0-beta.1" 4 | authors = ["Gabriel Wu "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Rust implementation of wechaty" 8 | keywords = ["chatbot", "wechaty"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | actix = "0.12" 14 | actix-rt = "2" 15 | async-trait = "0.1" 16 | futures = "0.3" 17 | log = "0.4" 18 | tokio = "1" 19 | tokio-stream = "0.1" 20 | wechaty_puppet = { version = "0.1.0-beta.1", path = "../wechaty-puppet" } 21 | 22 | [dev-dependencies] 23 | env_logger = "0.8" 24 | wechaty-puppet-service = { version = "0.1.0-beta.1", path = "../wechaty-puppet-service" } 25 | 26 | [[example]] 27 | name = "ding-dong-bot" 28 | path = "../examples/ding_dong_bot.rs" -------------------------------------------------------------------------------- /wechaty/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex, MutexGuard}; 3 | 4 | use futures::StreamExt; 5 | use log::{debug, error}; 6 | use wechaty_puppet::{ 7 | ContactPayload, ContactQueryFilter, FriendshipPayload, FriendshipSearchQueryFilter, MessagePayload, 8 | MessageQueryFilter, Puppet, PuppetImpl, RoomInvitationPayload, RoomPayload, RoomQueryFilter, 9 | }; 10 | 11 | use crate::{Contact, Friendship, IntoContact, Message, Room, WechatyError}; 12 | 13 | #[derive(Clone)] 14 | pub struct WechatyContext 15 | where 16 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 17 | { 18 | id_: Option, 19 | puppet_: Puppet, 20 | contacts_: Arc>>, 21 | friendships_: Arc>>, 22 | messages_: Arc>>, 23 | rooms_: Arc>>, 24 | room_invitations_: Arc>>, 25 | } 26 | 27 | impl WechatyContext 28 | where 29 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 30 | { 31 | pub(crate) fn new(puppet: Puppet) -> Self { 32 | Self { 33 | id_: None, 34 | puppet_: puppet, 35 | contacts_: Arc::new(Mutex::new(Default::default())), 36 | friendships_: Arc::new(Mutex::new(Default::default())), 37 | messages_: Arc::new(Mutex::new(Default::default())), 38 | rooms_: Arc::new(Mutex::new(Default::default())), 39 | room_invitations_: Arc::new(Mutex::new(Default::default())), 40 | } 41 | } 42 | 43 | pub(crate) fn puppet(&self) -> Puppet { 44 | self.puppet_.clone() 45 | } 46 | 47 | pub(crate) fn contacts(&self) -> MutexGuard> { 48 | self.contacts_.lock().unwrap() 49 | } 50 | 51 | pub(crate) fn friendships(&self) -> MutexGuard> { 52 | self.friendships_.lock().unwrap() 53 | } 54 | 55 | pub(crate) fn messages(&self) -> MutexGuard> { 56 | self.messages_.lock().unwrap() 57 | } 58 | 59 | pub(crate) fn rooms(&self) -> MutexGuard> { 60 | self.rooms_.lock().unwrap() 61 | } 62 | 63 | pub(crate) fn room_invitations(&self) -> MutexGuard> { 64 | self.room_invitations_.lock().unwrap() 65 | } 66 | 67 | pub(crate) fn id(&self) -> Option { 68 | self.id_.clone() 69 | } 70 | 71 | pub(crate) fn set_id(&mut self, id: String) { 72 | self.id_ = Some(id); 73 | } 74 | 75 | pub(crate) fn clear_id(&mut self) { 76 | self.id_ = None; 77 | } 78 | 79 | pub(crate) fn is_logged_in(&self) -> bool { 80 | self.id_.is_some() 81 | } 82 | 83 | /// Load a contact. 84 | /// 85 | /// Use contact store first, if the contact cannot be found in the local store, 86 | /// try to fetch from the puppet instead. 87 | pub(crate) async fn contact_load(&self, contact_id: String) -> Result, WechatyError> { 88 | debug!("contact_load(query = {})", contact_id); 89 | let payload = self.contacts().get(&contact_id).cloned(); 90 | match payload { 91 | Some(payload) => Ok(Contact::new(contact_id.clone(), self.clone(), Some(payload))), 92 | None => { 93 | let mut contact = Contact::new(contact_id.clone(), self.clone(), None); 94 | if let Err(e) = contact.sync().await { 95 | error!("Failed to get payload of contact {}", contact_id); 96 | return Err(e); 97 | } 98 | Ok(contact) 99 | } 100 | } 101 | } 102 | 103 | /// Batch load contacts with a default batch size of 16. 104 | /// 105 | /// Reference: [Batch execution of futures in the tokio runtime](https://users.rust-lang.org/t/batch-execution-of-futures-in-the-tokio-runtime-or-max-number-of-active-futures-at-a-time/47659). 106 | /// 107 | /// Note the API change: `tokio::stream::iter` is now temporarily `tokio_stream::iter`, according to 108 | /// [tokio's tutorial](https://tokio.rs/tokio/tutorial/streams), it will be moved back to the `tokio` 109 | /// crate when the `Stream` trait is stable. 110 | pub(crate) async fn contact_load_batch(&self, contact_id_list: Vec) -> Vec> { 111 | debug!("contact_load_batch(contact_id_list = {:?})", contact_id_list); 112 | let mut contact_list = vec![]; 113 | let mut stream = tokio_stream::iter(contact_id_list) 114 | .map(|contact_id| self.contact_load(contact_id)) 115 | .buffer_unordered(16); 116 | while let Some(result) = stream.next().await { 117 | if let Ok(contact) = result { 118 | contact_list.push(contact); 119 | } 120 | } 121 | contact_list 122 | } 123 | 124 | /// Find the first contact that matches the query 125 | pub async fn contact_find(&self, query: ContactQueryFilter) -> Result>, WechatyError> { 126 | debug!("contact_find(query = {:?})", query); 127 | match self.contact_find_all(Some(query)).await { 128 | Ok(contact_list) => { 129 | if contact_list.is_empty() { 130 | Ok(None) 131 | } else { 132 | Ok(Some(contact_list[0].clone())) 133 | } 134 | } 135 | Err(e) => Err(e), 136 | } 137 | } 138 | 139 | /// Find the first contact that matches the query string 140 | pub async fn contact_find_by_string(&self, query_str: String) -> Result>, WechatyError> { 141 | debug!("contact_find_by_string(query_str = {:?})", query_str); 142 | match self.contact_find_all_by_string(query_str).await { 143 | Ok(contact_list) => { 144 | if contact_list.is_empty() { 145 | Ok(None) 146 | } else { 147 | Ok(Some(contact_list[0].clone())) 148 | } 149 | } 150 | Err(e) => Err(e), 151 | } 152 | } 153 | 154 | /// Find all contacts that match the query 155 | pub async fn contact_find_all(&self, query: Option) -> Result>, WechatyError> { 156 | debug!("contact_find_all(query = {:?})", query); 157 | if !self.is_logged_in() { 158 | return Err(WechatyError::NotLoggedIn); 159 | } 160 | let query = match query { 161 | Some(query) => query, 162 | None => ContactQueryFilter::default(), 163 | }; 164 | match self.puppet().contact_search(query, None).await { 165 | Ok(contact_id_list) => Ok(self.contact_load_batch(contact_id_list).await), 166 | Err(e) => Err(WechatyError::from(e)), 167 | } 168 | } 169 | 170 | /// Find all contacts that match the query string 171 | pub async fn contact_find_all_by_string(&self, query_str: String) -> Result>, WechatyError> { 172 | debug!("contact_find_all_by_string(query_str = {:?})", query_str); 173 | if !self.is_logged_in() { 174 | return Err(WechatyError::NotLoggedIn); 175 | } 176 | match self.puppet().contact_search_by_string(query_str, None).await { 177 | Ok(contact_id_list) => Ok(self.contact_load_batch(contact_id_list).await), 178 | Err(e) => Err(WechatyError::from(e)), 179 | } 180 | } 181 | 182 | /// Load a message. 183 | /// 184 | /// Use message store first, if the message cannot be found in the local store, 185 | /// try to fetch from the puppet instead. 186 | pub(crate) async fn message_load(&self, message_id: String) -> Result, WechatyError> { 187 | debug!("message_load(query = {})", message_id); 188 | let payload = self.messages().get(&message_id).cloned(); 189 | match payload { 190 | Some(payload) => Ok(Message::new(message_id.clone(), self.clone(), Some(payload))), 191 | None => { 192 | let mut message = Message::new(message_id.clone(), self.clone(), None); 193 | if let Err(e) = message.ready().await { 194 | return Err(e); 195 | } 196 | Ok(message) 197 | } 198 | } 199 | } 200 | 201 | /// Batch load messages with a default batch size of 16. 202 | pub(crate) async fn message_load_batch(&self, message_id_list: Vec) -> Vec> { 203 | debug!("message_load_batch(message_id_list = {:?})", message_id_list); 204 | let mut message_list = vec![]; 205 | let mut stream = tokio_stream::iter(message_id_list) 206 | .map(|message_id| self.message_load(message_id)) 207 | .buffer_unordered(16); 208 | while let Some(result) = stream.next().await { 209 | if let Ok(message) = result { 210 | message_list.push(message); 211 | } 212 | } 213 | message_list 214 | } 215 | 216 | /// Find the first message that matches the query 217 | pub async fn message_find(&self, query: MessageQueryFilter) -> Result>, WechatyError> { 218 | debug!("message_find(query = {:?})", query); 219 | if !self.is_logged_in() { 220 | return Err(WechatyError::NotLoggedIn); 221 | } 222 | match self.message_find_all(query).await { 223 | Ok(message_list) => { 224 | if message_list.is_empty() { 225 | Ok(None) 226 | } else { 227 | Ok(Some(message_list[0].clone())) 228 | } 229 | } 230 | Err(e) => Err(e), 231 | } 232 | } 233 | 234 | /// Find all messages that match the query 235 | pub async fn message_find_all(&self, query: MessageQueryFilter) -> Result>, WechatyError> { 236 | debug!("message_find_all(query = {:?}", query); 237 | if !self.is_logged_in() { 238 | return Err(WechatyError::NotLoggedIn); 239 | } 240 | match self.puppet().message_search(query).await { 241 | Ok(message_id_list) => Ok(self.message_load_batch(message_id_list).await), 242 | Err(e) => Err(WechatyError::from(e)), 243 | } 244 | } 245 | 246 | /// Load a room. 247 | /// 248 | /// Use room store first, if the room cannot be found in the local store, 249 | /// try to fetch from the puppet instead. 250 | pub(crate) async fn room_load(&self, room_id: String) -> Result, WechatyError> { 251 | debug!("room_load(room_id = {})", room_id); 252 | if !self.is_logged_in() { 253 | return Err(WechatyError::NotLoggedIn); 254 | } 255 | let payload = self.rooms().get(&room_id).cloned(); 256 | match payload { 257 | Some(payload) => Ok(Room::new(room_id.clone(), self.clone(), Some(payload))), 258 | None => { 259 | let mut room = Room::new(room_id.clone(), self.clone(), None); 260 | if let Err(e) = room.sync().await { 261 | return Err(e); 262 | } 263 | Ok(room) 264 | } 265 | } 266 | } 267 | 268 | /// Batch load rooms with a default batch size of 16. 269 | pub(crate) async fn room_load_batch(&self, room_id_list: Vec) -> Vec> { 270 | debug!("room_load_batch(room_id_list = {:?})", room_id_list); 271 | let mut room_list = vec![]; 272 | let mut stream = tokio_stream::iter(room_id_list) 273 | .map(|room_id| self.room_load(room_id)) 274 | .buffer_unordered(16); 275 | while let Some(result) = stream.next().await { 276 | if let Ok(room) = result { 277 | room_list.push(room); 278 | } 279 | } 280 | room_list 281 | } 282 | 283 | /// Create a room. 284 | pub async fn room_create( 285 | &self, 286 | contact_list: Vec>, 287 | topic: Option, 288 | ) -> Result, WechatyError> { 289 | debug!("room_create(contact_list = {:?}, topic = {:?})", contact_list, topic); 290 | if !self.is_logged_in() { 291 | return Err(WechatyError::NotLoggedIn); 292 | } 293 | if contact_list.len() < 2 { 294 | Err(WechatyError::InvalidOperation( 295 | "Need at least 2 contacts to create a room".to_owned(), 296 | )) 297 | } else { 298 | let contact_id_list = contact_list.into_iter().map(|x| x.id()).collect(); 299 | match self.puppet().room_create(contact_id_list, topic).await { 300 | Ok(room_id) => { 301 | let mut room = Room::new(room_id, self.clone(), None); 302 | room.sync().await.unwrap_or_default(); 303 | Ok(room) 304 | } 305 | Err(e) => Err(WechatyError::from(e)), 306 | } 307 | } 308 | } 309 | 310 | /// Find the first room that matches the query 311 | pub async fn room_find(&self, query: RoomQueryFilter) -> Result>, WechatyError> { 312 | debug!("room_find(query = {:?})", query); 313 | if !self.is_logged_in() { 314 | return Err(WechatyError::NotLoggedIn); 315 | } 316 | match self.room_find_all(query).await { 317 | Ok(room_list) => { 318 | if room_list.is_empty() { 319 | Ok(None) 320 | } else { 321 | Ok(Some(room_list[0].clone())) 322 | } 323 | } 324 | Err(e) => Err(e), 325 | } 326 | } 327 | 328 | /// Find all rooms that match the query 329 | pub async fn room_find_all(&self, query: RoomQueryFilter) -> Result>, WechatyError> { 330 | debug!("room_find_all(query = {:?}", query); 331 | if !self.is_logged_in() { 332 | return Err(WechatyError::NotLoggedIn); 333 | } 334 | match self.puppet().room_search(query).await { 335 | Ok(room_id_list) => Ok(self.room_load_batch(room_id_list).await), 336 | Err(e) => Err(WechatyError::from(e)), 337 | } 338 | } 339 | 340 | /// Load a friendship. 341 | /// 342 | /// Use friendship store first, if the friendship cannot be found in the local store, 343 | /// try to fetch from the puppet instead. 344 | #[allow(dead_code)] 345 | pub(crate) async fn friendship_load(&self, friendship_id: String) -> Result, WechatyError> { 346 | debug!("friendship_load(friendship_id = {})", friendship_id); 347 | if !self.is_logged_in() { 348 | return Err(WechatyError::NotLoggedIn); 349 | } 350 | let payload = self.friendships().get(&friendship_id).cloned(); 351 | match payload { 352 | Some(payload) => Ok(Friendship::new(friendship_id.clone(), self.clone(), Some(payload))), 353 | None => { 354 | let mut friendship = Friendship::new(friendship_id.clone(), self.clone(), None); 355 | if let Err(e) = friendship.ready().await { 356 | return Err(e); 357 | } 358 | Ok(friendship) 359 | } 360 | } 361 | } 362 | 363 | /// Add friendship with contact. 364 | pub async fn friendship_add(&self, contact: Contact, hello: Option) -> Result<(), WechatyError> { 365 | debug!("friendship_add(contact = {}, hello = {:?}", contact, hello); 366 | if !self.is_logged_in() { 367 | return Err(WechatyError::NotLoggedIn); 368 | } 369 | match self.puppet().friendship_add(contact.id(), hello).await { 370 | Ok(_) => Ok(()), 371 | Err(e) => Err(WechatyError::from(e)), 372 | } 373 | } 374 | 375 | /// Search a friendship. 376 | /// 377 | /// First search by phone, then search by weixin. 378 | pub async fn friendship_search( 379 | &self, 380 | query: FriendshipSearchQueryFilter, 381 | ) -> Result>, WechatyError> { 382 | debug!("friendship_search(query = {:?}", query); 383 | if !self.is_logged_in() { 384 | return Err(WechatyError::NotLoggedIn); 385 | } 386 | if query.phone.is_none() && query.weixin.is_none() { 387 | return Err(WechatyError::InvalidOperation( 388 | "Must specify either phone or weixin".to_owned(), 389 | )); 390 | } 391 | match self.puppet().friendship_search(query).await { 392 | Ok(Some(contact_id)) => { 393 | let mut contact = Contact::new(contact_id, self.clone(), None); 394 | contact.sync().await.unwrap_or_default(); 395 | Ok(Some(contact)) 396 | } 397 | Ok(None) => Ok(None), 398 | Err(e) => Err(WechatyError::from(e)), 399 | } 400 | } 401 | 402 | /// Logout current account. 403 | pub async fn logout(&self) -> Result<(), WechatyError> { 404 | debug!("logout()"); 405 | if !self.is_logged_in() { 406 | return Err(WechatyError::NotLoggedIn); 407 | } 408 | match self.puppet().logout().await { 409 | Ok(_) => Ok(()), 410 | Err(e) => Err(WechatyError::from(e)), 411 | } 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /wechaty/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | use wechaty_puppet::PuppetError; 4 | 5 | pub enum WechatyError { 6 | Puppet(PuppetError), 7 | InvalidOperation(String), 8 | Maybe(String), 9 | NotLoggedIn, 10 | NoPayload, 11 | } 12 | 13 | impl fmt::Debug for WechatyError { 14 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | write!(fmt, "WechatyError({})", self) 16 | } 17 | } 18 | 19 | impl fmt::Display for WechatyError { 20 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | match self { 22 | WechatyError::Puppet(e) => write!(fmt, "Puppet error: {}", e), 23 | WechatyError::InvalidOperation(op) => write!(fmt, "Invalid operation: {}", op), 24 | WechatyError::Maybe(maybe) => write!(fmt, "An error may have occurred: {}", maybe), 25 | WechatyError::NotLoggedIn => write!(fmt, "User is not logged in"), 26 | WechatyError::NoPayload => write!(fmt, "Operation cannot be done because the current entity does not have payload due to an unknown previous issue"), 27 | } 28 | } 29 | } 30 | 31 | impl From for WechatyError { 32 | fn from(e: PuppetError) -> Self { 33 | WechatyError::Puppet(e) 34 | } 35 | } 36 | 37 | impl error::Error for WechatyError {} 38 | -------------------------------------------------------------------------------- /wechaty/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | mod error; 3 | mod payload; 4 | mod traits; 5 | mod user; 6 | mod wechaty; 7 | 8 | pub use actix_rt as wechaty_rt; 9 | pub use wechaty_puppet::{MessageType, PuppetOptions}; 10 | 11 | pub use crate::context::WechatyContext; 12 | pub use crate::error::WechatyError; 13 | pub use crate::payload::*; 14 | pub use crate::traits::contact::IntoContact; 15 | pub use crate::traits::event_listener::EventListener; 16 | pub(crate) use crate::traits::event_listener::EventListenerInner; 17 | pub use crate::traits::talkable::Talkable; 18 | pub use crate::user::contact::Contact; 19 | pub use crate::user::contact_self::ContactSelf; 20 | pub(crate) use crate::user::entity::Entity; 21 | pub use crate::user::favorite::Favorite; 22 | pub use crate::user::friendship::Friendship; 23 | pub use crate::user::image::Image; 24 | pub use crate::user::location::Location; 25 | pub use crate::user::message::Message; 26 | pub use crate::user::mini_program::MiniProgram; 27 | pub use crate::user::moment::Moment; 28 | pub use crate::user::money::Money; 29 | pub use crate::user::room::Room; 30 | pub use crate::user::room_invitation::RoomInvitation; 31 | pub use crate::user::tag::Tag; 32 | pub use crate::user::url_link::UrlLink; 33 | pub use crate::wechaty::Wechaty; 34 | 35 | pub mod prelude { 36 | pub use actix_rt as wechaty_rt; 37 | pub use wechaty_puppet::{MessageType, PuppetOptions}; 38 | 39 | pub use crate::context::WechatyContext; 40 | pub use crate::error::WechatyError; 41 | pub use crate::payload::*; 42 | pub use crate::traits::contact::IntoContact; 43 | pub use crate::traits::event_listener::EventListener; 44 | pub use crate::traits::talkable::Talkable; 45 | pub use crate::user::contact::Contact; 46 | pub use crate::user::contact_self::ContactSelf; 47 | pub use crate::user::favorite::Favorite; 48 | pub use crate::user::friendship::Friendship; 49 | pub use crate::user::image::Image; 50 | pub use crate::user::location::Location; 51 | pub use crate::user::message::Message; 52 | pub use crate::user::mini_program::MiniProgram; 53 | pub use crate::user::moment::Moment; 54 | pub use crate::user::money::Money; 55 | pub use crate::user::room::Room; 56 | pub use crate::user::room_invitation::RoomInvitation; 57 | pub use crate::user::tag::Tag; 58 | pub use crate::user::url_link::UrlLink; 59 | pub use crate::wechaty::Wechaty; 60 | } 61 | -------------------------------------------------------------------------------- /wechaty/src/payload.rs: -------------------------------------------------------------------------------- 1 | use wechaty_puppet::{ 2 | EventDongPayload, EventErrorPayload, EventHeartbeatPayload, EventReadyPayload, EventResetPayload, EventScanPayload, 3 | PuppetImpl, 4 | }; 5 | 6 | use crate::user::contact_self::ContactSelf; 7 | use crate::{Contact, Friendship, Message, Room, RoomInvitation}; 8 | 9 | pub type DongPayload = EventDongPayload; 10 | 11 | pub type ErrorPayload = EventErrorPayload; 12 | 13 | #[derive(Clone, Debug)] 14 | pub struct FriendshipPayload 15 | where 16 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 17 | { 18 | pub friendship: Friendship, 19 | } 20 | 21 | pub type HeartbeatPayload = EventHeartbeatPayload; 22 | 23 | #[derive(Clone, Debug)] 24 | pub struct LoginPayload 25 | where 26 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 27 | { 28 | pub contact: ContactSelf, 29 | } 30 | 31 | #[derive(Clone, Debug)] 32 | pub struct LogoutPayload 33 | where 34 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 35 | { 36 | pub contact: ContactSelf, 37 | pub data: String, 38 | } 39 | 40 | #[derive(Clone, Debug)] 41 | pub struct MessagePayload 42 | where 43 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 44 | { 45 | pub message: Message, 46 | } 47 | 48 | pub type ScanPayload = EventScanPayload; 49 | 50 | pub type ReadyPayload = EventReadyPayload; 51 | 52 | pub type ResetPayload = EventResetPayload; 53 | 54 | #[derive(Clone, Debug)] 55 | pub struct RoomInvitePayload 56 | where 57 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 58 | { 59 | pub room_invitation: RoomInvitation, 60 | } 61 | 62 | #[derive(Clone, Debug)] 63 | pub struct RoomJoinPayload 64 | where 65 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 66 | { 67 | pub room: Room, 68 | pub invitee_list: Vec>, 69 | pub inviter: Contact, 70 | pub timestamp: u64, 71 | } 72 | 73 | #[derive(Clone, Debug)] 74 | pub struct RoomLeavePayload 75 | where 76 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 77 | { 78 | pub room: Room, 79 | pub removee_list: Vec>, 80 | pub remover: Contact, 81 | pub timestamp: u64, 82 | } 83 | 84 | #[derive(Clone, Debug)] 85 | pub struct RoomTopicPayload 86 | where 87 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 88 | { 89 | pub room: Room, 90 | pub old_topic: String, 91 | pub new_topic: String, 92 | pub changer: Contact, 93 | pub timestamp: u64, 94 | } 95 | -------------------------------------------------------------------------------- /wechaty/src/traits/contact.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use log::{debug, error}; 3 | use wechaty_puppet::{ContactGender, ContactPayload, PayloadType, PuppetImpl}; 4 | 5 | use crate::{Talkable, WechatyError}; 6 | 7 | #[async_trait] 8 | pub trait IntoContact: Talkable 9 | where 10 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 11 | { 12 | fn payload(&self) -> Option; 13 | fn set_payload(&mut self, payload: Option); 14 | 15 | fn is_ready(&self) -> bool { 16 | debug!("contact.is_ready(id = {})", self.id()); 17 | self.payload().is_some() 18 | } 19 | 20 | async fn ready(&mut self, force_sync: bool) -> Result<(), WechatyError> { 21 | debug!("contact.ready(id = {}, force_sync = {})", self.id(), force_sync); 22 | if !force_sync && self.is_ready() { 23 | Ok(()) 24 | } else { 25 | let id = self.id(); 26 | let mut puppet = self.ctx().puppet(); 27 | if force_sync { 28 | if let Err(e) = puppet.dirty_payload(PayloadType::Contact, id.clone()).await { 29 | error!("Error occurred while syncing contact {}: {}", id, e); 30 | return Err(WechatyError::from(e)); 31 | } 32 | } 33 | match puppet.contact_payload(id.clone()).await { 34 | Ok(payload) => { 35 | self.ctx().contacts().insert(id, payload.clone()); 36 | self.set_payload(Some(payload)); 37 | Ok(()) 38 | } 39 | Err(e) => { 40 | error!("Error occurred while syncing contact {}: {}", id, e); 41 | Err(WechatyError::from(e)) 42 | } 43 | } 44 | } 45 | } 46 | 47 | async fn sync(&mut self) -> Result<(), WechatyError> { 48 | debug!("contact.sync(id = {})", self.id()); 49 | self.ready(true).await 50 | } 51 | 52 | fn name(&self) -> Option { 53 | debug!("contact.name(id = {})", self.id()); 54 | self.payload().as_ref().map(|payload| payload.name.clone()) 55 | } 56 | 57 | fn gender(&self) -> Option { 58 | debug!("contact.gender(id = {})", self.id()); 59 | self.payload().as_ref().map(|payload| payload.gender.clone()) 60 | } 61 | 62 | fn province(&self) -> Option { 63 | debug!("contact.province(id = {})", self.id()); 64 | self.payload().as_ref().map(|payload| payload.province.clone()) 65 | } 66 | 67 | fn city(&self) -> Option { 68 | debug!("contact.city(id = {})", self.id()); 69 | self.payload().as_ref().map(|payload| payload.city.clone()) 70 | } 71 | 72 | fn friend(&self) -> Option { 73 | debug!("contact.friend(id = {})", self.id()); 74 | self.payload().as_ref().map(|payload| payload.friend) 75 | } 76 | 77 | fn star(&self) -> Option { 78 | debug!("contact.star(id = {})", self.id()); 79 | self.payload().as_ref().map(|payload| payload.star) 80 | } 81 | 82 | fn alias(&self) -> Option { 83 | debug!("contact.alias(id = {})", self.id()); 84 | self.payload().as_ref().map(|payload| payload.alias.clone()) 85 | } 86 | 87 | async fn set_alias(&mut self, new_alias: String) -> Result<(), WechatyError> { 88 | debug!("contact.set_alias(id = {}, new_alias = {})", self.id(), new_alias); 89 | let mut puppet = self.ctx().puppet(); 90 | let id = self.id(); 91 | match puppet.contact_alias_set(id.clone(), new_alias.clone()).await { 92 | Err(e) => { 93 | error!("Failed to set alias for {}, reason: {}", self.identity(), e); 94 | Err(WechatyError::from(e)) 95 | } 96 | Ok(_) => { 97 | if let Err(e) = puppet.dirty_payload(PayloadType::Contact, id.clone()).await { 98 | error!("Failed to dirty payload for {}, reason: {}", self.identity(), e); 99 | } 100 | match puppet.contact_payload(id.clone()).await { 101 | Ok(payload) => { 102 | if payload.alias != new_alias { 103 | error!("Payload is not correctly set."); 104 | } 105 | } 106 | Err(e) => { 107 | error!("Failed to verify payload for {}, reason: {}", self.identity(), e); 108 | } 109 | }; 110 | Ok(()) 111 | } 112 | } 113 | } 114 | 115 | /// Check if current contact is the bot self. 116 | fn is_self(&self) -> bool { 117 | debug!("contact.is_self(id = {})", self.id()); 118 | match self.ctx().id() { 119 | Some(id) => self.id() == id, 120 | None => false, 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /wechaty/src/traits/event_listener.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::future::Future; 3 | use std::rc::Rc; 4 | 5 | use actix::{Actor, ActorFutureExt, AtomicResponse, Context, Handler, Recipient, WrapFuture}; 6 | use log::{error, info}; 7 | use wechaty_puppet::{ 8 | AsyncFnPtr, EventDongPayload, EventErrorPayload, EventFriendshipPayload, EventHeartbeatPayload, EventLoginPayload, 9 | EventLogoutPayload, EventMessagePayload, EventReadyPayload, EventResetPayload, EventRoomInvitePayload, 10 | EventRoomJoinPayload, EventRoomLeavePayload, EventRoomTopicPayload, EventScanPayload, IntoAsyncFnPtr, PayloadType, 11 | Puppet, PuppetEvent, PuppetImpl, Subscribe, 12 | }; 13 | 14 | use crate::{ 15 | Contact, ContactSelf, DongPayload, ErrorPayload, Friendship, FriendshipPayload, HeartbeatPayload, IntoContact, 16 | LoginPayload, LogoutPayload, Message, MessagePayload, ReadyPayload, ResetPayload, Room, RoomInvitation, 17 | RoomInvitePayload, RoomJoinPayload, RoomLeavePayload, RoomTopicPayload, ScanPayload, WechatyContext, 18 | }; 19 | 20 | pub trait EventListener 21 | where 22 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 23 | { 24 | fn get_listener(&self) -> &EventListenerInner; 25 | fn get_puppet(&self) -> Puppet; 26 | fn get_addr(&self) -> Recipient; 27 | fn get_name(&self) -> String { 28 | self.get_listener().name.clone() 29 | } 30 | 31 | fn on_event_with_handle( 32 | &mut self, 33 | handler: AsyncFnPtr, ()>, 34 | limit: Option, 35 | handlers: HandlersPtr, 36 | event_name: &'static str, 37 | ) -> (&mut Self, usize) { 38 | if let Err(e) = self.get_puppet().get_subscribe_addr().do_send(Subscribe { 39 | addr: self.get_addr(), 40 | name: self.get_name(), 41 | event_name, 42 | }) { 43 | error!("{} failed to subscribe to event {}: {}", self.get_name(), event_name, e); 44 | } 45 | let counter = handlers.borrow().len(); 46 | let limit = match limit { 47 | Some(limit) => limit, 48 | None => usize::MAX, 49 | }; 50 | handlers.borrow_mut().push((handler, limit)); 51 | (self, counter) 52 | } 53 | 54 | fn on_dong(&mut self, handler: F) -> &mut Self 55 | where 56 | F: IntoAsyncFnPtr, ()>, 57 | { 58 | self.on_dong_with_handle(handler, None); 59 | self 60 | } 61 | 62 | fn on_dong_with_handle(&mut self, handler: F, limit: Option) -> usize 63 | where 64 | F: IntoAsyncFnPtr, ()>, 65 | { 66 | let dong_handlers = self.get_listener().dong_handlers.clone(); 67 | self.on_event_with_handle(handler.into(), limit, dong_handlers, "dong") 68 | .1 69 | } 70 | 71 | fn on_error(&mut self, handler: F) -> &mut Self 72 | where 73 | F: IntoAsyncFnPtr, ()>, 74 | { 75 | self.on_error_with_handle(handler, None); 76 | self 77 | } 78 | 79 | fn on_error_with_handle(&mut self, handler: F, limit: Option) -> usize 80 | where 81 | F: IntoAsyncFnPtr, ()>, 82 | { 83 | let error_handlers = self.get_listener().error_handlers.clone(); 84 | self.on_event_with_handle(handler.into(), limit, error_handlers, "error") 85 | .1 86 | } 87 | 88 | fn on_friendship(&mut self, handler: F) -> &mut Self 89 | where 90 | F: IntoAsyncFnPtr, WechatyContext, ()>, 91 | { 92 | self.on_friendship_with_handle(handler, None); 93 | self 94 | } 95 | 96 | fn on_friendship_with_handle(&mut self, handler: F, limit: Option) -> usize 97 | where 98 | F: IntoAsyncFnPtr, WechatyContext, ()>, 99 | { 100 | let friendship_handlers = self.get_listener().friendship_handlers.clone(); 101 | self.on_event_with_handle(handler.into(), limit, friendship_handlers, "friendship") 102 | .1 103 | } 104 | 105 | fn on_heartbeat(&mut self, handler: F) -> &mut Self 106 | where 107 | F: IntoAsyncFnPtr, ()>, 108 | { 109 | self.on_heartbeat_with_handle(handler, None); 110 | self 111 | } 112 | 113 | fn on_heartbeat_with_handle(&mut self, handler: F, limit: Option) -> usize 114 | where 115 | F: IntoAsyncFnPtr, ()>, 116 | { 117 | let heartbeat_handlers = self.get_listener().heartbeat_handlers.clone(); 118 | self.on_event_with_handle(handler.into(), limit, heartbeat_handlers, "heartbeat") 119 | .1 120 | } 121 | 122 | fn on_login(&mut self, handler: F) -> &mut Self 123 | where 124 | F: IntoAsyncFnPtr, WechatyContext, ()>, 125 | { 126 | self.on_login_with_handle(handler, None); 127 | self 128 | } 129 | 130 | fn on_login_with_handle(&mut self, handler: F, limit: Option) -> usize 131 | where 132 | F: IntoAsyncFnPtr, WechatyContext, ()>, 133 | { 134 | let login_handlers = self.get_listener().login_handlers.clone(); 135 | self.on_event_with_handle(handler.into(), limit, login_handlers, "login") 136 | .1 137 | } 138 | 139 | fn on_logout(&mut self, handler: F) -> &mut Self 140 | where 141 | F: IntoAsyncFnPtr, WechatyContext, ()>, 142 | { 143 | self.on_logout_with_handle(handler, None); 144 | self 145 | } 146 | 147 | fn on_logout_with_handle(&mut self, handler: F, limit: Option) -> usize 148 | where 149 | F: IntoAsyncFnPtr, WechatyContext, ()>, 150 | { 151 | let logout_handlers = self.get_listener().logout_handlers.clone(); 152 | self.on_event_with_handle(handler.into(), limit, logout_handlers, "logout") 153 | .1 154 | } 155 | 156 | fn on_message(&mut self, handler: F) -> &mut Self 157 | where 158 | F: IntoAsyncFnPtr, WechatyContext, ()>, 159 | { 160 | self.on_message_with_handle(handler, None); 161 | self 162 | } 163 | 164 | fn on_message_with_handle(&mut self, handler: F, limit: Option) -> usize 165 | where 166 | F: IntoAsyncFnPtr, WechatyContext, ()>, 167 | { 168 | let message_handlers = self.get_listener().message_handlers.clone(); 169 | self.on_event_with_handle(handler.into(), limit, message_handlers, "message") 170 | .1 171 | } 172 | 173 | fn on_ready(&mut self, handler: F) -> &mut Self 174 | where 175 | F: IntoAsyncFnPtr, ()>, 176 | { 177 | self.on_ready_with_handle(handler, None); 178 | self 179 | } 180 | 181 | fn on_ready_with_handle(&mut self, handler: F, limit: Option) -> usize 182 | where 183 | F: IntoAsyncFnPtr, ()>, 184 | { 185 | let ready_handlers = self.get_listener().ready_handlers.clone(); 186 | self.on_event_with_handle(handler.into(), limit, ready_handlers, "ready") 187 | .1 188 | } 189 | 190 | fn on_reset(&mut self, handler: F) -> &mut Self 191 | where 192 | F: IntoAsyncFnPtr, ()>, 193 | { 194 | self.on_reset_with_handle(handler, None); 195 | self 196 | } 197 | 198 | fn on_reset_with_handle(&mut self, handler: F, limit: Option) -> usize 199 | where 200 | F: IntoAsyncFnPtr, ()>, 201 | { 202 | let reset_handlers = self.get_listener().reset_handlers.clone(); 203 | self.on_event_with_handle(handler.into(), limit, reset_handlers, "reset") 204 | .1 205 | } 206 | 207 | fn on_room_invite(&mut self, handler: F) -> &mut Self 208 | where 209 | F: IntoAsyncFnPtr, WechatyContext, ()>, 210 | { 211 | self.on_room_invite_with_handle(handler, None); 212 | self 213 | } 214 | 215 | fn on_room_invite_with_handle(&mut self, handler: F, limit: Option) -> usize 216 | where 217 | F: IntoAsyncFnPtr, WechatyContext, ()>, 218 | { 219 | let room_invite_handlers = self.get_listener().room_invite_handlers.clone(); 220 | self.on_event_with_handle(handler.into(), limit, room_invite_handlers, "room-invite") 221 | .1 222 | } 223 | 224 | fn on_room_join(&mut self, handler: F) -> &mut Self 225 | where 226 | F: IntoAsyncFnPtr, WechatyContext, ()>, 227 | { 228 | self.on_room_join_with_handle(handler, None); 229 | self 230 | } 231 | 232 | fn on_room_join_with_handle(&mut self, handler: F, limit: Option) -> usize 233 | where 234 | F: IntoAsyncFnPtr, WechatyContext, ()>, 235 | { 236 | let room_join_handlers = self.get_listener().room_join_handlers.clone(); 237 | self.on_event_with_handle(handler.into(), limit, room_join_handlers, "room-join") 238 | .1 239 | } 240 | 241 | fn on_room_leave(&mut self, handler: F) -> &mut Self 242 | where 243 | F: IntoAsyncFnPtr, WechatyContext, ()>, 244 | { 245 | self.on_room_leave_with_handle(handler, None); 246 | self 247 | } 248 | 249 | fn on_room_leave_with_handle(&mut self, handler: F, limit: Option) -> usize 250 | where 251 | F: IntoAsyncFnPtr, WechatyContext, ()>, 252 | { 253 | let room_leave_handlers = self.get_listener().room_leave_handlers.clone(); 254 | self.on_event_with_handle(handler.into(), limit, room_leave_handlers, "room-leave") 255 | .1 256 | } 257 | 258 | fn on_room_topic(&mut self, handler: F) -> &mut Self 259 | where 260 | F: IntoAsyncFnPtr, WechatyContext, ()>, 261 | { 262 | self.on_room_topic_with_handle(handler, None); 263 | self 264 | } 265 | 266 | fn on_room_topic_with_handle(&mut self, handler: F, limit: Option) -> usize 267 | where 268 | F: IntoAsyncFnPtr, WechatyContext, ()>, 269 | { 270 | let room_topic_handlers = self.get_listener().room_topic_handlers.clone(); 271 | self.on_event_with_handle(handler.into(), limit, room_topic_handlers, "room-topic") 272 | .1 273 | } 274 | 275 | fn on_scan(&mut self, handler: F) -> &mut Self 276 | where 277 | F: IntoAsyncFnPtr, ()>, 278 | { 279 | self.on_scan_with_handle(handler, None); 280 | self 281 | } 282 | 283 | fn on_scan_with_handle(&mut self, handler: F, limit: Option) -> usize 284 | where 285 | F: IntoAsyncFnPtr, ()>, 286 | { 287 | let scan_handlers = self.get_listener().scan_handlers.clone(); 288 | self.on_event_with_handle(handler.into(), limit, scan_handlers, "scan") 289 | .1 290 | } 291 | } 292 | 293 | type HandlersPtr = Rc, ()>, usize)>>>; 294 | 295 | #[derive(Clone)] 296 | pub struct EventListenerInner 297 | where 298 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 299 | { 300 | name: String, 301 | ctx: WechatyContext, 302 | dong_handlers: HandlersPtr, 303 | error_handlers: HandlersPtr, 304 | friendship_handlers: HandlersPtr>, 305 | heartbeat_handlers: HandlersPtr, 306 | login_handlers: HandlersPtr>, 307 | logout_handlers: HandlersPtr>, 308 | message_handlers: HandlersPtr>, 309 | ready_handlers: HandlersPtr, 310 | reset_handlers: HandlersPtr, 311 | room_invite_handlers: HandlersPtr>, 312 | room_join_handlers: HandlersPtr>, 313 | room_leave_handlers: HandlersPtr>, 314 | room_topic_handlers: HandlersPtr>, 315 | scan_handlers: HandlersPtr, 316 | } 317 | 318 | impl Actor for EventListenerInner 319 | where 320 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 321 | { 322 | type Context = Context; 323 | 324 | fn started(&mut self, _ctx: &mut Self::Context) { 325 | info!("{} started", self.name); 326 | } 327 | 328 | fn stopped(&mut self, _ctx: &mut Self::Context) { 329 | info!("{} stopped", self.name); 330 | } 331 | } 332 | 333 | impl Handler for EventListenerInner 334 | where 335 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 336 | { 337 | type Result = AtomicResponse; 338 | 339 | fn handle(&mut self, msg: PuppetEvent, _ctx: &mut Context) -> Self::Result { 340 | info!("{} receives puppet event: {:?}", self.name.clone(), msg); 341 | match msg { 342 | PuppetEvent::Dong(payload) => AtomicResponse::new(Box::pin( 343 | async {} 344 | .into_actor(self) 345 | .then(move |_, this, _| this.trigger_dong_handlers(payload).into_actor(this)), 346 | )), 347 | PuppetEvent::Error(payload) => AtomicResponse::new(Box::pin( 348 | async {} 349 | .into_actor(self) 350 | .then(move |_, this, _| this.trigger_error_handlers(payload).into_actor(this)), 351 | )), 352 | PuppetEvent::Friendship(payload) => AtomicResponse::new(Box::pin( 353 | async {} 354 | .into_actor(self) 355 | .then(move |_, this, _| this.trigger_friendship_handlers(payload).into_actor(this)), 356 | )), 357 | PuppetEvent::Heartbeat(payload) => AtomicResponse::new(Box::pin( 358 | async {} 359 | .into_actor(self) 360 | .then(move |_, this, _| this.trigger_heartbeat_handlers(payload).into_actor(this)), 361 | )), 362 | PuppetEvent::Login(payload) => { 363 | self.ctx.set_id(payload.contact_id.clone()); 364 | AtomicResponse::new(Box::pin( 365 | async {} 366 | .into_actor(self) 367 | .then(move |_, this, _| this.trigger_login_handlers(payload).into_actor(this)), 368 | )) 369 | } 370 | PuppetEvent::Logout(payload) => { 371 | self.ctx.clear_id(); 372 | AtomicResponse::new(Box::pin( 373 | async {} 374 | .into_actor(self) 375 | .then(move |_, this, _| this.trigger_logout_handlers(payload).into_actor(this)), 376 | )) 377 | } 378 | PuppetEvent::Message(payload) => AtomicResponse::new(Box::pin( 379 | async {} 380 | .into_actor(self) 381 | .then(move |_, this, _| this.trigger_message_handlers(payload).into_actor(this)), 382 | )), 383 | PuppetEvent::Ready(payload) => AtomicResponse::new(Box::pin( 384 | async {} 385 | .into_actor(self) 386 | .then(move |_, this, _| this.trigger_ready_handlers(payload).into_actor(this)), 387 | )), 388 | PuppetEvent::Reset(payload) => AtomicResponse::new(Box::pin( 389 | async {} 390 | .into_actor(self) 391 | .then(move |_, this, _| this.trigger_reset_handlers(payload).into_actor(this)), 392 | )), 393 | PuppetEvent::RoomInvite(payload) => AtomicResponse::new(Box::pin( 394 | async {} 395 | .into_actor(self) 396 | .then(move |_, this, _| this.trigger_room_invite_handlers(payload).into_actor(this)), 397 | )), 398 | PuppetEvent::RoomJoin(payload) => AtomicResponse::new(Box::pin( 399 | async {} 400 | .into_actor(self) 401 | .then(move |_, this, _| this.trigger_room_join_handlers(payload).into_actor(this)), 402 | )), 403 | PuppetEvent::RoomLeave(payload) => AtomicResponse::new(Box::pin( 404 | async {} 405 | .into_actor(self) 406 | .then(move |_, this, _| this.trigger_room_leave_handlers(payload).into_actor(this)), 407 | )), 408 | PuppetEvent::RoomTopic(payload) => AtomicResponse::new(Box::pin( 409 | async {} 410 | .into_actor(self) 411 | .then(move |_, this, _| this.trigger_room_topic_handlers(payload).into_actor(this)), 412 | )), 413 | PuppetEvent::Scan(payload) => AtomicResponse::new(Box::pin( 414 | async {} 415 | .into_actor(self) 416 | .then(move |_, this, _| this.trigger_scan_handlers(payload).into_actor(this)), 417 | )), 418 | _ => AtomicResponse::new(Box::pin(async {}.into_actor(self))), 419 | } 420 | } 421 | } 422 | 423 | impl EventListenerInner 424 | where 425 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 426 | { 427 | pub(crate) fn new(name: String, ctx: WechatyContext) -> Self { 428 | Self { 429 | name, 430 | ctx, 431 | dong_handlers: Rc::new(RefCell::new(vec![])), 432 | error_handlers: Rc::new(RefCell::new(vec![])), 433 | friendship_handlers: Rc::new(RefCell::new(vec![])), 434 | heartbeat_handlers: Rc::new(RefCell::new(vec![])), 435 | login_handlers: Rc::new(RefCell::new(vec![])), 436 | logout_handlers: Rc::new(RefCell::new(vec![])), 437 | message_handlers: Rc::new(RefCell::new(vec![])), 438 | ready_handlers: Rc::new(RefCell::new(vec![])), 439 | reset_handlers: Rc::new(RefCell::new(vec![])), 440 | room_invite_handlers: Rc::new(RefCell::new(vec![])), 441 | room_join_handlers: Rc::new(RefCell::new(vec![])), 442 | room_leave_handlers: Rc::new(RefCell::new(vec![])), 443 | room_topic_handlers: Rc::new(RefCell::new(vec![])), 444 | scan_handlers: Rc::new(RefCell::new(vec![])), 445 | } 446 | } 447 | 448 | async fn trigger_handlers( 449 | ctx: WechatyContext, 450 | payload: Payload, 451 | handlers: HandlersPtr, 452 | ) where 453 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 454 | { 455 | let len = handlers.borrow_mut().len(); 456 | for i in 0..len { 457 | let mut handler = &mut handlers.borrow_mut()[i]; 458 | if handler.1 > 0 { 459 | handler.0.run(payload.clone(), ctx.clone()).await; 460 | handler.1 -= 1; 461 | } 462 | } 463 | } 464 | 465 | fn trigger_dong_handlers(&mut self, payload: EventDongPayload) -> impl Future + 'static { 466 | let ctx = self.ctx.clone(); 467 | let handlers = self.dong_handlers.clone(); 468 | async move { EventListenerInner::::trigger_handlers(ctx, payload, handlers).await } 469 | } 470 | 471 | fn trigger_error_handlers(&mut self, payload: EventErrorPayload) -> impl Future + 'static { 472 | let ctx = self.ctx.clone(); 473 | let handlers = self.error_handlers.clone(); 474 | async move { EventListenerInner::::trigger_handlers(ctx, payload, handlers).await } 475 | } 476 | 477 | fn trigger_friendship_handlers(&mut self, payload: EventFriendshipPayload) -> impl Future + 'static { 478 | let ctx = self.ctx.clone(); 479 | let mut friendship = Friendship::new(payload.friendship_id, ctx.clone(), None); 480 | let handlers = self.friendship_handlers.clone(); 481 | async move { 482 | friendship.ready().await.unwrap_or_default(); 483 | EventListenerInner::::trigger_handlers(ctx, FriendshipPayload { friendship }, handlers).await 484 | } 485 | } 486 | 487 | fn trigger_heartbeat_handlers(&mut self, payload: EventHeartbeatPayload) -> impl Future + 'static { 488 | let ctx = self.ctx.clone(); 489 | let handlers = self.heartbeat_handlers.clone(); 490 | async move { EventListenerInner::::trigger_handlers(ctx, payload, handlers).await } 491 | } 492 | 493 | fn trigger_login_handlers(&mut self, payload: EventLoginPayload) -> impl Future + 'static { 494 | let mut contact = ContactSelf::new(payload.contact_id, self.ctx.clone(), None); 495 | let ctx = self.ctx.clone(); 496 | let handlers = self.login_handlers.clone(); 497 | async move { 498 | contact.sync().await.unwrap_or_default(); 499 | EventListenerInner::::trigger_handlers(ctx, LoginPayload { contact }, handlers).await 500 | } 501 | } 502 | 503 | fn trigger_logout_handlers(&mut self, payload: EventLogoutPayload) -> impl Future + 'static { 504 | let mut contact = ContactSelf::new(payload.contact_id.clone(), self.ctx.clone(), None); 505 | let ctx = self.ctx.clone(); 506 | let handlers = self.logout_handlers.clone(); 507 | async move { 508 | contact.ready(false).await.unwrap_or_default(); 509 | EventListenerInner::::trigger_handlers( 510 | ctx, 511 | LogoutPayload { 512 | contact, 513 | data: payload.data, 514 | }, 515 | handlers, 516 | ) 517 | .await 518 | } 519 | } 520 | 521 | fn trigger_message_handlers(&mut self, payload: EventMessagePayload) -> impl Future + 'static { 522 | let ctx = self.ctx.clone(); 523 | let mut message = Message::new(payload.message_id, ctx.clone(), None); 524 | let handlers = self.message_handlers.clone(); 525 | async move { 526 | message.ready().await.unwrap_or_default(); 527 | EventListenerInner::::trigger_handlers(ctx, MessagePayload { message }, handlers).await 528 | } 529 | } 530 | 531 | fn trigger_ready_handlers(&mut self, payload: EventReadyPayload) -> impl Future + 'static { 532 | let ctx = self.ctx.clone(); 533 | let handlers = self.ready_handlers.clone(); 534 | async move { EventListenerInner::::trigger_handlers(ctx, payload, handlers).await } 535 | } 536 | 537 | fn trigger_reset_handlers(&mut self, payload: EventResetPayload) -> impl Future + 'static { 538 | let ctx = self.ctx.clone(); 539 | let handlers = self.reset_handlers.clone(); 540 | async move { EventListenerInner::::trigger_handlers(ctx, payload, handlers).await } 541 | } 542 | 543 | fn trigger_room_invite_handlers(&mut self, payload: EventRoomInvitePayload) -> impl Future + 'static { 544 | let mut room_invitation = RoomInvitation::new(payload.room_invitation_id, self.ctx.clone(), None); 545 | let ctx = self.ctx.clone(); 546 | let handlers = self.room_invite_handlers.clone(); 547 | async move { 548 | room_invitation.ready().await.unwrap_or_default(); 549 | EventListenerInner::::trigger_handlers(ctx, RoomInvitePayload { room_invitation }, handlers).await 550 | } 551 | } 552 | 553 | fn trigger_room_join_handlers(&mut self, payload: EventRoomJoinPayload) -> impl Future + 'static { 554 | let ctx = self.ctx.clone(); 555 | let handlers = self.room_join_handlers.clone(); 556 | let mut room = Room::new(payload.room_id.clone(), ctx.clone(), None); 557 | let mut inviter = Contact::new(payload.inviter_id.clone(), ctx.clone(), None); 558 | async move { 559 | room.sync().await.unwrap_or_default(); 560 | inviter.sync().await.unwrap_or_default(); 561 | let invitee_list = ctx.contact_load_batch(payload.invitee_id_list).await; 562 | EventListenerInner::::trigger_handlers( 563 | ctx, 564 | RoomJoinPayload { 565 | room, 566 | invitee_list, 567 | inviter, 568 | timestamp: payload.timestamp, 569 | }, 570 | handlers, 571 | ) 572 | .await 573 | } 574 | } 575 | 576 | fn trigger_room_leave_handlers(&mut self, payload: EventRoomLeavePayload) -> impl Future + 'static { 577 | let ctx = self.ctx.clone(); 578 | let handlers = self.room_leave_handlers.clone(); 579 | let mut room = Room::new(payload.room_id.clone(), ctx.clone(), None); 580 | let mut remover = Contact::new(payload.remover_id.clone(), ctx.clone(), None); 581 | async move { 582 | room.sync().await.unwrap_or_default(); 583 | remover.sync().await.unwrap_or_default(); 584 | let removee_list = ctx.contact_load_batch(payload.removee_id_list.clone()).await; 585 | EventListenerInner::::trigger_handlers( 586 | ctx.clone(), 587 | RoomLeavePayload { 588 | room, 589 | removee_list, 590 | timestamp: payload.timestamp, 591 | remover, 592 | }, 593 | handlers, 594 | ) 595 | .await; 596 | let self_id = ctx.id().unwrap(); 597 | if payload.removee_id_list.contains(&self_id) { 598 | ctx.puppet() 599 | .dirty_payload(PayloadType::Room, payload.room_id.clone()) 600 | .await 601 | .unwrap_or_default(); 602 | ctx.puppet() 603 | .dirty_payload(PayloadType::RoomMember, payload.room_id) 604 | .await 605 | .unwrap_or_default(); 606 | } 607 | } 608 | } 609 | 610 | fn trigger_room_topic_handlers(&mut self, payload: EventRoomTopicPayload) -> impl Future + 'static { 611 | let ctx = self.ctx.clone(); 612 | let handlers = self.room_topic_handlers.clone(); 613 | let mut room = Room::new(payload.room_id.clone(), ctx.clone(), None); 614 | let mut changer = Contact::new(payload.changer_id.clone(), ctx.clone(), None); 615 | async move { 616 | room.sync().await.unwrap_or_default(); 617 | changer.sync().await.unwrap_or_default(); 618 | EventListenerInner::::trigger_handlers( 619 | ctx, 620 | RoomTopicPayload { 621 | room, 622 | old_topic: payload.old_topic, 623 | new_topic: payload.new_topic, 624 | changer, 625 | timestamp: payload.timestamp, 626 | }, 627 | handlers, 628 | ) 629 | .await 630 | } 631 | } 632 | 633 | fn trigger_scan_handlers(&mut self, payload: EventScanPayload) -> impl Future + 'static { 634 | let ctx = self.ctx.clone(); 635 | let handlers = self.scan_handlers.clone(); 636 | async move { EventListenerInner::::trigger_handlers(ctx, payload, handlers).await } 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /wechaty/src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod contact; 2 | pub(crate) mod event_listener; 3 | pub(crate) mod talkable; 4 | 5 | use log::{error, info}; 6 | use wechaty_puppet::PuppetImpl; 7 | 8 | use crate::{Message, WechatyContext, WechatyError}; 9 | 10 | async fn message_load( 11 | ctx: WechatyContext, 12 | message_id: String, 13 | identity: String, 14 | ) -> Result>, WechatyError> 15 | where 16 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 17 | { 18 | match ctx.message_load(message_id).await { 19 | Ok(message) => { 20 | info!("Message sent: {}", message); 21 | Ok(Some(message)) 22 | } 23 | Err(e) => { 24 | error!( 25 | "Message has been sent to {} but cannot get message payload, reason: {}", 26 | identity, e 27 | ); 28 | Ok(None) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /wechaty/src/traits/talkable.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use log::{debug, error}; 3 | use wechaty_puppet::{FileBox, MiniProgramPayload, PuppetImpl, UrlLinkPayload}; 4 | 5 | use super::message_load; 6 | use crate::{Message, WechatyContext, WechatyError}; 7 | 8 | #[async_trait] 9 | pub trait Talkable 10 | where 11 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 12 | { 13 | fn id(&self) -> String; 14 | fn ctx(&self) -> WechatyContext; 15 | fn identity(&self) -> String; 16 | 17 | async fn send_text(&self, text: String) -> Result>, WechatyError> { 18 | debug!("talkable.send_text(id = {}, text = {})", self.id(), text); 19 | let ctx = self.ctx(); 20 | let puppet = ctx.puppet(); 21 | let conversation_id = self.id(); 22 | let message_id = match puppet.message_send_text(conversation_id, text, vec![]).await { 23 | Ok(Some(id)) => id, 24 | Ok(None) => { 25 | error!("Message has been sent to {} but cannot get message id", self.identity()); 26 | return Ok(None); 27 | } 28 | Err(e) => return Err(WechatyError::from(e)), 29 | }; 30 | let identity = self.identity(); 31 | message_load(ctx, message_id, identity).await 32 | } 33 | 34 | async fn send_contact(&self, contact_id: String) -> Result>, WechatyError> { 35 | debug!("talkable.send_contact(id = {}, contact_id = {})", self.id(), contact_id); 36 | let ctx = self.ctx(); 37 | let puppet = ctx.puppet(); 38 | let conversation_id = self.id(); 39 | let message_id = match puppet.message_send_contact(conversation_id, contact_id).await { 40 | Ok(Some(id)) => id, 41 | Ok(None) => { 42 | error!("Message has been sent to {} but cannot get message id", self.identity()); 43 | return Ok(None); 44 | } 45 | Err(e) => return Err(WechatyError::from(e)), 46 | }; 47 | let identity = self.identity(); 48 | message_load(ctx, message_id, identity).await 49 | } 50 | 51 | async fn send_file(&self, file: FileBox) -> Result>, WechatyError> { 52 | debug!("talkable.send_file(id = {})", self.id()); 53 | let ctx = self.ctx(); 54 | let puppet = ctx.puppet(); 55 | let conversation_id = self.id(); 56 | let message_id = match puppet.message_send_file(conversation_id, file).await { 57 | Ok(Some(id)) => id, 58 | Ok(None) => { 59 | error!("Message has been sent to {} but cannot get message id", self.identity()); 60 | return Ok(None); 61 | } 62 | Err(e) => return Err(WechatyError::from(e)), 63 | }; 64 | let identity = self.identity(); 65 | message_load(ctx, message_id, identity).await 66 | } 67 | 68 | async fn send_mini_program(&self, mini_program: MiniProgramPayload) -> Result>, WechatyError> { 69 | debug!( 70 | "talkable.send_mini_program(id = {}, mini_program = {:?}", 71 | self.id(), 72 | mini_program 73 | ); 74 | let ctx = self.ctx(); 75 | let puppet = ctx.puppet(); 76 | let conversation_id = self.id(); 77 | let message_id = match puppet.message_send_mini_program(conversation_id, mini_program).await { 78 | Ok(Some(id)) => id, 79 | Ok(None) => { 80 | error!("Message has been sent to {} but cannot get message id", self.identity()); 81 | return Ok(None); 82 | } 83 | Err(e) => return Err(WechatyError::from(e)), 84 | }; 85 | let identity = self.identity(); 86 | message_load(ctx, message_id, identity).await 87 | } 88 | 89 | async fn send_url(&self, url: UrlLinkPayload) -> Result>, WechatyError> { 90 | debug!("talkable.send_url(id = {}, url = {:?})", self.id(), url); 91 | let ctx = self.ctx(); 92 | let puppet = ctx.puppet(); 93 | let conversation_id = self.id(); 94 | let message_id = match puppet.message_send_url(conversation_id, url).await { 95 | Ok(Some(id)) => id, 96 | Ok(None) => { 97 | error!("Message has been sent to {} but cannot get message id", self.identity()); 98 | return Ok(None); 99 | } 100 | Err(e) => return Err(WechatyError::from(e)), 101 | }; 102 | let identity = self.identity(); 103 | message_load(ctx, message_id, identity).await 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /wechaty/src/user/contact.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use log::{debug, trace}; 4 | use wechaty_puppet::{ContactPayload, PuppetImpl}; 5 | 6 | use crate::user::entity::Entity; 7 | use crate::{IntoContact, Talkable, WechatyContext}; 8 | 9 | pub type Contact = Entity; 10 | 11 | impl Contact 12 | where 13 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 14 | { 15 | pub(crate) fn new(id: String, ctx: WechatyContext, payload: Option) -> Self { 16 | debug!("create contact {}", id); 17 | let payload = match payload { 18 | Some(_) => payload, 19 | None => ctx.contacts().get(&id).cloned(), 20 | }; 21 | Self { 22 | id_: id, 23 | ctx_: ctx, 24 | payload_: payload, 25 | } 26 | } 27 | } 28 | 29 | impl Talkable for Contact 30 | where 31 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 32 | { 33 | fn id(&self) -> String { 34 | trace!("Contact.id(id = {})", self.id_); 35 | self.id_.clone() 36 | } 37 | 38 | fn ctx(&self) -> WechatyContext { 39 | trace!("Contact.ctx(id = {})", self.id_); 40 | self.ctx_.clone() 41 | } 42 | 43 | fn identity(&self) -> String { 44 | trace!("Contact.identity(id = {})", self.id_); 45 | match self.payload() { 46 | Some(payload) => { 47 | if !payload.alias.is_empty() { 48 | payload.alias 49 | } else if !payload.name.is_empty() { 50 | payload.name 51 | } else if !self.id().is_empty() { 52 | self.id() 53 | } else { 54 | "loading...".to_owned() 55 | } 56 | } 57 | None => "loading...".to_owned(), 58 | } 59 | } 60 | } 61 | 62 | impl IntoContact for Contact 63 | where 64 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 65 | { 66 | fn payload(&self) -> Option { 67 | trace!("Contact.payload(id = {})", self.id_); 68 | self.payload_.clone() 69 | } 70 | 71 | fn set_payload(&mut self, payload: Option) { 72 | debug!("Contact.set_payload(id = {}, payload = {:?})", self.id_, payload); 73 | self.payload_ = payload; 74 | } 75 | } 76 | 77 | impl fmt::Debug for Contact 78 | where 79 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 80 | { 81 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | write!(fmt, "Contact({})", self) 83 | } 84 | } 85 | 86 | impl fmt::Display for Contact 87 | where 88 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 89 | { 90 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 91 | write!(fmt, "{}", self.identity()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /wechaty/src/user/contact_self.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use log::{debug, error}; 4 | use wechaty_puppet::{ContactPayload, FileBox, PuppetImpl}; 5 | 6 | use crate::{Contact, IntoContact, Talkable, WechatyContext, WechatyError}; 7 | 8 | #[derive(Clone)] 9 | pub struct ContactSelf 10 | where 11 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 12 | { 13 | contact: Contact, 14 | } 15 | 16 | impl ContactSelf 17 | where 18 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 19 | { 20 | pub(crate) fn new(id: String, ctx: WechatyContext, payload: Option) -> Self { 21 | debug!("create contact self {}", id); 22 | let payload = match payload { 23 | Some(_) => payload, 24 | None => ctx.contacts().get(&id).cloned(), 25 | }; 26 | Self { 27 | contact: Contact::new(id, ctx, payload), 28 | } 29 | } 30 | 31 | pub async fn set_avatar(&mut self, file: FileBox) -> Result<(), WechatyError> { 32 | debug!("Contact_self.set_avatar(file = {})", file); 33 | 34 | if !self.is_self() { 35 | Err(WechatyError::NotLoggedIn) 36 | } else { 37 | let puppet = self.ctx().puppet(); 38 | let id = self.id(); 39 | match puppet.contact_avatar_set(id, file).await { 40 | Ok(_) => { 41 | match self.sync().await { 42 | Ok(_) => {} 43 | Err(e) => { 44 | error!("Failed to sync contact self after setting avatar, reason: {}", e); 45 | } 46 | } 47 | Ok(()) 48 | } 49 | Err(e) => Err(WechatyError::from(e)), 50 | } 51 | } 52 | } 53 | 54 | pub async fn set_name(&mut self, name: String) -> Result<(), WechatyError> { 55 | debug!("Contact_self.set_name(name = {})", name); 56 | 57 | if !self.is_self() { 58 | Err(WechatyError::NotLoggedIn) 59 | } else { 60 | let puppet = self.ctx().puppet(); 61 | match puppet.contact_self_name_set(name).await { 62 | Ok(_) => { 63 | match self.sync().await { 64 | Ok(_) => {} 65 | Err(e) => { 66 | error!("Failed to sync contact self after setting name, reason: {}", e); 67 | } 68 | } 69 | Ok(()) 70 | } 71 | Err(e) => Err(WechatyError::from(e)), 72 | } 73 | } 74 | } 75 | 76 | pub async fn set_signature(&mut self, signature: String) -> Result<(), WechatyError> { 77 | debug!("Contact_self.set_signature(signature = {})", signature); 78 | 79 | if !self.is_self() { 80 | Err(WechatyError::NotLoggedIn) 81 | } else { 82 | let puppet = self.ctx().puppet(); 83 | match puppet.contact_self_signature_set(signature).await { 84 | Ok(_) => { 85 | match self.sync().await { 86 | Ok(_) => {} 87 | Err(e) => { 88 | error!("Failed to sync contact self after setting signature, reason: {}", e); 89 | } 90 | } 91 | Ok(()) 92 | } 93 | Err(e) => Err(WechatyError::from(e)), 94 | } 95 | } 96 | } 97 | 98 | pub async fn qrcode(&self) -> Result { 99 | debug!("Contact_self.qrcode()"); 100 | 101 | if !self.is_self() { 102 | Err(WechatyError::NotLoggedIn) 103 | } else { 104 | let puppet = self.ctx().puppet(); 105 | match puppet.contact_self_qr_code().await { 106 | Ok(qrcode) => Ok(qrcode), 107 | Err(e) => Err(WechatyError::from(e)), 108 | } 109 | } 110 | } 111 | } 112 | 113 | impl Talkable for ContactSelf 114 | where 115 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 116 | { 117 | fn id(&self) -> String { 118 | self.contact.id() 119 | } 120 | 121 | fn ctx(&self) -> WechatyContext { 122 | self.contact.ctx() 123 | } 124 | 125 | fn identity(&self) -> String { 126 | self.contact.identity() 127 | } 128 | } 129 | 130 | impl IntoContact for ContactSelf 131 | where 132 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 133 | { 134 | fn payload(&self) -> Option { 135 | self.contact.payload() 136 | } 137 | 138 | fn set_payload(&mut self, payload: Option) { 139 | self.contact.set_payload(payload) 140 | } 141 | } 142 | 143 | impl fmt::Debug for ContactSelf 144 | where 145 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 146 | { 147 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 148 | write!(fmt, "ContactSelf({})", self) 149 | } 150 | } 151 | 152 | impl fmt::Display for ContactSelf 153 | where 154 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 155 | { 156 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 157 | write!(fmt, "{}", self.identity()) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /wechaty/src/user/entity.rs: -------------------------------------------------------------------------------- 1 | use std::any; 2 | use std::fmt::Debug; 3 | 4 | use log::trace; 5 | use wechaty_puppet::PuppetImpl; 6 | 7 | use crate::WechatyContext; 8 | 9 | #[derive(Clone)] 10 | pub struct Entity 11 | where 12 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 13 | { 14 | pub(crate) ctx_: WechatyContext, 15 | pub(crate) id_: String, 16 | pub(crate) payload_: Option, 17 | } 18 | 19 | impl Entity 20 | where 21 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 22 | Payload: Debug + Clone, 23 | { 24 | /// Get type name 25 | fn type_name() -> String { 26 | any::type_name::() 27 | .split("::") 28 | .last() 29 | .unwrap() 30 | .split("Payload") 31 | .next() 32 | .unwrap_or_default() 33 | .to_owned() 34 | } 35 | 36 | /// Get entity's id. 37 | pub fn id(&self) -> String { 38 | trace!("{}.id(id = {})", Entity::::type_name(), self.id_); 39 | self.id_.clone() 40 | } 41 | 42 | /// Check if an entity is ready. 43 | pub(crate) fn is_ready(&self) -> bool { 44 | trace!("{}.is_ready(id = {})", Entity::::type_name(), self.id_); 45 | self.payload_.is_some() 46 | } 47 | 48 | /// Get the Wechaty context. 49 | pub(crate) fn ctx(&self) -> WechatyContext { 50 | trace!("{}.ctx(id = {})", Entity::::type_name(), self.id_); 51 | self.ctx_.clone() 52 | } 53 | 54 | /// Get the entity's payload. 55 | pub(crate) fn payload(&self) -> Option { 56 | trace!("{}.payload(id = {})", Entity::::type_name(), self.id_); 57 | self.payload_.clone() 58 | } 59 | 60 | /// Set the entity's payload. 61 | pub(crate) fn set_payload(&mut self, payload: Option) { 62 | trace!( 63 | "{}.set_payload(id = {}, payload = {:?})", 64 | Entity::::type_name(), 65 | self.id_, 66 | payload 67 | ); 68 | self.payload_ = payload; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /wechaty/src/user/favorite.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Favorite {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/friendship.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use log::{debug, error}; 4 | use wechaty_puppet::{FriendshipPayload, FriendshipType, PuppetImpl}; 5 | 6 | use crate::{Contact, Entity, IntoContact, WechatyContext, WechatyError}; 7 | 8 | pub type Friendship = Entity; 9 | 10 | impl Friendship 11 | where 12 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 13 | { 14 | pub(crate) fn new(id: String, ctx: WechatyContext, payload: Option) -> Self { 15 | debug!("create friendship {}", id); 16 | let payload = match payload { 17 | Some(_) => payload, 18 | None => ctx.friendships().get(&id).cloned(), 19 | }; 20 | Self { 21 | id_: id, 22 | ctx_: ctx, 23 | payload_: payload, 24 | } 25 | } 26 | 27 | pub(crate) async fn ready(&mut self) -> Result<(), WechatyError> { 28 | debug!("Friendship.ready(id = {})", self.id_); 29 | if self.is_ready() { 30 | Ok(()) 31 | } else { 32 | let puppet = self.ctx_.puppet(); 33 | match puppet.friendship_payload(self.id()).await { 34 | Ok(payload) => { 35 | self.ctx_.friendships().insert(self.id(), payload.clone()); 36 | self.payload_ = Some(payload.clone()); 37 | if !payload.contact_id.is_empty() { 38 | let _result = self.ctx_.contact_load(payload.contact_id.clone()).await; 39 | } 40 | Ok(()) 41 | } 42 | Err(e) => { 43 | error!("Error occurred while syncing message {}: {}", self.id_, e); 44 | Err(WechatyError::from(e)) 45 | } 46 | } 47 | } 48 | } 49 | 50 | /// Get friendship's type. 51 | pub fn friendship_type(&self) -> Option { 52 | debug!("Friendship.friendship_type(id = {})", self.id_); 53 | self.payload_.as_ref().map(|payload| payload.friendship_type.clone()) 54 | } 55 | 56 | /// Get friendship's contact. 57 | pub fn contact(&self) -> Option> { 58 | debug!("Friendship.contact(id = {})", self.id_); 59 | match &self.payload_ { 60 | Some(payload) => { 61 | if !payload.contact_id.is_empty() { 62 | Some(Contact::new(payload.contact_id.clone(), self.ctx_.clone(), None)) 63 | } else { 64 | None 65 | } 66 | } 67 | None => None, 68 | } 69 | } 70 | 71 | /// Accept a friendship 72 | pub async fn accept(&mut self) -> Result<(), WechatyError> { 73 | debug!("Friendship.accept()"); 74 | if !self.is_ready() { 75 | Err(WechatyError::NoPayload) 76 | } else if self.friendship_type().unwrap() != FriendshipType::Receive { 77 | Err(WechatyError::InvalidOperation( 78 | "Can only accept a friendship of the Receive type".to_owned(), 79 | )) 80 | } else { 81 | match self.ctx().puppet().friendship_accept(self.id()).await { 82 | Ok(_) => { 83 | let mut contact = self.contact().unwrap(); 84 | contact.sync().await.unwrap_or_default(); 85 | if contact.is_ready() { 86 | Ok(()) 87 | } else { 88 | Err(WechatyError::Maybe(format!( 89 | "Failed to accept the friendship, contact: {}", 90 | contact 91 | ))) 92 | } 93 | } 94 | Err(e) => Err(WechatyError::from(e)), 95 | } 96 | } 97 | } 98 | } 99 | 100 | impl fmt::Debug for Friendship 101 | where 102 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 103 | { 104 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 105 | write!(fmt, "Friendship({})", self) 106 | } 107 | } 108 | 109 | impl fmt::Display for Friendship 110 | where 111 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 112 | { 113 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | let friendship_info = if self.is_ready() { 115 | format!( 116 | "From: {}", 117 | match self.contact() { 118 | Some(contact) => contact.to_string(), 119 | None => "Unknown".to_owned(), 120 | } 121 | ) 122 | } else { 123 | "loading".to_owned() 124 | }; 125 | write!(fmt, "{}", friendship_info) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /wechaty/src/user/image.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Image {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/location.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Location {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/message.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::SystemTime; 3 | 4 | use log::{debug, error, info}; 5 | use wechaty_puppet::{FileBox, MessagePayload, MessageType, MiniProgramPayload, PuppetImpl, UrlLinkPayload}; 6 | 7 | use crate::{Contact, Entity, IntoContact, Room, Talkable, WechatyContext, WechatyError}; 8 | 9 | pub type Message = Entity; 10 | 11 | impl Message 12 | where 13 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 14 | { 15 | pub(crate) fn new(id: String, ctx: WechatyContext, payload: Option) -> Self { 16 | debug!("create message {}", id); 17 | let payload = match payload { 18 | Some(_) => payload, 19 | None => ctx.messages().get(&id).cloned(), 20 | }; 21 | Self { 22 | id_: id, 23 | ctx_: ctx, 24 | payload_: payload, 25 | } 26 | } 27 | 28 | /// Check if the message is sent by the user self. 29 | pub fn is_self(&self) -> bool { 30 | debug!("Message.is_self(id = {})", self.id_); 31 | if !self.is_ready() { 32 | false 33 | } else { 34 | self.from().unwrap().is_self() 35 | } 36 | } 37 | 38 | /// Check if the message is sent in a room. 39 | pub fn is_in_room(&self) -> bool { 40 | debug!("Message.is_in_room(id = {})", self.id_); 41 | self.room().is_some() 42 | } 43 | 44 | /// Check if the message mentioned the user self. 45 | pub fn mentioned_self(&self) -> bool { 46 | debug!("Message.mentioned_self(id = {})", self.id_); 47 | if !self.is_ready() || !self.ctx_.is_logged_in() { 48 | false 49 | } else { 50 | self.payload() 51 | .unwrap() 52 | .mention_id_list 53 | .contains(&self.ctx_.id().unwrap()) 54 | } 55 | } 56 | 57 | pub(crate) async fn ready(&mut self) -> Result<(), WechatyError> { 58 | debug!("Message.ready(id = {})", self.id_); 59 | if self.is_ready() { 60 | Ok(()) 61 | } else { 62 | let puppet = self.ctx_.puppet(); 63 | match puppet.message_payload(self.id()).await { 64 | Ok(payload) => { 65 | self.ctx_.messages().insert(self.id(), payload.clone()); 66 | self.payload_ = Some(payload.clone()); 67 | if !payload.from_id.is_empty() { 68 | let _result = self.ctx_.contact_load(payload.from_id.clone()).await; 69 | } 70 | if !payload.to_id.is_empty() { 71 | let _result = self.ctx_.contact_load(payload.to_id.clone()).await; 72 | } 73 | if !payload.room_id.is_empty() { 74 | let _result = self.ctx_.room_load(payload.room_id.clone()).await; 75 | } 76 | Ok(()) 77 | } 78 | Err(e) => { 79 | error!("Error occurred while syncing message {}: {}", self.id_, e); 80 | Err(WechatyError::from(e)) 81 | } 82 | } 83 | } 84 | } 85 | 86 | /// Get message's conversation id. 87 | pub fn conversation_id(&self) -> Option { 88 | debug!("Message.conversation_id(id = {})", self.id_); 89 | if self.is_ready() { 90 | let payload = self.payload().unwrap(); 91 | if !payload.room_id.is_empty() { 92 | Some(payload.room_id) 93 | } else if !payload.from_id.is_empty() { 94 | Some(payload.from_id) 95 | } else { 96 | None 97 | } 98 | } else { 99 | None 100 | } 101 | } 102 | 103 | /// Get message's sender. 104 | pub fn from(&self) -> Option> { 105 | debug!("Message.from(id = {})", self.id_); 106 | match &self.payload_ { 107 | Some(payload) => { 108 | if !payload.from_id.is_empty() { 109 | Some(Contact::new(payload.from_id.clone(), self.ctx_.clone(), None)) 110 | } else { 111 | None 112 | } 113 | } 114 | None => None, 115 | } 116 | } 117 | 118 | /// Get message's receiver. 119 | pub fn to(&self) -> Option> { 120 | debug!("Message.to(id = {})", self.id_); 121 | match &self.payload_ { 122 | Some(payload) => { 123 | if !payload.to_id.is_empty() { 124 | Some(Contact::new(payload.to_id.clone(), self.ctx_.clone(), None)) 125 | } else { 126 | None 127 | } 128 | } 129 | None => None, 130 | } 131 | } 132 | 133 | /// Get the room that the message belongs to. 134 | pub fn room(&self) -> Option> { 135 | debug!("Message.room(id = {})", self.id_); 136 | match &self.payload_ { 137 | Some(payload) => { 138 | if !payload.room_id.is_empty() { 139 | Some(Room::new(payload.room_id.clone(), self.ctx_.clone(), None)) 140 | } else { 141 | None 142 | } 143 | } 144 | None => None, 145 | } 146 | } 147 | 148 | /// Get message's timestamp. 149 | pub fn timestamp(&self) -> Option { 150 | debug!("Message.timestamp(id = {})", self.id_); 151 | self.payload_.as_ref().map(|payload| payload.timestamp) 152 | } 153 | 154 | /// Get message's age in seconds. 155 | pub fn age(&self) -> u64 { 156 | debug!("Message.age(id = {})", self.id_); 157 | match &self.payload_ { 158 | Some(payload) => { 159 | SystemTime::now() 160 | .duration_since(SystemTime::UNIX_EPOCH) 161 | .unwrap() 162 | .as_secs() 163 | .max(payload.timestamp) 164 | - payload.timestamp 165 | } 166 | None => 0, 167 | } 168 | } 169 | 170 | /// Get the message type. 171 | pub fn message_type(&self) -> Option { 172 | debug!("Message.message_type(id = {})", self.id_); 173 | self.payload_.as_ref().map(|payload| payload.message_type.clone()) 174 | } 175 | 176 | /// Get the message's text content, if it is a text message. 177 | pub fn text(&self) -> Option { 178 | debug!("Message.text(id = {})", self.id_); 179 | self.payload_.as_ref().map(|payload| payload.text.clone()) 180 | } 181 | 182 | /// Get the trimmed version (no mentions) of the message's text content. 183 | pub async fn text_trimmed(&mut self) -> String { 184 | unimplemented!() 185 | } 186 | 187 | /// Get the message's mention list. 188 | /// 189 | /// TODO: Analyze message text 190 | pub async fn mention_list(&mut self) -> Option>> { 191 | debug!("Message.mention_list(id = {})", self.id_); 192 | match &self.payload_ { 193 | Some(payload) => Some(self.ctx_.contact_load_batch(payload.mention_id_list.clone()).await), 194 | None => None, 195 | } 196 | } 197 | 198 | /// Forward the current message to a conversation (contact or room). 199 | pub async fn forward(&mut self, conversation_id: String) -> Result>, WechatyError> { 200 | debug!("Message.forward(id = {}", self.id_); 201 | match self 202 | .ctx_ 203 | .puppet() 204 | .message_forward(conversation_id.clone(), self.id()) 205 | .await 206 | { 207 | Ok(Some(message_id)) => { 208 | info!("Message {} was forwarded to {}", self.id(), conversation_id); 209 | match self.ctx_.message_load(message_id.clone()).await { 210 | Ok(message) => Ok(Some(message)), 211 | Err(e) => { 212 | error!("Failed to load forwarded message {}, reason: {}", message_id, e); 213 | Ok(None) 214 | } 215 | } 216 | } 217 | Ok(None) => Ok(None), 218 | Err(e) => { 219 | error!("Failed to forward message {}, reason: {}", self.id_, e); 220 | Err(WechatyError::from(e)) 221 | } 222 | } 223 | } 224 | 225 | pub async fn reply_text(&mut self, text: String) -> Result>, WechatyError> { 226 | debug!("Message.reply_text(id = {}, text = {})", self.id_, text); 227 | if !self.is_ready() { 228 | return Err(WechatyError::NoPayload); 229 | } 230 | if self.is_in_room() { 231 | unimplemented!() 232 | } else { 233 | self.from().unwrap().send_text(text).await 234 | } 235 | } 236 | 237 | pub async fn reply_contact(&mut self, contact_id: String) -> Result>, WechatyError> { 238 | debug!("Message.reply_contact(id = {}, contact_id = {})", self.id_, contact_id); 239 | if !self.is_ready() { 240 | return Err(WechatyError::NoPayload); 241 | } 242 | if self.is_in_room() { 243 | unimplemented!() 244 | } else { 245 | self.from().unwrap().send_contact(contact_id).await 246 | } 247 | } 248 | 249 | pub async fn reply_file(&mut self, file: FileBox) -> Result>, WechatyError> { 250 | debug!("Message.reply_file(id = {})", self.id_); 251 | if !self.is_ready() { 252 | return Err(WechatyError::NoPayload); 253 | } 254 | if self.is_in_room() { 255 | unimplemented!() 256 | } else { 257 | self.from().unwrap().send_file(file).await 258 | } 259 | } 260 | 261 | pub async fn reply_mini_program( 262 | &mut self, 263 | mini_program: MiniProgramPayload, 264 | ) -> Result>, WechatyError> { 265 | debug!( 266 | "message.reply_mini_program(id = {}, mini_program = {:?})", 267 | self.id_, mini_program 268 | ); 269 | if !self.is_ready() { 270 | return Err(WechatyError::NoPayload); 271 | } 272 | if self.is_in_room() { 273 | unimplemented!() 274 | } else { 275 | self.from().unwrap().send_mini_program(mini_program).await 276 | } 277 | } 278 | 279 | pub async fn reply_url(&mut self, url: UrlLinkPayload) -> Result>, WechatyError> { 280 | debug!("Message.reply_url(id = {}, url = {:?})", self.id_, url); 281 | if !self.is_ready() { 282 | return Err(WechatyError::NoPayload); 283 | } 284 | if self.is_in_room() { 285 | unimplemented!() 286 | } else { 287 | self.from().unwrap().send_url(url).await 288 | } 289 | } 290 | } 291 | 292 | impl fmt::Debug for Message 293 | where 294 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 295 | { 296 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 297 | write!(fmt, "Message({})", self) 298 | } 299 | } 300 | 301 | impl fmt::Display for Message 302 | where 303 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 304 | { 305 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 306 | let from = match self.from() { 307 | Some(contact) => format!("From: {} ", contact), 308 | None => String::new(), 309 | }; 310 | let to = match self.to() { 311 | Some(contact) => format!("To: {} ", contact), 312 | None => String::new(), 313 | }; 314 | let room = match self.room() { 315 | Some(room) => format!("Room: {} ", room), 316 | None => String::new(), 317 | }; 318 | let message_type = match self.message_type() { 319 | Some(message_type) => format!("Type: {:?} ", message_type), 320 | None => String::new(), 321 | }; 322 | let text = if self.is_ready() && self.message_type().unwrap() == MessageType::Text { 323 | let text = self.text().unwrap().chars().collect::>(); 324 | let len = text.len().min(70); 325 | format!("Text: {} ", text[0..len].iter().collect::()) 326 | } else { 327 | String::new() 328 | }; 329 | write!(fmt, "{}", [from, to, room, message_type, text].join("")) 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /wechaty/src/user/mini_program.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct MiniProgram {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod contact; 2 | pub(crate) mod contact_self; 3 | pub(crate) mod entity; 4 | pub(crate) mod favorite; 5 | pub(crate) mod friendship; 6 | pub(crate) mod image; 7 | pub(crate) mod location; 8 | pub(crate) mod message; 9 | pub(crate) mod mini_program; 10 | pub(crate) mod moment; 11 | pub(crate) mod money; 12 | pub(crate) mod room; 13 | pub(crate) mod room_invitation; 14 | pub(crate) mod tag; 15 | pub(crate) mod url_link; 16 | -------------------------------------------------------------------------------- /wechaty/src/user/moment.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Moment {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/money.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Money {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/room.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use async_trait::async_trait; 4 | use log::{debug, error, trace}; 5 | use wechaty_puppet::{PayloadType, PuppetImpl, RoomMemberQueryFilter, RoomPayload}; 6 | 7 | use crate::{Contact, Entity, Talkable, WechatyContext, WechatyError}; 8 | 9 | pub type Room = Entity; 10 | 11 | impl Room 12 | where 13 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 14 | { 15 | pub(crate) fn new(id: String, ctx: WechatyContext, payload: Option) -> Self { 16 | debug!("create room {}", id); 17 | let payload = match payload { 18 | Some(_) => payload, 19 | None => ctx.rooms().get(&id).cloned(), 20 | }; 21 | Self { 22 | id_: id, 23 | ctx_: ctx, 24 | payload_: payload, 25 | } 26 | } 27 | 28 | pub(crate) async fn ready(&mut self, force_sync: bool) -> Result<(), WechatyError> { 29 | debug!("Room.ready(id = {})", self.id_); 30 | if !force_sync && self.is_ready() { 31 | Ok(()) 32 | } else { 33 | let id = self.id(); 34 | let mut puppet = self.ctx().puppet(); 35 | if force_sync { 36 | if let Err(e) = puppet.dirty_payload(PayloadType::Room, id.clone()).await { 37 | error!("Error occurred while dirtying room {}: {}", id, e); 38 | return Err(WechatyError::from(e)); 39 | } 40 | if let Err(e) = puppet.dirty_payload(PayloadType::RoomMember, id.clone()).await { 41 | error!("Error occurred while dirtying members of room {}: {}", id, e); 42 | return Err(WechatyError::from(e)); 43 | } 44 | } 45 | match puppet.room_payload(id.clone()).await { 46 | Ok(payload) => { 47 | self.ctx().rooms().insert(id, payload.clone()); 48 | self.set_payload(Some(payload.clone())); 49 | self.ctx().contact_load_batch(payload.member_id_list).await; 50 | Ok(()) 51 | } 52 | Err(e) => { 53 | error!("Error occurred while syncing contact {}: {}", id, e); 54 | Err(WechatyError::from(e)) 55 | } 56 | } 57 | } 58 | } 59 | 60 | pub(crate) async fn sync(&mut self) -> Result<(), WechatyError> { 61 | debug!("Room.sync(id = {})", self.id_); 62 | self.ready(true).await 63 | } 64 | 65 | pub async fn member_find(&self, query: RoomMemberQueryFilter) -> Result>, WechatyError> { 66 | debug!("Room.member_find(id = {}, query = {:?})", self.id_, query); 67 | let ctx = self.ctx(); 68 | match ctx.puppet().room_member_search(self.id(), query).await { 69 | Ok(member_id_list) => Ok(ctx.contact_load_batch(member_id_list).await), 70 | Err(e) => Err(WechatyError::from(e)), 71 | } 72 | } 73 | 74 | pub async fn member_find_by_string(&self, query_str: String) -> Result>, WechatyError> { 75 | debug!( 76 | "Room.member_find_by_string(id = {}, query_str = {:?})", 77 | self.id_, query_str 78 | ); 79 | let ctx = self.ctx(); 80 | match ctx.puppet().room_member_search_by_string(self.id(), query_str).await { 81 | Ok(member_id_list) => Ok(ctx.contact_load_batch(member_id_list).await), 82 | Err(e) => Err(WechatyError::from(e)), 83 | } 84 | } 85 | 86 | pub async fn member_find_all(&self) -> Result>, WechatyError> { 87 | debug!("Room.member_find_all(id = {})", self.id_); 88 | let ctx = self.ctx(); 89 | match ctx.puppet().room_member_list(self.id()).await { 90 | Ok(member_id_list) => Ok(ctx.contact_load_batch(member_id_list).await), 91 | Err(e) => Err(WechatyError::from(e)), 92 | } 93 | } 94 | } 95 | 96 | #[async_trait] 97 | impl Talkable for Room 98 | where 99 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 100 | { 101 | fn id(&self) -> String { 102 | trace!("Room.id(id = {})", self.id_); 103 | self.id_.clone() 104 | } 105 | 106 | fn ctx(&self) -> WechatyContext { 107 | trace!("Room.id(id = {})", self.id_); 108 | self.ctx_.clone() 109 | } 110 | 111 | fn identity(&self) -> String { 112 | match &self.payload_ { 113 | Some(payload) => { 114 | if !payload.topic.is_empty() { 115 | payload.topic.clone() 116 | } else if !self.id_.is_empty() { 117 | self.id_.clone() 118 | } else { 119 | "loading...".to_owned() 120 | } 121 | } 122 | None => "loading...".to_owned(), 123 | } 124 | } 125 | } 126 | 127 | impl fmt::Debug for Room 128 | where 129 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 130 | { 131 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 132 | write!(fmt, "Room({})", self) 133 | } 134 | } 135 | 136 | impl fmt::Display for Room 137 | where 138 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 139 | { 140 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 141 | write!(fmt, "{}", self.identity()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /wechaty/src/user/room_invitation.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use log::{debug, error}; 4 | use wechaty_puppet::{PuppetImpl, RoomInvitationPayload}; 5 | 6 | use crate::{Entity, WechatyContext, WechatyError}; 7 | 8 | pub type RoomInvitation = Entity; 9 | 10 | impl RoomInvitation 11 | where 12 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 13 | { 14 | pub(crate) fn new(id: String, ctx: WechatyContext, payload: Option) -> Self { 15 | debug!("create room invitation {}", id); 16 | let payload = match payload { 17 | Some(_) => payload, 18 | None => ctx.room_invitations().get(&id).cloned(), 19 | }; 20 | Self { 21 | id_: id, 22 | ctx_: ctx, 23 | payload_: payload, 24 | } 25 | } 26 | 27 | pub async fn accept(&self) -> Result<(), WechatyError> { 28 | debug!("RoomInvitation.accept(id = {})", self.id_); 29 | match self.ctx().puppet().room_invitation_accept(self.id()).await { 30 | Ok(_) => Ok(()), 31 | Err(e) => Err(WechatyError::from(e)), 32 | } 33 | } 34 | 35 | pub(crate) async fn ready(&mut self) -> Result<(), WechatyError> { 36 | debug!("RoomInvitation.ready(id = {})", self.id_); 37 | if self.is_ready() { 38 | Ok(()) 39 | } else { 40 | let puppet = self.ctx_.puppet(); 41 | match puppet.room_invitation_payload(self.id()).await { 42 | Ok(payload) => { 43 | self.ctx_.room_invitations().insert(self.id(), payload.clone()); 44 | self.payload_ = Some(payload.clone()); 45 | if !payload.inviter_id.is_empty() { 46 | let _result = self.ctx_.contact_load(payload.inviter_id.clone()).await; 47 | } 48 | if !payload.receiver_id.is_empty() { 49 | let _result = self.ctx_.contact_load(payload.receiver_id.clone()).await; 50 | } 51 | Ok(()) 52 | } 53 | Err(e) => { 54 | error!("Error occurred while syncing room_invitation {}: {}", self.id_, e); 55 | Err(WechatyError::from(e)) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | impl fmt::Debug for RoomInvitation 63 | where 64 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 65 | { 66 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | write!(fmt, "RoomInvitation({})", self) 68 | } 69 | } 70 | 71 | impl fmt::Display for RoomInvitation 72 | where 73 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 74 | { 75 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | write!(fmt, "{}", self.id()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /wechaty/src/user/tag.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Tag {} 3 | -------------------------------------------------------------------------------- /wechaty/src/user/url_link.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct UrlLink {} 3 | -------------------------------------------------------------------------------- /wechaty/src/wechaty.rs: -------------------------------------------------------------------------------- 1 | use actix::{Actor, Addr, Recipient}; 2 | use tokio::signal; 3 | use wechaty_puppet::{Puppet, PuppetEvent, PuppetImpl}; 4 | 5 | use crate::{EventListener, EventListenerInner, WechatyContext}; 6 | 7 | type WechatyListener = EventListenerInner; 8 | 9 | pub struct Wechaty 10 | where 11 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 12 | { 13 | puppet: Puppet, 14 | listener: WechatyListener, 15 | addr: Addr>, 16 | } 17 | 18 | impl Wechaty 19 | where 20 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 21 | { 22 | pub fn new(puppet: Puppet) -> Self { 23 | let listener = EventListenerInner::new("Wechaty".to_owned(), WechatyContext::new(puppet.clone())); 24 | let addr = listener.clone().start(); 25 | Self { puppet, listener, addr } 26 | } 27 | 28 | pub async fn start(&self) { 29 | signal::ctrl_c() 30 | .await 31 | .expect("Failed to establish the listener for graceful exit"); 32 | } 33 | } 34 | 35 | impl EventListener for Wechaty 36 | where 37 | T: 'static + PuppetImpl + Clone + Unpin + Send + Sync, 38 | { 39 | fn get_listener(&self) -> &EventListenerInner { 40 | &self.listener 41 | } 42 | 43 | fn get_puppet(&self) -> Puppet { 44 | self.puppet.clone() 45 | } 46 | 47 | fn get_addr(&self) -> Recipient { 48 | self.addr.clone().recipient() 49 | } 50 | } 51 | --------------------------------------------------------------------------------