Compare commits

...

92 Commits

Author SHA1 Message Date
Michael Pfaff 2b2b47454c
Remove build.sbt 2022-09-09 07:29:10 -04:00
Michael Pfaff b1ef65e2a9
Suppress std::io::NotFound errors 2022-08-16 14:31:44 -04:00
Michael Pfaff 2aaa043bd2
See message
- **Breaking change**: Multiple functions renamed in `src/java.rs`. This is not bumping the major version because these functions are considered internal.
- Fix disconnecting dead-lock
- `Client::execute` now disconnects when `Error::ConnectionClosed` or `Error::ConnectionClosedWhileSending` is encountered
- Use `tracing` instead of `log`
- Add the `instrument` attribute to most `Client` functions
- Remove unnecessary `Arc` from JNI bindings
2022-03-26 23:06:08 -04:00
Michael Pfaff 5d0243df9a
Add disconnect and clearActivity 2022-03-21 13:19:12 -04:00
Michael Pfaff 49f58f2623
Remove badges and add link to original repo 2022-02-03 18:40:32 -05:00
Michael Pfaff 910871dcf9
Update CHANGELOG.md 2022-02-03 17:01:02 -05:00
Michael Pfaff ad78e64e64
Bump version 2022-02-03 17:00:42 -05:00
Michael Pfaff 30eebae644
Cleanup some stuff 2022-02-03 16:57:40 -05:00
Michael Pfaff d99145133d
Add partial Java bindings 2022-02-03 16:56:26 -05:00
Michael Pfaff 04af242ca7
Overhaul (part 2)
- No more worker thread
- Fully async io thanks to Tokio
- Updated libs
- Client is no longer bound to a specific client id
2022-02-03 15:56:37 -05:00
Michael Pfaff 326354d3b4
Overhaul (part 1) 2022-02-03 09:52:53 -05:00
Patrick Auernig 6ea1c93bae Update changelog 2018-12-06 02:21:22 +01:00
Patrick Auernig 7c10e1dbc5 Update Cargo.toml
Update version to 0.3.0
2018-12-06 02:09:37 +01:00
Patrick Auernig 84de596ffb Allow cloning of Client instances 2018-12-06 02:08:26 +01:00
Patrick Auernig eb6152effe Add longer delay after reconnect attempts 2018-12-06 02:05:23 +01:00
Patrick Auernig 52d31f4420 Update readme 2018-12-06 01:27:39 +01:00
Patrick Auernig 4b99188f23 Update changelog 2018-12-06 01:05:06 +01:00
Patrick Auernig 184abdc07f Rewrite connection manager 2018-12-06 01:05:06 +01:00
Patrick Auernig ecd391a44e Remove disconnect method from Connection
Use Drop implementation instead
2018-12-06 01:05:06 +01:00
Patrick Auernig caaae615f0 Move handshake and ping into Connection trait 2018-12-06 01:05:06 +01:00
Patrick Auernig b3d12ff760 Update gitignore 2018-12-04 22:28:53 +01:00
Patrick Auernig 21915d9acf Update Cargo.toml
Update version to 0.2.4
2018-12-04 22:19:54 +01:00
Patrick Auernig 3c8e8453e6 Update GitLab CI configuration 2018-12-04 22:16:47 +01:00
Patrick Auernig 3ebc936b10 Remove libc dependency
The function std::process::id() is available in the
"stable" channel now.
2018-12-04 21:31:12 +01:00
Patrick Auernig 6c1b159435 Just a bit of cleanup 2018-04-11 23:30:46 +02:00
Patrick Auernig aaaf2474a6 Implement invite handling commands
Handles SEND_ACTIVITY_JOIN_INVITE and CLOSE_ACTIVITY_REQUEST
2018-04-10 15:14:48 +02:00
Patrick Auernig 9c90034ee7 Move subscribe examples into own file 2018-04-10 14:26:41 +02:00
Patrick Auernig 2ffc0cd97c Remove useless static variable 2018-04-10 13:06:44 +02:00
Patrick Auernig 889c440112
Update CHANGELOG.md
Remove unnecessary clutter and redundant Add prefix in Added sections
2018-04-10 03:25:48 +02:00
Patrick Auernig 45bcbb4b71
Update CHANGELOG.md 2018-04-10 03:20:20 +02:00
Patrick Auernig ab2b38aa47 Create CHANGELOG.md 2018-04-10 01:27:04 +02:00
Patrick Auernig 7024efd5c7 Build examples in CI 2018-04-09 19:34:31 +02:00
Patrick Auernig 7764ef3e8c Use dev-dependencies for examples 2018-04-09 19:34:08 +02:00
Patrick Auernig bbf489d78b Fix typo in CONTRIBUTING.md 2018-04-09 16:51:08 +02:00
Patrick Auernig d51107247f Update appveyor.yml
Don't build tags
2018-04-08 21:46:25 +02:00
Patrick Auernig a7b9a86d4e Bump crate version
0.2.2 -> 0.2.3
2018-04-08 21:35:30 +02:00
Patrick Auernig 6522a5b4a0 Update example
Clear presence if input is empty.
2018-04-08 21:08:20 +02:00
Patrick Auernig a9480b9d72 Add method to clear a Rich Presence status 2018-04-08 21:07:19 +02:00
Bond-009 6bbc9f85d7 Change timestamps to u64
They are documented as u64: https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields
2018-04-08 20:29:35 +02:00
Patrick Auernig 5845632ebc Update README.md
Use small Discord badge
2018-04-08 20:27:14 +02:00
Patrick Auernig 56326618a4 Remove unused imports 2018-04-07 13:39:06 +02:00
Patrick Auernig f43c9697ae Fix Windows disconnect method 2018-04-07 13:34:00 +02:00
Patrick Auernig 4443423407 Add dummy disconnect method for Windows connection 2018-04-07 13:27:59 +02:00
Patrick Auernig 06728d21cf Update example 2018-04-07 13:24:25 +02:00
Patrick Auernig a1e77c9c35 Implement reconnection logic
Still needs some changes to handle
* Retrying message send
* Resubscribing current event subscriptions
* Make non-blocking again
2018-04-07 13:22:50 +02:00
Patrick Auernig d370cb6432 Add connection manager 2018-04-06 21:51:01 +02:00
Patrick Auernig 520da02162 Add unsubscribe command to client 2018-04-06 13:29:01 +02:00
Patrick Auernig 20044cbea4 Update example 2018-04-06 02:32:03 +02:00
Patrick Auernig 95d748f211 Model restructuring
* Remove old Command payload and replace with generic one.
* Move Rich Presence model back to src/models.
* Add Subscription command, Ready event and Error event models.
* Add subscribe method to client and implement simple error detection.
2018-04-06 02:28:14 +02:00
Patrick Auernig a585bb6495 Bump crate version
0.2.1 -> 0.2.2
2018-04-03 17:05:11 +02:00
Patrick Auernig 95ab885fb4 Update GitLab CI configuration
Add deploy stage for crates.io
2018-04-03 16:52:38 +02:00
Patrick Auernig 60c7f4762f Use default SocketConnection for current platform 2018-04-03 16:01:23 +02:00
Patrick Auernig b89fddcac4 Bump crate version
0.2.0 -> 0.2.1
2018-04-03 12:22:31 +02:00
Patrick Auernig 2b06570fa2 Fix named_pipe import and remove unused ones 2018-04-03 12:14:12 +02:00
Patrick Auernig ced34974cf Update Rich Presence example
Add Windows connection and remove version dependency from Cargo.toml
2018-04-03 12:04:14 +02:00
Patrick Auernig b8e4697555 Move some common methods into Connection trait 2018-04-03 12:03:08 +02:00
Patrick Auernig 51cd4413f7 Add AppVeyor CI configuration 2018-04-03 00:48:57 +02:00
Patrick Auernig af50e1318e Bump crate version
0.1.5 -> 0.2.0
2018-04-02 23:47:07 +02:00
tenrys 620e9a6b26 Added Windows connection support 2018-04-02 21:42:57 +00:00
Patrick Auernig 1e3e9485df Import restructuring
Use new grouping syntax from Rust 1.25
Group `use` in following order:
std, external, internal
2018-03-29 23:40:56 +02:00
Patrick Auernig 94fd95d516 Add test for opcode conversion 2018-03-29 23:11:24 +02:00
Patrick Auernig fd1dc15a29 Use Result value for OpCode conversion
Conversion could fail if integer is not in range 0...4.
2018-03-29 23:00:58 +02:00
Patrick Auernig 40be78dbd1 Add error type 2018-03-29 22:57:00 +02:00
Patrick Auernig 332dbab413 Move nonce generator to utils module 2018-03-28 23:50:25 +02:00
Patrick Auernig 185b012fc0 Bump crate version
0.1.4 -> 0.1.5
2018-03-28 22:55:33 +02:00
Patrick Auernig 3c79bb15fa Move Rich Presence code into own submodule 2018-03-28 22:55:26 +02:00
Patrick Auernig eb64564333 Update Cargo.toml
Disable GitLab badge and workspaces.
2018-03-28 16:43:59 +02:00
Patrick Auernig cbe540dcff Update tests for SetActivity
Only test full and empty object serialization.
2018-03-28 16:37:41 +02:00
Patrick Auernig 681d3d08b6 Update README.md
Add invite to support server.
2018-03-28 12:06:41 +02:00
Patrick Auernig afe1503ad9 Move getpid to utils module 2018-03-25 20:55:38 +02:00
Patrick Auernig 54ace3a5e9 Implement From<u32> trait for OpCode
And store Message.opcode as OpCode instead of u32.
2018-03-24 21:42:39 +01:00
Patrick Auernig 2635086e97 Use byteorder crate instead of byte 2018-03-24 15:15:48 +01:00
Patrick Auernig a5acb3b975 Bump crate version
0.1.3 -> 0.1.4
2018-03-23 22:57:48 +01:00
Patrick Auernig c5ae80007e Remove useless shrink_to_fit in message encoder 2018-03-23 22:55:31 +01:00
Patrick Auernig e853fa94a1 Set fixed read buffer size 2018-03-23 22:54:30 +01:00
Patrick Auernig 00c61cfa72 Represent opcodes as enum instead of u32 2018-03-23 22:54:23 +01:00
Patrick Auernig 8929bd4f96 Bump crate version
0.1.2 -> 0.1.3
2018-03-23 11:12:36 +01:00
Patrick Auernig 0e38bf634d Use libc::getpid instead of std::process::id
Until the std::process::id interface becomes stable.
2018-03-23 11:04:19 +01:00
Patrick Auernig 6d83bc0af2 Create CONTRIBUTING.md 2018-03-23 10:44:52 +01:00
Patrick Auernig 53bab6857e Add Connection trait and implement UnixConnection 2018-03-22 22:54:49 +01:00
Patrick Auernig 43c8a6af5f Privatize struct fields 2018-03-22 20:54:18 +01:00
Patrick Auernig 045d7cdf91 Add badges to Cargo.toml and bump version
0.1.1 -> 0.1.2
2018-03-22 20:21:14 +01:00
Patrick Auernig 86fa860499 Add log crate 2018-03-22 20:21:08 +01:00
Patrick Auernig f79b52f60f Add Travis CI configuration 2018-03-22 19:17:07 +01:00
Patrick Auernig 38c125945e Add LICENSE 2018-03-22 17:43:30 +00:00
Patrick Auernig 704b947729 Bump crate version
0.1.0 -> 0.1.1
2018-03-22 18:40:24 +01:00
Patrick Auernig a078b9c322 Fix alias modifier and add serialization tests 2018-03-22 18:39:09 +01:00
Patrick Auernig ca1a84eeee Make models and client modules public 2018-03-22 18:12:41 +01:00
Patrick Auernig 9a9fc504fe Add macro to build message structs and impls 2018-03-22 18:11:41 +01:00
Patrick Auernig a7e7bb8446 Add crates.io badge 2018-03-22 16:08:59 +01:00
Patrick Auernig 58af08b3f7 Add GitLab CI configuration 2018-03-22 16:03:20 +01:00
Patrick Auernig c4900b959c Update README.md
Add Unix-only notice
2018-03-22 15:40:36 +01:00
41 changed files with 1906 additions and 455 deletions

