diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bb75ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ffc7b6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: crystal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77f5638 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 triinoxys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f04c13b --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# RP42 + +A Discord Rich Presence integration for [@42School](https://github.com/42School). + +## Installation + +Just download a prebuilt binary from the [releases](https://github.com/triinoxys/RP42/releases) page, or build it yourself. +To build it: +1. Clone the repo: `git clone https://github.com/triinoxys/RP42.git` +2. Install dependencies: `shards install` (no deps right now) +3. Compile: `crystal build src/RP42.cr --release --no-debug` + +## Usage + +Just execute the newly created file (in background): `./RP42 &` + +## Contributing + +1. Fork it () +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request + +## Contributors + +- [triinoxys/aguiot--](https://github.com/triinoxys) - creator and maintainer diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..fe51798 --- /dev/null +++ b/shard.yml @@ -0,0 +1,13 @@ +name: RP42 +version: 0.1.0 + +authors: + - aguiot-- + +targets: + RP42: + main: src/RP42.cr + +crystal: 0.27.0 + +license: MIT diff --git a/spec/RP42_spec.cr b/spec/RP42_spec.cr new file mode 100644 index 0000000..048da71 --- /dev/null +++ b/spec/RP42_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe RP42 do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..ec7dd10 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/RP42" diff --git a/src/RP42.cr b/src/RP42.cr new file mode 100644 index 0000000..ab802fc --- /dev/null +++ b/src/RP42.cr @@ -0,0 +1,20 @@ +require "./client" + +module RP42 + VERSION = "0.1.0" + + hostname = System.hostname.split(".", 2) + exit if hostname[1] != "42.fr" + + client = RichCrystal::Client.new(531103976029028367_u64) + client.login + client.activity({ + "details" => "Location: #{hostname[0]}", + "assets" => { + "large_image" => "logo", + "large_text" => "42", + } + }) + + sleep +end diff --git a/src/client.cr b/src/client.cr new file mode 100644 index 0000000..0217a04 --- /dev/null +++ b/src/client.cr @@ -0,0 +1,42 @@ +require "json" +require "./ipc" + +module RichCrystal + class Client + # Creates a new Rich-crystal client used for set rich presence activity + def initialize(@client_id : UInt64) + @ipc = RichCrystal::Ipc.new + end + + # Log the RichCrystal client by sending a first handshake to the IPC + def login + # Generate the payload in JSON + payload = { + "v" => 1, + "client_id" => "#{@client_id}", + "nonce" => Time.now.to_s("%s"), + }.to_json + + # Send the handshake to the IPC + @ipc.send(RichCrystal::Ipc::Opcode::Handshake, payload) + end + + # Retrieves a Hash of Strings for sending the frame payload to the IPC + # with discord-rich-presence parameters (see here https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md#new-rpc-command) + # and return the JSON response + def activity(activity) + # Generate the payload in JSON + payload = { + "cmd" => "SET_ACTIVITY", + "args" => { + "pid" => Process.pid, + "activity" => activity, + }, + "nonce" => Time.now.to_s("%s"), + }.to_json + + # Send the frame to the IPC + @ipc.send(RichCrystal::Ipc::Opcode::Frame, payload) + end + end +end diff --git a/src/ipc.cr b/src/ipc.cr new file mode 100644 index 0000000..c10ff5f --- /dev/null +++ b/src/ipc.cr @@ -0,0 +1,58 @@ +require "socket" + +module RichCrystal + class Ipc + # Enumerate the differents opcodes + enum Opcode : Int32 + Handshake = 0 + Frame = 1 + end + + # Create the UNIXSocket with ipc_path and socket name 'discord-ipc-0' + def initialize + @socket = UNIXSocket.new("#{ipc_path}discord-ipc-0") + end + + # Return where is the discord-ipc-0 socket with + # different environment variables + def ipc_path : String + # Possibles path environment variables names + variables = %w(XDG_RUNTIME_DIR TMPDIR TMP TEMP) + + # Iterate environment variables names + variables.each do |variable_name| + # Handling a key error if the environment variable does not exists + begin + variable = ENV[variable_name] + return variable + rescue KeyError + next # Continue the loop if the variable does not exists + end + end + + # If none of the environment variables have been found return '\tmp' + "\tmp" + end + + # Send a payload to the UNIXSocket with the opcode, returns + # the JSON response + def send(opcode : Opcode, payload : String) : String + # Write the opcode and the payload size as LitteEndian + @socket.write_bytes(opcode.value, IO::ByteFormat::LittleEndian) + @socket.write_bytes(payload.size, IO::ByteFormat::LittleEndian) + + # And then add the payload + @socket << payload + + # Read the response code + code = @socket.read_bytes(Int32, IO::ByteFormat::LittleEndian) + # Read the size of the data + data_size = @socket.read_bytes(Int32, IO::ByteFormat::LittleEndian) + + # Then fully read the data_size number of bytes and convert them into a String + bytes = Bytes.new(data_size) + @socket.read_fully(bytes) + String.new(bytes) + end + end +end