Compare commits

...

12 Commits

Author SHA1 Message Date
Michael Pfaff 2b2b47454c
Remove build.sbt 2022-09-09 07:29:10 -04:00
Michael Pfaff b1ef65e2a9
Suppress std::io::NotFound errors 2022-08-16 14:31:44 -04:00
Michael Pfaff 2aaa043bd2
See message
- **Breaking change**: Multiple functions renamed in `src/java.rs`. This is not bumping the major version because these functions are considered internal.
- Fix disconnecting dead-lock
- `Client::execute` now disconnects when `Error::ConnectionClosed` or `Error::ConnectionClosedWhileSending` is encountered
- Use `tracing` instead of `log`
- Add the `instrument` attribute to most `Client` functions
- Remove unnecessary `Arc` from JNI bindings
2022-03-26 23:06:08 -04:00
Michael Pfaff 5d0243df9a
Add disconnect and clearActivity 2022-03-21 13:19:12 -04:00
Michael Pfaff 49f58f2623
Remove badges and add link to original repo 2022-02-03 18:40:32 -05:00
Michael Pfaff 910871dcf9
Update CHANGELOG.md 2022-02-03 17:01:02 -05:00
Michael Pfaff ad78e64e64
Bump version 2022-02-03 17:00:42 -05:00
Michael Pfaff 30eebae644
Cleanup some stuff 2022-02-03 16:57:40 -05:00
Michael Pfaff d99145133d
Add partial Java bindings 2022-02-03 16:56:26 -05:00
Michael Pfaff 04af242ca7
Overhaul (part 2)
- No more worker thread
- Fully async io thanks to Tokio
- Updated libs
- Client is no longer bound to a specific client id
2022-02-03 15:56:37 -05:00
Michael Pfaff 326354d3b4
Overhaul (part 1) 2022-02-03 09:52:53 -05:00
Patrick Auernig 6ea1c93bae Update changelog 2018-12-06 02:21:22 +01:00
39 changed files with 1178 additions and 644 deletions

View File

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

5
.gitignore vendored
View File

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

View File

@ -1,99 +0,0 @@
image: "debian:stretch-slim"
stages:
- prepare
- build
- publish
variables:
RUSTUP_HOME: "$CI_PROJECT_DIR/.rustup"
CARGO_HOME: "$CI_PROJECT_DIR/.cargo"
.write_cache:
cache:
key: "$CI_COMMIT_REF_SLUG-rustup"
paths:
- .rustup
- .cargo
.read_cache:
cache:
key: "$CI_COMMIT_REF_SLUG-rustup"
paths:
- .rustup
- .cargo
policy: pull
.toolchain-stable:
extends: .read_cache
before_script:
- apt-get update
- apt-get install -y --no-install-recommends ca-certificates libssl-dev libc6-dev pkg-config gcc
- export PATH="$CARGO_HOME/bin:$PATH"
- rustup default stable
.toolchain-nightly:
extends: .read_cache
allow_failure: true
before_script:
- apt-get update
- apt-get install -y --no-install-recommends ca-certificates libssl-dev libc6-dev pkg-config gcc
- export PATH="$CARGO_HOME/bin:$PATH"
- rustup default nightly
.build-only-when-changes: &build-only-when-changes
only:
changes:
- Cargo.toml
- Cargo.lock
- src/**/*.rs
####### Jobs #######
install-rust:
extends: .write_cache
stage: prepare
script:
- apt-get update
- apt-get install -y --no-install-recommends ca-certificates curl
- curl https://sh.rustup.rs > rustup.sh
- sh rustup.sh -y --default-host x86_64-unknown-linux-gnu
- export PATH="$CARGO_HOME/bin:$PATH"
- rustup install stable
- rustup install nightly
<<: *build-only-when-changes
build-stable-no-default-features:
extends: .toolchain-stable
stage: build
script:
- cargo test --tests --no-default-features
<<: *build-only-when-changes
build-stable-features-rich_presence:
extends: .toolchain-stable
stage: build
script:
- cargo test --tests --no-default-features
--features "rich_presence"
- cargo test --no-default-features
--features "rich_presence"
--example "discord_presence"
--example "discord_presence_subscribe"
<<: *build-only-when-changes
build-nightly:
extends: .toolchain-nightly
stage: build
script:
- cargo test --tests
- cargo test --examples
<<: *build-only-when-changes
deploy-crates-io:
extends: .toolchain-stable
stage: publish
script:
- cargo publish --token $CRATES_IO_API_TOKEN
only:
- tags@valeth/discord-rpc-client.rs