View File

@ -1,13 +0,0 @@
module Overcommit::Hook::PrePush
# Runs `cargo test` before push
#
class CargoTest < Base
def run
result = execute(command)
return :pass if result.success?
output = result.stdout + result.stderr
[:fail, output]
end
end
end

8
.gitignore vendored
View File

@ -1,4 +1,6 @@
/target/
/Cargo.lock
/target
**/*.rs.bk
Cargo.lock
# CI
/.rustup
/.cargo

View File

@ -1,13 +0,0 @@
PreCommit:
TrailingWhitespace:
enabled: true
exclude:
- 'target/**/*'
PrePush:
CargoTest:
enabled: true
description: 'Run Cargo tests'
required_executable: 'cargo'
flags: ['test', '--all']
include: "**/*.rs"

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"files.watcherExclude": {
"**/target": true
}
}

129
CHANGELOG.md Normal file
View File

@ -0,0 +1,129 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.5.1] - 2021-02-03
### Changed
- Add partial Java bindings (missing `disconnect`, `clear_activity`, `send_activity_join_invite`, `close_activity_request`, `subscribe`, `unsubscribe`)
## [0.5.0] - 2021-02-03
### Changed
- Rewrite `Client`, eliminating `ConnectionManager`
- `Client` is now fully async and no worker thread is needed
## [0.4.0] - 2021-02-03
### Changed
- Update libs
- Update to Rust edition 2021
- Connection manager mostly rewritten
- Added support for Discord installed as a flatpak
- Reformat
- Derive `Debug` on more types
- Disconnect actually works now
## [0.3.0] - 2018-12-06
### Changed
- Connection manager completely rewritten
- Allow cloning of clients
## [0.2.4] - 2018-12-04
### Changed
- No longer depends on `libc` for process id lookup
## [0.2.3] - 2018-04-08
### Added
- Connection manager with reconnection
- Method to clear the current Rich Presence state
### Changed
- Move rich presence code back into *models*
- Remove command payload and add generic one
- Timestamps are now 64 bit unsigned integers instead of 32 bit ([@Bond-009]) [6bbc9f8][c:6bbc9f8]
## [0.2.2] - 2018-04-03
### Changed
- Use a default socket connection for the current platform
## [0.2.1] - 2018-04-03
### Changed
- Move common connection methods into trait
## [0.2.0] - 2018-04-02
### Added
- Error type
- Windows support ([@Tenrys]) [620e9a6][c:620e9a6]
### Changed
- Convert OpCode with `try_from` instead of `try`
- Use Rust 1.25 style nested imports
## [0.1.5] - 2018-03-28
### Changed
- Opcode stored in Message is now an OpCode enum
- Rich Presence now lives in it's own submodule
## [0.1.4] - 2018-03-23
### Changed
- Opcodes are now represented as enum instead of integers
## [0.1.3] - 2018-03-23
### Added
- Contributing information
### Changed
- Use `libc::getpid` to allow builds with *stable* instead of *nightly*
- Make client struct fields private
- Make models private again and add prelude
- Connections are now using a shared Connection trait
## [0.1.2] - 2018-03-22
### Added
- Logging support
## [0.1.1] - 2018-03-22
### Changed
- Make models publicly accessible
## [0.1.0] - 2018-03-22
### Added
- Setting Rich Presence status
- Unix socket connection support
<!-- links -->
[Unreleased]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/develop
[0.2.4]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.4
[0.2.3]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.3
[0.2.2]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.2
[0.2.1]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.1
[0.2.0]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.2.0
[0.1.5]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.5
[0.1.4]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.4
[0.1.3]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.3
[0.1.2]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.2
[0.1.1]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.1
[0.1.0]: https://gitlab.com/valeth/discord-rpc-client.rs/tree/v0.1.0
[c:620e9a6]: https://github.com/valeth/discord-rpc-client.rs/commit/620e9a6b26650d825392cf0fbfd097a7ed1662aa
[c:6bbc9f8]: https://github.com/valeth/discord-rpc-client.rs/commit/6bbc9f85d77bc6792c36d9317e804fcf5a306fb2
[@Tenrys]: https://github.com/Tenrys
[@Bond-009]: https://github.com/Bond-009

11
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,11 @@
# Contributing Guidelines
Contributions to this project are welcome, just follow these steps.
1. Fork this repository and create a feature branch named after the feature you want to implement
2. Make your changes on your branch
3. Add some test if possibe
4. Make sure all tests pass
5. Submit a PR/MR on GitHub or GitLab
> **Note**: Make sure you rebase your feature branch on top of master from time to time.

View File

