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:
Patrick Auernig 2018-03-22 15:35:48 +01:00
commit f24793d7f2
14 changed files with 561 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -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

View File

@ -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

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock

13
.overcommit.yml Normal file
View File

@ -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"

23
Cargo.toml Normal file
View File

@ -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 = []

16
README.md Normal file
View File

@ -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.

View File

@ -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)) };
}

76
src/client.rs Normal file
View File

@ -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(())
}
}

13
src/lib.rs Normal file
View File

@ -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;

29
src/models/command.rs Normal file
View File

@ -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 {}

21
src/models/handshake.rs Normal file
View File

@ -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 {}

73
src/models/message.rs Normal file
View File

@ -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);
}
}

22
src/models/mod.rs Normal file
View File

@ -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;
}

218
src/models/set_activity.rs Normal file
View File

@ -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
}
}