Compare commits

...

76 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
44 changed files with 1782 additions and 474 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,20 +0,0 @@
variables:
CARGO_HOME: $CI_PROJECT_DIR/.cargo
.cached: &cached
cache:
key: $CI_COMMIT_REF_NAME
untracked: true
paths:
- target/
- .cargo/
.test_job: &test_job
<<: *cached
environment: test
test:rust_1_23_0:
<<: *test_job
image: "rust:1.23.0"
script:
- cargo test --all

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"

View File

@ -1,13 +0,0 @@
language: rust
cache: cargo
rust:
- stable
- nightly
matrix:
allow_failures:
- rust: nightly
fast_finish: true
script:
- cargo test --all

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

View File

@ -1,13 +1,11 @@
# Contributing Guidelines
Contributions to this project are welcome, just follow there steps.
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 (I recommend installing [Overcommit][overcommit])
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.
[overcommit]: https://github.com/brigade/overcommit

View File

@ -1,33 +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.3"
[badges]
gitlab = { repository = "valeth/discord-rpc-client.rs" }
travis-ci = { repository = "valeth/discord-rpc-client.rs" }
maintenance = { status = "experimental" }
[dependencies]
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
byte = "0.2"
log = "~0.4"
libc = "0.2.39" # until std::process::id is stable
[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"]
[workspace]
members = ["examples/discord_presence"]
[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"]

View File

@ -1,10 +1,9 @@
[![Build Status][travis-ci-badge]][travis-ci-page] [![crates.io][crates-io-badge]][crates-io-page]
# 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
> *Note*: Only works on **Unix** systems right now.
## Installation
@ -12,20 +11,41 @@ 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)
[gitlab-ci-badge]: https://gitlab.com/valeth/discord-rpc-client.rs/badges/master/pipeline.svg
[gitlab-repo-master]: https://gitlab.com/valeth/discord-rpc-client.rs/commits/master
[crates-io-badge]: https://img.shields.io/crates/v/discord-rpc-client.svg
[crates-io-page]: https://crates.io/crates/discord-rpc-client
[travis-ci-badge]: https://travis-ci.org/valeth/discord-rpc-client.rs.svg?branch=master
[travis-ci-page]: https://travis-ci.org/valeth/discord-rpc-client.rs

View File

@ -0,0 +1,39 @@
#[macro_use]
extern crate tracing;
use discord_rpc_client::{models::Activity, Client as DiscordRPC};
use std::io;
#[tokio::main]
async fn main() -> discord_rpc_client::Result<()> {
tracing_subscriber::fmt::init();
let drpc = DiscordRPC::default();
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

@ -1,12 +0,0 @@
[package]
authors = ["Patrick Auernig <dev.patrick.auernig@gmail.com>"]
name = "discord-presence"
version = "0.0.0"
[dependencies]
simplelog = "~0.5"
[dependencies.discord-rpc-client]
path = "../../"
version = "^0.1"
default-features = true

View File

@ -1,27 +0,0 @@
extern crate simplelog;
extern crate discord_rpc_client;
use simplelog::*;
use std::{thread, time};
use discord_rpc_client::Client as DiscordRPC;
use discord_rpc_client::UnixConnection as Connection;
fn main() {
TermLogger::init(LevelFilter::Debug, Config::default()).unwrap();
let mut drpc =
DiscordRPC::<Connection>::new(425407036495495169)
.and_then(|rpc| rpc.start())
.expect("Failed to start client");
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");
loop { thread::sleep(time::Duration::from_secs(10)) };
}

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,46 +1,351 @@
use std::io::Result;
use connection::Connection;
use models::Handshake;
#[cfg(feature = "rich_presence")]
use models::{SetActivityArgs, SetActivity};
use serde::{de::DeserializeOwned, Serialize};
#[allow(unused)]
use serde_json::Value;
use tokio::select;
use tokio::sync::watch;
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct Client<T>
where T: Connection
{
client_id: u64,
version: u32,
socket: T,
use crate::connection::{Connection, SocketConnection};
use crate::error::{Error, Result};
#[cfg(feature = "rich_presence")]
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()
}};
}
impl<T> Client<T>
where T: Connection
{
pub fn new(client_id: u64) -> Result<Self> {
let socket = T::connect()?;
Ok(Self { version: 1, client_id, socket})
#[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 {
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 {
/// 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
{
let args = SetActivityArgs::command(f(SetActivity::new()));
self.socket.send(1, args)?;
Ok(())
pub async fn set_activity(&self, activity: Activity) -> Result<Payload<Activity>> {
self.execute(Command::SetActivity, SetActivityArgs::new(activity), None)
.await
}
// private
#[cfg(feature = "rich_presence")]
pub async fn clear_activity(&self) -> Result<Payload<Activity>> {
self.execute(Command::SetActivity, SetActivityArgs::default(), None)
.await
}
fn handshake(&mut self) -> Result<()> {
let client_id = self.client_id;
let version = self.version;
self.socket.send(0, Handshake::new(client_id, version))?;
Ok(())
// 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,
{
self.execute(Command::Subscribe, f(SubscriptionArgs::new()), Some(evt))
.await
}
pub async fn unsubscribe<F>(&self, evt: Event, f: F) -> Result<Payload<Subscription>>
where
F: FnOnce(SubscriptionArgs) -> SubscriptionArgs,
{
self.execute(Command::Unsubscribe, f(SubscriptionArgs::new()), Some(evt))
.await
}
}

View File

@ -1,15 +1,90 @@
use std::io::Result;
use std::marker::Sized;
use std::fmt::Debug;
use models::Payload;
use std::{marker::Sized, path::PathBuf};
pub trait Connection
where Self: Sized
{
fn connect() -> Result<Self>;
use bytes::BytesMut;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
fn send<T>(&mut self, opcode: u32, payload: T) -> Result<()>
where T: Payload + Debug;
use crate::error::{Error, Result};
use crate::models::message::{Message, OpCode};
use crate::utils;
fn recv(&mut self) -> Result<Vec<u8>>;
#[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)
}
}

View File

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

View File

@ -1,65 +1,48 @@
use std::os::unix::net::UnixStream;
use std::io::{Write, Read, Result};
use std::time;
use std::path::PathBuf;
use std::env;
use std::fmt::Debug;
use models::Payload;
use std::{env, path::PathBuf};
use tokio::{io::AsyncWriteExt, net::UnixStream};
use models::Message;
use super::base::Connection;
use crate::error::Result;
#[derive(Debug)]
pub struct UnixConnection {
socket: UnixStream,
}
impl UnixConnection {
fn ipc_path() -> PathBuf {
let tmp = env::var("XDG_RUNTIME_DIR")
.or_else(|_| env::var("TMPDIR"))
.or_else(|_| {
match env::temp_dir().to_str() {
None => Err("Failed to convert temp_dir"),
Some(tmp) => Ok(tmp.to_string())
}
})
.unwrap_or("/tmp".to_string());
PathBuf::from(tmp)
}
fn socket_path(n: u8) -> PathBuf {
Self::ipc_path().join(format!("discord-ipc-{}", n))
}
}
#[async_trait::async_trait]
impl Connection for UnixConnection {
fn connect() -> Result<Self> {
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)?;
socket.set_write_timeout(Some(time::Duration::from_secs(30)))?;
socket.set_read_timeout(Some(time::Duration::from_secs(30)))?;
let socket = UnixStream::connect(connection_name).await?;
Ok(Self { socket })
}
fn send<T>(&mut self, opcode: u32, payload: T) -> Result<()>
where T: Payload + Debug
{
debug!("payload: {:#?}", payload);
match Message::new(opcode, payload).encode() {
Err(why) => error!("{:?}", why),
Ok(bytes) => {
self.socket.write_all(bytes.as_ref())?;
debug!("sent opcode: {}", opcode);
self.recv()?;
}
};
async fn disconnect(mut self) -> Result<()> {
self.socket.shutdown().await?;
Ok(())
}
fn recv(&mut self) -> Result<Vec<u8>> {
let mut buf: Vec<u8> = Vec::with_capacity(1024);
self.socket.read(buf.as_mut_slice())?;
debug!("{:?}", buf);
Ok(buf)
}
}

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,22 +1,26 @@
#![feature(try_blocks)]
#[macro_use]
extern crate log;
extern crate anyhow;
#[macro_use]
extern crate serde_derive;
extern crate serde;
#[macro_use]
extern crate serde_json;
extern crate byte;
extern crate uuid;
extern crate libc;
#[macro_use]
extern crate tracing;
#[macro_use]
mod macros;
mod connection;
mod models;
pub mod client;
mod connection;
mod error;
pub mod models;
mod utils;
pub use client::Client;
pub use models::prelude;
pub use connection::{Connection, SocketConnection};
#[cfg(unix)]
pub use connection::UnixConnection;
pub use error::*;
#[cfg(feature = "java-bindings")]
pub mod java;

View File

@ -1,4 +1,4 @@
macro_rules! message_func {
macro_rules! builder_func {
[ $name:ident, $type:tt func ] => {
pub fn $name<F>(mut self, func: F) -> Self
where F: FnOnce($type) -> $type
@ -22,46 +22,46 @@ macro_rules! message_func {
};
}
macro_rules! message_format {
macro_rules! builder {
[ @st ( $name:ident $field:tt: $type:tt alias = $alias:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
message_format![ @st
builder![ @st
( $name $($rest)* ) -> (
$($out)*
#[serde(skip_serializing_if = "Option::is_none", rename = $alias)]
pub $field: Option<$type>,
$($out)*
)
];
};
[ @st ( $name:ident $field:tt: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
message_format![ @st ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ];
builder![ @st ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ];
};
[ @st ( $name:ident $field:ident: $type:ty, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
message_format![ @st
builder![ @st
( $name $($rest)* ) -> (
$($out)*
#[serde(skip_serializing_if = "Option::is_none")]
$field: Option<$type>,
$($out)*
)
];
};
[ @st ( $name:ident ) -> ( $($out:tt)* ) ] => {
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct $name { $($out)* }
};
[ @im ( $name:ident $field:ident: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
message_format![ @im ( $name $($rest)* ) -> ( message_func![$field, $type func]; $($out)* ) ];
builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type func]; $($out)* ) ];
};
[ @im ( $name:ident $field:ident: $type:tt alias = $modifier:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
message_format![ @im ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ];
builder![ @im ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ];
};
[ @im ( $name:ident $field:ident: $type:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
message_format![ @im ( $name $($rest)* ) -> ( message_func![$field, $type]; $($out)* ) ];
builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type]; $($out)* ) ];
};
[ @im ( $name:ident ) -> ( $($out:tt)* ) ] => {
@ -75,7 +75,7 @@ macro_rules! message_format {
};
[ $name:ident $($body:tt)* ] => {
message_format![@st ( $name $($body)* ) -> () ];
message_format![@im ( $name $($body)* ) -> () ];
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 {
nonce: String,
v: u32,
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,24 +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::set_activity::{
SetActivity,
SetActivityAssets,
SetActivityParty,
SetActivitySecrets,
SetActivityTimestamps
pub use super::commands::{Subscription, SubscriptionArgs};
pub use super::events::{ErrorEvent, ReadyEvent};
#[cfg(feature = "rich_presence")]
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,102 +0,0 @@
use libc::getpid;
use models::Command;
#[derive(Debug, Default, Serialize)]
pub struct SetActivityArgs {
pid: u32,
activity: SetActivity,
}
impl SetActivityArgs {
pub fn command(args: SetActivity) -> Command<Self> {
Command::new("SET_ACTIVITY", Self {
pid: unsafe { getpid() as u32 },
activity: args
})
}
}
message_format![SetActivity
state: String,
details: String,
instance: bool,
timestamps: SetActivityTimestamps func,
assets: SetActivityAssets func,
party: SetActivityParty func,
secrets: SetActivitySecrets func,
];
message_format![SetActivityTimestamps
start: u32,
end: u32,
];
message_format![SetActivityAssets
large_image: String,
large_text: String,
small_image: String,
small_text: String,
];
message_format![SetActivityParty
id: u32,
size: (u32, u32),
];
message_format![SetActivitySecrets
join: String,
spectate: String,
game: String alias = "match",
];
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_set_activity_serialize() {
let activity = SetActivity::new()
.state("rusting")
.instance(true);
let json = serde_json::to_string(&activity).unwrap();
assert_eq![json, r#"{"instance":true,"state":"rusting"}"#];
}
#[test]
fn test_set_activity_timestamps_serialize() {
let timestamps = SetActivityTimestamps::new()
.start(1000)
.end(2000);
let json = serde_json::to_string(&timestamps).unwrap();
assert_eq![json, r#"{"end":2000,"start":1000}"#];
}
#[test]
fn test_set_activity_assets_serialize() {
let assets = SetActivityAssets::new()
.large_image("ferris")
.small_image("rusting");
let json = serde_json::to_string(&assets).unwrap();
assert_eq![json, r#"{"small_image":"rusting","large_image":"ferris"}"#];
}
#[test]
fn test_set_activity_party_serialize() {
let party = SetActivityParty::new()
.id(1)
.size((1, 10));
let json = serde_json::to_string(&party).unwrap();
assert_eq![json, r#"{"size":[1,10],"id":1}"#];
}
#[test]
fn test_set_activity_secrets_serialize() {
let secrets = SetActivitySecrets::new()
.join("j1")
.spectate("s1")
.game("g1");
let json = serde_json::to_string(&secrets).unwrap();
assert_eq![json, r#"{"match":"g1","spectate":"s1","join":"j1"}"#.to_string()];
}
}

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()
}