@ -1,23 +1,49 @@
[package]
authors = ["Patrick Auernig <dev.patrick.auernig@gmail.com>"]
name = "discord-rpc-client"
name = "discord-rpc-client"
version = "0.5.3"
description = "A Rust client for Discord RPC."
keywords = ["discord", "rpc", "ipc"]
license = "MIT"
readme = "README.md"
repository = "https://gitlab.com/valeth/discord-rpc-client.rs.git"
version = "0.1.0"
[dependencies]
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
byte = "0.2"
[dependencies.uuid]
version = "^0.6.2"
features = ["v4"]
authors = [
"Patrick Auernig <dev.patrick.auernig@gmail.com>",
"Michael Pfaff <michael@pfaff.dev>",
]
keywords = ["discord", "rpc", "ipc"]
license = "MIT"
readme = "README.md"
repository = "https://gitlab.com/valeth/discord-rpc-client.rs.git"
edition = "2021"
[features]
default = ["rich_presence"]
rich_presence = []
tokio-parking_lot = ["tokio/parking_lot"]
java-bindings = ["lazy_static", "jni", "tokio/rt-multi-thread"]
[dependencies]
anyhow = "1"
async-trait = "0.1.52"
byteorder = "1.0"
bytes = "1.1.0"
const_format = "0.2.22"
jni = { version = "0.19", optional = true }
lazy_static = { version = "1.4", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.16", features = ["io-util", "net", "sync", "macros", "rt"] }
tracing = "0.1.32"
uuid = { version = "0.8", features = ["v4"] }
[dev-dependencies]
tokio = { version = "1.16", features = [
"time",
"rt-multi-thread",
"macros",
"parking_lot",
] }
tracing-subscriber = "0.3.9"
# this is a workaround to not being able to specify either multiple libs or conditional compilation based on crate-type.
[[example]]
name = "discord_rpc_java"
path = "examples/java.rs"
crate-type = ["cdylib"]
required-features = ["java-bindings"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Patrick Auernig
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,16 +1,51 @@
# Discord RPC Client
This is a fork of [https://gitlab.com/valeth/discord-rpc-client.rs](https://gitlab.com/valeth/discord-rpc-client.rs)
Discord RPC client for Rust
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
discord-rpc-client = "^0.1"
discord-rpc-client = "^0.2"
```
## Examples
Examples can be found in the examples directory in this repo.
## Example
```rust
extern crate discord_rpc_client;
use std::{env, thread, time};
use discord_rpc_client::Client;
fn main() {
// Get our main status message
let state_message = env::args().nth(1).expect("Requires at least one argument");
// Create the client
let mut drpc = Client::new(425407036495495169);
// Start up the client connection, so that we can actually send and receive stuff
drpc.start();
// Set the activity
drpc.set_activity(|act| act.state(state_message))
.expect("Failed to set activity");
// Wait 10 seconds before exiting
thread::sleep(time::Duration::from_secs(10));
}
```
> More examples can be found in the examples directory.
## Contributions
See [CONTRIBUTING.md](CONTRIBUTING.md)

View File

@ -1,22 +1,39 @@
extern crate discord_rpc_client;
#[macro_use]
extern crate tracing;
use std::{thread, time};
use discord_rpc_client::Client as DiscordRPC;
use discord_rpc_client::{models::Activity, Client as DiscordRPC};
use std::io;
fn main() {
let mut drpc =
DiscordRPC::new(425407036495495169)
.and_then(|rpc| rpc.start())
.expect("Failed to start client");
#[tokio::main]
async fn main() -> discord_rpc_client::Result<()> {
tracing_subscriber::fmt::init();
drpc.set_activity(|a| a
.state("Rusting")
.assets(|ass| ass
.large_image("ferris_wat")
.large_text("wat.")
.small_image("rusting")
.small_text("rusting...")))
.expect("Failed to set presence");
let drpc = DiscordRPC::default();
loop { thread::sleep(time::Duration::from_secs(10)) };
drpc.connect(425407036495495169).await?;
loop {
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
buf.pop();
if buf.is_empty() {
if let Err(why) = drpc.clear_activity().await {
error!("Failed to clear presence: {}", why);
}
} else {
if let Err(why) = drpc
.set_activity(Activity::new().state(buf).assets(|ass| {
ass.large_image("ferris_wat")
.large_text("wat.")
.small_image("rusting")
.small_text("rusting...")
}))
.await
{
error!("Failed to set presence: {}", why);
}
}
}
}

View File

@ -0,0 +1,34 @@
#[macro_use]
extern crate tracing;
use discord_rpc_client::{models::Event, Client as DiscordRPC};
use std::{thread, time};
#[tokio::main]
async fn main() -> discord_rpc_client::Result<()> {
tracing_subscriber::fmt::init();
let drpc = DiscordRPC::default();
drpc.connect(425407036495495169).await?;
drpc.subscribe(Event::ActivityJoin, |j| j.secret("123456"))
.await
.expect("Failed to subscribe to event");
drpc.subscribe(Event::ActivitySpectate, |s| s.secret("123456"))
.await
.expect("Failed to subscribe to event");
drpc.subscribe(Event::ActivityJoinRequest, |s| s)
.await
.expect("Failed to subscribe to event");
drpc.unsubscribe(Event::ActivityJoinRequest, |j| j)
.await
.expect("Failed to unsubscribe from event");
loop {
tokio::time::sleep(time::Duration::from_millis(500)).await;
}
}

116
examples/java.rs Normal file
View File

@ -0,0 +1,116 @@
#[macro_use]
extern crate tracing;
use discord_rpc_client::java::*;
use jni::objects::{JClass, JObject, JString};
use jni::JNIEnv;
// can't just put these in src/java.rs and re-export because of some tree-shaking that the compiler does.
#[no_mangle]
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_create<'a>(
env: JNIEnv<'a>,
class: JClass,
) -> JObject<'a> {
let _ = tracing_subscriber::fmt::try_init();
match jni_create(env, class) {
Ok(obj) => obj,
Err(e) => {
error!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
JObject::null()
}
}
}
#[no_mangle]
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_connect(
env: JNIEnv,
obj: JObject,
client_id: JString,
) -> bool {
match jni_connect(env, obj, client_id) {
Ok(_) => true,
Err(e) => {
match e.downcast::<std::io::Error>() {
Ok(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
debug!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
return false;
} else {
error!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
}
}
Err(e) => {
error!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
}
}
false
}
}
}
#[no_mangle]
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_disconnect(
env: JNIEnv,
obj: JObject,
) -> bool {
match jni_disconnect(env, obj) {
Ok(_) => true,
Err(e) => {
error!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
false
}
}
}
#[no_mangle]
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_setActivity(
env: JNIEnv,
obj: JObject,
j_activity: JObject,
) -> bool {
match jni_set_activity(env, obj, j_activity) {
Ok(_) => true,
Err(e) => {
error!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
false
}
}
}
#[no_mangle]
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_clearActivity(
env: JNIEnv,
obj: JObject,
) -> bool {
match jni_clear_activity(env, obj) {
Ok(_) => true,
Err(e) => {
error!(
concat!("at ", file!(), ":", line!(), ":", column!(), ": {}"),
e
);
false
}
}
}

3
java/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.bsp/
/target/
/project/

View File

@ -0,0 +1,61 @@
package com.discord.rpc;
public final class Activity {
public String state;
public String details;
public boolean instance;
public ActivityTimestamps timestamps;
public ActivityAssets assets;
public ActivityParty party;
public ActivitySecrets secrets;
public Activity() {
this.state = null;
this.details = null;
this.instance = false;
this.timestamps = null;
this.assets = null;
this.party = null;
this.secrets = null;
}
public Activity withState(String state) {
this.state = state;
return this;
}
public Activity withDetails(String details) {
this.details = details;
return this;
}
public Activity withInstance(boolean instance) {
this.instance = instance;
return this;
}
public Activity withTimestamps(ActivityTimestamps timestamps) {
this.timestamps = timestamps;
return this;
}
public Activity withAssets(ActivityAssets assets) {
this.assets = assets;
return this;
}
public Activity withParty(ActivityParty party) {
this.party = party;
return this;
}
public Activity withSecrets(ActivitySecrets secrets) {
this.secrets = secrets;
return this;
}
public Activity copy() {
return new Activity().withState(this.state).withDetails(this.details).withInstance(this.instance).withTimestamps(this.timestamps).withAssets(this.assets).withParty(this.party).withSecrets(this.secrets);
}
}

View File

@ -0,0 +1,9 @@
package com.discord.rpc;
public record ActivityAssets(
String largeImage,
String largeText,
String smallImage,
String smallText
) {}

View File

@ -0,0 +1,4 @@
package com.discord.rpc;
public record ActivityParty(@Unsigned int id, @Unsigned int minSize, @Unsigned int maxSize) {}

View File

@ -0,0 +1,4 @@
package com.discord.rpc;
public record ActivitySecrets(String join, String spectate, String game) {}

View File

@ -0,0 +1,4 @@
package com.discord.rpc;
public record ActivityTimestamps(@Unsigned Long start, @Unsigned Long end) {}

View File

@ -0,0 +1,39 @@
package com.discord.rpc;
import java.io.File;
public final class DiscordRPC {
private final long handle;
private DiscordRPC(long handle) {
this.handle = handle;
}
/**
* @return the new client instance, or null if an error occurred.
*/
public static native DiscordRPC create();
public native boolean connect(String clientId);
public native boolean disconnect();
public native boolean setActivity(Activity activity);
public native boolean clearActivity();
static {
final var dir = System.getProperty("com.discord.librarypath");
if (dir != null) {
System.load(dir + File.separator + System.mapLibraryName("discord_rpc"));
} else {
System.loadLibrary("discord_rpc");
}
}
/**
* This method does nothing, but ensures that the native library will be loaded.
*/
public static void initialize() {
}
}

View File

@ -0,0 +1,7 @@
package com.discord.rpc;
/**
* Marks an integer that will be re-interpreted natively as unsigned. Use Kotlin's unsigned types with these.
*/
@interface Unsigned {}

View File

@ -0,0 +1,11 @@
import org.scalatest._
import flatspec._
import matchers._
import com.discord.rpc.DiscordRPC
class DiscordRPCSpec extends AnyFlatSpec with should.Matchers {
"DiscordRPC.initialize()" should "load library and bind native methods" in {
// FIXME: this test fails because the lib isn't in the java.library.path and com.discord.librarypath is not set.
DiscordRPC.initialize()
}
}

View File

@ -1,76 +1,351 @@
use std::env;
use std::io::{Write, Read, Result};
use std::os::unix::net::UnixStream;
use std::time;
use std::fmt::Debug;
use models::{Message, Handshake, Payload};
use serde::{de::DeserializeOwned, Serialize};
#[allow(unused)]
use serde_json::Value;
use tokio::select;
use tokio::sync::watch;
use tokio::sync::Mutex;
use crate::connection::{Connection, SocketConnection};
use crate::error::{Error, Result};
#[cfg(feature = "rich_presence")]
use models::{SetActivityArgs, SetActivity};
use crate::models::rich_presence::{
Activity, CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs,
};
use crate::models::{
commands::{Subscription, SubscriptionArgs},
message::Message,
payload::Payload,
Command, Event, OpCode,
};
macro_rules! hollow {
($expr:expr) => {{
let ref_ = $expr.borrow();
ref_.hollow()
}};
}
#[derive(Debug)]
enum ConnectionState<T> {
Disconnected,
Connecting,
Connected(T),
Disconnecting,
}
impl<T: Clone> Clone for ConnectionState<T> {
fn clone(&self) -> Self {
match self {
Self::Disconnected => Self::Disconnected,
Self::Connecting => Self::Connecting,
Self::Connected(arg0) => Self::Connected(arg0.clone()),
Self::Disconnecting => Self::Disconnecting,
}
}
}
impl<T: Copy> Copy for ConnectionState<T> {}
impl<T> ConnectionState<T> {
pub fn hollow(&self) -> ConnectionState<()> {
match self {
ConnectionState::Disconnected => ConnectionState::Disconnected,
ConnectionState::Connecting => ConnectionState::Connecting,
ConnectionState::Connected(_) => ConnectionState::Connected(()),
ConnectionState::Disconnecting => ConnectionState::Disconnecting,
}
}
}
macro_rules! yield_while {
($receive:expr, $pat:pat) => {{
let mut new_state: _;
loop {
new_state = $receive;
match new_state {
$pat => tokio::task::yield_now().await,
_ => break,
}
}
new_state
}};
}
type FullConnectionState = ConnectionState<(u64, Mutex<SocketConnection>)>;
#[derive(Debug)]
pub struct Client {
pub client_id: u64,
pub version: u32,
socket: UnixStream,
state_sender: watch::Sender<FullConnectionState>,
state_receiver: watch::Receiver<FullConnectionState>,
update: Mutex<()>,
}
impl Default for Client {
fn default() -> Self {
let (state_sender, state_receiver) = watch::channel(ConnectionState::Disconnected);
Self {
state_sender,
state_receiver,
update: Mutex::new(()),
}
}
}
impl Client {
pub fn new(client_id: u64) -> Result<Self> {
let connection_name = Self::ipc_path();
let socket = UnixStream::connect(connection_name)?;
socket.set_write_timeout(Some(time::Duration::from_secs(30)))?;
socket.set_read_timeout(Some(time::Duration::from_secs(30)))?;
Ok(Self { version: 1, client_id, socket })
/// Returns the client id used by the current connection, or [`None`] if the client is not [`ConnectionState::Connected`].
pub fn client_id(&self) -> Option<u64> {
match *self.state_receiver.borrow() {
ConnectionState::Connected((client_id, _)) => Some(client_id),
_ => None,
}
}
pub fn start(mut self) -> Result<Self> {
self.handshake()?;
Ok(self)
#[instrument(level = "debug")]
async fn connect_and_handshake(client_id: u64) -> Result<SocketConnection> {
debug!("Connecting");
let mut new_connection = SocketConnection::connect().await?;
debug!("Performing handshake");
new_connection.handshake(client_id).await?;
debug!("Handshake completed");
Ok(new_connection)
}
#[instrument(level = "debug")]
async fn connect0(&self, client_id: u64, conn: Result<SocketConnection>) -> Result<()> {
let _state_guard = self.update.lock().await;
match hollow!(self.state_receiver) {
state @ ConnectionState::Disconnected => panic!(
"Illegal state during connection process {:?} -> {:?}",
ConnectionState::<()>::Connecting,
state
),
ConnectionState::Connecting => match conn {
Ok(conn) => {
self.state_sender
.send(ConnectionState::Connected((client_id, Mutex::new(conn))))
.expect("the receiver cannot be dropped without the sender!");
debug!("Connected");
Ok(())
}
Err(e) => {
self.state_sender
.send(ConnectionState::Disconnected)
.expect("the receiver cannot be dropped without the sender!");
debug!("Failed to connect and disconnected");
Err(e)
}
},
ConnectionState::Connected(_) => panic!("Illegal concurrent connection!"),
ConnectionState::Disconnecting => {
match conn {
Ok(conn) => {
if let Err(e) = conn.disconnect().await {
error!("failed to disconnect properly: {}", e);
}
}
Err(e) => {
error!("failed connection: {}", e);
}
}
self.state_sender
.send(ConnectionState::Disconnected)
.expect("the receiver cannot be dropped without the sender!");
Err(Error::ConnectionClosed)
}
}
}
#[instrument(level = "info")]
pub async fn connect(&self, client_id: u64) -> Result<()> {
match hollow!(self.state_receiver) {
ConnectionState::Connected(_) => Ok(()),
_ => {
let state_guard = self.update.lock().await;
match hollow!(self.state_receiver) {
ConnectionState::Connected(_) => Ok(()),
ConnectionState::Disconnecting => Err(Error::ConnectionClosed),
ConnectionState::Connecting => {
match yield_while!(
hollow!(self.state_receiver),
ConnectionState::Connecting
) {
ConnectionState::Connected(_) => Ok(()),
ConnectionState::Disconnecting => Err(Error::ConnectionClosed),
ConnectionState::Disconnected => Err(Error::ConnectionClosed),
ConnectionState::Connecting => unreachable!(),
}
}
ConnectionState::Disconnected => {
self.state_sender
.send(ConnectionState::Connecting)
.expect("the receiver cannot be dropped without the sender!");
drop(state_guard);
select! {
conn = Self::connect_and_handshake(client_id) => {
self.connect0(client_id, conn).await
}
// _ = tokio::task::yield_now() if self.state_receiver.borrow().is_disconnecting() => {
// self.state_sender.send(ConnectionState::Disconnected).expect("the receiver cannot be dropped without the sender!");
// Err(Error::ConnectionClosed)
// }
}
}
}
}
}
}
/// If currently connected, the function will close the connection.
/// If currently connecting, the function will wait for the connection to be established and will immediately close it.
/// If currently disconnecting, the function will wait for the connection to be closed.
#[instrument(level = "info")]
pub async fn disconnect(&self) {
let _state_guard = self.update.lock().await;
trace!("aquired state guard for disconnect");
match hollow!(self.state_receiver) {
ConnectionState::Disconnected => {}
ref state @ ConnectionState::Disconnecting => {
trace!("Waiting while in disconnecting state(b)");
match yield_while!(hollow!(self.state_receiver), ConnectionState::Disconnecting) {
ConnectionState::Disconnected => {}
ConnectionState::Disconnecting => unreachable!(),
new_state => panic!("Illegal state change {:?} -> {:?}", state, new_state),
}
}
ConnectionState::Connecting => {
self.state_sender
.send(ConnectionState::Disconnecting)
.expect("the receiver cannot be dropped without the sender!");
}
state @ ConnectionState::Connected(()) => {
trace!("Sending disconnecting state");
let s = self
.state_sender
.send_replace(ConnectionState::Disconnecting);
trace!("Sent disconnecting state");
match s {
ConnectionState::Connected(conn) => {
match conn.1.into_inner().disconnect().await {
Err(e) => {
error!("failed to disconnect properly: {}", e);
}
_ => self
.state_sender
.send(ConnectionState::Disconnected)
.expect("the receiver cannot be dropped without the sender!"),
}
}
new_state @ ConnectionState::Connecting => {
panic!("Illegal state change {:?} -> {:?}", state, new_state);
}
state @ ConnectionState::Disconnecting => {
trace!("Waiting while in disconnecting state(b)");
match yield_while!(
hollow!(self.state_receiver),
ConnectionState::Disconnecting
) {
ConnectionState::Disconnected => {}
ConnectionState::Disconnecting => unreachable!(),
new_state => {
panic!("Illegal state change {:?} -> {:?}", state, new_state)
}
}
}
ConnectionState::Disconnected => {}
}
}
}
}
#[instrument(level = "info")]
async fn execute<A, E>(&self, cmd: Command, args: A, evt: Option<Event>) -> Result<Payload<E>>
where
A: std::fmt::Debug + Serialize + Send + Sync,
E: std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync,
{
let message = Message::new(
OpCode::Frame,
Payload::with_nonce(cmd, Some(args), None, evt),
);
let result = match &*self.state_receiver.borrow() {
ConnectionState::Connected((_, conn)) => {
try {
let mut conn = conn.lock().await;
conn.send(message).await?;
conn.recv().await?
}
}
_ => Err(Error::ConnectionClosed),
};
let Message { payload, .. } = match result {
Ok(msg) => Ok(msg),
Err(e @ Error::ConnectionClosed | e @ Error::ConnectionClosedWhileSending(_)) => {
debug!("disconnecting because connection is closed.");
self.disconnect().await;
Err(e)
}
Err(e) => Err(e),
}?;
let response: Payload<E> = serde_json::from_str(&payload)?;
match response.evt {
Some(Event::Error) => Err(Error::SubscriptionFailed),
_ => Ok(response),
}
}
#[cfg(feature = "rich_presence")]
pub fn set_activity<F>(&mut self, f: F) -> Result<()>
where F: FnOnce(SetActivity) -> SetActivity
pub async fn set_activity(&self, activity: Activity) -> Result<Payload<Activity>> {
self.execute(Command::SetActivity, SetActivityArgs::new(activity), None)
.await
}
#[cfg(feature = "rich_presence")]
pub async fn clear_activity(&self) -> Result<Payload<Activity>> {
self.execute(Command::SetActivity, SetActivityArgs::default(), None)
.await
}
// NOTE: Not sure what the actual response values of
// SEND_ACTIVITY_JOIN_INVITE and CLOSE_ACTIVITY_REQUEST are,
// they are not documented.
#[cfg(feature = "rich_presence")]
pub async fn send_activity_join_invite(&self, user_id: u64) -> Result<Payload<Value>> {
self.execute(
Command::SendActivityJoinInvite,
SendActivityJoinInviteArgs::new(user_id),
None,
)
.await
}
#[cfg(feature = "rich_presence")]
pub async fn close_activity_request(&self, user_id: u64) -> Result<Payload<Value>> {
self.execute(
Command::CloseActivityRequest,
CloseActivityRequestArgs::new(user_id),
None,
)
.await
}
pub async fn subscribe<F>(&self, evt: Event, f: F) -> Result<Payload<Subscription>>
where
F: FnOnce(SubscriptionArgs) -> SubscriptionArgs,
{
let args = SetActivityArgs::command(f(SetActivity::new()));
self.send(1, args)?;
Ok(())
self.execute(Command::Subscribe, f(SubscriptionArgs::new()), Some(evt))
.await
}
// private
fn handshake(&mut self) -> Result<()> {
let client_id = self.client_id;
let version = self.version;
self.send(0, Handshake::new(client_id, version))?;
Ok(())
}
fn ipc_path() -> String {
let tmp = env::var("XDG_RUNTIME_DIR").unwrap_or("/tmp".into());
format!("{}/discord-ipc-0", tmp)
}
fn send<T>(&mut self, opcode: u32, payload: T) -> Result<()>
where T: Payload + Debug
pub async fn unsubscribe<F>(&self, evt: Event, f: F) -> Result<Payload<Subscription>>
where
F: FnOnce(SubscriptionArgs) -> SubscriptionArgs,
{
println!("payload: {:#?}", payload);
match Message::new(opcode, payload).encode() {
Err(why) => println!("{:?}", why),
Ok(bytes) => {
self.socket.write_all(bytes.as_ref())?;
println!("sent opcode: {}", opcode);
self.receive()?;
}
};
Ok(())
}
fn receive(&mut self) -> Result<()> {
let mut buf: Vec<u8> = Vec::with_capacity(1024);
self.socket.read(buf.as_mut_slice())?;
println!("{:?}", buf);
Ok(())
self.execute(Command::Unsubscribe, f(SubscriptionArgs::new()), Some(evt))
.await
}
}

90
src/connection/base.rs Normal file
View File

@ -0,0 +1,90 @@
use std::{marker::Sized, path::PathBuf};
use bytes::BytesMut;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::error::{Error, Result};
use crate::models::message::{Message, OpCode};
use crate::utils;
#[async_trait::async_trait]
pub trait Connection: Sized + Send {
type Socket: AsyncWrite + AsyncRead + Unpin + Send;
fn socket(&mut self) -> &mut Self::Socket;
fn ipc_path() -> PathBuf;
async fn connect() -> Result<Self>;
async fn disconnect(self) -> Result<()>;
fn socket_path(n: u8) -> PathBuf {
Self::ipc_path().join(format!("discord-ipc-{}", n))
}
async fn handshake(&mut self, client_id: u64) -> Result<()> {
let hs = json![{
"client_id": client_id.to_string(),
"v": 1,
"nonce": utils::nonce()
}];
self.send(Message::new(OpCode::Handshake, hs.clone()))
.await?;
self.recv().await?;
Ok(())
}
async fn ping(&mut self) -> Result<OpCode> {
let message = Message::new(OpCode::Ping, json![{}]);
self.send(message).await?;
let response = self.recv().await?;
Ok(response.opcode)
}
async fn send(&mut self, message: Message) -> Result<()> {
let bytes = message.encode()?;
match self.socket().write_all(bytes.as_ref()).await {
Ok(()) => {}
Err(e) => {
return match e.kind() {
std::io::ErrorKind::BrokenPipe => {
Err(Error::ConnectionClosedWhileSending(message))
}
_ => Err(e.into()),
}
}
}
debug!("-> {:?}", message);
Ok(())
}
async fn recv(&mut self) -> Result<Message> {
let mut buf = BytesMut::new();
buf.resize(1024, 0);
let n = match self.socket().read(&mut buf).await {
Ok(n) => n,
Err(e) => {
return match e.kind() {
std::io::ErrorKind::BrokenPipe => Err(Error::ConnectionClosed),
_ => Err(e.into()),
}
}
};
debug!("Received {} bytes", n);
if n == 0 {
return Err(Error::ConnectionClosed);
}
buf = buf.split_to(n);
let message = Message::decode(&buf)?;
debug!("<- {:?}", message);
Ok(message)
}
}

11
src/connection/mod.rs Normal file
View File

@ -0,0 +1,11 @@
mod base;
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
pub use self::base::Connection;
#[cfg(unix)]
pub use self::unix::UnixConnection as SocketConnection;
#[cfg(windows)]
pub use self::windows::WindowsConnection as SocketConnection;

48
src/connection/unix.rs Normal file
View File

@ -0,0 +1,48 @@
use std::{env, path::PathBuf};
use tokio::{io::AsyncWriteExt, net::UnixStream};
use super::base::Connection;
use crate::error::Result;
#[derive(Debug)]
pub struct UnixConnection {
socket: UnixStream,
}
#[async_trait::async_trait]
impl Connection for UnixConnection {
type Socket = UnixStream;
fn socket(&mut self) -> &mut Self::Socket {
&mut self.socket
}
fn ipc_path() -> PathBuf {
env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.map(|mut p| {
// try flatpak dir
p.push("app/com.discordapp.Discord");
if !p.exists() {
p.pop();
p.pop();
}
p
})
.or_else(|| env::var_os("TMPDIR").map(PathBuf::from))
.or_else(|| env::temp_dir().to_str().map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("/tmp"))
}
async fn connect() -> Result<Self> {
let connection_name = Self::socket_path(0);
let socket = UnixStream::connect(connection_name).await?;
Ok(Self { socket })
}
async fn disconnect(mut self) -> Result<()> {
self.socket.shutdown().await?;
Ok(())
}
}

35
src/connection/windows.rs Normal file
View File

@ -0,0 +1,35 @@
use std::path::PathBuf;
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
use super::base::Connection;
use crate::error::Result;
#[derive(Debug)]
pub struct WindowsConnection {
socket: NamedPipeClient,
}
#[async_trait::async_trait]
impl Connection for WindowsConnection {
type Socket = NamedPipeClient;
fn socket(&mut self) -> &mut Self::Socket {
&mut self.socket
}
fn ipc_path() -> PathBuf {
PathBuf::from(r"\\.\pipe\")
}
async fn connect() -> Result<Self> {
let connection_name = Self::socket_path(0);
Ok(Self {
socket: ClientOptions::new().open(connection_name)?,
})
}
async fn disconnect(mut self) -> Result<()> {
Ok(())
}
}

68
src/error.rs Normal file
View File

@ -0,0 +1,68 @@
use serde_json::Error as JsonError;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io::Error as IoError,
result::Result as StdResult,
sync::mpsc::RecvTimeoutError as ChannelTimeout,
};
use tokio::sync::mpsc::error::SendError;
use crate::models::Message;
#[derive(Debug)]
pub enum Error {
IoError(IoError),
JsonError(JsonError),
Timeout(ChannelTimeout),
Conversion,
SubscriptionFailed,
ConnectionClosed,
ConnectionClosedWhileSending(Message),
Busy,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Conversion => f.write_str("Failed to convert values"),
Self::SubscriptionFailed => f.write_str("Failed to subscribe to event"),
Self::ConnectionClosed => f.write_str("Connection closed"),
Self::ConnectionClosedWhileSending(msg) => {
write!(f, "Connection closed while sending {:?}", msg)
}
Self::Busy => f.write_str("A resource was busy"),
Self::IoError(err) => write!(f, "{}", err),
Self::JsonError(err) => write!(f, "{}", err),
Self::Timeout(err) => write!(f, "{}", err),
}
}
}
impl StdError for Error {}
impl From<IoError> for Error {
fn from(err: IoError) -> Self {
Self::IoError(err)
}
}
impl From<JsonError> for Error {
fn from(err: JsonError) -> Self {
Self::JsonError(err)
}
}
impl From<ChannelTimeout> for Error {
fn from(err: ChannelTimeout) -> Self {
Self::Timeout(err)
}
}
impl From<SendError<Message>> for Error {
fn from(err: SendError<Message>) -> Self {
Self::ConnectionClosedWhileSending(err.0)
}
}
pub type Result<T, E = Error> = StdResult<T, E>;

279
src/java.rs Normal file
View File

@ -0,0 +1,279 @@
#![allow(clippy::await_holding_lock)]
use std::sync::MutexGuard;
use anyhow::Result;
use jni::objects::{JClass, JObject, JString, JValue};
use jni::signature::{JavaType, Primitive};
use jni::JNIEnv;
pub use jni;
use crate as drpc;
mod jvm_types {
pub const BOOLEAN: &str = "java/lang/Boolean";
pub const INTEGER: &str = "java/lang/Integer";
pub const LONG: &str = "java/lang/Long";
pub const STRING: &str = "java/lang/String";
}
macro_rules! signature {
(bool) => {
"Z"
};
(i32) => {
"I"
};
(i64) => {
"J"
};
(class $path:expr) => {
const_format::formatcp!("L{};", $path)
};
}
const PATH_DISCORD_RPC: &str = "com/discord/rpc/DiscordRPC";
lazy_static::lazy_static! {
static ref RUNTIME: tokio::runtime::Runtime = tokio::runtime::Runtime::new().expect("unable to create tokio runtime");
}
fn is_null<'b, O>(env: JNIEnv, ref1: O) -> jni::errors::Result<bool>
where
O: Into<JObject<'b>>,
{
env.is_same_object(ref1, JObject::null())
}
// taking the [`JNIEnv`] as a reference is needed to satisfy the lifetime checker.
fn get_client<'a>(env: &'a JNIEnv<'a>, obj: JObject<'a>) -> Result<MutexGuard<'a, drpc::Client>> {
if !env.is_instance_of(obj, PATH_DISCORD_RPC)? {
bail!("not an instance of DiscordRPC");
}
let client = env.get_rust_field::<_, _, drpc::Client>(obj, "handle")?;
Ok(client)
}
// TODO: method to destory afterwards.
#[inline(always)]
pub fn jni_create<'a>(env: JNIEnv<'a>, _class: JClass) -> Result<JObject<'a>> {
let client = drpc::Client::default();
let jobj = env.alloc_object(PATH_DISCORD_RPC)?;
env.set_rust_field(jobj, "handle", client)?;
Ok(jobj)
}
#[inline(always)]
pub fn jni_connect(env: JNIEnv, obj: JObject, client_id: JString) -> Result<()> {
let client = get_client(&env, obj)?;
let client_id = env.get_string(client_id)?.to_str()?.parse::<u64>()?;
if let Some(current_client_id) = client.client_id() {
if current_client_id != client_id {
RUNTIME.block_on(async { client.disconnect().await });
}
}
RUNTIME.block_on(async { client.connect(client_id).await })?;
Ok(())
}
#[inline(always)]
pub fn jni_disconnect(env: JNIEnv, obj: JObject) -> Result<()> {
let client = get_client(&env, obj)?;
RUNTIME.block_on(async { client.disconnect().await });
Ok(())
}
#[inline(always)]
pub fn jni_set_activity(env: JNIEnv, obj: JObject, j_activity: JObject) -> Result<()> {
let client = get_client(&env, obj)?;
let activity = jobject_to_activity(env, j_activity)?;
RUNTIME.block_on(async { client.set_activity(activity).await })?;
Ok(())
}
#[inline(always)]
pub fn jni_clear_activity(env: JNIEnv, obj: JObject) -> Result<()> {
let client = get_client(&env, obj)?;
RUNTIME.block_on(async { client.clear_activity().await })?;
Ok(())
}
fn jobject_to_activity(env: JNIEnv, jobject: JObject) -> Result<drpc::models::Activity> {
let j_state = env.get_field(jobject, "state", signature!(class jvm_types::STRING))?;
let j_details = env.get_field(jobject, "details", signature!(class jvm_types::STRING))?;
let j_instance = env.get_field(jobject, "instance", signature!(bool))?;
let j_timestamps = env.get_field(
jobject,
"timestamps",
signature!(class "com/discord/rpc/ActivityTimestamps"),
)?;
let j_assets = env.get_field(jobject, "assets", signature!(class "com/discord/rpc/ActivityAssets"))?;
let j_party = env.get_field(jobject, "party", signature!(class "com/discord/rpc/ActivityParty"))?;
let j_secrets = env.get_field(jobject, "secrets", signature!(class "com/discord/rpc/ActivitySecrets"))?;
let mut activity = drpc::models::Activity::new();
if let JValue::Object(obj) = j_state {
if !is_null(env, obj)? {
activity = activity.state(env.get_string(obj.into())?);
}
}
if let JValue::Object(obj) = j_details {
if !is_null(env, obj)? {
activity = activity.details(env.get_string(obj.into())?);
}
}
if let JValue::Bool(b) = j_instance {
if b != 0 {
activity = activity.instance(true);
}
}
if let JValue::Object(obj) = j_timestamps {
if !is_null(env, obj)? {
let timestamps = jobject_to_activity_timestamps(env, obj)?;
activity = activity.timestamps(|_| timestamps);
}
}
if let JValue::Object(obj) = j_assets {
if !is_null(env, obj)? {
let assets = jobject_to_activity_assets(env, obj)?;
activity = activity.assets(|_| assets);
}
}
if let JValue::Object(obj) = j_party {
if !is_null(env, obj)? {
let party = jobject_to_activity_party(env, obj)?;
activity = activity.party(|_| party);
}
}
if let JValue::Object(obj) = j_secrets {
if !is_null(env, obj)? {
let secrets = jobject_to_activity_secrets(env, obj)?;
activity = activity.secrets(|_| secrets);
}
}
Ok(activity)
}
fn jobject_to_activity_timestamps(
env: JNIEnv,
jobject: JObject,
) -> Result<drpc::models::ActivityTimestamps> {
let j_start = env.get_field(jobject, "start", signature!(class jvm_types::LONG))?;
let j_end = env.get_field(jobject, "end", signature!(class jvm_types::LONG))?;
let mut timestamps = drpc::models::ActivityTimestamps::new();
if let JValue::Object(obj) = j_start {
if !is_null(env, obj)? {
if let JValue::Long(l) = env.call_method_unchecked(
obj,
(obj, "longValue", "()J"),
JavaType::Primitive(Primitive::Long),
&[],
)? {
timestamps = timestamps.start(l as u64);
}
}
}
if let JValue::Object(obj) = j_end {
if !is_null(env, obj)? {
if let JValue::Long(l) = env.call_method_unchecked(
obj,
(obj, "longValue", "()J"),
JavaType::Primitive(Primitive::Long),
&[],
)? {
timestamps = timestamps.end(l as u64);
}
}
}
Ok(timestamps)
}
fn jobject_to_activity_assets(
env: JNIEnv,
jobject: JObject,
) -> Result<drpc::models::ActivityAssets> {
let j_lrg_img = env.get_field(jobject, "largeImage", signature!(class jvm_types::STRING))?;
let j_lrg_txt = env.get_field(jobject, "largeText", signature!(class jvm_types::STRING))?;
let j_sml_img = env.get_field(jobject, "smallImage", signature!(class jvm_types::STRING))?;
let j_sml_txt = env.get_field(jobject, "smallText", signature!(class jvm_types::STRING))?;
let mut assets = drpc::models::ActivityAssets::new();
if let JValue::Object(obj) = j_lrg_img {
if !is_null(env, obj)? {
assets = assets.large_image(env.get_string(obj.into())?);
}
}
if let JValue::Object(obj) = j_lrg_txt {
if !is_null(env, obj)? {
assets = assets.large_text(env.get_string(obj.into())?);
}
}
if let JValue::Object(obj) = j_sml_img {
if !is_null(env, obj)? {
assets = assets.small_image(env.get_string(obj.into())?);
}
}
if let JValue::Object(obj) = j_sml_txt {
if !is_null(env, obj)? {
assets = assets.small_text(env.get_string(obj.into())?);
}
}
Ok(assets)
}
fn jobject_to_activity_party(env: JNIEnv, jobject: JObject) -> Result<drpc::models::ActivityParty> {
let j_id = env.get_field(jobject, "id", signature!(i32))?;
let j_min_size = env.get_field(jobject, "minSize", signature!(i32))?;
let j_max_size = env.get_field(jobject, "maxSize", signature!(i32))?;
let mut party = drpc::models::ActivityParty::new();
if let JValue::Int(l) = j_id {
party = party.id(l as u32);
}
if let (JValue::Int(l1), JValue::Int(l2)) = (j_min_size, j_max_size) {
party = party.size((l1 as u32, l2 as u32));
}
Ok(party)
}
fn jobject_to_activity_secrets(
env: JNIEnv,
jobject: JObject,
) -> Result<drpc::models::ActivitySecrets> {
let j_join = env.get_field(jobject, "join", signature!(class jvm_types::STRING))?;
let j_spectate = env.get_field(jobject, "spectate", signature!(class jvm_types::STRING))?;
let j_game = env.get_field(jobject, "game", signature!(class jvm_types::STRING))?;
let mut secrets = drpc::models::ActivitySecrets::new();
if let JValue::Object(obj) = j_join {
if !is_null(env, obj)? {
secrets = secrets.join(env.get_string(obj.into())?);
}
}
if let JValue::Object(obj) = j_spectate {
if !is_null(env, obj)? {
secrets = secrets.spectate(env.get_string(obj.into())?);
}
}
if let JValue::Object(obj) = j_game {
if !is_null(env, obj)? {
secrets = secrets.game(env.get_string(obj.into())?);
}
}
Ok(secrets)
}

View File

@ -1,13 +1,26 @@
#![feature(getpid)]
#![feature(try_blocks)]
#[macro_use]
extern crate serde_derive;
extern crate anyhow;
#[macro_use]
extern crate serde;
#[macro_use]
extern crate serde_json;
extern crate byte;
extern crate uuid;
#[macro_use]
extern crate tracing;
mod models;
mod client;
#[macro_use]
mod macros;
pub mod client;
mod connection;
mod error;
pub mod models;
mod utils;
pub use client::Client;
pub use connection::{Connection, SocketConnection};
pub use error::*;
#[cfg(feature = "java-bindings")]
pub mod java;

81
src/macros.rs Normal file
View File

@ -0,0 +1,81 @@
macro_rules! builder_func {
[ $name:ident, $type:tt func ] => {
pub fn $name<F>(mut self, func: F) -> Self
where F: FnOnce($type) -> $type
{
self.$name = Some(func($type::default())); self
}
};
[ $name:ident, String ] => {
pub fn $name<S>(mut self, value: S) -> Self
where S: Into<String>
{
self.$name = Some(value.into()); self
}
};
[ $name:ident, $type:ty ] => {
pub fn $name(mut self, value: $type) -> Self {
self.$name = Some(value); self
}
};
}
macro_rules! builder {
[ @st ( $name:ident $field:tt: $type:tt alias = $alias:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
builder![ @st
( $name $($rest)* ) -> (
$($out)*
#[serde(skip_serializing_if = "Option::is_none", rename = $alias)]
pub $field: Option<$type>,
)
];
};
[ @st ( $name:ident $field:tt: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
builder![ @st ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ];
};
[ @st ( $name:ident $field:ident: $type:ty, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
builder![ @st
( $name $($rest)* ) -> (
$($out)*
#[serde(skip_serializing_if = "Option::is_none")]
$field: Option<$type>,
)
];
};
[ @st ( $name:ident ) -> ( $($out:tt)* ) ] => {
#[derive(Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct $name { $($out)* }
};
[ @im ( $name:ident $field:ident: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type func]; $($out)* ) ];
};
[ @im ( $name:ident $field:ident: $type:tt alias = $modifier:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
builder![ @im ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ];
};
[ @im ( $name:ident $field:ident: $type:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type]; $($out)* ) ];
};
[ @im ( $name:ident ) -> ( $($out:tt)* ) ] => {
impl $name {
pub fn new() -> Self {
Self::default()
}
$($out)*
}
};
[ $name:ident $($body:tt)* ] => {
builder![@st ( $name $($body)* ) -> () ];
builder![@im ( $name $($body)* ) -> () ];
}
}

View File

@ -1,29 +0,0 @@
use serde::Serialize;
use uuid::Uuid;
use super::Payload;
#[derive(Debug, Default, Serialize)]
pub struct Command<T>
where T: Serialize
{
pub nonce: String,
pub cmd: String,
pub args: T,
}
impl<T> Command<T>
where T: Serialize
{
pub fn new<S>(cmd: S, args: T) -> Self
where S: Into<String>
{
Command {
cmd: cmd.into(),
nonce: Uuid::new_v4().to_string(),
args: args
}
}
}
impl<T> Payload for Command<T>
where T: Serialize {}

10
src/models/commands.rs Normal file
View File

@ -0,0 +1,10 @@
use super::shared::PartialUser;
builder! {SubscriptionArgs
secret: String, // Activity{Join,Spectate}
user: PartialUser, // ActivityJoinRequest
}
builder! {Subscription
evt: String,
}

18
src/models/events.rs Normal file
View File

@ -0,0 +1,18 @@
use super::shared::PartialUser;
builder! {ReadyEvent
v: u32,
config: RpcServerConfiguration,
user: PartialUser,
}
builder! {ErrorEvent
code: u32,
message: String,
}
builder! {RpcServerConfiguration
cdn_host: String,
api_endpoint: String,
environment: String,
}

View File

@ -1,21 +0,0 @@
use uuid::Uuid;
use super::Payload;
#[derive(Debug, Default, Serialize)]
pub struct Handshake {
pub nonce: String,
pub v: u32,
pub client_id: String,
}
impl Handshake {
pub fn new(client_id: u64, version: u32) -> Self {
Self {
nonce: Uuid::new_v4().to_string(),
v: version,
client_id: client_id.to_string()
}
}
}
impl Payload for Handshake {}

View File

@ -1,56 +1,71 @@
use byte::{TryRead, TryWrite, BytesExt, Result};
use byte::ctx::{Endian, LE, Str};
use serde_json;
use std::io::{self, Read, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use serde::Serialize;
use serde_json;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[repr(C)]
use crate::error::{Error, Result};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum OpCode {
Handshake,
Frame,
Close,
Ping,
Pong,
}
impl TryFrom<u32> for OpCode {
type Error = Error;
fn try_from(int: u32) -> Result<Self, Self::Error> {
match int {
0 => Ok(OpCode::Handshake),
1 => Ok(OpCode::Frame),
2 => Ok(OpCode::Close),
3 => Ok(OpCode::Ping),
4 => Ok(OpCode::Pong),
_ => Err(Error::Conversion),
}
}
}
#[derive(Debug, PartialEq)]
pub struct Message {
opcode: u32,
message: String,
}
impl<'a> TryRead<'a, Endian> for Message {
fn try_read(bytes: &'a [u8], endian: Endian) -> Result<(Self, usize)> {
let offset = &mut 0;
let opcode: u32 = bytes.read_with(offset, endian)?;
let message_length = bytes.read_with::<u32>(offset, endian)? as usize;
let message = bytes.read_with::<&str>(offset, Str::Len(message_length))?.to_string();
Ok((Message { opcode, message }, *offset))
}
}
impl TryWrite<Endian> for Message {
fn try_write(self, bytes: &mut [u8], endian: Endian) -> Result<usize> {
let offset = &mut 0;
bytes.write_with::<u32>(offset, self.opcode, endian)?;
bytes.write_with::<u32>(offset, self.message.len() as u32, endian)?;
bytes.write_with::<&str>(offset, self.message.as_ref(), ())?;
Ok(*offset)
}
pub opcode: OpCode,
pub payload: String,
}
impl Message {
pub fn new<T>(opcode: u32, message: T) -> Self
where T: Serialize
pub fn new<T>(opcode: OpCode, payload: T) -> Self
where
T: Serialize,
{
Message {
Self {
opcode,
message: serde_json::to_string(&message).unwrap()
payload: serde_json::to_string(&payload).unwrap(),
}
}
pub fn encode(&self) -> Result<Vec<u8>> {
let mut bytes: Vec<u8> = vec![0; 2*4+self.message.len()];
bytes.write_with(&mut 0, self.clone(), LE)?;
bytes.shrink_to_fit();
let mut bytes: Vec<u8> = vec![];
bytes.write_u32::<LittleEndian>(self.opcode as u32)?;
bytes.write_u32::<LittleEndian>(self.payload.len() as u32)?;
write!(bytes, "{}", self.payload)?;
Ok(bytes)
}
#[allow(dead_code)]
pub fn decode<'a>(bytes: &'a [u8]) -> Result<Self> {
let message: Message = bytes.read_with(&mut 0, LE)?;
Ok(message)
pub fn decode(bytes: &[u8]) -> Result<Self> {
let mut reader = io::Cursor::new(bytes);
let mut payload = String::new();
let opcode = OpCode::try_from(reader.read_u32::<LittleEndian>()?)?;
reader.read_u32::<LittleEndian>()?;
reader.read_to_string(&mut payload)?;
Ok(Self { opcode, payload })
}
}
@ -60,14 +75,21 @@ mod tests {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Something {
empty: bool
empty: bool,
}
#[test]
fn test_encoder() {
let msg = Message::new(1, Something { empty: true });
let msg = Message::new(OpCode::Frame, Something { empty: true });
let encoded = msg.encode().unwrap();
let decoded = Message::decode(encoded.as_ref()).unwrap();
let decoded = Message::decode(&encoded).unwrap();
assert_eq!(msg, decoded);
}
#[test]
fn test_opcode() {
assert_eq!(OpCode::try_from(0).ok(), Some(OpCode::Handshake));
assert_eq!(OpCode::try_from(4).ok(), Some(OpCode::Pong));
assert_eq!(OpCode::try_from(5).ok(), None);
}
}

View File

@ -1,22 +1,53 @@
mod message;
mod command;
mod handshake;
#[cfg(feature = "rich_presence")]
mod set_activity;
pub mod commands;
pub mod events;
pub mod message;
pub mod payload;
pub mod rich_presence;
mod shared;
use serde::Serialize;
pub use self::message::Message;
pub use self::command::Command;
pub use self::handshake::Handshake;
#[cfg(feature = "rich_presence")]
pub use self::set_activity::*;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Command {
Dispatch,
Authorize,
Subscribe,
Unsubscribe,
#[cfg(feature = "rich_presence")]
SetActivity,
#[cfg(feature = "rich_presence")]
SendActivityJoinInvite,
#[cfg(feature = "rich_presence")]
CloseActivityRequest,
}
pub trait Payload: Serialize {}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Event {
Ready,
Error,
#[cfg(feature = "rich_presence")]
ActivityJoin,
#[cfg(feature = "rich_presence")]
ActivitySpectate,
#[cfg(feature = "rich_presence")]
ActivityJoinRequest,
}
pub use self::commands::*;
pub use self::events::*;
pub use self::message::{Message, OpCode};
#[cfg(feature = "rich_presence")]
pub use self::rich_presence::*;
pub mod prelude {
pub use super::message::Message;
pub use super::command::Command;
pub use super::handshake::Handshake;
pub use super::commands::{Subscription, SubscriptionArgs};
pub use super::events::{ErrorEvent, ReadyEvent};
#[cfg(feature = "rich_presence")]
pub use super::set_activity::SetActivity;
pub use super::rich_presence::{
ActivityJoinEvent, ActivityJoinRequestEvent, ActivitySpectateEvent,
CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs,
};
pub use super::Command;
pub use super::Event;
}

51
src/models/payload.rs Normal file
View File

@ -0,0 +1,51 @@
use std::convert::From;
use serde::{de::DeserializeOwned, Serialize};
use serde_json;
use super::{Command, Event, Message};
use crate::utils;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Payload<T>
where
T: Serialize,
{
pub cmd: Command,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evt: Option<Event>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
}
impl<T> Payload<T>
where
T: Serialize,
{
pub fn with_nonce(cmd: Command, args: Option<T>, data: Option<T>, evt: Option<Event>) -> Self {
Self {
cmd,
args,
data,
evt,
nonce: Some(utils::nonce()),
}
}
}
impl<T> From<Message> for Payload<T>
where
T: Serialize + DeserializeOwned,
{
fn from(message: Message) -> Self {
serde_json::from_str(&message.payload).unwrap()
}
}

159
src/models/rich_presence.rs Normal file
View File

@ -0,0 +1,159 @@
#![cfg(feature = "rich_presence")]
use std::default::Default;
use super::shared::PartialUser;
use crate::utils;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct SetActivityArgs {
pid: u32,
#[serde(skip_serializing_if = "Option::is_none")]
activity: Option<Activity>,
}
impl SetActivityArgs {
pub fn new(activity: Activity) -> Self {
Self {
pid: utils::pid(),
activity: Some(activity),
}
}
}
impl Default for SetActivityArgs {
fn default() -> Self {
Self {
pid: utils::pid(),
activity: None,
}
}
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct SendActivityJoinInviteArgs {
pub user_id: String,
}
pub type CloseActivityRequestArgs = SendActivityJoinInviteArgs;
impl SendActivityJoinInviteArgs {
pub fn new(user_id: u64) -> Self {
Self {
user_id: user_id.to_string(),
}
}
}
builder! {ActivityJoinEvent
secret: String,
}
builder! {ActivitySpectateEvent
secret: String,
}
builder! {ActivityJoinRequestEvent
user: PartialUser,
}
// TODO: remove this stupid builder func pattern.
builder! {Activity
state: String,
details: String,
instance: bool,
timestamps: ActivityTimestamps func,
assets: ActivityAssets func,
party: ActivityParty func,
secrets: ActivitySecrets func,
}
builder! {ActivityTimestamps
start: u64,
end: u64,
}
builder! {ActivityAssets
large_image: String,
large_text: String,
small_image: String,
small_text: String,
}
builder! {ActivityParty
id: u32,
size: (u32, u32),
}
builder! {ActivitySecrets
join: String,
spectate: String,
game: String alias = "match",
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
const FULL_JSON: &'static str = r###"{
"state": "rusting",
"details": "detailed",
"instance": true,
"timestamps": {
"start": 1000,
"end": 2000
},
"assets": {
"large_image": "ferris",
"large_text": "Ferris",
"small_image": "rusting",
"small_text": "Rusting..."
},
"party": {
"id": 1,
"size": [
3,
6
]
},
"secrets": {
"join": "025ed05c71f639de8bfaa0d679d7c94b2fdce12f",
"spectate": "e7eb30d2ee025ed05c71ea495f770b76454ee4e0",
"match": "4b2fdce12f639de8bfa7e3591b71a0d679d7c93f"
}
}"###;
#[test]
fn test_serialize_full_activity() {
let activity = Activity::new()
.state("rusting")
.details("detailed")
.instance(true)
.timestamps(|t| t.start(1000).end(2000))
.assets(|a| {
a.large_image("ferris")
.large_text("Ferris")
.small_image("rusting")
.small_text("Rusting...")
})
.party(|p| p.id(1).size((3, 6)))
.secrets(|s| {
s.join("025ed05c71f639de8bfaa0d679d7c94b2fdce12f")
.spectate("e7eb30d2ee025ed05c71ea495f770b76454ee4e0")
.game("4b2fdce12f639de8bfa7e3591b71a0d679d7c93f")
});
let json = serde_json::to_string_pretty(&activity).unwrap();
assert_eq![json, FULL_JSON];
}
#[test]
fn test_serialize_empty_activity() {
let activity = Activity::new();
let json = serde_json::to_string(&activity).unwrap();
assert_eq![json, "{}"];
}
}

View File

@ -1,218 +0,0 @@
use std::process::id as pid;
use models::prelude::Command;
#[derive(Debug, Default, Serialize)]
pub struct SetActivityArgs {
pub pid: u32,
pub activity: SetActivity,
}
impl SetActivityArgs {
pub fn command(args: SetActivity) -> Command<Self> {
Command::new("SET_ACTIVITY", Self {
pid: pid(),
activity: args
})
}
}
#[derive(Debug, Default, Serialize)]
pub struct SetActivity {
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamps: Option<ActivityTimestamps>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assets: Option<ActivityAssets>,
#[serde(skip_serializing_if = "Option::is_none")]
pub party: Option<ActivityParty>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secrets: Option<ActivitySecrets>,
}
impl SetActivity {
pub fn new() -> Self {
Self::default()
}
pub fn state<S>(mut self, s: S) -> Self
where S: Into<String>
{
self.state = Some(s.into());
self
}
pub fn details<S>(mut self, d: S) -> Self
where S: Into<String>
{
self.details = Some(d.into());
self
}
pub fn instance(mut self, i: bool) -> Self {
self.instance = Some(i);
self
}
pub fn timestamps<F>(mut self, f: F) -> Self
where F: FnOnce(ActivityTimestamps) -> ActivityTimestamps
{
self.timestamps = Some(f(ActivityTimestamps::default()));
self
}
pub fn assets<F>(mut self, f: F) -> Self
where F: FnOnce(ActivityAssets) -> ActivityAssets
{
self.assets = Some(f(ActivityAssets::default()));
self
}
pub fn party<F>(mut self, f: F) -> Self
where F: FnOnce(ActivityParty) -> ActivityParty
{
self.party = Some(f(ActivityParty::default()));
self
}
pub fn secrets<F>(mut self, f: F) -> Self
where F: FnOnce(ActivitySecrets) -> ActivitySecrets
{
self.secrets = Some(f(ActivitySecrets::default()));
self
}
}
#[derive(Debug, Default, Serialize)]
pub struct ActivityTimestamps {
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<u32>,
}
impl ActivityTimestamps {
pub fn start(mut self, i: u32) -> Self {
self.start = Some(i);
self
}
pub fn end(mut self, i: u32) -> Self {
self.end = Some(i);
self
}
}
#[derive(Debug, Default, Serialize)]
pub struct ActivityAssets {
#[serde(skip_serializing_if = "Option::is_none")]
pub large_image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub large_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub small_image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub small_text: Option<String>,
}
impl ActivityAssets {
pub fn large_image<S>(mut self, i: S) -> Self
where S: Into<String>
{
self.large_image = Some(i.into());
self
}
pub fn large_text<S>(mut self, t: S) -> Self
where S: Into<String>
{
self.large_text = Some(t.into());
self
}
pub fn small_image<S>(mut self, i: S) -> Self
where S: Into<String>
{
self.small_image = Some(i.into());
self
}
pub fn small_text<S>(mut self, t: S) -> Self
where S: Into<String>
{
self.small_text = Some(t.into());
self
}
}
#[derive(Debug, Default, Serialize)]
pub struct ActivityParty {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<[u32; 2]>,
}
impl ActivityParty {
pub fn id(mut self, i: u32) -> Self {
self.id = Some(i);
self
}
pub fn size(mut self, current: u32, max: u32) -> Self {
self.size = Some([current, max]);
self
}
}
#[derive(Debug, Default, Serialize)]
pub struct ActivitySecrets {
#[serde(skip_serializing_if = "Option::is_none")]
pub join: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spectate: Option<String>,
// NOTE: think of a better name for this
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "match")]
pub shoubu: Option<String>,
}
impl ActivitySecrets {
pub fn join<S>(mut self, secret: S) -> Self
where S: Into<String>
{
self.join = Some(secret.into());
self
}
pub fn spectate<S>(mut self, secret: S) -> Self
where S: Into<String>
{
self.spectate = Some(secret.into());
self
}
pub fn game<S>(mut self, secret: S) -> Self
where S: Into<String>
{
self.shoubu = Some(secret.into());
self
}
}

6
src/models/shared.rs Normal file
View File

@ -0,0 +1,6 @@
builder! {PartialUser
id: String,
username: String,
discriminator: String,
avatar: String,
}

10
src/utils.rs Normal file
View File

@ -0,0 +1,10 @@
use uuid::Uuid;
#[allow(unused)]
pub fn pid() -> u32 {
std::process::id()
}
pub fn nonce() -> String {
Uuid::new_v4().to_string()
}