Compare commits
76 Commits
Author | SHA1 | Date |
---|---|---|
Michael Pfaff | 2b2b47454c | |
Michael Pfaff | b1ef65e2a9 | |
Michael Pfaff | 2aaa043bd2 | |
Michael Pfaff | 5d0243df9a | |
Michael Pfaff | 49f58f2623 | |
Michael Pfaff | 910871dcf9 | |
Michael Pfaff | ad78e64e64 | |
Michael Pfaff | 30eebae644 | |
Michael Pfaff | d99145133d | |
Michael Pfaff | 04af242ca7 | |
Michael Pfaff | 326354d3b4 | |
Patrick Auernig | 6ea1c93bae | |
Patrick Auernig | 7c10e1dbc5 | |
Patrick Auernig | 84de596ffb | |
Patrick Auernig | eb6152effe | |
Patrick Auernig | 52d31f4420 | |
Patrick Auernig | 4b99188f23 | |
Patrick Auernig | 184abdc07f | |
Patrick Auernig | ecd391a44e | |
Patrick Auernig | caaae615f0 | |
Patrick Auernig | b3d12ff760 | |
Patrick Auernig | 21915d9acf | |
Patrick Auernig | 3c8e8453e6 | |
Patrick Auernig | 3ebc936b10 | |
Patrick Auernig | 6c1b159435 | |
Patrick Auernig | aaaf2474a6 | |
Patrick Auernig | 9c90034ee7 | |
Patrick Auernig | 2ffc0cd97c | |
Patrick Auernig | 889c440112 | |
Patrick Auernig | 45bcbb4b71 | |
Patrick Auernig | ab2b38aa47 | |
Patrick Auernig | 7024efd5c7 | |
Patrick Auernig | 7764ef3e8c | |
Patrick Auernig | bbf489d78b | |
Patrick Auernig | d51107247f | |
Patrick Auernig | a7b9a86d4e | |
Patrick Auernig | 6522a5b4a0 | |
Patrick Auernig | a9480b9d72 | |
Bond-009 | 6bbc9f85d7 | |
Patrick Auernig | 5845632ebc | |
Patrick Auernig | 56326618a4 | |
Patrick Auernig | f43c9697ae | |
Patrick Auernig | 4443423407 | |
Patrick Auernig | 06728d21cf | |
Patrick Auernig | a1e77c9c35 | |
Patrick Auernig | d370cb6432 | |
Patrick Auernig | 520da02162 | |
Patrick Auernig | 20044cbea4 | |
Patrick Auernig | 95d748f211 | |
Patrick Auernig | a585bb6495 | |
Patrick Auernig | 95ab885fb4 | |
Patrick Auernig | 60c7f4762f | |
Patrick Auernig | b89fddcac4 | |
Patrick Auernig | 2b06570fa2 | |
Patrick Auernig | ced34974cf | |
Patrick Auernig | b8e4697555 | |
Patrick Auernig | 51cd4413f7 | |
Patrick Auernig | af50e1318e | |
tenrys | 620e9a6b26 | |
Patrick Auernig | 1e3e9485df | |
Patrick Auernig | 94fd95d516 | |
Patrick Auernig | fd1dc15a29 | |
Patrick Auernig | 40be78dbd1 | |
Patrick Auernig | 332dbab413 | |
Patrick Auernig | 185b012fc0 | |
Patrick Auernig | 3c79bb15fa | |
Patrick Auernig | eb64564333 | |
Patrick Auernig | cbe540dcff | |
Patrick Auernig | 681d3d08b6 | |
Patrick Auernig | afe1503ad9 | |
Patrick Auernig | 54ace3a5e9 | |
Patrick Auernig | 2635086e97 | |
Patrick Auernig | a5acb3b975 | |
Patrick Auernig | c5ae80007e | |
Patrick Auernig | e853fa94a1 | |
Patrick Auernig | 00c61cfa72 |
|
@ -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
|
|
@ -1,4 +1,6 @@
|
|||
/target/
|
||||
/Cargo.lock
|
||||
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
# CI
|
||||
/.rustup
|
||||
/.cargo
|
||||
|
|
|
@ -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
|
|
@ -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"
|
13
.travis.yml
13
.travis.yml
|
@ -1,13 +0,0 @@
|
|||
language: rust
|
||||
cache: cargo
|
||||
rust:
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
fast_finish: true
|
||||
|
||||
script:
|
||||
- cargo test --all
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"files.watcherExclude": {
|
||||
"**/target": true
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
68
Cargo.toml
68
Cargo.toml
|
@ -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"]
|
||||
|
|
44
README.md
44
README.md
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)) };
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
/.bsp/
|
||||
/target/
|
||||
/project/
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivityAssets(
|
||||
String largeImage,
|
||||
String largeText,
|
||||
String smallImage,
|
||||
String smallText
|
||||
) {}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivityParty(@Unsigned int id, @Unsigned int minSize, @Unsigned int maxSize) {}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivitySecrets(String join, String spectate, String game) {}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivityTimestamps(@Unsigned Long start, @Unsigned Long end) {}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
371
src/client.rs
371
src/client.rs
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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>;
|
|
@ -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)
|
||||
}
|
26
src/lib.rs
26
src/lib.rs
|
@ -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;
|
||||
|
|
|
@ -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)* ) -> () ];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -0,0 +1,10 @@
|
|||
use super::shared::PartialUser;
|
||||
|
||||
builder! {SubscriptionArgs
|
||||
secret: String, // Activity{Join,Spectate}
|
||||
user: PartialUser, // ActivityJoinRequest
|
||||
}
|
||||
|
||||
builder! {Subscription
|
||||
evt: String,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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, "{}"];
|
||||
}
|
||||
}
|
|
@ -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(×tamps).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()];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
builder! {PartialUser
|
||||
id: String,
|
||||
username: String,
|
||||
discriminator: String,
|
||||
avatar: String,
|
||||
}
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue