Compare commits
57 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 |
|
@ -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/
|
# CI
|
||||||
**/*.rs.bk
|
/.rustup
|
||||||
Cargo.lock
|
/.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_25_0:
|
|
||||||
<<: *test_job
|
|
||||||
image: "rust:1.25.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
|
# 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
|
1. Fork this repository and create a feature branch named after the feature you want to implement
|
||||||
2. Make your changes on your branch
|
2. Make your changes on your branch
|
||||||
3. Add some test if possibe
|
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
|
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.
|
> **Note**: Make sure you rebase your feature branch on top of master from time to time.
|
||||||
|
|
||||||
[overcommit]: https://github.com/brigade/overcommit
|
|
||||||
|
|
71
Cargo.toml
71
Cargo.toml
|
@ -1,36 +1,49 @@
|
||||||
[package]
|
[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."
|
description = "A Rust client for Discord RPC."
|
||||||
keywords = ["discord", "rpc", "ipc"]
|
authors = [
|
||||||
license = "MIT"
|
"Patrick Auernig <dev.patrick.auernig@gmail.com>",
|
||||||
readme = "README.md"
|
"Michael Pfaff <michael@pfaff.dev>",
|
||||||
repository = "https://gitlab.com/valeth/discord-rpc-client.rs.git"
|
]
|
||||||
version = "0.2.0"
|
keywords = ["discord", "rpc", "ipc"]
|
||||||
|
license = "MIT"
|
||||||
[badges]
|
readme = "README.md"
|
||||||
# gitlab = { repository = "valeth/discord-rpc-client.rs" }
|
repository = "https://gitlab.com/valeth/discord-rpc-client.rs.git"
|
||||||
travis-ci = { repository = "valeth/discord-rpc-client.rs" }
|
edition = "2021"
|
||||||
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"]
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["rich_presence"]
|
default = ["rich_presence"]
|
||||||
rich_presence = []
|
rich_presence = []
|
||||||
|
tokio-parking_lot = ["tokio/parking_lot"]
|
||||||
|
java-bindings = ["lazy_static", "jni", "tokio/rt-multi-thread"]
|
||||||
|
|
||||||
# [workspace]
|
[dependencies]
|
||||||
# members = ["examples/discord_presence"]
|
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"]
|
||||||
|
|
51
README.md
51
README.md
|
@ -1,9 +1,10 @@
|
||||||
[![Build Status][travis-ci-badge]][travis-ci-page] [![crates.io][crates-io-badge]][crates-io-page]
|
|
||||||
|
|
||||||
# Discord RPC Client
|
# 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
|
Discord RPC client for Rust
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Add this to your `Cargo.toml`:
|
Add this to your `Cargo.toml`:
|
||||||
|
@ -13,26 +14,38 @@ Add this to your `Cargo.toml`:
|
||||||
discord-rpc-client = "^0.2"
|
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
|
## Contributions
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
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
|
|
||||||
[discord-invite]: https://discordapp.com/invite/zfavwrA
|
|
||||||
[discord-banner]: https://discordapp.com/api/guilds/200751504175398912/widget.png?style=banner2
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
370
src/client.rs
370
src/client.rs
|
@ -1,47 +1,351 @@
|
||||||
use connection::Connection;
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use models::{Handshake, OpCode};
|
#[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")]
|
#[cfg(feature = "rich_presence")]
|
||||||
use rich_presence::{SetActivityArgs, SetActivity};
|
use crate::models::rich_presence::{
|
||||||
use error::Result;
|
Activity, CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs,
|
||||||
|
};
|
||||||
|
use crate::models::{
|
||||||
|
commands::{Subscription, SubscriptionArgs},
|
||||||
|
message::Message,
|
||||||
|
payload::Payload,
|
||||||
|
Command, Event, OpCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! hollow {
|
||||||
#[derive(Debug)]
|
($expr:expr) => {{
|
||||||
pub struct Client<T>
|
let ref_ = $expr.borrow();
|
||||||
where T: Connection
|
ref_.hollow()
|
||||||
{
|
}};
|
||||||
client_id: u64,
|
|
||||||
version: u32,
|
|
||||||
socket: T,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Client<T>
|
#[derive(Debug)]
|
||||||
where T: Connection
|
enum ConnectionState<T> {
|
||||||
{
|
Disconnected,
|
||||||
pub fn new(client_id: u64) -> Result<Self> {
|
Connecting,
|
||||||
let socket = T::connect()?;
|
Connected(T),
|
||||||
Ok(Self { version: 1, client_id, socket})
|
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> {
|
#[instrument(level = "debug")]
|
||||||
self.handshake()?;
|
async fn connect_and_handshake(client_id: u64) -> Result<SocketConnection> {
|
||||||
Ok(self)
|
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")]
|
#[cfg(feature = "rich_presence")]
|
||||||
pub fn set_activity<F>(&mut self, f: F) -> Result<()>
|
pub async fn set_activity(&self, activity: Activity) -> Result<Payload<Activity>> {
|
||||||
where F: FnOnce(SetActivity) -> SetActivity
|
self.execute(Command::SetActivity, SetActivityArgs::new(activity), None)
|
||||||
{
|
.await
|
||||||
let args = SetActivityArgs::command(f(SetActivity::new()));
|
|
||||||
self.socket.send(OpCode::Frame, args)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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<()> {
|
// NOTE: Not sure what the actual response values of
|
||||||
let client_id = self.client_id;
|
// SEND_ACTIVITY_JOIN_INVITE and CLOSE_ACTIVITY_REQUEST are,
|
||||||
let version = self.version;
|
// they are not documented.
|
||||||
self.socket.send(OpCode::Handshake, Handshake::new(client_id, version))?;
|
#[cfg(feature = "rich_presence")]
|
||||||
Ok(())
|
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,19 +1,90 @@
|
||||||
use std::{
|
use std::{marker::Sized, path::PathBuf};
|
||||||
marker::Sized,
|
|
||||||
fmt::Debug
|
|
||||||
};
|
|
||||||
|
|
||||||
use models::{Payload, OpCode};
|
use bytes::BytesMut;
|
||||||
use error::Result;
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::models::message::{Message, OpCode};
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
pub trait Connection
|
#[async_trait::async_trait]
|
||||||
where Self: Sized
|
pub trait Connection: Sized + Send {
|
||||||
{
|
type Socket: AsyncWrite + AsyncRead + Unpin + Send;
|
||||||
fn connect() -> Result<Self>;
|
|
||||||
|
|
||||||
fn send<T>(&mut self, opcode: OpCode, payload: T) -> Result<()>
|
fn socket(&mut self) -> &mut Self::Socket;
|
||||||
where T: Payload + Debug;
|
|
||||||
|
|
||||||
fn recv(&mut self) -> Result<Vec<u8>>;
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ mod windows;
|
||||||
|
|
||||||
pub use self::base::Connection;
|
pub use self::base::Connection;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use self::unix::UnixConnection;
|
pub use self::unix::UnixConnection as SocketConnection;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use self::windows::WindowsConnection;
|
pub use self::windows::WindowsConnection as SocketConnection;
|
||||||
|
|
|
@ -1,69 +1,48 @@
|
||||||
use std::{
|
use std::{env, path::PathBuf};
|
||||||
os::unix::net::UnixStream,
|
|
||||||
io::{Write, Read},
|
use tokio::{io::AsyncWriteExt, net::UnixStream};
|
||||||
time,
|
|
||||||
path::PathBuf,
|
|
||||||
env,
|
|
||||||
fmt::Debug
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::base::Connection;
|
use super::base::Connection;
|
||||||
use models::{Payload, Message, OpCode};
|
use crate::error::Result;
|
||||||
use error::Result;
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct UnixConnection {
|
pub struct UnixConnection {
|
||||||
socket: UnixStream,
|
socket: UnixStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnixConnection {
|
#[async_trait::async_trait]
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connection for UnixConnection {
|
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 connection_name = Self::socket_path(0);
|
||||||
let socket = UnixStream::connect(connection_name)?;
|
let socket = UnixStream::connect(connection_name).await?;
|
||||||
socket.set_write_timeout(Some(time::Duration::from_secs(30)))?;
|
|
||||||
socket.set_read_timeout(Some(time::Duration::from_secs(30)))?;
|
|
||||||
Ok(Self { socket })
|
Ok(Self { socket })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send<T>(&mut self, opcode: OpCode, payload: T) -> Result<()>
|
async fn disconnect(mut self) -> Result<()> {
|
||||||
where T: Payload + Debug
|
self.socket.shutdown().await?;
|
||||||
{
|
|
||||||
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()?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,35 @@
|
||||||
extern crate named_pipe;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use std::{
|
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient};
|
||||||
io::{Write, Read},
|
|
||||||
time,
|
|
||||||
path::PathBuf,
|
|
||||||
fmt::Debug
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::base::Connection;
|
use super::base::Connection;
|
||||||
use models::{Payload, Message, OpCode};
|
use crate::error::Result;
|
||||||
use error::Result;
|
|
||||||
|
|
||||||
use self::named_pipe::PipeClient;
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct WindowsConnection {
|
pub struct WindowsConnection {
|
||||||
socket: PipeClient,
|
socket: NamedPipeClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowsConnection {
|
#[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 {
|
fn ipc_path() -> PathBuf {
|
||||||
PathBuf::from(r"\\.\pipe\")
|
PathBuf::from(r"\\.\pipe\")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn socket_path(n: u8) -> PathBuf {
|
async fn connect() -> Result<Self> {
|
||||||
Self::ipc_path().join(format!("discord-ipc-{}", n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connection for WindowsConnection {
|
|
||||||
fn connect() -> Result<Self> {
|
|
||||||
let connection_name = Self::socket_path(0);
|
let connection_name = Self::socket_path(0);
|
||||||
let mut socket = PipeClient::connect(connection_name)?;
|
Ok(Self {
|
||||||
socket.set_write_timeout(Some(time::Duration::from_secs(30)));
|
socket: ClientOptions::new().open(connection_name)?,
|
||||||
socket.set_read_timeout(Some(time::Duration::from_secs(30)));
|
})
|
||||||
Ok(Self { socket })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send<T>(&mut self, opcode: OpCode, payload: T) -> Result<()>
|
async fn disconnect(mut self) -> 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()?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
62
src/error.rs
62
src/error.rs
|
@ -1,40 +1,68 @@
|
||||||
|
use serde_json::Error as JsonError;
|
||||||
use std::{
|
use std::{
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
io::Error as IoError,
|
io::Error as IoError,
|
||||||
result::Result as StdResult,
|
result::Result as StdResult,
|
||||||
fmt::{
|
sync::mpsc::RecvTimeoutError as ChannelTimeout,
|
||||||
self,
|
|
||||||
Display,
|
|
||||||
Formatter
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
use tokio::sync::mpsc::error::SendError;
|
||||||
|
|
||||||
|
use crate::models::Message;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Io(IoError),
|
IoError(IoError),
|
||||||
|
JsonError(JsonError),
|
||||||
|
Timeout(ChannelTimeout),
|
||||||
Conversion,
|
Conversion,
|
||||||
|
SubscriptionFailed,
|
||||||
|
ConnectionClosed,
|
||||||
|
ConnectionClosedWhileSending(Message),
|
||||||
|
Busy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str(self.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"),
|
||||||
impl StdError for Error {
|
Self::ConnectionClosedWhileSending(msg) => {
|
||||||
fn description(&self) -> &str {
|
write!(f, "Connection closed while sending {:?}", msg)
|
||||||
match *self {
|
}
|
||||||
Error::Conversion => "Failed to convert values",
|
Self::Busy => f.write_str("A resource was busy"),
|
||||||
Error::Io(ref err) => err.description()
|
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 {
|
impl From<IoError> for Error {
|
||||||
fn from(err: IoError) -> Self {
|
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>;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
33
src/lib.rs
33
src/lib.rs
|
@ -1,27 +1,26 @@
|
||||||
|
#![feature(try_blocks)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate anyhow;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate byteorder;
|
#[macro_use]
|
||||||
extern crate uuid;
|
extern crate tracing;
|
||||||
extern crate libc;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
mod error;
|
|
||||||
mod utils;
|
|
||||||
mod connection;
|
|
||||||
mod models;
|
|
||||||
mod rich_presence;
|
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
mod connection;
|
||||||
|
mod error;
|
||||||
|
pub mod models;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
#[cfg(feature = "rich_presence")]
|
pub use connection::{Connection, SocketConnection};
|
||||||
pub use rich_presence::*;
|
|
||||||
#[cfg(unix)]
|
pub use error::*;
|
||||||
pub use connection::UnixConnection;
|
|
||||||
#[cfg(windows)]
|
#[cfg(feature = "java-bindings")]
|
||||||
pub use connection::WindowsConnection;
|
pub mod java;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
macro_rules! message_func {
|
macro_rules! builder_func {
|
||||||
[ $name:ident, $type:tt func ] => {
|
[ $name:ident, $type:tt func ] => {
|
||||||
pub fn $name<F>(mut self, func: F) -> Self
|
pub fn $name<F>(mut self, func: F) -> Self
|
||||||
where F: FnOnce($type) -> $type
|
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)* ) ] => {
|
[ @st ( $name:ident $field:tt: $type:tt alias = $alias:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
|
||||||
message_format![ @st
|
builder![ @st
|
||||||
( $name $($rest)* ) -> (
|
( $name $($rest)* ) -> (
|
||||||
$($out)*
|
$($out)*
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = $alias)]
|
#[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)* ) ] => {
|
[ @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)* ) ] => {
|
[ @st ( $name:ident $field:ident: $type:ty, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
|
||||||
message_format![ @st
|
builder![ @st
|
||||||
( $name $($rest)* ) -> (
|
( $name $($rest)* ) -> (
|
||||||
$($out)*
|
$($out)*
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -48,20 +48,20 @@ macro_rules! message_format {
|
||||||
};
|
};
|
||||||
|
|
||||||
[ @st ( $name:ident ) -> ( $($out:tt)* ) ] => {
|
[ @st ( $name:ident ) -> ( $($out:tt)* ) ] => {
|
||||||
#[derive(Debug, Default, Serialize)]
|
#[derive(Debug, Default, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct $name { $($out)* }
|
pub struct $name { $($out)* }
|
||||||
};
|
};
|
||||||
|
|
||||||
[ @im ( $name:ident $field:ident: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => {
|
[ @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)* ) ] => {
|
[ @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)* ) ] => {
|
[ @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)* ) ] => {
|
[ @im ( $name:ident ) -> ( $($out:tt)* ) ] => {
|
||||||
|
@ -75,7 +75,7 @@ macro_rules! message_format {
|
||||||
};
|
};
|
||||||
|
|
||||||
[ $name:ident $($body:tt)* ] => {
|
[ $name:ident $($body:tt)* ] => {
|
||||||
message_format![@st ( $name $($body)* ) -> () ];
|
builder![@st ( $name $($body)* ) -> () ];
|
||||||
message_format![@im ( $name $($body)* ) -> () ];
|
builder![@im ( $name $($body)* ) -> () ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
|
|
@ -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,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 {}
|
|
|
@ -1,11 +1,10 @@
|
||||||
use std::io::{self, Write, Read};
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
use byteorder::{WriteBytesExt, ReadBytesExt, LittleEndian};
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use serde_json;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
use error::{Result, Error};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum OpCode {
|
pub enum OpCode {
|
||||||
|
@ -16,51 +15,57 @@ pub enum OpCode {
|
||||||
Pong,
|
Pong,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Use TryFrom trait when stable
|
impl TryFrom<u32> for OpCode {
|
||||||
impl OpCode {
|
type Error = Error;
|
||||||
fn try_from(int: u32) -> Result<Self> {
|
|
||||||
|
fn try_from(int: u32) -> Result<Self, Self::Error> {
|
||||||
match int {
|
match int {
|
||||||
0 => Ok(OpCode::Handshake),
|
0 => Ok(OpCode::Handshake),
|
||||||
1 => Ok(OpCode::Frame),
|
1 => Ok(OpCode::Frame),
|
||||||
2 => Ok(OpCode::Close),
|
2 => Ok(OpCode::Close),
|
||||||
3 => Ok(OpCode::Ping),
|
3 => Ok(OpCode::Ping),
|
||||||
4 => Ok(OpCode::Pong),
|
4 => Ok(OpCode::Pong),
|
||||||
_ => Err(Error::Conversion)
|
_ => Err(Error::Conversion),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
opcode: OpCode,
|
pub opcode: OpCode,
|
||||||
message: String,
|
pub payload: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
pub fn new<T>(opcode: OpCode, message: T) -> Self
|
pub fn new<T>(opcode: OpCode, payload: T) -> Self
|
||||||
where T: Serialize
|
where
|
||||||
|
T: Serialize,
|
||||||
{
|
{
|
||||||
Message {
|
Self {
|
||||||
opcode: opcode,
|
opcode,
|
||||||
message: serde_json::to_string(&message).unwrap()
|
payload: serde_json::to_string(&payload).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||||
let mut bytes: Vec<u8> = vec![];
|
let mut bytes: Vec<u8> = vec![];
|
||||||
|
|
||||||
bytes.write_u32::<LittleEndian>(self.opcode as u32)?;
|
bytes.write_u32::<LittleEndian>(self.opcode as u32)?;
|
||||||
bytes.write_u32::<LittleEndian>(self.message.len() as u32)?;
|
bytes.write_u32::<LittleEndian>(self.payload.len() as u32)?;
|
||||||
write!(bytes, "{}", self.message)?;
|
write!(bytes, "{}", self.payload)?;
|
||||||
|
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode(bytes: &[u8]) -> Result<Self> {
|
pub fn decode(bytes: &[u8]) -> Result<Self> {
|
||||||
let mut reader = io::Cursor::new(bytes);
|
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>()?)?;
|
let opcode = OpCode::try_from(reader.read_u32::<LittleEndian>()?)?;
|
||||||
reader.read_u32::<LittleEndian>()?;
|
reader.read_u32::<LittleEndian>()?;
|
||||||
reader.read_to_string(&mut message)?;
|
reader.read_to_string(&mut payload)?;
|
||||||
Ok(Message { opcode, message })
|
|
||||||
|
Ok(Self { opcode, payload })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +75,7 @@ mod tests {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
struct Something {
|
struct Something {
|
||||||
empty: bool
|
empty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,11 +1,53 @@
|
||||||
mod message;
|
pub mod commands;
|
||||||
mod command;
|
pub mod events;
|
||||||
mod handshake;
|
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::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;
|
||||||
|
}
|
||||||
|
|
|
@ -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, "{}"];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
builder! {PartialUser
|
||||||
|
id: String,
|
||||||
|
username: String,
|
||||||
|
discriminator: String,
|
||||||
|
avatar: String,
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
#![cfg(feature = "rich_presence")]
|
|
||||||
|
|
||||||
mod set_activity;
|
|
||||||
|
|
||||||
pub use self::set_activity::*;
|
|
|
@ -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, "{}"];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,8 @@
|
||||||
use libc::getpid;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn pid() -> i32 {
|
pub fn pid() -> u32 {
|
||||||
unsafe { getpid() as i32 }
|
std::process::id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nonce() -> String {
|
pub fn nonce() -> String {
|
||||||
|
|
Loading…
Reference in New Issue