Compare commits

...

52 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
46 changed files with 1685 additions and 548 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_25_0:
<<: *test_job
image: "rust:1.25.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,37 +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.2.1"
[badges]
# gitlab = { repository = "valeth/discord-rpc-client.rs" }
travis-ci = { repository = "valeth/discord-rpc-client.rs" }
appveyor = { repository = "valeth/discord-rpc-client.rs", service = "gitlab" }
maintenance = { status = "experimental" }
[dependencies]
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
byteorder = "^1.0"
log = "~0.4"
libc = "0.2.39" # until std::process::id is stable
[target.'cfg(windows)'.dependencies]
named_pipe = "0.3.0"
[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,9 +1,10 @@
[![Build Status][travis-ci-badge]][travis-ci-page] [![Build status][appveyor-ci-badge]][appveyor-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
## Installation
Add this to your `Cargo.toml`:
@ -13,28 +14,38 @@ Add this to your `Cargo.toml`:
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)
## Support
[![Discord Banner][discord-banner]][discord-invite]
<!-- links -->
[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
[appveyor-ci-badge]: https://ci.appveyor.com/api/projects/status/3fba86eipx0sgsjp?svg=true
[appveyor-ci-page]: https://ci.appveyor.com/project/valeth/discord-rpc-client-rs
[discord-invite]: https://discordapp.com/invite/zfavwrA
[discord-banner]: https://discordapp.com/api/guilds/200751504175398912/widget.png?style=banner2

View File

@ -1,19 +0,0 @@
version: 1.0.{build}
environment:
matrix:
- TARGET: x86_64-pc-windows-msvc
install:
- appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain stable
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustup -V
- cargo -V
clone_depth: 1
build: false
test_script:
- cargo test

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,10 +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 = "../../"

View File

@ -1,30 +0,0 @@
extern crate simplelog;
extern crate discord_rpc_client;
use simplelog::*;
use std::{thread, time};
use discord_rpc_client::Client as DiscordRPC;
#[cfg(unix)]
use discord_rpc_client::UnixConnection as Connection;
#[cfg(windows)]
use discord_rpc_client::WindowsConnection 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,47 +1,351 @@
use connection::Connection;
use models::{Handshake, OpCode};
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 rich_presence::{SetActivityArgs, SetActivity};
use error::Result;
use crate::models::rich_presence::{
Activity, CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs,
};
use crate::models::{
commands::{Subscription, SubscriptionArgs},
message::Message,
payload::Payload,
Command, Event, OpCode,
};
#[derive(Debug)]
pub struct Client<T>
where T: Connection
{
client_id: u64,
version: u32,
socket: T,
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(OpCode::Frame, 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(OpCode::Handshake, 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,50 +1,90 @@
use std::{
io::{Write, Read},
marker::Sized,
fmt::Debug,
path::PathBuf,
};
use std::{marker::Sized, path::PathBuf};
use models::{Payload, Message, OpCode};
use error::Result;
use bytes::BytesMut;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::error::{Error, Result};
use crate::models::message::{Message, OpCode};
use crate::utils;
pub trait Connection
where Self: Sized
{
type Socket: Write + Read;
#[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;
fn connect() -> Result<Self>;
async fn connect() -> Result<Self>;
async fn disconnect(self) -> Result<()>;
fn socket_path(n: u8) -> PathBuf {
Self::ipc_path().join(format!("discord-ipc-{}", n))
}
fn send<T>(&mut self, opcode: OpCode, 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 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(())
}
fn recv(&mut self) -> Result<Vec<u8>> {
let mut buf: Vec<u8> = vec![0; 1024];
let n = self.socket().read(buf.as_mut_slice())?;
buf.resize(n, 0);
debug!("{:?}", Message::decode(&buf));
Ok(buf)
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

@ -6,6 +6,6 @@ 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;
pub use self::windows::WindowsConnection as SocketConnection;

View File

@ -1,43 +1,48 @@
use std::{
time,
path::PathBuf,
env,
os::unix::net::UnixStream,
};
use std::{env, path::PathBuf};
use tokio::{io::AsyncWriteExt, net::UnixStream};
use super::base::Connection;
use error::Result;
use crate::error::Result;
#[derive(Debug)]
pub struct UnixConnection {
socket: UnixStream,
}
#[async_trait::async_trait]
impl Connection for UnixConnection {
type Socket = UnixStream;
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)))?;
Ok(Self { socket })
}
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(&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(())
}
}

View File

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

View File

@ -1,40 +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,
fmt::{
self,
Display,
Formatter
}
sync::mpsc::RecvTimeoutError as ChannelTimeout,
};
use tokio::sync::mpsc::error::SendError;
use crate::models::Message;
#[derive(Debug)]
pub enum Error {
Io(IoError),
IoError(IoError),
JsonError(JsonError),
Timeout(ChannelTimeout),
Conversion,
SubscriptionFailed,
ConnectionClosed,
ConnectionClosedWhileSending(Message),
Busy,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Error::Conversion => "Failed to convert values",
Error::Io(ref err) => err.description()
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 {
Error::Io(err)
Self::IoError(err)
}
}
pub type Result<T> = StdResult<T, Error>;
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,29 +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 byteorder;
extern crate uuid;
extern crate libc;
#[cfg(windows)]
extern crate named_pipe;
#[macro_use]
extern crate tracing;
#[macro_use]
mod macros;
mod error;
mod utils;
mod connection;
mod models;
mod rich_presence;
pub mod client;
mod connection;
mod error;
pub mod models;
mod utils;
pub use client::Client;
#[cfg(feature = "rich_presence")]
pub use rich_presence::*;
#[cfg(unix)]
pub use connection::UnixConnection;
#[cfg(windows)]
pub use connection::WindowsConnection;
pub use connection::{Connection, SocketConnection};
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,9 +22,9 @@ 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)]
@ -34,11 +34,11 @@ macro_rules! message_format {
};
[ @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")]
@ -48,20 +48,20 @@ macro_rules! message_format {
};
[ @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,31 +0,0 @@
use serde::Serialize;
use super::Payload;
use utils::nonce;
#[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: nonce(),
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,22 +0,0 @@
use super::Payload;
use utils::nonce;
#[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: nonce(),
v: version,
client_id: client_id.to_string()
}
}
}
impl Payload for Handshake {}

