Add Discord rich presence support
Allow sending rich presence status to your Discord client. Currently only supports setting the status once. Only Unix systems supported for now.
This commit is contained in:
commit
f24793d7f2
|
@ -0,0 +1,18 @@
|
|||
# for the sake of sanity
|
||||
|
||||
# topmost editorconfig file
|
||||
root = true
|
||||
|
||||
# always use Unix-style newlines,
|
||||
# use 4 spaces for indentation
|
||||
# and use UTF-8 encoding
|
||||
[*]
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
|
@ -0,0 +1,13 @@
|
|||
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
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
|
@ -0,0 +1,13 @@
|
|||
PreCommit:
|
||||
TrailingWhitespace:
|
||||
enabled: true
|
||||
exclude:
|
||||
- 'target/**/*'
|
||||
|
||||
PrePush:
|
||||
CargoTest:
|
||||
enabled: true
|
||||
description: 'Run Cargo tests'
|
||||
required_executable: 'cargo'
|
||||
flags: ['test', '--all']
|
||||
include: "**/*.rs"
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
authors = ["Patrick Auernig <dev.patrick.auernig@gmail.com>"]
|
||||
name = "discord-rpc-client"
|
||||
description = "A Rust client for Discord RPC."
|
||||
keywords = ["discord", "rpc", "ipc"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://gitlab.com/valeth/discord-rpc-client.rs.git"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
serde = "^1.0"
|
||||
serde_derive = "^1.0"
|
||||
serde_json = "^1.0"
|
||||
byte = "0.2"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "^0.6.2"
|
||||
features = ["v4"]
|
||||
|
||||
[features]
|
||||
default = ["rich_presence"]
|
||||
rich_presence = []
|
|
@ -0,0 +1,16 @@
|
|||
# Discord RPC Client
|
||||
|
||||
Discord RPC client for Rust
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
discord-rpc-client = "^0.1"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Examples can be found in the examples directory in this repo.
|
|
@ -0,0 +1,22 @@
|
|||
extern crate discord_rpc_client;
|
||||
|
||||
use std::{thread, time};
|
||||
use discord_rpc_client::Client as DiscordRPC;
|
||||
|
||||
fn main() {
|
||||
let mut drpc =
|
||||
DiscordRPC::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,76 @@
|
|||
use std::env;
|
||||
use std::io::{Write, Read, Result};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::time;
|
||||
use std::fmt::Debug;
|
||||
use models::{Message, Handshake, Payload};
|
||||
#[cfg(feature = "rich_presence")]
|
||||
use models::{SetActivityArgs, SetActivity};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
pub client_id: u64,
|
||||
pub version: u32,
|
||||
socket: UnixStream,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(client_id: u64) -> Result<Self> {
|
||||
let connection_name = Self::ipc_path();
|
||||
let socket = UnixStream::connect(connection_name)?;
|
||||
socket.set_write_timeout(Some(time::Duration::from_secs(30)))?;
|
||||
socket.set_read_timeout(Some(time::Duration::from_secs(30)))?;
|
||||
Ok(Self { version: 1, client_id, socket })
|
||||
}
|
||||
|
||||
pub fn start(mut self) -> Result<Self> {
|
||||
self.handshake()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rich_presence")]
|
||||
pub fn set_activity<F>(&mut self, f: F) -> Result<()>
|
||||
where F: FnOnce(SetActivity) -> SetActivity
|
||||
{
|
||||
let args = SetActivityArgs::command(f(SetActivity::new()));
|
||||
self.send(1, args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// private
|
||||
|
||||
fn handshake(&mut self) -> Result<()> {
|
||||
let client_id = self.client_id;
|
||||
let version = self.version;
|
||||
self.send(0, Handshake::new(client_id, version))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ipc_path() -> String {
|
||||
let tmp = env::var("XDG_RUNTIME_DIR").unwrap_or("/tmp".into());
|
||||
format!("{}/discord-ipc-0", tmp)
|
||||
}
|
||||
|
||||
fn send<T>(&mut self, opcode: u32, payload: T) -> Result<()>
|
||||
where T: Payload + Debug
|
||||
{
|
||||
println!("payload: {:#?}", payload);
|
||||
match Message::new(opcode, payload).encode() {
|
||||
Err(why) => println!("{:?}", why),
|
||||
Ok(bytes) => {
|
||||
self.socket.write_all(bytes.as_ref())?;
|
||||
println!("sent opcode: {}", opcode);
|
||||
self.receive()?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive(&mut self) -> Result<()> {
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(1024);
|
||||
self.socket.read(buf.as_mut_slice())?;
|
||||
println!("{:?}", buf);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
#![feature(getpid)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate byte;
|
||||
extern crate uuid;
|
||||
|
||||
mod models;
|
||||
mod client;
|
||||
|
||||
pub use client::Client;
|
|
@ -0,0 +1,29 @@
|
|||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
use super::Payload;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Command<T>
|
||||
where T: Serialize
|
||||
{
|
||||
pub nonce: String,
|
||||
pub cmd: String,
|
||||
pub args: T,
|
||||
}
|
||||
|
||||
impl<T> Command<T>
|
||||
where T: Serialize
|
||||
{
|
||||
pub fn new<S>(cmd: S, args: T) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
Command {
|
||||
cmd: cmd.into(),
|
||||
nonce: Uuid::new_v4().to_string(),
|
||||
args: args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Payload for Command<T>
|
||||
where T: Serialize {}
|
|
@ -0,0 +1,21 @@
|
|||
use uuid::Uuid;
|
||||
use super::Payload;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Handshake {
|
||||
pub nonce: String,
|
||||
pub v: u32,
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
impl Handshake {
|
||||
pub fn new(client_id: u64, version: u32) -> Self {
|
||||
Self {
|
||||
nonce: Uuid::new_v4().to_string(),
|
||||
v: version,
|
||||
client_id: client_id.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Payload for Handshake {}
|
|
@ -0,0 +1,73 @@
|
|||
use byte::{TryRead, TryWrite, BytesExt, Result};
|
||||
use byte::ctx::{Endian, LE, Str};
|
||||
use serde_json;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[repr(C)]
|
||||
pub struct Message {
|
||||
opcode: u32,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl<'a> TryRead<'a, Endian> for Message {
|
||||
fn try_read(bytes: &'a [u8], endian: Endian) -> Result<(Self, usize)> {
|
||||
let offset = &mut 0;
|
||||
let opcode: u32 = bytes.read_with(offset, endian)?;
|
||||
let message_length = bytes.read_with::<u32>(offset, endian)? as usize;
|
||||
let message = bytes.read_with::<&str>(offset, Str::Len(message_length))?.to_string();
|
||||
Ok((Message { opcode, message }, *offset))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryWrite<Endian> for Message {
|
||||
fn try_write(self, bytes: &mut [u8], endian: Endian) -> Result<usize> {
|
||||
let offset = &mut 0;
|
||||
bytes.write_with::<u32>(offset, self.opcode, endian)?;
|
||||
bytes.write_with::<u32>(offset, self.message.len() as u32, endian)?;
|
||||
bytes.write_with::<&str>(offset, self.message.as_ref(), ())?;
|
||||
Ok(*offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new<T>(opcode: u32, message: T) -> Self
|
||||
where T: Serialize
|
||||
{
|
||||
Message {
|
||||
opcode,
|
||||
message: serde_json::to_string(&message).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||
let mut bytes: Vec<u8> = vec![0; 2*4+self.message.len()];
|
||||
bytes.write_with(&mut 0, self.clone(), LE)?;
|
||||
bytes.shrink_to_fit();
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn decode<'a>(bytes: &'a [u8]) -> Result<Self> {
|
||||
let message: Message = bytes.read_with(&mut 0, LE)?;
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct Something {
|
||||
empty: bool
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoder() {
|
||||
let msg = Message::new(1, Something { empty: true });
|
||||
let encoded = msg.encode().unwrap();
|
||||
let decoded = Message::decode(encoded.as_ref()).unwrap();
|
||||
assert_eq!(msg, decoded);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
mod message;
|
||||
mod command;
|
||||
mod handshake;
|
||||
#[cfg(feature = "rich_presence")]
|
||||
mod set_activity;
|
||||
|
||||
use serde::Serialize;
|
||||
pub use self::message::Message;
|
||||
pub use self::command::Command;
|
||||
pub use self::handshake::Handshake;
|
||||
#[cfg(feature = "rich_presence")]
|
||||
pub use self::set_activity::*;
|
||||
|
||||
pub trait Payload: Serialize {}
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::message::Message;
|
||||
pub use super::command::Command;
|
||||
pub use super::handshake::Handshake;
|
||||
#[cfg(feature = "rich_presence")]
|
||||
pub use super::set_activity::SetActivity;
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
use std::process::id as pid;
|
||||
use models::prelude::Command;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct SetActivityArgs {
|
||||
pub pid: u32,
|
||||
pub activity: SetActivity,
|
||||
}
|
||||
|
||||
impl SetActivityArgs {
|
||||
pub fn command(args: SetActivity) -> Command<Self> {
|
||||
Command::new("SET_ACTIVITY", Self {
|
||||
pid: pid(),
|
||||
activity: args
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct SetActivity {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub state: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub instance: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timestamps: Option<ActivityTimestamps>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assets: Option<ActivityAssets>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub party: Option<ActivityParty>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub secrets: Option<ActivitySecrets>,
|
||||
}
|
||||
|
||||
impl SetActivity {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn state<S>(mut self, s: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.state = Some(s.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn details<S>(mut self, d: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.details = Some(d.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn instance(mut self, i: bool) -> Self {
|
||||
self.instance = Some(i);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamps<F>(mut self, f: F) -> Self
|
||||
where F: FnOnce(ActivityTimestamps) -> ActivityTimestamps
|
||||
{
|
||||
self.timestamps = Some(f(ActivityTimestamps::default()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assets<F>(mut self, f: F) -> Self
|
||||
where F: FnOnce(ActivityAssets) -> ActivityAssets
|
||||
{
|
||||
self.assets = Some(f(ActivityAssets::default()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn party<F>(mut self, f: F) -> Self
|
||||
where F: FnOnce(ActivityParty) -> ActivityParty
|
||||
{
|
||||
self.party = Some(f(ActivityParty::default()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn secrets<F>(mut self, f: F) -> Self
|
||||
where F: FnOnce(ActivitySecrets) -> ActivitySecrets
|
||||
{
|
||||
self.secrets = Some(f(ActivitySecrets::default()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ActivityTimestamps {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start: Option<u32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end: Option<u32>,
|
||||
}
|
||||
|
||||
impl ActivityTimestamps {
|
||||
pub fn start(mut self, i: u32) -> Self {
|
||||
self.start = Some(i);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end(mut self, i: u32) -> Self {
|
||||
self.end = Some(i);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ActivityAssets {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub large_image: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub large_text: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub small_image: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub small_text: Option<String>,
|
||||
}
|
||||
|
||||
impl ActivityAssets {
|
||||
pub fn large_image<S>(mut self, i: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.large_image = Some(i.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn large_text<S>(mut self, t: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.large_text = Some(t.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn small_image<S>(mut self, i: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.small_image = Some(i.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn small_text<S>(mut self, t: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.small_text = Some(t.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ActivityParty {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<u32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<[u32; 2]>,
|
||||
}
|
||||
|
||||
impl ActivityParty {
|
||||
pub fn id(mut self, i: u32) -> Self {
|
||||
self.id = Some(i);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, current: u32, max: u32) -> Self {
|
||||
self.size = Some([current, max]);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ActivitySecrets {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub join: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub spectate: Option<String>,
|
||||
|
||||
// NOTE: think of a better name for this
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "match")]
|
||||
pub shoubu: Option<String>,
|
||||
}
|
||||
|
||||
impl ActivitySecrets {
|
||||
pub fn join<S>(mut self, secret: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.join = Some(secret.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn spectate<S>(mut self, secret: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.spectate = Some(secret.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn game<S>(mut self, secret: S) -> Self
|
||||
where S: Into<String>
|
||||
{
|
||||
self.shoubu = Some(secret.into());
|
||||
self
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue