├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── publish.yml │ └── rust.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── 5_seconds.rs ├── blocking.rs ├── blocking_shutdown.rs ├── discord_presence.rs ├── discord_presence_subscriber.rs ├── event_data.rs ├── helpers │ ├── logging.rs │ └── mod.rs ├── large_response.rs └── unblocking.rs ├── renovate.json ├── src ├── client.rs ├── connection │ ├── base.rs │ ├── manager.rs │ ├── mod.rs │ ├── unix.rs │ └── windows.rs ├── error.rs ├── event_handler.rs ├── lib.rs ├── macros.rs ├── models │ ├── commands.rs │ ├── events.rs │ ├── message.rs │ ├── mod.rs │ ├── payload.rs │ └── rich_presence.rs └── utils.rs └── tests ├── fixtures └── activity_full.json └── version_numbers.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # for the sake of sanity 2 | 3 | # topmost editorconfig file 4 | root = true 5 | 6 | # always use Unix-style newlines, 7 | # use 4 spaces for indentation 8 | # and use UTF-8 encoding 9 | [*] 10 | indent_style = space 11 | end_of_line = lf 12 | indent_size = 4 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.yml] 18 | indent_size = 2 19 | 20 | [*.json] 21 | ident_size = 2 22 | insert_final_newline = false 23 | 24 | [*.md] 25 | indent_size = 1 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: jewlexx 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. Windows] 29 | - Version [e.g. 2.1.7] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: jewlexx 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like. Also mention of you would be able to get involved in implementing the solution** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test and Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_PUBLISH_TOKEN }} 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Build 19 | run: cargo build --verbose 20 | - name: Run tests 21 | run: cargo test --verbose 22 | - name: Publish to crates.io 23 | run: cargo publish 24 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [trunk] 6 | 7 | pull_request: 8 | 9 | workflow_dispatch: 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | os: [macos-latest, ubuntu-latest, windows-latest] 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: rustup toolchain install stable --profile minimal 25 | - name: Cache 26 | uses: Swatinem/rust-cache@v2 27 | with: 28 | key: ${{ matrix.os }} 29 | 30 | - name: Build 31 | run: cargo build --verbose 32 | - name: Run tests 33 | run: cargo test --verbose 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "discord.enabled": false, 3 | "rust-analyzer.cargo.features": "all" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased](https://github.com/jewlexx/discord-presence/tree/main) 9 | 10 | ## [1.6.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.6.0) 11 | 12 | ### Added 13 | 14 | - Support for snap installation of Discord 15 | 16 | ## [1.5.1](https://github.com/jewlexx/discord-presence/releases/tag/v1.5.1) 17 | 18 | ### Fixed 19 | 20 | - Client would disconnect from pipe when trying to update presence incrementally 21 | 22 | ## [1.5.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.5.0) 23 | 24 | ### Added 25 | 26 | - Support for properly handling frame headers from Discord 27 | - This should result in slight performance improvements 28 | from not allocating more than necessary. 29 | - This also allows for larger payloads to be sent 30 | 31 | ### Changed 32 | 33 | - Removed `tracing` crate in favour of `log` 34 | 35 | ## [1.4.1](https://github.com/jewlexx/discord-presence/releases/tag/v1.4.1) 36 | 37 | ### Fixed 38 | 39 | - Compilation errors on Rust v1.69.x 40 | 41 | ### Changed 42 | 43 | - Downgrade quork to 0.7.1 to fix compilation error on Rust v1.69.x 44 | - This downgrades the windows crate to a version that is compatible with the MSRV 45 | - `Manager::new` function is now `pub(crate)`. This makes no difference to the public API, as the `Manager` struct was never public. 46 | 47 | ## [1.4.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.4.0) 48 | 49 | ### Added 50 | 51 | - `Connected` and `Disconnected` events for when the client successfully connects and disconnects. Thanks to @JakeStanger. 52 | 53 | ### Changed 54 | 55 | - `EventData` documentation references to reference the `Event` enum instead of `EventData` 56 | 57 | ## [1.3.1](https://github.com/jewlexx/discord-presence/releases/tag/v1.3.1) 58 | 59 | ### Added 60 | 61 | - Expose `event_handler` module 62 | - Exposed types are `Context`, `EventCallbackHandle` and `Handler` 63 | 64 | ## [1.3.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.2.0) 65 | 66 | ### Changed 67 | 68 | - Update MSRV to 1.69.0 69 | 70 | ### Added 71 | 72 | - Support for different activity types by [@natawie](https://github.com/natawie) [#118](https://github.com/jewlexx/discord-presence/pull/118) 73 | 74 | ## [1.2.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.2.0) 75 | 76 | ### Changed 77 | 78 | - Sleep on connection failure 79 | 80 | ## [1.1.2](https://github.com/jewlexx/discord-presence/releases/tag/v1.1.2) 81 | 82 | ### Fixed 83 | 84 | - Missing buttons field 85 | 86 | ## [1.1.1](https://github.com/jewlexx/discord-presence/releases/tag/v1.1.1) 87 | 88 | ### Fixed 89 | 90 | - Shutdown function incorrectly throwing NotStarted error 91 | 92 | ## [1.1.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.1.0) 93 | 94 | ### Fixed 95 | 96 | - Debug printing in release 97 | 98 | ### Added 99 | 100 | - PartialUser struct 101 | 102 | ## [1.0.0](https://github.com/jewlexx/discord-presence/releases/tag/v1.0.0) 103 | 104 | ### Breaking Changes 105 | 106 | - Send & Receive Loop now breaks for `ConnectionRefused` error kind, rather than `WouldBlock` 107 | - Removed client thread handle (now is kept internally on the Client struct) 108 | - Removed `STARTED` boolean. (Pretty much pointless as it is only different between when the client has been started, but is not yet ready) 109 | - Increase connection timeout on Windows to 16 seconds 110 | - `on_event` now returns an EventCallbackHandle, which, if dropped, removes the event handler 111 | 112 | ### Added 113 | 114 | - Ability to remove event handlers [#40](https://github.com/jewlexx/discord-presence/issues/40) 115 | - Support buttons [#38](https://github.com/jewlexx/discord-presence/issues/38) 116 | - Client can now be cloned 117 | - Better types for the Error event 118 | 119 | ### Fixed 120 | 121 | - Ready event called every single connection in send & receive loop 122 | 123 | ## [0.5.17](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.17) - 2023-08-16 124 | 125 | ### Fixed 126 | 127 | - Added back list of events for Bevy crate 128 | 129 | ## [0.5.16](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.16) - 2023-08-16 130 | 131 | ### Added 132 | 133 | - Implemented means for stopping client send and receive thread 134 | 135 | ### Removed 136 | 137 | - Removed unused strum dependency 138 | 139 | ## [0.5.15](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.15) - 2023-07-13 140 | 141 | ### Added 142 | 143 | - Add is_ready and is_started checks 144 | 145 | ### Removed 146 | 147 | - Removed unused deps by 148 | 149 | ## [0.5.14](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.14) - 2022-12-16 150 | 151 | Full Changelog: [`v0.5.13...v0.5.14`](https://github.com/jewlexx/discord-presence/compare/v0.5.13...v0.5.14) 152 | 153 | ## [0.5.13](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.13) - 2022-12-16 154 | 155 | ### Changed 156 | 157 | - Update Rust crate bytes to 1.3 by @renovate in #31 158 | 159 | ## [0.5.12](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.12) - 2022-11-07 160 | 161 | Full Changelog: [`v0.5.11...v0.5.12`](https://github.com/jewlexx/discord-presence/compare/v0.5.11...v0.5.12) 162 | 163 | ## [0.5.11](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.11) - 2022-11-07 164 | 165 | ### Added 166 | 167 | - `block_until_event` function which blocks the current thread until a given event is fired 168 | 169 | ### Changed 170 | 171 | - Use `AtomicBool` instead of `Mutex` 172 | 173 | ## [0.5.9](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.9) - 2022-10-04 174 | 175 | ### Fixed 176 | 177 | - Send/Receive loop would timeout indefinitely 178 | 179 | ### Changed 180 | 181 | - Use [`tracing`](https://crates.io/crates/tracing) crate for logs 182 | 183 | ## [0.5.8](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.8) - 2022-09-18 184 | 185 | ### Fixed 186 | 187 | - party.id should be String, not u32 by @bigfarts in 188 | 189 | ### Changed 190 | 191 | - Update actions/cache action to v3.0.8 by @renovate in 192 | 193 | ## [0.5.7](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.7) - 2022-08-05 194 | 195 | ### Changed 196 | 197 | - Downgrade compiler edition by @jewlexx in 198 | 199 | ## [0.5.6](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.6) - 2022-08-01 200 | 201 | ### Fixed 202 | 203 | - Minor bug fix relating to empty RPC pipe 204 | 205 | ### Changed 206 | 207 | - Configure Renovate by @renovate in 208 | - Update actions/cache action to v3.0.5 by @renovate in 209 | - Update Rust crate bytes to 1.2 by @renovate in 210 | 211 | ## [0.5.5](https://github.com/jewlexx/discord-presence/releases/tag/v0.5.5) - 2022-07-27 212 | 213 | Full Changelog: [v0.5.4...v0.5.5](https://github.com/jewlexx/discord-presence/compare/v0.5.4...v0.5.5) 214 | 215 | ## [0.5.4](https://github.com/jewlexx/discord-presence/releases/tag/discord-rpc%400.5.0) - 2022-06-19 216 | 217 | ### Fixed 218 | 219 | - Fixed issues with timeouts on Discord connections 220 | - Fixed issues with Unix connections 221 | 222 | ## [0.5.0](https://github.com/jewlexx/discord-presence/releases/tag/discord-rpc%400.5.0) - 2022-04-21 223 | 224 | ### Changed 225 | 226 | - Removed `rich_presence` as a feature option, as it is redundant 227 | 228 | ## 0.4.2-0.4.4 - 2022-04-12 229 | 230 | ### Changed 231 | 232 | - Updated Readme and metadata 233 | 234 | ## 0.4.1 - 2022-04-12 235 | 236 | ### Changed 237 | 238 | - Minor bug fixes and performance improvements 239 | 240 | ## 0.4.0 - 2022-04-12 241 | 242 | ### Admin 243 | 244 | - Under new ownership, forked by [Juliette Codor (jewlexx)](https://github.com/jewlexx) 245 | 246 | ### Changed 247 | 248 | - Updated to newest Rust compiler edition (2021) 249 | 250 | - Updated deps to latest version 251 | 252 | - Fixed issues that came with the above changes 253 | 254 | ## 0.3.0 - 2018-12-06 255 | 256 | ### Changed 257 | 258 | - Connection manager completely rewritten 259 | - Allow cloning of clients 260 | 261 | ## [0.2.4] - 2018-12-04 262 | 263 | ### Changed 264 | 265 | - No longer depends on `libc` for process id lookup 266 | 267 | ## [0.2.3] - 2018-04-08 268 | 269 | ### Added 270 | 271 | - Connection manager with reconnection 272 | - Method to clear the current Rich Presence state 273 | 274 | ### Changed 275 | 276 | - Move rich presence code back into _models_ 277 | - Remove command payload and add generic one 278 | - Timestamps are now 64 bit unsigned integers instead of 32 bit ([@Bond-009]) [6bbc9f8][c:6bbc9f8] 279 | 280 | ## [0.2.2] - 2018-04-03 281 | 282 | ### Changed 283 | 284 | - Use a default socket connection for the current platform 285 | 286 | ## [0.2.1] - 2018-04-03 287 | 288 | ### Changed 289 | 290 | - Move common connection methods into trait 291 | 292 | ## [0.2.0] - 2018-04-02 293 | 294 | ### Added 295 | 296 | - Error type 297 | - Windows support ([@Tenrys]) [620e9a6][c:620e9a6] 298 | 299 | ### Changed 300 | 301 | - Convert OpCode with `try_from` instead of `try` 302 | - Use Rust 1.25 style nested imports 303 | 304 | ## [0.1.5] - 2018-03-28 305 | 306 | ### Changed 307 | 308 | - Opcode stored in Message is now an OpCode enum 309 | - Rich Presence now lives in it's own submodule 310 | 311 | ## [0.1.4] - 2018-03-23 312 | 313 | ### Changed 314 | 315 | - Opcodes are now represented as enum instead of integers 316 | 317 | ## [0.1.3] - 2018-03-23 318 | 319 | ### Added 320 | 321 | - Contributing information 322 | 323 | ### Changed 324 | 325 | - Use `libc::getpid` to allow builds with _stable_ instead of _nightly_ 326 | - Make client struct fields private 327 | - Make models private again and add prelude 328 | - Connections are now using a shared Connection trait 329 | 330 | ## [0.1.2] - 2018-03-22 331 | 332 | ### Added 333 | 334 | - Logging support 335 | 336 | ## [0.1.1] - 2018-03-22 337 | 338 | ### Changed 339 | 340 | - Make models publicly accessible 341 | 342 | ## [0.1.0] - 2018-03-22 343 | 344 | ### Added 345 | 346 | - Setting Rich Presence status 347 | - Unix socket connection support 348 | 349 | 350 | 351 | [0.2.4]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.4 352 | [0.2.3]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.3 353 | [0.2.2]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.2 354 | [0.2.1]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.1 355 | [0.2.0]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.0 356 | [0.1.5]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.5 357 | [0.1.4]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.4 358 | [0.1.3]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.3 359 | [0.1.2]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.2 360 | [0.1.1]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.1 361 | [0.1.0]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.0 362 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Contributions to this project are welcome, just follow these steps. 4 | 5 | 1. Fork this repository and create a feature branch named after the feature you want to implement 6 | 2. Make your changes on your branch 7 | 3. Add some test if possibe 8 | 4. Make sure all tests pass (I recommend installing [Overcommit](https://github.com/brigade/overcommit)) 9 | 5. Submit a PR/MR on GitHub or GitLab 10 | 11 | > **Note**: Make sure you rebase your feature branch on top of master from time to time. 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Juliette Cordor", "Patrick Auernig "] 3 | description = "A Rust client for Discord RPC." 4 | edition = "2021" 5 | keywords = ["discord", "ipc", "rpc"] 6 | license = "MIT" 7 | name = "discord-presence" 8 | readme = "README.md" 9 | repository = "https://github.com/jewlexx/discord-presence.git" 10 | rust-version = "1.70.0" 11 | version = "1.6.0" 12 | 13 | [features] 14 | activity_type = ["dep:serde_repr"] 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | 20 | [dependencies] 21 | byteorder = "1.5" 22 | bytes = "1.6" 23 | cfg-if = "1.0" 24 | crossbeam-channel = "0.5" 25 | log = "0.4" 26 | num-derive = "0.4" 27 | num-traits = "0.2" 28 | parking_lot = "0.12" 29 | paste = "1.0" 30 | quork = { version = "0.9", default-features = false, features = [ 31 | "macros", 32 | "traits", 33 | ] } 34 | serde_json = "1.0" 35 | serde_repr = { version = "0.1", optional = true } 36 | thiserror = "2.0" 37 | 38 | [dependencies.serde] 39 | features = ["derive"] 40 | version = "1.0" 41 | 42 | [dependencies.uuid] 43 | features = ["v4"] 44 | version = "1.8" 45 | 46 | [dev-dependencies] 47 | anyhow = "1.0" 48 | ctrlc = "3.4" 49 | tracing-subscriber = "0.3" 50 | version-sync = "0.9" 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Juliette Cordor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord RPC 2 | 3 | [![crates.io](https://img.shields.io/crates/v/discord-presence.svg)](https://crates.io/crates/discord-presence) 4 | [![crates.io](https://img.shields.io/crates/d/discord-presence.svg)](https://crates.io/crates/discord-presence) 5 | [![docs.rs](https://docs.rs/discord-presence/badge.svg)](https://docs.rs/discord-presence) 6 | 7 | Discord RPC client for Rust forked from [Discord RPC Client](https://gitlab.com/valeth/discord-rpc-client.rs) 8 | 9 | > Note: If you are looking to add this into a game, check out the [Bevy implementation](https://github.com/jewlexx/bevy-discord-rpc) 10 | 11 | ## Installation 12 | 13 | Add this to your `Cargo.toml`: 14 | 15 | ```toml 16 | [dependencies] 17 | discord-presence = "1.6" 18 | ``` 19 | 20 | or run: 21 | 22 | ```shell 23 | cargo add discord-presence 24 | ``` 25 | 26 | ## Example 27 | 28 | ```rust 29 | use std::{env, thread, time}; 30 | use discord_presence::{Client, Event}; 31 | 32 | fn main() { 33 | // Get our main status message 34 | let state_message = env::args().nth(1).expect("Requires at least one argument"); 35 | 36 | // Create the client 37 | let mut drpc = Client::new(1003450375732482138); 38 | 39 | // Register event handlers with the corresponding methods 40 | drpc.on_ready(|_ctx| { 41 | println!("ready?"); 42 | }); 43 | 44 | // or 45 | 46 | drpc.on_event(Event::Ready, |ctx| { 47 | println!("READY!"); 48 | }); 49 | 50 | // Start up the client connection, so that we can actually send and receive stuff 51 | drpc.start(); 52 | 53 | // Set the activity 54 | drpc.set_activity(|act| act.state(state_message)) 55 | .expect("Failed to set activity"); 56 | 57 | // Wait 10 seconds before exiting 58 | thread::sleep(time::Duration::from_secs(10)); 59 | } 60 | ``` 61 | 62 | > More examples can be found in the examples directory. 63 | 64 | ## Changelog 65 | 66 | See [CHANGELOG.md](CHANGELOG.md) 67 | 68 | ## Contributions 69 | 70 | See [CONTRIBUTING.md](/CONTRIBUTING.md) 71 | -------------------------------------------------------------------------------- /examples/5_seconds.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use discord_presence::{Client, Event}; 4 | 5 | mod helpers; 6 | 7 | fn main() { 8 | helpers::logging::init_logging(); 9 | 10 | let mut drpc = Client::new(1003450375732482138); 11 | 12 | drpc.on_ready(|_ctx| { 13 | println!("ready?"); 14 | }) 15 | .persist(); 16 | 17 | drpc.on_activity_join_request(|ctx| { 18 | println!("Join request: {:?}", ctx.event); 19 | }) 20 | .persist(); 21 | 22 | drpc.on_activity_join(|ctx| { 23 | println!("Joined: {:?}", ctx.event); 24 | }) 25 | .persist(); 26 | 27 | drpc.on_activity_spectate(|ctx| { 28 | println!("Spectate: {:?}", ctx.event); 29 | }) 30 | .persist(); 31 | 32 | drpc.start(); 33 | 34 | drpc.block_until_event(Event::Ready).unwrap(); 35 | 36 | assert!(Client::is_ready()); 37 | 38 | // Set the activity 39 | drpc.set_activity(|act| { 40 | act.state("rusting frfr") 41 | .append_buttons(|b| b.label("Go to Google...").url("https://google.com")) 42 | .append_buttons(|b| b.label("Go to DuckDuckGo...").url("https://duckduckgo.com")) 43 | }) 44 | .expect("Failed to set activity"); 45 | 46 | { 47 | let mut drpc = drpc.clone(); 48 | 49 | ctrlc::set_handler(move || { 50 | println!("Exiting..."); 51 | drpc.clear_activity().unwrap(); 52 | std::process::exit(0); 53 | }) 54 | .unwrap(); 55 | } 56 | 57 | thread::sleep(Duration::from_secs(5)); 58 | 59 | drpc.block_on().unwrap(); 60 | } 61 | -------------------------------------------------------------------------------- /examples/blocking.rs: -------------------------------------------------------------------------------- 1 | use discord_presence::{Client, Event}; 2 | 3 | mod helpers; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | helpers::logging::init_logging(); 7 | 8 | let mut drpc = Client::new(1003450375732482138); 9 | 10 | drpc.on_ready(|_ctx| { 11 | println!("ready?"); 12 | }) 13 | .persist(); 14 | 15 | drpc.on_connected(|_ctx| { 16 | println!("Connected!"); 17 | }) 18 | .persist(); 19 | 20 | drpc.on_disconnected(|_ctx| { 21 | println!("Disconnected..."); 22 | }) 23 | .persist(); 24 | 25 | drpc.on_activity_join_request(|ctx| { 26 | println!("Join request: {:?}", ctx.event); 27 | }) 28 | .persist(); 29 | 30 | drpc.on_activity_join(|ctx| { 31 | println!("Joined: {:?}", ctx.event); 32 | }) 33 | .persist(); 34 | 35 | drpc.on_activity_spectate(|ctx| { 36 | println!("Spectate: {:?}", ctx.event); 37 | }) 38 | .persist(); 39 | 40 | drpc.start(); 41 | 42 | drpc.block_until_event(Event::Ready)?; 43 | 44 | assert!(Client::is_ready()); 45 | 46 | // Set the activity 47 | drpc.set_activity(|act| { 48 | act.state("rusting frfr") 49 | .append_buttons(|button| button.label("Click Me!").url("https://google.com/")) 50 | })?; 51 | 52 | // TODO: Implement "remote" shutdown 53 | // ctrlc::set_handler(move || { 54 | // println!("Exiting..."); 55 | // drpc.clear_activity().unwrap(); 56 | // std::process::exit(0); 57 | // })?; 58 | 59 | drpc.block_on()?; 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /examples/blocking_shutdown.rs: -------------------------------------------------------------------------------- 1 | use discord_presence::{Client, Event}; 2 | 3 | mod helpers; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | helpers::logging::init_logging(); 7 | 8 | let mut drpc = Client::new(1003450375732482138); 9 | 10 | drpc.on_ready(|_ctx| { 11 | println!("ready?"); 12 | }) 13 | .persist(); 14 | 15 | drpc.start(); 16 | 17 | drpc.block_until_event(Event::Ready)?; 18 | 19 | assert!(Client::is_ready()); 20 | 21 | // Set the activity 22 | // drpc.set_activity(|act| { 23 | // act.state("rusting frfr") 24 | // .append_buttons(|button| button.label("Click Me!").url("https://google.com/")) 25 | // }) 26 | // .unwrap(); 27 | 28 | drpc.shutdown()?; 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /examples/discord_presence.rs: -------------------------------------------------------------------------------- 1 | use discord_presence::Client; 2 | 3 | mod helpers; 4 | 5 | fn main() { 6 | helpers::logging::init_logging(); 7 | 8 | let mut drpc = Client::new(1003450375732482138); 9 | 10 | drpc.on_ready(|_ctx| { 11 | println!("READY!"); 12 | }) 13 | .persist(); 14 | 15 | drpc.on_error(|ctx| { 16 | eprintln!("An error occured, {:?}", ctx.event); 17 | }) 18 | .persist(); 19 | 20 | drpc.start(); 21 | 22 | loop { 23 | let mut buf = String::new(); 24 | 25 | std::io::stdin().read_line(&mut buf).unwrap(); 26 | buf.pop(); 27 | 28 | if buf.is_empty() { 29 | if let Err(why) = drpc.clear_activity() { 30 | println!("Failed to clear presence: {}", why); 31 | } 32 | } else if let Err(why) = drpc.set_activity(|a| { 33 | a.state("Running examples") 34 | .assets(|ass| { 35 | ass.large_image("ferris_wat") 36 | .large_text("wat.") 37 | .small_image("rusting") 38 | .small_text("rusting...") 39 | }) 40 | .append_buttons(|button| button.label("Click Me!").url("https://google.com/")) 41 | }) { 42 | println!("Failed to set presence: {}", why); 43 | } 44 | } 45 | // drpc.block_on().unwrap(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/discord_presence_subscriber.rs: -------------------------------------------------------------------------------- 1 | use discord_presence::Client; 2 | 3 | mod helpers; 4 | 5 | fn main() { 6 | helpers::logging::init_logging(); 7 | 8 | let mut drpc = Client::new(1003450375732482138); 9 | 10 | let _ready = drpc.on_ready(|_ctx| { 11 | println!("ready?"); 12 | }); 13 | 14 | let _activity_join_request = drpc.on_activity_join_request(|ctx| { 15 | println!("Join request: {:?}", ctx.event); 16 | }); 17 | 18 | let _activity_join = drpc.on_activity_join(|ctx| { 19 | println!("Joined: {:?}", ctx.event); 20 | }); 21 | 22 | let _activity_spectate = drpc.on_activity_spectate(|ctx| { 23 | println!("Spectate: {:?}", ctx.event); 24 | }); 25 | 26 | drpc.start(); 27 | 28 | if let Err(why) = drpc.set_activity(|a| { 29 | a.state("Running examples").assets(|ass| { 30 | ass.large_image("ferris_wat") 31 | .large_text("wat.") 32 | .small_image("rusting") 33 | .small_text("rusting...") 34 | }) 35 | }) { 36 | println!("Failed to set presence: {}", why); 37 | } 38 | 39 | drpc.block_on().unwrap(); 40 | } 41 | -------------------------------------------------------------------------------- /examples/event_data.rs: -------------------------------------------------------------------------------- 1 | use discord_presence::{models::EventData, Client, Event}; 2 | 3 | mod helpers; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | helpers::logging::init_logging(); 7 | 8 | let mut drpc = Client::new(1003450375732482138); 9 | 10 | drpc.on_ready(|ctx| { 11 | let EventData::Ready(data) = ctx.event else { 12 | unreachable!() 13 | }; 14 | 15 | let _user = data.user; 16 | }) 17 | .persist(); 18 | 19 | drpc.start(); 20 | 21 | drpc.block_until_event(Event::Ready)?; 22 | 23 | assert!(Client::is_ready()); 24 | 25 | // Set the activity 26 | drpc.set_activity(|act| { 27 | act.state("rusting frfr") 28 | .append_buttons(|button| button.label("Click Me!").url("https://google.com/")) 29 | }) 30 | .unwrap(); 31 | 32 | // TODO: Implement "remote" shutdown 33 | // ctrlc::set_handler(move || { 34 | // println!("Exiting..."); 35 | // drpc.clear_activity().unwrap(); 36 | // std::process::exit(0); 37 | // })?; 38 | 39 | drpc.block_on()?; 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /examples/helpers/logging.rs: -------------------------------------------------------------------------------- 1 | pub fn init_logging() { 2 | tracing_subscriber::fmt() 3 | .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE) 4 | .init(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logging; 2 | -------------------------------------------------------------------------------- /examples/large_response.rs: -------------------------------------------------------------------------------- 1 | use discord_presence::Event; 2 | 3 | mod helpers; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | helpers::logging::init_logging(); 7 | 8 | let url_base = "https://example.com/".to_string(); 9 | let mut client = discord_presence::Client::new(1286481105410588672); 10 | client.start(); 11 | client.block_until_event(Event::Ready)?; 12 | client.set_activity(|activity| { 13 | const ACTIVITY_TEXT_LIMIT: usize = 128; 14 | const ASSET_URL_LIMIT: usize = 256; 15 | 16 | activity 17 | .state("A".repeat(128)) 18 | .details("A".repeat(128)) 19 | .assets(|assets| { 20 | assets 21 | .large_text("A".repeat(ACTIVITY_TEXT_LIMIT)) 22 | .large_image("A".repeat(ASSET_URL_LIMIT)) 23 | .small_text("A".repeat(ACTIVITY_TEXT_LIMIT)) 24 | .small_image("A".repeat(ASSET_URL_LIMIT)) 25 | }) 26 | .append_buttons(|buttons| { 27 | buttons 28 | .url(url_base.clone() + &"A".repeat(ASSET_URL_LIMIT - url_base.len())) 29 | .label("A".repeat(32)) 30 | }) 31 | })?; 32 | 33 | client.block_on()?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/unblocking.rs: -------------------------------------------------------------------------------- 1 | use std::{mem::forget, thread::sleep, time::Duration}; 2 | 3 | use discord_presence::Client; 4 | 5 | mod helpers; 6 | 7 | fn main() { 8 | helpers::logging::init_logging(); 9 | 10 | let mut client = Client::new(1003450375732482138); 11 | 12 | client.start(); 13 | 14 | log::error!("Due to the way unblocking activity setting works, this example does not seem to work currently (at least on Windows)."); 15 | { 16 | let ready = client.on_ready({ 17 | let client = client.clone(); 18 | move |_ctx| { 19 | let mut client = client.clone(); 20 | println!("READY!"); 21 | 22 | client 23 | .set_activity(|a| { 24 | a.state("Rust") 25 | .details("Programming") 26 | .assets(|a| a.large_image("rust")) 27 | }) 28 | .unwrap(); 29 | } 30 | }); 31 | 32 | // we can `std::mem::forget` the event listener's handle to keep it 33 | // registered until `drpc` is dropped 34 | forget(ready); 35 | } 36 | 37 | // an alternative is to store the handle until you're ready to unregister the 38 | // listener 39 | let _error = client.on_error(|ctx| { 40 | eprintln!("An error occured, {:?}", ctx.event); 41 | }); 42 | 43 | log::trace!("Made it to the final line"); 44 | 45 | // keep the main thread alive 46 | loop { 47 | sleep(Duration::from_secs(100)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{atomic::Ordering, Arc}, 3 | thread::{JoinHandle, Thread}, 4 | time::Duration, 5 | }; 6 | 7 | use crate::{ 8 | connection::Manager as ConnectionManager, 9 | event_handler::{Context as EventContext, EventCallbackHandle, HandlerRegistry}, 10 | models::{ 11 | commands::{Subscription, SubscriptionArgs}, 12 | message::Message, 13 | payload::Payload, 14 | rich_presence::{ 15 | Activity, CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs, 16 | }, 17 | Command, Event, OpCode, 18 | }, 19 | DiscordError, Result, 20 | }; 21 | use crossbeam_channel::Sender; 22 | use serde::{de::DeserializeOwned, Serialize}; 23 | use serde_json::Value; 24 | 25 | macro_rules! event_handler_function { 26 | ( $( $name:ident, $event:expr ),* ) => { 27 | event_handler_function!{@gen $([ $name, $event])*} 28 | }; 29 | 30 | (@gen $( [ $name:ident, $event:expr ] ), *) => { 31 | $( 32 | #[doc = concat!("Listens for the `", stringify!($event), "` event")] 33 | pub fn $name(&self, handler: F) -> EventCallbackHandle 34 | where F: Fn(EventContext) + 'static + Send + Sync 35 | { 36 | self.on_event($event, handler) 37 | } 38 | )* 39 | } 40 | } 41 | 42 | /// Wrapper around the [`JoinHandle`] returned by [`Client::start`] 43 | #[allow(clippy::module_name_repetitions)] 44 | pub struct ClientThread(JoinHandle<()>, Sender<()>); 45 | 46 | impl ClientThread { 47 | // Ignore missing error docs because it's an alias of `join` 48 | #[allow(clippy::missing_errors_doc)] 49 | /// Alias of [`JoinHandle::join()`] 50 | pub fn join(self) -> std::thread::Result<()> { 51 | self.0.join() 52 | } 53 | 54 | // Ignore missing error docs because it's an alias of `is_finished` 55 | #[allow(clippy::missing_errors_doc)] 56 | #[must_use] 57 | /// Alias of [`JoinHandle::is_finished`] 58 | pub fn is_finished(&self) -> bool { 59 | self.0.is_finished() 60 | } 61 | // Ignore missing error docs because it's an alias of `thread` 62 | #[allow(clippy::missing_errors_doc)] 63 | #[must_use] 64 | /// Alias of [`JoinHandle::thread`] 65 | pub fn thread(&self) -> &Thread { 66 | self.0.thread() 67 | } 68 | 69 | /// Attempt to stop the client's send and receive loop 70 | /// 71 | /// # Errors 72 | /// - Failed to send stop message (maybe it has already stopped?) 73 | /// - The event loop had its own error 74 | pub fn stop(self) -> Result<()> { 75 | // Attempt to send the message to stop the thread 76 | self.1.send(())?; 77 | 78 | self.join().map_err(|_| DiscordError::EventLoopError)?; 79 | 80 | Ok(()) 81 | } 82 | 83 | /// "Forgets" client thread, removing the variable, but keeping the client running indefinitely. 84 | pub fn persist(self) { 85 | std::mem::forget(self); 86 | } 87 | } 88 | 89 | #[derive(Clone)] 90 | /// The Discord client 91 | pub struct Client { 92 | connection_manager: ConnectionManager, 93 | event_handler_registry: Arc, 94 | thread: Option>, 95 | } 96 | 97 | impl Client { 98 | /// Creates a new `Client` with default error sleep duration of 5 seconds, and no limit on connection attempts 99 | #[must_use] 100 | pub fn new(client_id: u64) -> Self { 101 | Self::with_error_config(client_id, Duration::from_secs(5), None) 102 | } 103 | 104 | /// Creates a new `Client` with a custom error sleep duration, and number of attempts 105 | #[must_use] 106 | pub fn with_error_config( 107 | client_id: u64, 108 | sleep_duration: Duration, 109 | attempts: Option, 110 | ) -> Self { 111 | let event_handler_registry = Arc::new(HandlerRegistry::new()); 112 | let connection_manager = ConnectionManager::new( 113 | client_id, 114 | event_handler_registry.clone(), 115 | sleep_duration, 116 | attempts, 117 | ); 118 | 119 | Self { 120 | connection_manager, 121 | event_handler_registry, 122 | thread: None, 123 | } 124 | } 125 | 126 | // TODO: Add examples 127 | /// Start the connection manager 128 | /// 129 | /// Only join the thread if there is no other task keeping the program alive. 130 | /// 131 | /// This must be called before all and any actions such as `set_activity` 132 | pub fn start(&mut self) { 133 | // Shutdown notify channel 134 | let (tx, rx) = crossbeam_channel::bounded::<()>(1); 135 | 136 | let thread = self.connection_manager.start(rx); 137 | 138 | self.thread = Some(Arc::new(ClientThread(thread, tx))); 139 | } 140 | 141 | /// Shutdown the client and its thread 142 | /// 143 | /// # Errors 144 | /// - The internal connection thread ran into an error 145 | /// - The client was not started, or has already been shutdown 146 | pub fn shutdown(self) -> Result<()> { 147 | if let Some(thread) = self.thread.as_ref() { 148 | thread.1.send(())?; 149 | 150 | crate::READY.store(false, Ordering::Relaxed); 151 | 152 | self.block_on() 153 | } else { 154 | Err(DiscordError::NotStarted) 155 | } 156 | } 157 | 158 | /// Block indefinitely until the client shuts down 159 | /// 160 | /// This is nearly the same as [`Client::shutdown()`], 161 | /// except that it does not attempt to stop the internal thread, 162 | /// and rather waits for it to finish, which could never happen. 163 | /// 164 | /// # Errors 165 | /// - The internal connection thread ran into an error 166 | /// - The client was not started, or has already been shutdown 167 | pub fn block_on(mut self) -> Result<()> { 168 | let thread = self.unwrap_thread()?; 169 | 170 | // If into_inner succeeds, await the thread completing. 171 | // Otherwise, the thread will be dropped and shut down anyway 172 | thread.join().map_err(|_| DiscordError::ThreadError)?; 173 | 174 | Ok(()) 175 | } 176 | 177 | fn unwrap_thread(&mut self) -> Result { 178 | if let Some(thread) = self.thread.take() { 179 | let thread = Arc::try_unwrap(thread).map_err(|_| DiscordError::ThreadInUse)?; 180 | 181 | Ok(thread) 182 | } else { 183 | Err(DiscordError::NotStarted) 184 | } 185 | } 186 | 187 | #[must_use] 188 | /// Check if the client is ready 189 | pub fn is_ready() -> bool { 190 | crate::READY.load(Ordering::Relaxed) 191 | } 192 | 193 | fn execute(&mut self, cmd: Command, args: A, evt: Option) -> Result> 194 | where 195 | A: Serialize + Send + Sync, 196 | E: Serialize + DeserializeOwned + Send + Sync, 197 | { 198 | if !crate::READY.load(Ordering::Relaxed) { 199 | return Err(DiscordError::NotStarted); 200 | } 201 | 202 | trace!("Executing command: {:?}", cmd); 203 | 204 | let message = Message::new( 205 | OpCode::Frame, 206 | Payload::with_nonce(cmd, Some(args), None, evt), 207 | ); 208 | self.connection_manager.send(message?)?; 209 | let Message { payload, .. } = self.connection_manager.recv()?; 210 | let response: Payload = serde_json::from_str(&payload)?; 211 | 212 | match response.evt { 213 | Some(Event::Error) => Err(DiscordError::SubscriptionFailed), 214 | _ => Ok(response), 215 | } 216 | } 217 | 218 | /// Set the users current activity 219 | /// 220 | /// # Errors 221 | /// - See [`DiscordError`] for more info 222 | pub fn set_activity(&mut self, f: F) -> Result> 223 | where 224 | F: FnOnce(Activity) -> Activity, 225 | { 226 | self.execute(Command::SetActivity, SetActivityArgs::new(f), None) 227 | } 228 | 229 | /// Clear the users current activity 230 | /// 231 | /// # Errors 232 | /// - See [`DiscordError`] for more info 233 | pub fn clear_activity(&mut self) -> Result> { 234 | self.execute(Command::SetActivity, SetActivityArgs::default(), None) 235 | } 236 | 237 | // NOTE: Not sure what the actual response values of 238 | // SEND_ACTIVITY_JOIN_INVITE and CLOSE_ACTIVITY_REQUEST are, 239 | // they are not documented. 240 | /// Send an invite to a user to join a game 241 | /// 242 | /// # Errors 243 | /// - See [`DiscordError`] for more info 244 | pub fn send_activity_join_invite(&mut self, user_id: u64) -> Result> { 245 | self.execute( 246 | Command::SendActivityJoinInvite, 247 | SendActivityJoinInviteArgs::new(user_id), 248 | None, 249 | ) 250 | } 251 | 252 | /// Close request to join a game 253 | /// 254 | /// # Errors 255 | /// - See [`DiscordError`] for more info 256 | pub fn close_activity_request(&mut self, user_id: u64) -> Result> { 257 | self.execute( 258 | Command::CloseActivityRequest, 259 | CloseActivityRequestArgs::new(user_id), 260 | None, 261 | ) 262 | } 263 | 264 | /// Subscribe to a given event 265 | /// 266 | /// # Errors 267 | /// - See [`DiscordError`] for more info 268 | pub fn subscribe(&mut self, evt: Event, f: F) -> Result> 269 | where 270 | F: FnOnce(SubscriptionArgs) -> SubscriptionArgs, 271 | { 272 | self.execute(Command::Subscribe, f(SubscriptionArgs::new()), Some(evt)) 273 | } 274 | 275 | /// Unsubscribe from a given event 276 | /// 277 | /// # Errors 278 | /// - See [`DiscordError`] for more info 279 | pub fn unsubscribe(&mut self, evt: Event, f: F) -> Result> 280 | where 281 | F: FnOnce(SubscriptionArgs) -> SubscriptionArgs, 282 | { 283 | self.execute(Command::Unsubscribe, f(SubscriptionArgs::new()), Some(evt)) 284 | } 285 | 286 | /// Listens for a given event, and returns a handle that unregisters the listener when it is dropped. 287 | /// 288 | /// # Examples 289 | /// 290 | /// ```no_run 291 | /// # use std::{thread::sleep, time::Duration}; 292 | /// # use discord_presence::Client; 293 | /// let mut drpc = Client::new(1003450375732482138); 294 | /// let _ready = drpc.on_ready(|_ctx| { 295 | /// println!("READY!"); 296 | /// }); 297 | /// 298 | /// drpc.start(); 299 | /// 300 | /// { 301 | /// let _ready_first_3_seconds = drpc.on_ready(|_ctx| { 302 | /// println!("READY, IN THE FIRST 3 SECONDS!"); 303 | /// }); 304 | /// sleep(Duration::from_secs(3)); 305 | /// } 306 | /// 307 | /// // You can also manually remove the handler 308 | /// 309 | /// let never_ready = drpc.on_ready(|_ctx| { 310 | /// println!("I will never be ready!"); 311 | /// }); 312 | /// never_ready.remove(); 313 | /// 314 | /// // Or via [`std::mem::drop`] 315 | /// let never_ready = drpc.on_ready(|_ctx| { 316 | /// println!("I will never be ready!"); 317 | /// }); 318 | /// drop(never_ready); 319 | /// 320 | /// drpc.block_on().unwrap(); 321 | /// ``` 322 | /// 323 | /// You can use `.persist` or [`std::mem::forget`] to disable the automatic unregister-on-drop: 324 | /// 325 | /// ```no_run 326 | /// # use discord_presence::Client; 327 | /// # let mut drpc = Client::new(1003450375732482138); 328 | /// 329 | /// { 330 | /// let ready = drpc.on_ready(|_ctx| { 331 | /// println!("READY!"); 332 | /// }).persist(); 333 | /// } 334 | /// // Or 335 | /// { 336 | /// let ready = drpc.on_ready(|_ctx| { 337 | /// println!("READY!"); 338 | /// }); 339 | /// std::mem::forget(ready); 340 | /// } 341 | /// // the event listener is still registered 342 | /// 343 | /// # drpc.start(); 344 | /// # drpc.block_on().unwrap(); 345 | /// ``` 346 | pub fn on_event(&self, event: Event, handler: F) -> EventCallbackHandle 347 | where 348 | F: Fn(EventContext) + 'static + Send + Sync, 349 | { 350 | self.event_handler_registry.register(event, handler) 351 | } 352 | 353 | /// Block the current thread until the event is fired 354 | /// 355 | /// Returns the context the event was fired in 356 | /// 357 | /// NOTE: Please only use this for the ready event, or if you know what you are doing. 358 | /// 359 | /// # Errors 360 | /// - Channel disconnected 361 | /// 362 | /// # Panics 363 | /// - Panics if the channel is disconnected for whatever reason. 364 | pub fn block_until_event(&mut self, event: Event) -> Result { 365 | // TODO: Use bounded channel 366 | let (tx, rx) = crossbeam_channel::unbounded::(); 367 | 368 | let handler = move |info| { 369 | // dbg!("Blocked until at ", std::time::SystemTime::now()); 370 | if let Err(e) = tx.send(info) { 371 | error!("{e}"); 372 | } 373 | }; 374 | 375 | // `handler` is automatically unregistered once this variable drops 376 | let cb_handle = self.on_event(event, handler); 377 | 378 | let response = rx.recv()?; 379 | 380 | drop(cb_handle); 381 | 382 | Ok(response) 383 | } 384 | 385 | event_handler_function!(on_ready, Event::Ready); 386 | 387 | event_handler_function!(on_error, Event::Error); 388 | 389 | event_handler_function!(on_activity_join, Event::ActivityJoin); 390 | 391 | event_handler_function!(on_activity_join_request, Event::ActivityJoinRequest); 392 | 393 | event_handler_function!(on_activity_spectate, Event::ActivitySpectate); 394 | 395 | event_handler_function!(on_connected, Event::Connected); 396 | 397 | event_handler_function!(on_disconnected, Event::Disconnected); 398 | } 399 | 400 | #[cfg(test)] 401 | mod tests { 402 | use super::*; 403 | 404 | #[test] 405 | fn test_is_ready() { 406 | assert!(!Client::is_ready()); 407 | 408 | crate::READY.store(true, Ordering::Relaxed); 409 | 410 | assert!(Client::is_ready()); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/connection/base.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{DiscordError, Result}, 3 | models::message::{FrameHeader, Message, OpCode, MAX_RPC_FRAME_SIZE}, 4 | utils, 5 | }; 6 | use bytes::BytesMut; 7 | use quork::prelude::ListVariants; 8 | use serde_json::json; 9 | use std::{ 10 | io::{Read, Write}, 11 | marker::Sized, 12 | path::{Path, PathBuf}, 13 | thread, 14 | time::{self, Duration}, 15 | }; 16 | 17 | /// Wait for a non-blocking connection until it's complete. 18 | fn try_until_done(result: Result) -> Result { 19 | loop { 20 | match result { 21 | Ok(v) => return Ok(v), 22 | Err(why) if !why.io_would_block() => return Err(why), 23 | _ => {} 24 | } 25 | 26 | thread::sleep(time::Duration::from_micros(500)); 27 | } 28 | } 29 | 30 | #[derive(Debug, Copy, Clone, ListVariants)] 31 | enum SocketLocation { 32 | Root, 33 | Flatpak, 34 | Snap, 35 | SnapCanary, 36 | } 37 | 38 | impl SocketLocation { 39 | pub fn append_path(self, ipc_path: impl AsRef) -> PathBuf { 40 | match self { 41 | SocketLocation::Root => ipc_path.as_ref().to_owned(), 42 | SocketLocation::Flatpak => ipc_path.as_ref().join("app").join("com.discordapp.Discord"), 43 | SocketLocation::Snap => ipc_path.as_ref().join("snap.discord"), 44 | SocketLocation::SnapCanary => ipc_path.as_ref().join("snap.discord-canary"), 45 | } 46 | } 47 | 48 | pub fn test_paths( 49 | ipc_path: impl AsRef, 50 | socket_path: impl AsRef, 51 | ) -> Option { 52 | if cfg!(windows) { 53 | let path = Self::Root.append_path(ipc_path).join(socket_path); 54 | return path.exists().then_some(path); 55 | } else { 56 | for location in Self::VARIANTS { 57 | let path = location 58 | .append_path(ipc_path.as_ref()) 59 | .join(socket_path.as_ref()); 60 | 61 | if path.exists() { 62 | return Some(path); 63 | } 64 | } 65 | } 66 | 67 | None 68 | } 69 | } 70 | 71 | pub trait Connection: Sized { 72 | type Socket: Write + Read; 73 | 74 | /// Time for socket read/write operations 75 | /// 1 second higher than Discord's rate limit timeout of 15 seconds 76 | const READ_WRITE_TIMEOUT: Duration = Duration::from_secs(16); 77 | 78 | /// The internally stored socket connection. 79 | fn socket(&mut self) -> &mut Self::Socket; 80 | 81 | /// The base path were the socket is located. 82 | fn ipc_path() -> PathBuf; 83 | 84 | /// Establish a new connection to the server. 85 | fn connect() -> Result; 86 | 87 | /// The full socket path. 88 | fn socket_path(n: u8) -> PathBuf { 89 | let socket_path = format!("discord-ipc-{n}"); 90 | let ipc_path = Self::ipc_path(); 91 | 92 | SocketLocation::test_paths(&ipc_path, &socket_path).unwrap_or(ipc_path.join(socket_path)) 93 | } 94 | 95 | /// Perform a handshake on this socket connection. 96 | /// Will block until complete. 97 | fn handshake(&mut self, client_id: u64) -> Result { 98 | let hs = json![{ 99 | "client_id": client_id.to_string(), 100 | "v": 1, 101 | "nonce": utils::nonce() 102 | }]; 103 | 104 | let msg = Message::new(OpCode::Handshake, hs)?; 105 | try_until_done(self.send(&msg))?; 106 | let msg = try_until_done(self.recv())?; 107 | 108 | Ok(msg) 109 | } 110 | 111 | /// Ping the server and get a pong response. 112 | /// Will block until complete. 113 | fn ping(&mut self) -> Result { 114 | let message = Message::new(OpCode::Ping, json![{}])?; 115 | try_until_done(self.send(&message))?; 116 | let response = try_until_done(self.recv())?; 117 | Ok(response.opcode) 118 | } 119 | 120 | /// Send a message to the server. 121 | fn send(&mut self, message: &Message) -> Result<()> { 122 | match message.encode() { 123 | Err(why) => error!("{:?}", why), 124 | Ok(bytes) => { 125 | assert!(bytes.len() <= MAX_RPC_FRAME_SIZE); 126 | self.socket().write_all(&bytes)?; 127 | } 128 | }; 129 | trace!("-> {:?}", message); 130 | Ok(()) 131 | } 132 | 133 | /// Receive a message from the server. 134 | fn recv(&mut self) -> Result { 135 | // Read header 136 | let mut buf = BytesMut::new(); 137 | buf.resize(std::mem::size_of::(), 0); 138 | 139 | trace!("Reading header"); 140 | let n = self.try_read(&mut buf)?; 141 | trace!("Received {} bytes for header", n); 142 | 143 | if n == 0 { 144 | return Err(DiscordError::ConnectionClosed); 145 | } 146 | 147 | if n != std::mem::size_of::() { 148 | return Err(DiscordError::HeaderLength); 149 | } 150 | 151 | // SAFETY: the length of buf is already checked that the header is the correct size 152 | let header = unsafe { FrameHeader::from_bytes(buf.as_ref()).unwrap_unchecked() }; 153 | 154 | let mut message_buf = BytesMut::new(); 155 | message_buf.resize(header.message_length(), 0); 156 | 157 | trace!("Reading payload"); 158 | let n = self.try_read(&mut message_buf)?; 159 | trace!("Received {} bytes for payload", n); 160 | 161 | if n == 0 { 162 | return Err(DiscordError::NoMessage); 163 | } 164 | 165 | let mut payload = String::with_capacity(header.message_length()); 166 | message_buf.as_ref().read_to_string(&mut payload)?; 167 | trace!("<- {:?} = {:?}", header.opcode(), payload); 168 | 169 | Ok(Message { 170 | opcode: header.opcode(), 171 | payload, 172 | }) 173 | } 174 | 175 | fn try_read(&mut self, buf: &mut [u8]) -> Result { 176 | Ok(self.socket().read(buf)?) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/connection/manager.rs: -------------------------------------------------------------------------------- 1 | use super::{Connection, Socket}; 2 | use crate::models::EventData; 3 | use crate::{ 4 | error::{DiscordError, Result}, 5 | event_handler::HandlerRegistry, 6 | models::{payload::Payload, ErrorEvent, Event, Message}, 7 | }; 8 | use crossbeam_channel::{unbounded, Receiver, Sender}; 9 | use parking_lot::Mutex; 10 | use serde_json::Value as JsonValue; 11 | use std::{ 12 | io::ErrorKind, 13 | sync::{atomic::Ordering, Arc}, 14 | thread, 15 | time::{self, Duration}, 16 | }; 17 | 18 | type Tx = Sender; 19 | type Rx = Receiver; 20 | 21 | // TODO: Refactor connection manager 22 | #[derive(Clone)] 23 | pub struct Manager { 24 | connection: Arc>>, 25 | client_id: u64, 26 | outbound: (Rx, Tx), 27 | inbound: (Rx, Tx), 28 | handshake_completed: bool, 29 | event_handler_registry: Arc, 30 | error_sleep: Duration, 31 | connection_attempts: Arc>>, 32 | } 33 | 34 | impl Manager { 35 | pub(crate) fn new( 36 | client_id: u64, 37 | event_handler_registry: Arc, 38 | error_sleep: Duration, 39 | connection_attempts: Option, 40 | ) -> Self { 41 | let connection = Arc::new(None); 42 | let (sender_o, receiver_o) = unbounded(); 43 | let (sender_i, receiver_i) = unbounded(); 44 | 45 | Self { 46 | connection, 47 | client_id, 48 | handshake_completed: false, 49 | inbound: (receiver_i, sender_i), 50 | outbound: (receiver_o, sender_o), 51 | event_handler_registry, 52 | error_sleep, 53 | connection_attempts: Arc::new(Mutex::new(connection_attempts)), 54 | } 55 | } 56 | 57 | pub fn start(&mut self, rx: Receiver<()>) -> std::thread::JoinHandle<()> { 58 | let mut manager_inner = self.clone(); 59 | let error_sleep = self.error_sleep; 60 | let connection_attempts = self.connection_attempts.clone(); 61 | thread::spawn(move || { 62 | // TODO: Refactor so that JSON values are consistent across errors 63 | send_and_receive_loop(&mut manager_inner, &rx, error_sleep, &connection_attempts); 64 | }) 65 | } 66 | 67 | pub fn send(&self, message: Message) -> Result<()> { 68 | self.outbound.1.send(message)?; 69 | 70 | Ok(()) 71 | } 72 | 73 | pub fn recv(&self) -> Result { 74 | self.inbound.0.recv().map_err(DiscordError::from) 75 | } 76 | 77 | fn connect(&mut self) -> Result<()> { 78 | if self.connection.is_some() { 79 | return Ok(()); 80 | } 81 | 82 | trace!("Connecting"); 83 | 84 | let mut new_connection = Socket::connect()?; 85 | 86 | trace!("Performing handshake"); 87 | let msg = new_connection.handshake(self.client_id)?; 88 | let payload: Payload = serde_json::from_str(&msg.payload)?; 89 | 90 | // TODO: Ensure it works without clone 91 | // Only handle the ready event if the client was not already ready 92 | if !crate::READY.load(std::sync::atomic::Ordering::Relaxed) { 93 | trace!("Discord client is ready!"); 94 | crate::READY.store(true, Ordering::Relaxed); 95 | 96 | self.event_handler_registry.handle( 97 | Event::Ready, 98 | Event::Ready.parse_data(into_error!(payload.data)?), 99 | ); 100 | } 101 | 102 | self.event_handler_registry 103 | .handle(Event::Connected, EventData::None); 104 | 105 | trace!("Handshake completed"); 106 | 107 | self.connection = Arc::new(Some(Mutex::new(new_connection))); 108 | 109 | trace!("Connected"); 110 | 111 | Ok(()) 112 | } 113 | 114 | fn disconnect(&mut self) { 115 | self.handshake_completed = false; 116 | self.connection = Arc::new(None); 117 | } 118 | } 119 | 120 | fn send_and_receive_loop( 121 | manager: &mut Manager, 122 | rx: &Receiver<()>, 123 | err_sleep: Duration, 124 | connection_attempts: &Arc>>, 125 | ) { 126 | trace!("Starting sender loop"); 127 | 128 | let mut inbound = manager.inbound.1.clone(); 129 | let outbound = manager.outbound.0.clone(); 130 | 131 | loop { 132 | if rx.try_recv().is_ok() { 133 | break; 134 | } 135 | 136 | let connection = manager.connection.clone(); 137 | 138 | match *connection { 139 | Some(ref conn) => { 140 | match send_and_receive( 141 | &mut conn.lock(), 142 | &manager.event_handler_registry, 143 | &mut inbound, 144 | &outbound, 145 | ) { 146 | Err(DiscordError::IoError(ref err)) if err.kind() == ErrorKind::WouldBlock => {} 147 | Err(DiscordError::IoError(_) | DiscordError::ConnectionClosed) => { 148 | manager.disconnect(); 149 | manager 150 | .event_handler_registry 151 | .handle(Event::Disconnected, EventData::None); 152 | } 153 | Err(DiscordError::TimeoutError(_)) => continue, 154 | Err(why) => trace!("discord error: {}", why), 155 | _ => {} 156 | } 157 | 158 | trace!("Finished send and receive loop iteration"); 159 | 160 | thread::sleep(time::Duration::from_millis(500)); 161 | } 162 | None => match manager.connect() { 163 | Err(err) => { 164 | manager.event_handler_registry.handle( 165 | Event::Error, 166 | crate::models::EventData::Error(ErrorEvent { 167 | code: None, 168 | message: Some(err.to_string()), 169 | }), 170 | ); 171 | 172 | if err.should_break() { 173 | break; 174 | } 175 | error!("Failed to connect: {:?}", err); 176 | 177 | let mut attempts = connection_attempts.lock(); 178 | if let Some(ref mut attempts) = *attempts { 179 | if *attempts == 0 { 180 | break; 181 | } 182 | 183 | *attempts -= 1; 184 | } 185 | 186 | thread::sleep(err_sleep); 187 | } 188 | _ => manager.handshake_completed = true, 189 | }, 190 | } 191 | } 192 | } 193 | 194 | fn send_and_receive( 195 | connection: &mut Socket, 196 | event_handler_registry: &Arc, 197 | inbound: &mut Tx, 198 | outbound: &Rx, 199 | ) -> Result<()> { 200 | while let Ok(msg) = outbound.try_recv() { 201 | trace!("Sending message"); 202 | connection.send(&msg)?; 203 | trace!("Sent message"); 204 | } 205 | 206 | trace!("Receiving from connection"); 207 | let msg = connection.recv()?; 208 | trace!("Received from connection"); 209 | 210 | let payload: Payload = serde_json::from_str(&msg.payload)?; 211 | 212 | trace!("Received payload"); 213 | 214 | if let Payload { 215 | evt: Some(event), .. 216 | } = &payload 217 | { 218 | trace!("Got event"); 219 | let event_data = event.parse_data(into_error!(payload.data.clone())?); 220 | event_handler_registry.handle(*event, event_data); 221 | } else { 222 | trace!("Got message"); 223 | inbound.send(msg)?; 224 | } 225 | 226 | Ok(()) 227 | } 228 | -------------------------------------------------------------------------------- /src/connection/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | mod manager; 3 | 4 | pub use base::Connection; 5 | pub use manager::Manager; 6 | 7 | cfg_if::cfg_if! { 8 | if #[cfg(unix)] { 9 | mod unix; 10 | pub use unix::Socket; 11 | } else if #[cfg(windows)] { 12 | mod windows; 13 | pub use windows::Socket; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/connection/unix.rs: -------------------------------------------------------------------------------- 1 | use super::base::Connection; 2 | use crate::Result; 3 | use std::{env, net::Shutdown, os::unix::net::UnixStream, path::PathBuf}; 4 | 5 | pub struct Socket { 6 | socket: UnixStream, 7 | } 8 | 9 | impl Connection for Socket { 10 | type Socket = UnixStream; 11 | 12 | fn connect() -> Result { 13 | let connection_name = Self::socket_path(0); 14 | let socket = UnixStream::connect(connection_name)?; 15 | socket.set_nonblocking(true)?; 16 | socket.set_read_timeout(Some(Self::READ_WRITE_TIMEOUT))?; 17 | socket.set_write_timeout(Some(Self::READ_WRITE_TIMEOUT))?; 18 | Ok(Self { socket }) 19 | } 20 | 21 | fn ipc_path() -> PathBuf { 22 | let tmp = env::var("XDG_RUNTIME_DIR") 23 | .or_else(|_| env::var("TMPDIR")) 24 | .or_else(|_| match env::temp_dir().to_str() { 25 | None => Err("Failed to convert temp_dir"), 26 | Some(tmp) => Ok(tmp.to_owned()), 27 | }) 28 | .unwrap_or_else(|_| "/tmp".to_owned()); 29 | PathBuf::from(tmp) 30 | } 31 | 32 | fn socket(&mut self) -> &mut Self::Socket { 33 | &mut self.socket 34 | } 35 | } 36 | 37 | impl Drop for Socket { 38 | fn drop(&mut self) { 39 | if self.socket.shutdown(Shutdown::Both).is_err() { 40 | error!("Failed to properly shut down socket"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/connection/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{File, OpenOptions}, 3 | io::{ErrorKind, Read}, 4 | os::windows::fs::OpenOptionsExt, 5 | path::PathBuf, 6 | }; 7 | 8 | use super::base::Connection; 9 | use crate::{DiscordError, Result}; 10 | 11 | pub struct Socket { 12 | socket: File, 13 | } 14 | 15 | impl Connection for Socket { 16 | type Socket = File; 17 | 18 | fn connect() -> Result { 19 | let path = Self::socket_path(0); 20 | 21 | let socket = OpenOptions::new().access_mode(0x3).open(&path)?; 22 | 23 | Ok(Self { socket }) 24 | } 25 | 26 | fn ipc_path() -> PathBuf { 27 | PathBuf::from(r"\\.\pipe\") 28 | } 29 | 30 | fn socket(&mut self) -> &mut Self::Socket { 31 | &mut self.socket 32 | } 33 | 34 | fn try_read(&mut self, buf: &mut [u8]) -> Result { 35 | if self.socket().metadata()?.len() == 0 { 36 | return Err(DiscordError::IoError(std::io::Error::new( 37 | ErrorKind::WouldBlock, 38 | "No data available", 39 | ))); 40 | } 41 | 42 | Ok(self.socket().read(buf)?) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel::{RecvTimeoutError, SendError}; 2 | use serde_json::Error as JsonError; 3 | use std::{ 4 | io::Error as IoError, 5 | result::Result as StdResult, 6 | sync::mpsc::{RecvError as ChannelRecv, RecvTimeoutError as ChannelTimeout}, 7 | }; 8 | 9 | use crate::models::Message; 10 | 11 | /// Error types from Discord 12 | #[derive(Debug, thiserror::Error)] 13 | #[allow(clippy::module_name_repetitions)] 14 | pub enum DiscordError { 15 | #[error("Io Error")] 16 | /// Io Error 17 | IoError(#[from] IoError), 18 | #[error("Could not send message: {0}")] 19 | /// tx.send returned error 20 | SendMessage(#[from] SendError), 21 | #[error("Could not close event loop: {0}")] 22 | /// tx.send returned error 23 | CloseError(#[from] SendError<()>), 24 | #[error("Error Receiving message")] 25 | /// Error Receiving message 26 | ReceiveError(#[from] crossbeam_channel::RecvError), 27 | #[error("Error Receiving message")] 28 | /// Error Receiving message 29 | MPSCReceiveError(#[from] ChannelRecv), 30 | #[error("Error on Channel Timeout")] 31 | /// Timeout Error 32 | MPSCTimeout(#[from] ChannelTimeout), 33 | #[error("Receiving timed out")] 34 | /// Receiving timed out 35 | TimeoutError(#[from] RecvTimeoutError), 36 | #[error("Error parsing Json")] 37 | /// Json Error 38 | JsonError(#[from] JsonError), 39 | #[error("A thread ran into an error. See logs for more info.")] 40 | /// A thread ran into an error 41 | ThreadError, 42 | #[error("{0}")] 43 | /// Option unwrapped to None 44 | NoneError(String), 45 | #[error("Error converting values")] 46 | /// Conversion Error 47 | Conversion, 48 | #[error("Error decoding response. Incorrect header length")] 49 | /// Header Length Error 50 | HeaderLength, 51 | #[error("Header was received, but no message was received")] 52 | /// Header was received, but no message was received 53 | NoMessage, 54 | #[error("Error subscribing to an event")] 55 | /// Subscription Joining Error 56 | SubscriptionFailed, 57 | #[error("Connection was closed prematurely")] 58 | /// Connection Closing error 59 | ConnectionClosed, 60 | #[error("Connection has not been started")] 61 | /// Connection has not been started 62 | NotStarted, 63 | #[error("Event loop ran into an unknown error")] 64 | /// The send & receive loop ran into an error 65 | EventLoopError, 66 | /// No changes were made to the event handler 67 | #[error("No changes were made to the event handler. This can usually be ignored")] 68 | NoChangesMade, 69 | #[error("Could not safely shut down client. Thread is in use.")] 70 | /// RPC thread is in use 71 | ThreadInUse, 72 | } 73 | 74 | impl DiscordError { 75 | #[must_use] 76 | /// Tell whether an [`IoError`] would block the connection 77 | pub fn io_would_block(&self) -> bool { 78 | match self { 79 | Self::IoError(ref err) => err.kind() == std::io::ErrorKind::WouldBlock, 80 | _ => false, 81 | } 82 | } 83 | 84 | #[must_use] 85 | /// Checks if the error should break the connection 86 | pub fn should_break(&self) -> bool { 87 | match self { 88 | Self::IoError(ref err) => err.kind() == std::io::ErrorKind::ConnectionRefused, 89 | _ => false, 90 | } 91 | } 92 | } 93 | 94 | /// Result type for Discord RPC error types 95 | pub type Result = StdResult; 96 | -------------------------------------------------------------------------------- /src/event_handler.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | use std::{sync::Weak, thread}; 3 | 4 | use parking_lot::RwLock; 5 | 6 | use crate::models::{Event, EventData}; 7 | 8 | /// Event handler callback 9 | pub type Handler = dyn Fn(Context) + 'static + Send + Sync; 10 | 11 | type HandlerList = Vec>; 12 | 13 | #[derive(Debug, Clone)] 14 | /// Event context 15 | pub struct Context { 16 | /// Event data 17 | pub event: EventData, 18 | } 19 | 20 | impl Context { 21 | pub(crate) fn new(event: EventData) -> Self { 22 | Self { event } 23 | } 24 | } 25 | 26 | type Handlers = RwLock>; 27 | 28 | #[must_use = "event listeners will be immediately dropped if the handle is not kept. Use `.persist` to stop them from being removed."] 29 | /// Handle to an event listener 30 | pub struct EventCallbackHandle { 31 | event: Event, 32 | registry: Weak, 33 | handler: Weak, 34 | } 35 | 36 | impl EventCallbackHandle { 37 | /// Immediately drops the event handler, thus removing the handler from the registry. 38 | pub fn remove(self) { 39 | drop(self); 40 | } 41 | 42 | /// "Forgets" event handler, removing the variable, but keeping the handler in the registry until the registry itself is dropped. 43 | pub fn persist(self) { 44 | std::mem::forget(self); 45 | } 46 | } 47 | 48 | impl Drop for EventCallbackHandle { 49 | fn drop(&mut self) { 50 | // if the registry or this event handler has already been dropped, there's no reason to try and do it again 51 | if let (Some(registry), Some(handler)) = (self.registry.upgrade(), self.handler.upgrade()) { 52 | let handler = registry.remove(self.event, &handler); 53 | if handler.is_err() { 54 | error!("Failed to remove event handler. This can usually be ignored."); 55 | } 56 | } 57 | } 58 | } 59 | 60 | pub(crate) struct HandlerRegistry { 61 | handlers: Handlers, 62 | } 63 | 64 | impl HandlerRegistry { 65 | pub fn new() -> Self { 66 | Self { 67 | handlers: RwLock::new(HashMap::new()), 68 | } 69 | } 70 | 71 | pub fn register(self: &Arc, event: Event, handler: F) -> EventCallbackHandle 72 | where 73 | F: Fn(Context) + Send + Sync + 'static, 74 | { 75 | let handler: Arc = Arc::new(handler); 76 | let callback_handle = EventCallbackHandle { 77 | event, 78 | registry: Arc::downgrade(self), 79 | handler: Arc::downgrade(&handler), 80 | }; 81 | 82 | let mut event_handlers = self.handlers.write(); 83 | let event_handler = event_handlers.entry(event).or_default(); 84 | event_handler.push(handler); 85 | 86 | callback_handle 87 | } 88 | 89 | // TODO: Replace data type with stronger types 90 | pub fn handle(&self, event: Event, data: EventData) { 91 | let handlers = self.handlers.read(); 92 | if let Some(handlers) = handlers.get(&event) { 93 | let context = Context::new(data); 94 | 95 | for handler in handlers { 96 | let handler = handler.clone(); 97 | let context = context.clone(); 98 | thread::spawn(move || { 99 | handler(context); 100 | }); 101 | } 102 | } 103 | } 104 | 105 | /// Removes a handler from the registry, if it exists 106 | /// 107 | /// # Errors 108 | /// - Returns an error if no changes were made to the registry. This generally means that the handler has already been removed, and can thus generally be ignored. 109 | // TODO: Change return type to Result 110 | pub fn remove( 111 | self: &Arc, 112 | event: Event, 113 | target: &Arc, 114 | ) -> crate::Result> { 115 | let mut handlers = self.handlers.write(); 116 | if let Some(handlers) = handlers.get_mut(&event) { 117 | if let Some(index) = handlers 118 | .iter() 119 | .position(|handler| Arc::ptr_eq(handler, target)) 120 | { 121 | return Ok(handlers.remove(index)); 122 | } 123 | } 124 | 125 | Err(crate::DiscordError::NoChangesMade) 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use std::mem::forget; 132 | 133 | use super::*; 134 | 135 | #[test] 136 | fn can_register_event_handlers() { 137 | let registry = Arc::new(HandlerRegistry::new()); 138 | let _ready1 = registry.register(Event::Ready, |_| unimplemented!()); 139 | let _ready2 = registry.register(Event::Ready, |_| unimplemented!()); 140 | let _error = registry.register(Event::Error, |_| unimplemented!()); 141 | 142 | let handlers = registry.handlers.read(); 143 | assert_eq!(handlers.len(), 2); 144 | assert_eq!(handlers[&Event::Ready].len(), 2); 145 | assert_eq!(handlers[&Event::Error].len(), 1); 146 | } 147 | 148 | /// Removes event handlers once they go out of scope to prevent memory leaks 149 | #[test] 150 | fn auto_remove_event_handlers() { 151 | let registry = Arc::new(HandlerRegistry::new()); 152 | let _ready1 = registry.register(Event::Ready, |_| unimplemented!()); 153 | let _error = registry.register(Event::Error, |_| unimplemented!()); 154 | 155 | { 156 | let _ready2 = registry.register(Event::Ready, |_| unimplemented!()); 157 | } 158 | // _ready2 is automatically removed 159 | 160 | let handlers = registry.handlers.read(); 161 | assert_eq!(handlers.len(), 2); 162 | assert_eq!(handlers[&Event::Ready].len(), 1); 163 | assert_eq!(handlers[&Event::Error].len(), 1); 164 | } 165 | 166 | /// Enables keeping an event callback for the entire lifetime of the client. 167 | /// This disables the functionality tested in `auto_remove_event_handlers`. 168 | #[test] 169 | fn forget_cb_handles() { 170 | let registry = Arc::new(HandlerRegistry::new()); 171 | 172 | { 173 | let ready = registry.register(Event::Ready, |_| unimplemented!()); 174 | // skip the Drop impl by running std::mem::forget 175 | forget(ready); 176 | } 177 | // _ready2 is not automatically removed 178 | 179 | let handlers = registry.handlers.read(); 180 | assert_eq!(handlers.len(), 1); 181 | assert_eq!(handlers[&Event::Ready].len(), 1); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | missing_docs, 3 | rust_2018_compatibility, 4 | rust_2018_idioms, 5 | clippy::all, 6 | clippy::pedantic 7 | )] 8 | // #![forbid(unsafe_code)] 9 | #![cfg_attr(docsrs, feature(doc_cfg))] 10 | 11 | //! A Rust library that allows the developer to interact with the Discord Presence API with ease 12 | 13 | pub(crate) static READY: AtomicBool = AtomicBool::new(false); 14 | 15 | // Cannot remove this *macro_use*, would break derive inside of macros 16 | #[macro_use] 17 | extern crate serde; 18 | 19 | #[macro_use] 20 | extern crate log; 21 | 22 | #[macro_use] 23 | mod macros; 24 | /// A client for the Discord Presence API 25 | pub mod client; 26 | mod connection; 27 | /// Errors that can occur when interacting with the Discord Presence API 28 | pub mod error; 29 | /// Event handlers 30 | pub mod event_handler; 31 | /// Models for discord activity 32 | pub mod models; 33 | mod utils; 34 | 35 | use std::sync::atomic::AtomicBool; 36 | 37 | pub use client::Client; 38 | pub use error::{DiscordError, Result}; 39 | pub use models::Event; 40 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! builder_func_doc { 2 | [ $type:tt ] => { 3 | concat!( 4 | "Instantiates the current struct with the given [`", 5 | stringify!($type), 6 | "`] value." 7 | ) 8 | }; 9 | } 10 | 11 | macro_rules! builder_func { 12 | [ $name:ident, $type:tt func $(=> if feature = $feature:tt)? ] => { 13 | $(#[cfg(feature = $feature)])? 14 | #[doc = builder_func_doc!($type)] 15 | #[must_use] 16 | pub fn $name(mut self, func: F) -> Self 17 | where F: FnOnce($type) -> $type 18 | { 19 | self.$name = Some(func($type::default())); self 20 | } 21 | }; 22 | 23 | [ $name:ident, String $(=> if feature = $feature:tt)? ] => { 24 | $(#[cfg(feature = $feature)])? 25 | #[doc = builder_func_doc!(Stringish)] 26 | #[must_use] 27 | pub fn $name(mut self, value: S) -> Self 28 | where S: Into 29 | { 30 | self.$name = Some(value.into()); self 31 | } 32 | }; 33 | 34 | [ $name:ident, $type:ty $(=> if feature = $feature:tt)? ] => { 35 | $(#[cfg(feature = $feature)])? 36 | #[doc = builder_func_doc!($type)] 37 | #[must_use] 38 | pub fn $name(mut self, value: $type) -> Self { 39 | self.$name = Some(value); self 40 | } 41 | }; 42 | } 43 | 44 | macro_rules! builder_array_doc { 45 | [ $type:tt ] => { 46 | concat!( 47 | "Appends a new [`", 48 | stringify!($type), 49 | "`] to the current struct with the given value." 50 | ) 51 | } 52 | } 53 | 54 | macro_rules! builder_array { 55 | [ $name:ident, $type:tt array ] => { 56 | paste::paste! { 57 | #[doc = builder_array_doc!($type)] 58 | #[must_use] 59 | pub fn [](mut self, func: F) -> Self 60 | where F: FnOnce($type) -> $type 61 | { 62 | self.$name.push(func($type::default())); self 63 | } 64 | } 65 | }; 66 | } 67 | 68 | macro_rules! into_error { 69 | [ $opt:expr, $msg:expr ] => { 70 | match $opt { 71 | Some(v) => Ok(v), 72 | None => Err(crate::error::DiscordError::NoneError($msg)), 73 | } 74 | }; 75 | 76 | [ $opt:expr ] => { 77 | into_error!($opt, String::from("Option unwrapped to None")) 78 | }; 79 | } 80 | 81 | macro_rules! builder { 82 | [ @st ( $name:ident $field:tt: $type:tt alias = $alias:tt $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 83 | builder![ @st 84 | ( $name $($rest)* ) -> ( 85 | $($out)* 86 | $(#[cfg(feature = $feature)])? 87 | #[doc = concat!("Optional " , stringify!($field), " field")] 88 | #[serde(skip_serializing_if = "Option::is_none", rename = $alias)] 89 | pub $field: Option<$type>, 90 | ) 91 | ]; 92 | }; 93 | 94 | [ @st ( $name:ident $field:tt: $type:tt func $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 95 | builder![ @st ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ]; 96 | }; 97 | 98 | // TODO: Make this more applicable for other types than just buttons 99 | // Currently the implementation here only works for buttons, thanks to the deserialize_with attribute 100 | [ @st ( $name:ident $field:ident: $type:ty as array, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 101 | builder![ @st 102 | ( $name $($rest)* ) -> ( 103 | $($out)* 104 | #[doc = concat!("Optional ", stringify!($field), " field")] 105 | #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "serialize_activity_button")] 106 | pub $field: Vec<$type>, 107 | ) 108 | ]; 109 | }; 110 | 111 | 112 | [ @st ( $name:ident $field:ident: $type:ty $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 113 | builder![ @st 114 | ( $name $($rest)* ) -> ( 115 | $($out)* 116 | #[doc = concat!("Optional " , stringify!($field), " field")] 117 | #[serde(skip_serializing_if = "Option::is_none")] 118 | pub $field: Option<$type>, 119 | ) 120 | ]; 121 | }; 122 | 123 | [ @st ( $name:ident ) -> ( $($out:tt)* ) ] => { 124 | #[doc = concat!(stringify!($name), " struct")] 125 | #[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, Hash, Eq)] 126 | pub struct $name { $($out)* } 127 | }; 128 | 129 | [ @im ( $name:ident $field:ident: $type:tt func $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 130 | builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type func $(=> if feature = $feature)?]; $($out)* ) ]; 131 | }; 132 | 133 | [ @im ( $name:ident $field:ident: $type:tt as array $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 134 | builder![ @im ( $name $($rest)* ) -> ( builder_array![$field, $type array]; $($out)* ) ]; 135 | }; 136 | 137 | [ @im ( $name:ident $field:ident: $type:tt alias = $modifier:tt $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 138 | builder![ @im ( $name $field: $type $(=> if feature = $feature)?, $($rest)* ) -> ( $($out)* ) ]; 139 | }; 140 | 141 | [ @im ( $name:ident $field:ident: $type:tt $(=> if feature = $feature:tt)?, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { 142 | builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type $(=> if feature = $feature)?]; $($out)* ) ]; 143 | }; 144 | 145 | [ @im ( $name:ident ) -> ( $($out:tt)* ) ] => { 146 | impl $name { 147 | #[doc = concat!("Instantiates the `", stringify!($name), "` struct using the `Default` implementation")] 148 | #[must_use] 149 | pub fn new() -> Self { 150 | Self::default() 151 | } 152 | 153 | $($out)* 154 | } 155 | }; 156 | 157 | [ $name:ident $($body:tt)* ] => { 158 | builder![@st ( $name $($body)* ) -> () ]; 159 | builder![@im ( $name $($body)* ) -> () ]; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/models/commands.rs: -------------------------------------------------------------------------------- 1 | use super::events::PartialUser; 2 | 3 | builder! {SubscriptionArgs 4 | secret: String, // Activity{Join,Spectate} 5 | user: PartialUser, // ActivityJoinRequest 6 | } 7 | 8 | builder! {Subscription 9 | evt: String, 10 | } 11 | -------------------------------------------------------------------------------- /src/models/events.rs: -------------------------------------------------------------------------------- 1 | builder! {ReadyEvent 2 | v: u32, 3 | config: RpcServerConfiguration, 4 | user: PartialUser, 5 | } 6 | 7 | builder! {ErrorEvent 8 | code: u32, 9 | message: String, 10 | } 11 | 12 | builder! {RpcServerConfiguration 13 | cdn_host: String, 14 | api_endpoint: String, 15 | environment: String, 16 | } 17 | 18 | builder! {PartialUser 19 | id: String, 20 | username: String, 21 | discriminator: String, 22 | avatar: String, 23 | } 24 | -------------------------------------------------------------------------------- /src/models/message.rs: -------------------------------------------------------------------------------- 1 | use crate::{DiscordError, Result}; 2 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 3 | use num_derive::FromPrimitive; 4 | use num_traits::FromPrimitive; 5 | use serde::Serialize; 6 | use std::io::{Read, Write}; 7 | 8 | pub(crate) const MAX_RPC_FRAME_SIZE: usize = 64 * 1024; 9 | pub(crate) const MAX_RPC_MESSAGE_SIZE: usize = 10 | MAX_RPC_FRAME_SIZE - std::mem::size_of::(); 11 | 12 | /// Codes for payload types 13 | #[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] 14 | #[repr(u32)] 15 | pub enum OpCode { 16 | /// Handshake payload 17 | Handshake = 0, 18 | /// Frame payload 19 | Frame = 1, 20 | /// Close payload 21 | Close = 2, 22 | /// Ping payload 23 | Ping = 3, 24 | /// Pong payload 25 | Pong = 4, 26 | } 27 | 28 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 29 | #[repr(C)] 30 | /// Header for the payload 31 | /// 32 | /// Determines the length of the payload, and the type of payload 33 | pub struct FrameHeader { 34 | /// The opcode for the payload 35 | opcode: OpCode, 36 | /// The length of the payload 37 | length: u32, 38 | } 39 | 40 | impl FrameHeader { 41 | #[must_use] 42 | /// Convert an array of bytes to a [`FrameHeader`] 43 | /// 44 | /// # Safety 45 | /// This reinterprets the bytes as a [`FrameHeader`]. It is up to the caller to ensure that the 46 | /// bytes are valid. 47 | pub unsafe fn from_bytes(bytes: &[u8]) -> Option { 48 | if bytes.len() != std::mem::size_of::() { 49 | return None; 50 | } 51 | 52 | let header: Self = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast()) }; 53 | 54 | if header.message_length() > MAX_RPC_MESSAGE_SIZE { 55 | return None; 56 | } 57 | 58 | Some(header) 59 | } 60 | 61 | #[must_use] 62 | /// Get the expected message length 63 | pub fn message_length(&self) -> usize { 64 | self.length as usize 65 | } 66 | 67 | #[must_use] 68 | /// Get the opcode 69 | pub fn opcode(&self) -> OpCode { 70 | self.opcode 71 | } 72 | } 73 | 74 | // NOTE: Currently unused 75 | // Probably remove in future 76 | // #[derive(Debug, Copy, Clone, PartialEq, Eq)] 77 | // #[repr(C)] 78 | // /// Frame passed over the socket 79 | // /// 80 | // /// Contains the header and the payload 81 | // pub struct Frame { 82 | // /// The header for the payload 83 | // header: FrameHeader, 84 | // /// The actual payload 85 | // message: [std::os::raw::c_char; MAX_RPC_FRAME_SIZE - std::mem::size_of::()], 86 | // } 87 | 88 | // impl From for Message { 89 | // fn from(header: Frame) -> Self { 90 | // Self { 91 | // opcode: header.header.opcode, 92 | // payload: unsafe { CStr::from_ptr(header.message.as_ptr()) } 93 | // .to_string_lossy() 94 | // .into_owned(), 95 | // } 96 | // } 97 | // } 98 | 99 | /// Message struct for the Discord RPC 100 | #[derive(Debug, PartialEq, Eq, Clone)] 101 | pub struct Message { 102 | /// The payload type for this `Message` 103 | pub opcode: OpCode, 104 | /// The actual payload 105 | pub payload: String, 106 | } 107 | 108 | impl Message { 109 | /// Create a new `Message` 110 | /// 111 | /// # Errors 112 | /// - Could not serialize the payload 113 | pub fn new(opcode: OpCode, payload: T) -> Result 114 | where 115 | T: Serialize, 116 | { 117 | Ok(Self { 118 | opcode, 119 | payload: serde_json::to_string(&payload)?, 120 | }) 121 | } 122 | 123 | /// Encode message 124 | /// 125 | /// # Errors 126 | /// - Failed to write to the buffer 127 | /// 128 | /// # Panics 129 | /// - The payload length is not a 32 bit number 130 | pub fn encode(&self) -> Result> { 131 | let mut bytes: Vec = vec![]; 132 | 133 | let payload_length = u32::try_from(self.payload.len()).expect("32-bit payload length"); 134 | 135 | bytes.write_u32::(self.opcode as u32)?; 136 | bytes.write_u32::(payload_length)?; 137 | bytes.write_all(self.payload.as_bytes())?; 138 | 139 | Ok(bytes) 140 | } 141 | 142 | /// Decode message 143 | /// 144 | /// # Errors 145 | /// - Failed to read from buffer 146 | pub fn decode(mut bytes: &[u8]) -> Result { 147 | let opcode = 148 | OpCode::from_u32(bytes.read_u32::()?).ok_or(DiscordError::Conversion)?; 149 | let len = bytes.read_u32::()? as usize; 150 | let mut payload = String::with_capacity(len); 151 | bytes.read_to_string(&mut payload)?; 152 | 153 | Ok(Self { opcode, payload }) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use super::*; 160 | 161 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 162 | struct Something { 163 | empty: bool, 164 | } 165 | 166 | #[test] 167 | fn test_encoder() { 168 | let msg = Message::new(OpCode::Frame, Something { empty: true }) 169 | .expect("Failed to serialize message"); 170 | let encoded = msg.encode().expect("Failed to encode message"); 171 | let decoded = Message::decode(&encoded).expect("Failed to decode message"); 172 | assert_eq!(msg, decoded); 173 | } 174 | 175 | #[test] 176 | fn test_opcode() { 177 | assert_eq!(OpCode::from_u32(0), Some(OpCode::Handshake)); 178 | assert_eq!(OpCode::from_u32(4), Some(OpCode::Pong)); 179 | assert_eq!(OpCode::from_u32(5), None); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | /// The Discord commands module 2 | pub mod commands; 3 | /// The events module 4 | pub mod events; 5 | /// The module to handle messages 6 | pub mod message; 7 | /// The module to handle payloads 8 | pub mod payload; 9 | /// The rich presence module 10 | pub mod rich_presence; 11 | 12 | use quork::traits::list::ListVariants; 13 | 14 | /// Different Discord commands 15 | #[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize)] 16 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 17 | pub enum Command { 18 | /// Dispatch something to Discord 19 | Dispatch, 20 | /// Authorize connection 21 | Authorize, 22 | /// Subscribe to an event 23 | Subscribe, 24 | /// Unsubscribe from Discord 25 | Unsubscribe, 26 | /// Set the current user's activity 27 | SetActivity, 28 | /// Send an invite to join a game 29 | SendActivityJoinInvite, 30 | /// Close the invite to join a game 31 | CloseActivityRequest, 32 | } 33 | 34 | // NOTE: ListVariants is required to bevy-discord-rpc 35 | /// Discord events 36 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Copy, Clone, Hash, ListVariants)] 37 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 38 | pub enum Event { 39 | /// [`Event::Ready`] event, fired when the client is ready, but not if an error occurs 40 | Ready, 41 | /// [`Event::Connected`] event, fired when the client successfully connects (including re-connections) 42 | Connected, 43 | /// [`Event::Disconnected]` event, fired when the client was connected but loses connection 44 | Disconnected, 45 | /// [`Event::Error`] event, overrides the `Ready` event 46 | Error, 47 | /// [`Event::ActivityJoin`] event, fired when the client's game is joined by a player 48 | ActivityJoin, 49 | /// [`Event::ActivitySpectate`] event, fired when the client receives a spectate request 50 | ActivitySpectate, 51 | /// [`Event::ActivityJoinRequest`] event, fired when the client receives a join request 52 | ActivityJoinRequest, 53 | } 54 | 55 | impl Event { 56 | #[must_use] 57 | /// Parse event data from a [`JsonValue`] 58 | pub fn parse_data(self, data: JsonValue) -> EventData { 59 | match self { 60 | Event::Ready => serde_json::from_value(data.clone()) 61 | .map(EventData::Ready) 62 | .unwrap_or(EventData::Unknown(data)), 63 | 64 | Event::Error => serde_json::from_value(data.clone()) 65 | .map(EventData::Error) 66 | .unwrap_or(EventData::Unknown(data)), 67 | 68 | Event::ActivityJoin => serde_json::from_value(data.clone()) 69 | .map(EventData::ActivityJoin) 70 | .unwrap_or(EventData::Unknown(data)), 71 | 72 | Event::ActivitySpectate => serde_json::from_value(data.clone()) 73 | .map(EventData::ActivitySpectate) 74 | .unwrap_or(EventData::Unknown(data)), 75 | 76 | Event::ActivityJoinRequest => serde_json::from_value(data.clone()) 77 | .map(EventData::ActivityJoinRequest) 78 | .unwrap_or(EventData::Unknown(data)), 79 | 80 | Event::Connected | Event::Disconnected => EventData::None, 81 | } 82 | } 83 | } 84 | 85 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] 86 | /// Internal data for the [`Event`] enum 87 | pub enum EventData { 88 | /// [`Event::Ready`] event data 89 | Ready(ReadyEvent), 90 | /// [`Event::Error`] event data 91 | Error(ErrorEvent), 92 | /// [`Event::ActivityJoin`] event data 93 | ActivityJoin(ActivityJoinEvent), 94 | /// [`Event::ActivitySpectate`] event data 95 | ActivitySpectate(ActivitySpectateEvent), 96 | /// [`Event::ActivityJoinRequest`] event data 97 | ActivityJoinRequest(ActivityJoinRequestEvent), 98 | /// Unknown event data 99 | Unknown(JsonValue), 100 | /// Event had no data 101 | None, 102 | } 103 | 104 | pub use commands::*; 105 | pub use events::*; 106 | pub use message::{Message, OpCode}; 107 | 108 | pub use rich_presence::*; 109 | use serde_json::Value as JsonValue; 110 | 111 | /// Prelude for all Discord RPC types 112 | pub mod prelude { 113 | pub use super::commands::{Subscription, SubscriptionArgs}; 114 | pub use super::events::{ErrorEvent, ReadyEvent}; 115 | pub use super::rich_presence::{ 116 | ActivityJoinEvent, ActivityJoinRequestEvent, ActivitySpectateEvent, 117 | CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs, 118 | }; 119 | pub use super::Command; 120 | pub use super::Event; 121 | } 122 | -------------------------------------------------------------------------------- /src/models/payload.rs: -------------------------------------------------------------------------------- 1 | use super::{Command, Event, Message}; 2 | use crate::utils; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | /// The Discord client payload 6 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] 7 | pub struct Payload 8 | where 9 | T: Serialize, 10 | { 11 | /// The payload command 12 | pub cmd: Command, 13 | 14 | /// The payload args 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub args: Option, 17 | 18 | /// The payload data 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub data: Option, 21 | 22 | /// The payload event 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub evt: Option, 25 | 26 | /// The payload nonce 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub nonce: Option, 29 | } 30 | 31 | impl Payload 32 | where 33 | T: Serialize, 34 | { 35 | /// Create a `Payload`, by generating a nonce 36 | pub fn with_nonce(cmd: Command, args: Option, data: Option, evt: Option) -> Self { 37 | Self { 38 | cmd, 39 | args, 40 | data, 41 | evt, 42 | nonce: Some(utils::nonce()), 43 | } 44 | } 45 | } 46 | 47 | impl From for Payload 48 | where 49 | T: Serialize + DeserializeOwned, 50 | { 51 | fn from(message: Message) -> Self { 52 | serde_json::from_str(&message.payload).unwrap() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/models/rich_presence.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | 3 | use serde::Deserializer; 4 | 5 | #[cfg(feature = "activity_type")] 6 | use serde_repr::{Deserialize_repr, Serialize_repr}; 7 | 8 | use super::events::PartialUser; 9 | use crate::utils; 10 | 11 | /// Args to set Discord activity 12 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] 13 | pub struct SetActivityArgs { 14 | pid: u32, 15 | 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | activity: Option, 18 | } 19 | 20 | impl SetActivityArgs { 21 | /// Create a new `SetActivityArgs` 22 | pub fn new(f: F) -> Self 23 | where 24 | F: FnOnce(Activity) -> Activity, 25 | { 26 | Self { 27 | pid: utils::pid(), 28 | activity: Some(f(Activity::new())), 29 | } 30 | } 31 | } 32 | 33 | impl Default for SetActivityArgs { 34 | fn default() -> Self { 35 | Self { 36 | pid: utils::pid(), 37 | activity: None, 38 | } 39 | } 40 | } 41 | 42 | /// Args to invite a player to join a game 43 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] 44 | pub struct SendActivityJoinInviteArgs { 45 | /// The user to invite 46 | pub user_id: String, 47 | } 48 | 49 | /// The args to close an activity request 50 | pub type CloseActivityRequestArgs = SendActivityJoinInviteArgs; 51 | 52 | impl SendActivityJoinInviteArgs { 53 | #[must_use] 54 | /// Create a new `SendActivityJoinInviteArgs` 55 | pub fn new(user_id: u64) -> Self { 56 | Self { 57 | user_id: user_id.to_string(), 58 | } 59 | } 60 | } 61 | 62 | /// [`ActivityType`] enum 63 | /// 64 | /// Lists all activity types currently supported by Discord. 65 | /// 66 | /// This may change in future if Discord adds support for more types, 67 | /// or removes support for some. 68 | #[cfg(feature = "activity_type")] 69 | #[cfg_attr(docsrs, doc(cfg(feature = "activity_type")))] 70 | #[repr(u8)] 71 | #[non_exhaustive] 72 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize_repr, Serialize_repr, Hash)] 73 | pub enum ActivityType { 74 | /// Playing a game 75 | Playing = 0, 76 | /// Listening to... 77 | Listening = 2, 78 | /// Watching... 79 | Watching = 3, 80 | /// Competing in... 81 | Competing = 5, 82 | } 83 | 84 | builder! {ActivityJoinEvent 85 | secret: String, 86 | } 87 | 88 | builder! {ActivitySpectateEvent 89 | secret: String, 90 | } 91 | 92 | builder! {ActivityJoinRequestEvent 93 | user: PartialUser, 94 | } 95 | 96 | builder! {Activity 97 | state: String, 98 | details: String, 99 | instance: bool, 100 | _type: ActivityType alias = "type" => if feature = "activity_type", 101 | timestamps: ActivityTimestamps func, 102 | assets: ActivityAssets func, 103 | party: ActivityParty func, 104 | secrets: ActivitySecrets func, 105 | buttons: ActivityButton as array, 106 | } 107 | 108 | builder! {ActivityTimestamps 109 | start: u64, 110 | end: u64, 111 | } 112 | 113 | builder! {ActivityAssets 114 | large_image: String, 115 | large_text: String, 116 | small_image: String, 117 | small_text: String, 118 | } 119 | 120 | builder! {ActivityParty 121 | id: String, 122 | size: (u32, u32), 123 | } 124 | 125 | builder! {ActivitySecrets 126 | join: String, 127 | spectate: String, 128 | game: String alias = "match", 129 | } 130 | 131 | // pub type ActivityButtons = Vec; 132 | 133 | // A probably overcomplicated way to convert the array of strings returned by Discord, into buttons 134 | fn serialize_activity_button<'de, D>(data: D) -> Result, D::Error> 135 | where 136 | D: Deserializer<'de>, 137 | { 138 | use serde::de; 139 | use std::fmt; 140 | 141 | struct JsonStringVisitor; 142 | 143 | impl<'de> de::Visitor<'de> for JsonStringVisitor { 144 | type Value = Vec; 145 | 146 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 147 | formatter.write_str("a string containing the label for the button") 148 | } 149 | 150 | fn visit_seq(self, mut seq: A) -> Result 151 | where 152 | A: de::SeqAccess<'de>, 153 | { 154 | let mut buttons = vec![]; 155 | 156 | while let Ok(Some(label)) = seq.next_element::() { 157 | let button = ActivityButton { 158 | label: Some(label.clone()), 159 | url: None, 160 | }; 161 | 162 | buttons.push(button); 163 | } 164 | 165 | Ok(buttons) 166 | } 167 | } 168 | 169 | data.deserialize_any(JsonStringVisitor) 170 | } 171 | 172 | builder! {ActivityButton 173 | // Text shown on the button (1-32 characters) 174 | label: String, 175 | // URL opened when clicking the button (1-512 characters) 176 | url: String, 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | use super::*; 182 | use serde_json; 183 | 184 | #[test] 185 | fn can_serialize_full_activity() { 186 | let expected = include_str!("../../tests/fixtures/activity_full.json"); 187 | let parsed_expected = serde_json::from_str::(expected).unwrap(); 188 | 189 | let activity = Activity::new() 190 | .state("rusting") 191 | .details("detailed") 192 | .instance(true) 193 | .timestamps(|t| t.start(1000).end(2000)) 194 | .assets(|a| { 195 | a.large_image("ferris") 196 | .large_text("Ferris") 197 | .small_image("rusting") 198 | .small_text("Rusting...") 199 | }) 200 | .append_buttons(|button| button.label("Click Me!")) 201 | .party(|p| p.id(String::from("party")).size((3, 6))) 202 | .secrets(|s| { 203 | s.join("025ed05c71f639de8bfaa0d679d7c94b2fdce12f") 204 | .spectate("e7eb30d2ee025ed05c71ea495f770b76454ee4e0") 205 | .game("4b2fdce12f639de8bfa7e3591b71a0d679d7c93f") 206 | }); 207 | 208 | assert_eq!(parsed_expected, activity); 209 | } 210 | 211 | #[test] 212 | fn can_serialize_empty_activity() { 213 | let activity = Activity::new(); 214 | let json = serde_json::to_string(&activity).expect("Failed to serialize into String"); 215 | assert_eq![json, "{}"]; 216 | } 217 | } 218 | 219 | #[cfg(test)] 220 | #[cfg(feature = "activity_type")] 221 | mod activity_type_tests { 222 | use super::*; 223 | 224 | #[test] 225 | fn can_serialize_activity_type() { 226 | let activity = Activity::new()._type(ActivityType::Watching); 227 | let json = serde_json::to_string(&activity).expect("Failed to serialize into String"); 228 | 229 | assert_eq![json, r#"{"type":3}"#]; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | pub fn pid() -> u32 { 4 | std::process::id() 5 | } 6 | 7 | pub fn nonce() -> String { 8 | Uuid::new_v4().to_string() 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/activity_full.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": "rusting", 3 | "details": "detailed", 4 | "instance": true, 5 | "timestamps": { 6 | "start": 1000, 7 | "end": 2000 8 | }, 9 | "assets": { 10 | "large_image": "ferris", 11 | "large_text": "Ferris", 12 | "small_image": "rusting", 13 | "small_text": "Rusting..." 14 | }, 15 | "party": { 16 | "id": "party", 17 | "size": [3, 6] 18 | }, 19 | "buttons": ["Click Me!"], 20 | "secrets": { 21 | "join": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f", 22 | "spectate": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0", 23 | "match": "4b2fdce12f639de8bfa7e3591b71a0d679d7c93f" 24 | } 25 | } -------------------------------------------------------------------------------- /tests/version_numbers.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate version_sync; 3 | 4 | #[test] 5 | fn test_readme_version() { 6 | assert_markdown_deps_updated!("README.md"); 7 | } 8 | --------------------------------------------------------------------------------