View File

@ -1,11 +1,10 @@
use std::io::{self, Write, Read};
use std::io::{self, Read, Write};
use byteorder::{WriteBytesExt, ReadBytesExt, LittleEndian};
use serde_json;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use serde::Serialize;
use serde_json;
use error::{Result, Error};
use crate::error::{Error, Result};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum OpCode {
@ -16,51 +15,57 @@ pub enum OpCode {
Pong,
}
// FIXME: Use TryFrom trait when stable
impl OpCode {
fn try_from(int: u32) -> Result<Self> {
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)
_ => Err(Error::Conversion),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, PartialEq)]
pub struct Message {
opcode: OpCode,
message: String,
pub opcode: OpCode,
pub payload: String,
}
impl Message {
pub fn new<T>(opcode: OpCode, message: T) -> Self
where T: Serialize
pub fn new<T>(opcode: OpCode, payload: T) -> Self
where
T: Serialize,
{
Message {
opcode: opcode,
message: serde_json::to_string(&message).unwrap()
Self {
opcode,
payload: serde_json::to_string(&payload).unwrap(),
}
}
pub fn encode(&self) -> Result<Vec<u8>> {
let mut bytes: Vec<u8> = vec![];
bytes.write_u32::<LittleEndian>(self.opcode as u32)?;
bytes.write_u32::<LittleEndian>(self.message.len() as u32)?;
write!(bytes, "{}", self.message)?;
bytes.write_u32::<LittleEndian>(self.payload.len() as u32)?;
write!(bytes, "{}", self.payload)?;
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
let mut reader = io::Cursor::new(bytes);
let mut message = String::new();
let mut payload = String::new();
let opcode = OpCode::try_from(reader.read_u32::<LittleEndian>()?)?;
reader.read_u32::<LittleEndian>()?;
reader.read_to_string(&mut message)?;
Ok(Message { opcode, message })
reader.read_to_string(&mut payload)?;
Ok(Self { opcode, payload })
}
}
@ -70,7 +75,7 @@ mod tests {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Something {
empty: bool
empty: bool,
}
#[test]

View File

@ -1,11 +1,53 @@
mod message;
mod command;
mod handshake;
pub mod commands;
pub mod events;
pub mod message;
pub mod payload;
pub mod rich_presence;
mod shared;
use serde::Serialize;
#[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,
}
#[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};
pub use self::command::Command;
pub use self::handshake::Handshake;
pub trait Payload: Serialize {}
#[cfg(feature = "rich_presence")]
pub use self::rich_presence::*;
pub mod prelude {
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, "{}"];
}
}

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

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

View File

@ -1,5 +0,0 @@
#![cfg(feature = "rich_presence")]
mod set_activity;
pub use self::set_activity::*;

View File

@ -1,120 +0,0 @@
use models::Command;
use utils::pid;
#[derive(Debug, Default, Serialize)]
pub struct SetActivityArgs {
pid: i32,
activity: SetActivity,
}
impl SetActivityArgs {
pub fn command(args: SetActivity) -> Command<Self> {
Command::new("SET_ACTIVITY", Self {
pid: pid(),
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;
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 = SetActivity::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 = SetActivity::new();
let json = serde_json::to_string(&activity).unwrap();
assert_eq![json, "{}"];
}
}

View File

@ -1,9 +1,8 @@
use libc::getpid;
use uuid::Uuid;
pub fn pid() -> i32 {
unsafe { getpid() as i32 }
#[allow(unused)]
pub fn pid() -> u32 {
std::process::id()
}
pub fn nonce() -> String {