View File

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

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

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

View File

@ -6,13 +6,36 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
## [0.5.1] - 2021-02-03
### Changed
- Connection manager completely rewritten.
- 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.
- No longer depends on `libc` for process id lookup
## [0.2.3] - 2018-04-08

View File

@ -5,9 +5,7 @@ Contributions to this project are welcome, just follow these steps.
1. Fork this repository and create a feature branch named after the feature you want to implement
2. Make your changes on your branch
3. Add some test if possibe
4. Make sure all tests pass (I recommend installing [Overcommit][overcommit])
4. Make sure all tests pass
5. Submit a PR/MR on GitHub or GitLab
> **Note**: Make sure you rebase your feature branch on top of master from time to time.
[overcommit]: https://github.com/brigade/overcommit

View File

@ -1,38 +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.3.0"
[badges]
travis-ci = { repository = "valeth/discord-rpc-client.rs" }
appveyor = { repository = "valeth/discord-rpc-client.rs", service = "gitlab" }
maintenance = { status = "experimental" }
[dependencies]
serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
byteorder = "^1.0"
log = "~0.4"
bytes = "^0.4"
parking_lot = "^0.7"
crossbeam-channel = "^0.3"
[target.'cfg(windows)'.dependencies]
named_pipe = "0.3.0"
[dependencies.uuid]
version = "^0.6.2"
features = ["v4"]
[dev-dependencies]
simplelog = "~0.5"
authors = [
"Patrick Auernig <dev.patrick.auernig@gmail.com>",
"Michael Pfaff <michael@pfaff.dev>",
]
keywords = ["discord", "rpc", "ipc"]
license = "MIT"
readme = "README.md"
repository = "https://gitlab.com/valeth/discord-rpc-client.rs.git"
edition = "2021"
[features]
default = ["rich_presence"]
rich_presence = []
tokio-parking_lot = ["tokio/parking_lot"]
java-bindings = ["lazy_static", "jni", "tokio/rt-multi-thread"]
[dependencies]
anyhow = "1"
async-trait = "0.1.52"
byteorder = "1.0"
bytes = "1.1.0"
const_format = "0.2.22"
jni = { version = "0.19", optional = true }
lazy_static = { version = "1.4", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.16", features = ["io-util", "net", "sync", "macros", "rt"] }
tracing = "0.1.32"
uuid = { version = "0.8", features = ["v4"] }
[dev-dependencies]
tokio = { version = "1.16", features = [
"time",
"rt-multi-thread",
"macros",
"parking_lot",
] }
tracing-subscriber = "0.3.9"
# this is a workaround to not being able to specify either multiple libs or conditional compilation based on crate-type.
[[example]]
name = "discord_rpc_java"
path = "examples/java.rs"
crate-type = ["cdylib"]
required-features = ["java-bindings"]

View File

@ -1,8 +1,7 @@
[![Build Status][travis-ci-badge]][travis-ci-page] [![Build status][appveyor-ci-badge]][appveyor-ci-page] [![crates.io][crates-io-badge-ver]][crates-io-page] [![crates.io][crates-io-badge-dl]][crates-io-page] [![Discord][discord-badge]][discord-invite]
# 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
@ -50,17 +49,3 @@ fn main() {
See [CONTRIBUTING.md](CONTRIBUTING.md)
<!-- 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-ver]: https://img.shields.io/crates/v/discord-rpc-client.svg
[crates-io-badge-dl]: https://img.shields.io/crates/d/discord-rpc-client.svg
[crates-io-page]: https://crates.io/crates/discord-rpc-client
[travis-ci-badge]: https://travis-ci.org/valeth/discord-rpc-client.rs.svg?branch=master
[travis-ci-page]: https://travis-ci.org/valeth/discord-rpc-client.rs
[appveyor-ci-badge]: https://ci.appveyor.com/api/projects/status/3fba86eipx0sgsjp?svg=true
[appveyor-ci-page]: https://ci.appveyor.com/project/valeth/discord-rpc-client-rs
[discord-invite]: https://discordapp.com/invite/zfavwrA
[discord-badge]: https://discordapp.com/api/guilds/200751504175398912/widget.png

View File

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

View File

@ -1,16 +1,16 @@
extern crate simplelog;
extern crate discord_rpc_client;
#[macro_use]
extern crate tracing;
use discord_rpc_client::{models::Activity, Client as DiscordRPC};
use std::io;
use simplelog::*;
use discord_rpc_client::Client as DiscordRPC;
fn main() {
TermLogger::init(LevelFilter::Debug, Config::default()).unwrap();
#[tokio::main]
async fn main() -> discord_rpc_client::Result<()> {
tracing_subscriber::fmt::init();
let mut drpc = DiscordRPC::new(425407036495495169);
let drpc = DiscordRPC::default();
drpc.start();
drpc.connect(425407036495495169).await?;
loop {
let mut buf = String::new();
@ -19,20 +19,21 @@ fn main() {
buf.pop();
if buf.is_empty() {
if let Err(why) = drpc.clear_activity() {
println!("Failed to clear presence: {}", why);
if let Err(why) = drpc.clear_activity().await {
error!("Failed to clear presence: {}", why);
}
} else {
if let Err(why) = drpc.set_activity(|a| a
.state(buf)
.assets(|ass| ass
.large_image("ferris_wat")
.large_text("wat.")
.small_image("rusting")
.small_text("rusting...")))
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
{
println!("Failed to set presence: {}", why);
error!("Failed to set presence: {}", why);
}
}
};
}
}

View File

@ -1,33 +1,34 @@
extern crate simplelog;
extern crate discord_rpc_client;
#[macro_use]
extern crate tracing;
use discord_rpc_client::{models::Event, Client as DiscordRPC};
use std::{thread, time};
use simplelog::*;
use discord_rpc_client::{
Client as DiscordRPC,
models::Event,
};
fn main() {
TermLogger::init(LevelFilter::Debug, Config::default()).unwrap();
#[tokio::main]
async fn main() -> discord_rpc_client::Result<()> {
tracing_subscriber::fmt::init();
let mut drpc = DiscordRPC::new(425407036495495169);
let drpc = DiscordRPC::default();
drpc.start();
drpc.connect(425407036495495169).await?;
drpc.subscribe(Event::ActivityJoin, |j| j
.secret("123456"))
drpc.subscribe(Event::ActivityJoin, |j| j.secret("123456"))
.await
.expect("Failed to subscribe to event");
drpc.subscribe(Event::ActivitySpectate, |s| s
.secret("123456"))
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 { thread::sleep(time::Duration::from_millis(500)); }
loop {
tokio::time::sleep(time::Duration::from_millis(500)).await;
}
}

116
examples/java.rs Normal file
View File

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

3
java/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,90 +1,351 @@
use serde::{Serialize, de::DeserializeOwned};
use serde::{de::DeserializeOwned, Serialize};
#[allow(unused)]
use serde_json::Value;
use tokio::select;
use tokio::sync::watch;
use tokio::sync::Mutex;
use connection::Manager as ConnectionManager;
use models::{
OpCode,
Command,
Event,
payload::Payload,
message::Message,
commands::{SubscriptionArgs, Subscription},
};
use crate::connection::{Connection, SocketConnection};
use crate::error::{Error, Result};
#[cfg(feature = "rich_presence")]
use models::rich_presence::{
SetActivityArgs,
Activity,
SendActivityJoinInviteArgs,
CloseActivityRequestArgs,
use crate::models::rich_presence::{
Activity, CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs,
};
use crate::models::{
commands::{Subscription, SubscriptionArgs},
message::Message,
payload::Payload,
Command, Event, OpCode,
};
use error::{Result, Error};
macro_rules! hollow {
($expr:expr) => {{
let ref_ = $expr.borrow();
ref_.hollow()
}};
}
#[derive(Clone)]
#[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 {
connection_manager: ConnectionManager,
state_sender: watch::Sender<FullConnectionState>,
state_receiver: watch::Receiver<FullConnectionState>,
update: Mutex<()>,
}
impl Default for Client {
fn default() -> Self {
let (state_sender, state_receiver) = watch::channel(ConnectionState::Disconnected);
Self {
state_sender,
state_receiver,
update: Mutex::new(()),
}
}
}
impl Client {
pub fn new(client_id: u64) -> Self {
let connection_manager = ConnectionManager::new(client_id);
Self { connection_manager }
/// 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) {
self.connection_manager.start();
#[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)
}
fn execute<A, E>(&mut self, cmd: Command, args: A, evt: Option<Event>) -> Result<Payload<E>>
where A: Serialize + Send + Sync,
E: Serialize + DeserializeOwned + Send + Sync
#[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));
self.connection_manager.send(message)?;
let Message { payload, .. } = self.connection_manager.recv()?;
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)
_ => Ok(response),
}
}
#[cfg(feature = "rich_presence")]
pub fn set_activity<F>(&mut self, f: F) -> Result<Payload<Activity>>
where F: FnOnce(Activity) -> Activity
{
self.execute(Command::SetActivity, SetActivityArgs::new(f), None)
pub async fn set_activity(&self, activity: Activity) -> Result<Payload<Activity>> {
self.execute(Command::SetActivity, SetActivityArgs::new(activity), None)
.await
}
#[cfg(feature = "rich_presence")]
pub fn clear_activity(&mut self) -> Result<Payload<Activity>> {
pub async fn clear_activity(&self) -> Result<Payload<Activity>> {
self.execute(Command::SetActivity, SetActivityArgs::default(), None)
.await
}
// NOTE: Not sure what the actual response values of
// SEND_ACTIVITY_JOIN_INVITE and CLOSE_ACTIVITY_REQUEST are,
// they are not documented.
#[cfg(feature = "rich_presence")]
pub fn send_activity_join_invite(&mut self, user_id: u64) -> Result<Payload<Value>> {
self.execute(Command::SendActivityJoinInvite, SendActivityJoinInviteArgs::new(user_id), None)
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 fn close_activity_request(&mut self, user_id: u64) -> Result<Payload<Value>> {
self.execute(Command::CloseActivityRequest, CloseActivityRequestArgs::new(user_id), None)
pub async fn close_activity_request(&self, user_id: u64) -> Result<Payload<Value>> {
self.execute(
Command::CloseActivityRequest,
CloseActivityRequestArgs::new(user_id),
None,
)
.await
}
pub fn subscribe<F>(&mut self, evt: Event, f: F) -> Result<Payload<Subscription>>
where F: FnOnce(SubscriptionArgs) -> SubscriptionArgs
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 fn unsubscribe<F>(&mut self, evt: Event, f: F) -> Result<Payload<Subscription>>
where F: FnOnce(SubscriptionArgs) -> SubscriptionArgs
pub async fn unsubscribe<F>(&self, evt: Event, f: F) -> Result<Payload<Subscription>>
where
F: FnOnce(SubscriptionArgs) -> SubscriptionArgs,
{
self.execute(Command::Unsubscribe, f(SubscriptionArgs::new()), Some(evt))
.await
}
}

View File

@ -1,82 +1,80 @@
use std::{
io::{Write, Read, ErrorKind},
marker::Sized,
path::PathBuf,
thread,
time,
};
use std::{marker::Sized, path::PathBuf};
use bytes::BytesMut;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use utils;
use models::message::{Message, OpCode};
use error::{Error, Result};
use crate::error::{Error, Result};
use crate::models::message::{Message, OpCode};
use crate::utils;
/// Wait for a non-blocking connection until it's complete.
macro_rules! try_until_done {
[ $e:expr ] => {
loop {
match $e {
Ok(_) => break,
Err(Error::IoError(ref err)) if err.kind() == ErrorKind::WouldBlock => (),
Err(why) => return Err(why),
}
thread::sleep(time::Duration::from_millis(500));
}
}
}
pub trait Connection: Sized {
type Socket: Write + Read;
#[async_trait::async_trait]
pub trait Connection: Sized + Send {
type Socket: AsyncWrite + AsyncRead + Unpin + Send;
fn socket(&mut self) -> &mut Self::Socket;
fn ipc_path() -> PathBuf;
fn connect() -> Result<Self>;
async fn connect() -> Result<Self>;
async fn disconnect(self) -> Result<()>;
fn socket_path(n: u8) -> PathBuf {
Self::ipc_path().join(format!("discord-ipc-{}", n))
}
fn handshake(&mut self, client_id: u64) -> Result<()> {
async fn handshake(&mut self, client_id: u64) -> Result<()> {
let hs = json![{
"client_id": client_id.to_string(),
"v": 1,
"nonce": utils::nonce()
}];
try_until_done!(self.send(Message::new(OpCode::Handshake, hs.clone())));
try_until_done!(self.recv());
self.send(Message::new(OpCode::Handshake, hs.clone()))
.await?;
self.recv().await?;
Ok(())
}
fn ping(&mut self) -> Result<OpCode> {
async fn ping(&mut self) -> Result<OpCode> {
let message = Message::new(OpCode::Ping, json![{}]);
self.send(message)?;
let response = self.recv()?;
self.send(message).await?;
let response = self.recv().await?;
Ok(response.opcode)
}
fn send(&mut self, message: Message) -> Result<()> {
match message.encode() {
Err(why) => error!("{:?}", why),
Ok(bytes) => {
self.socket().write_all(bytes.as_ref())?;
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(())
}
fn recv(&mut self) -> Result<Message> {
async fn recv(&mut self) -> Result<Message> {
let mut buf = BytesMut::new();
buf.resize(1024, 0);
let n = self.socket().read(&mut buf)?;
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 {

View File

@ -1,138 +0,0 @@
use std::{
thread,
sync::{
Arc,
},
time,
io::ErrorKind
};
use crossbeam_channel::{unbounded, Receiver, Sender};
use parking_lot::Mutex;
use super::{
Connection,
SocketConnection,
};
use models::Message;
use error::{Result, Error};
type Tx = Sender<Message>;
type Rx = Receiver<Message>;
#[derive(Clone)]
pub struct Manager {
connection: Arc<Option<Mutex<SocketConnection>>>,
client_id: u64,
outbound: (Rx, Tx),
inbound: (Rx, Tx),
handshake_completed: bool,
}
impl Manager {
pub fn new(client_id: u64) -> Self {
let connection = Arc::new(None);
let (sender_o, receiver_o) = unbounded();
let (sender_i, receiver_i) = unbounded();
Self {
connection,
client_id,
handshake_completed: false,
inbound: (receiver_i, sender_i),
outbound: (receiver_o, sender_o),
}
}
pub fn start(&mut self) {
let manager_inner = self.clone();
thread::spawn(move || {
send_and_receive_loop(manager_inner);
});
}
pub fn send(&self, message: Message) -> Result<()> {
self.outbound.1.send(message).unwrap();
Ok(())
}
pub fn recv(&self) -> Result<Message> {
let message = self.inbound.0.recv().unwrap();
Ok(message)
}
fn connect(&mut self) -> Result<()> {
if self.connection.is_some() {
return Ok(());
}
debug!("Connecting");
let mut new_connection = SocketConnection::connect()?;
debug!("Performing handshake");
new_connection.handshake(self.client_id)?;
debug!("Handshake completed");
self.connection = Arc::new(Some(Mutex::new(new_connection)));
debug!("Connected");
Ok(())
}
fn disconnect(&mut self) {
self.handshake_completed = false;
self.connection = Arc::new(None);
}
}
fn send_and_receive_loop(mut manager: Manager) {
debug!("Starting sender loop");
let mut inbound = manager.inbound.1.clone();
let outbound = manager.outbound.0.clone();
loop {
let connection = manager.connection.clone();
match *connection {
Some(ref conn) => {
let mut connection = conn.lock();
match send_and_receive(&mut *connection, &mut inbound, &outbound) {
Err(Error::IoError(ref err)) if err.kind() == ErrorKind::WouldBlock => (),
Err(Error::IoError(_)) | Err(Error::ConnectionClosed) => manager.disconnect(),
Err(why) => error!("error: {}", why),
_ => (),
}
thread::sleep(time::Duration::from_millis(500));
},
None => {
match manager.connect() {
Err(err) => {
match err {
Error::IoError(ref err) if err.kind() == ErrorKind::ConnectionRefused => (),
why => error!("Failed to connect: {:?}", why),
}
thread::sleep(time::Duration::from_secs(10));
},
_ => manager.handshake_completed = true,
}
}
}
}
}
fn send_and_receive(connection: &mut SocketConnection, inbound: &mut Tx, outbound: &Rx) -> Result<()> {
while let Ok(msg) = outbound.try_recv() {
connection.send(msg).expect("Failed to send outgoing data");
}
let msg = connection.recv()?;
inbound.send(msg).expect("Failed to send received data");
Ok(())
}

View File

@ -1,12 +1,10 @@
mod base;
mod manager;
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
pub use self::base::Connection as Connection;
pub use self::manager::Manager;
pub use self::base::Connection;
#[cfg(unix)]
pub use self::unix::UnixConnection as SocketConnection;
#[cfg(windows)]

View File

@ -1,52 +1,48 @@
use std::{
time,
path::PathBuf,
env,
os::unix::net::UnixStream,
net::Shutdown,
};
use std::{env, path::PathBuf};
use tokio::{io::AsyncWriteExt, net::UnixStream};
use super::base::Connection;
use error::Result;
use crate::error::Result;
#[derive(Debug)]
pub struct UnixConnection {
socket: UnixStream,
}
#[async_trait::async_trait]
impl Connection for UnixConnection {
type Socket = UnixStream;
fn connect() -> Result<Self> {
let connection_name = Self::socket_path(0);
let socket = UnixStream::connect(connection_name)?;
socket.set_nonblocking(true)?;
socket.set_write_timeout(Some(time::Duration::from_secs(30)))?;
socket.set_read_timeout(Some(time::Duration::from_secs(30)))?;
Ok(Self { socket })
}
fn ipc_path() -> PathBuf {
let tmp = env::var("XDG_RUNTIME_DIR")
.or_else(|_| env::var("TMPDIR"))
.or_else(|_| {
match env::temp_dir().to_str() {
None => Err("Failed to convert temp_dir"),
Some(tmp) => Ok(tmp.to_string())
}
})
.unwrap_or("/tmp".to_string());
PathBuf::from(tmp)
}
fn socket(&mut self) -> &mut Self::Socket {
&mut self.socket
}
}
impl Drop for UnixConnection {
fn drop(&mut self) {
self.socket.shutdown(Shutdown::Both)
.expect("Failed to properly shut down socket");
fn ipc_path() -> PathBuf {
env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.map(|mut p| {
// try flatpak dir
p.push("app/com.discordapp.Discord");
if !p.exists() {
p.pop();
p.pop();
}
p
})
.or_else(|| env::var_os("TMPDIR").map(PathBuf::from))
.or_else(|| env::temp_dir().to_str().map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("/tmp"))
}
async fn connect() -> Result<Self> {
let connection_name = Self::socket_path(0);
let socket = UnixStream::connect(connection_name).await?;
Ok(Self { socket })
}
async fn disconnect(mut self) -> Result<()> {
self.socket.shutdown().await?;
Ok(())
}
}

View File

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

View File

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

279
src/java.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
use std::io::{self, Write, Read};
use std::io::{self, Read, Write};
use byteorder::{WriteBytesExt, ReadBytesExt, LittleEndian};
use serde_json;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use serde::Serialize;
use serde_json;
use error::{Result, Error};
use crate::error::{Error, Result};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum OpCode {
@ -16,16 +15,17 @@ pub enum OpCode {
Pong,
}
// FIXME: Use TryFrom trait when stable
impl OpCode {
fn try_from(int: u32) -> Result<Self> {
impl TryFrom<u32> for OpCode {
type Error = Error;
fn try_from(int: u32) -> Result<Self, Self::Error> {
match int {
0 => Ok(OpCode::Handshake),
1 => Ok(OpCode::Frame),
2 => Ok(OpCode::Close),
3 => Ok(OpCode::Ping),
4 => Ok(OpCode::Pong),
_ => Err(Error::Conversion)
_ => Err(Error::Conversion),
}
}
}
@ -38,9 +38,13 @@ pub struct Message {
impl Message {
pub fn new<T>(opcode: OpCode, payload: T) -> Self
where T: Serialize
where
T: Serialize,
{
Self { opcode, payload: serde_json::to_string(&payload).unwrap() }
Self {
opcode,
payload: serde_json::to_string(&payload).unwrap(),
}
}
pub fn encode(&self) -> Result<Vec<u8>> {
@ -65,14 +69,13 @@ impl Message {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Something {
empty: bool
empty: bool,
}
#[test]

View File

@ -1,10 +1,9 @@
mod shared;
pub mod message;
pub mod payload;
pub mod commands;
pub mod events;
pub mod message;
pub mod payload;
pub mod rich_presence;
mod shared;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
@ -34,30 +33,21 @@ pub enum Event {
ActivityJoinRequest,
}
pub use self::message::{Message, OpCode};
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::Command;
pub use super::Event;
pub use super::commands::{Subscription, SubscriptionArgs};
pub use super::events::{ErrorEvent, ReadyEvent};
#[cfg(feature = "rich_presence")]
pub use super::rich_presence::{
SetActivityArgs,
SendActivityJoinInviteArgs,
CloseActivityRequestArgs,
ActivityJoinEvent,
ActivitySpectateEvent,
ActivityJoinRequestEvent
};
pub use super::commands::{
SubscriptionArgs, Subscription
};
pub use super::events::{
ReadyEvent,
ErrorEvent,
ActivityJoinEvent, ActivityJoinRequestEvent, ActivitySpectateEvent,
CloseActivityRequestArgs, SendActivityJoinInviteArgs, SetActivityArgs,
};
pub use super::Command;
pub use super::Event;
}

View File

@ -1,17 +1,15 @@
use std::{
convert::From,
};
use std::convert::From;
use serde::{Serialize, de::DeserializeOwned};
use serde::{de::DeserializeOwned, Serialize};
use serde_json;
use super::{Command, Event, Message};
use utils;
use crate::utils;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Payload<T>
where T: Serialize
where
T: Serialize,
{
pub cmd: Command,
@ -29,15 +27,23 @@ pub struct Payload<T>
}
impl<T> Payload<T>
where T: Serialize
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()) }
Self {
cmd,
args,
data,
evt,
nonce: Some(utils::nonce()),
}
}
}
impl<T> From<Message> for Payload<T>
where T: Serialize + DeserializeOwned
where
T: Serialize + DeserializeOwned,
{
fn from(message: Message) -> Self {
serde_json::from_str(&message.payload).unwrap()

View File

@ -3,8 +3,7 @@
use std::default::Default;
use super::shared::PartialUser;
use utils;
use crate::utils;
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct SetActivityArgs {
@ -15,16 +14,20 @@ pub struct SetActivityArgs {
}
impl SetActivityArgs {
pub fn new<F>(f: F) -> Self
where F: FnOnce(Activity) -> Activity
{
Self { pid: utils::pid(), activity: Some(f(Activity::new())) }
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 }
Self {
pid: utils::pid(),
activity: None,
}
}
}
@ -37,24 +40,26 @@ pub type CloseActivityRequestArgs = SendActivityJoinInviteArgs;
impl SendActivityJoinInviteArgs {
pub fn new(user_id: u64) -> Self {
Self { user_id: user_id.to_string() }
Self {
user_id: user_id.to_string(),
}
}
}
builder!{ActivityJoinEvent
builder! {ActivityJoinEvent
secret: String,
}
builder!{ActivitySpectateEvent
builder! {ActivitySpectateEvent
secret: String,
}
builder!{ActivityJoinRequestEvent
builder! {ActivityJoinRequestEvent
user: PartialUser,
}
builder!{Activity
// TODO: remove this stupid builder func pattern.
builder! {Activity
state: String,
details: String,
instance: bool,
@ -64,37 +69,35 @@ builder!{Activity
secrets: ActivitySecrets func,
}
builder!{ActivityTimestamps
builder! {ActivityTimestamps
start: u64,
end: u64,
}
builder!{ActivityAssets
builder! {ActivityAssets
large_image: String,
large_text: String,
small_image: String,
small_text: String,
}
builder!{ActivityParty
builder! {ActivityParty
id: u32,
size: (u32, u32),
}
builder!{ActivitySecrets
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###"{
const FULL_JSON: &'static str = r###"{
"state": "rusting",
"details": "detailed",
"instance": true,
@ -128,21 +131,19 @@ r###"{
.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"));
.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();

View File

@ -1,4 +1,4 @@
builder!{PartialUser
builder! {PartialUser
id: String,
username: String,
discriminator: String,

View File

@ -1,6 +1,5 @@
use uuid::Uuid;
#[allow(unused)]
pub fn pid() -> u32 {
std::process::id()