mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-04 02:16:03 -04:00
868 lines
26 KiB
C++
868 lines
26 KiB
C++
#ifdef DUSK_DISCORD
|
|
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
|
|
#include "discord.hpp"
|
|
|
|
#include "dusk/logging.h"
|
|
#include "nlohmann/json.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMCX
|
|
#define NOSERVICE
|
|
#define NOIME
|
|
#include <windows.h>
|
|
#else
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <fcntl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
namespace dusk::discord::rpc {
|
|
namespace {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
constexpr uint32_t kRpcVersion = 1;
|
|
constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2;
|
|
constexpr size_t kMaxFramePayloadSize = 64 * 1024;
|
|
constexpr auto kIoWait = std::chrono::milliseconds(500);
|
|
constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200);
|
|
constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500);
|
|
constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000);
|
|
|
|
enum class Opcode : uint32_t {
|
|
Handshake = 0,
|
|
Frame = 1,
|
|
Close = 2,
|
|
Ping = 3,
|
|
Pong = 4,
|
|
};
|
|
|
|
enum class ConnectionState {
|
|
Disconnected,
|
|
SentHandshake,
|
|
Connected,
|
|
};
|
|
|
|
enum class DisconnectCode : int {
|
|
PipeClosed = 1,
|
|
ReadCorrupt = 2,
|
|
BadFrame = 3,
|
|
};
|
|
|
|
struct Frame {
|
|
Opcode opcode = Opcode::Frame;
|
|
std::string payload;
|
|
};
|
|
|
|
struct QueuedEvent {
|
|
enum class Type {
|
|
Ready,
|
|
Disconnected,
|
|
Error,
|
|
};
|
|
|
|
Type type = Type::Ready;
|
|
User user;
|
|
int code = 0;
|
|
std::string message;
|
|
};
|
|
|
|
class Backoff {
|
|
public:
|
|
std::chrono::milliseconds next_delay() {
|
|
const auto delay = currentDelay;
|
|
currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay);
|
|
return delay;
|
|
}
|
|
|
|
void reset() { currentDelay = kInitialReconnectDelay; }
|
|
|
|
private:
|
|
std::chrono::milliseconds currentDelay = kInitialReconnectDelay;
|
|
};
|
|
|
|
class IpcConnection {
|
|
public:
|
|
IpcConnection() = default;
|
|
~IpcConnection() { close(); }
|
|
|
|
IpcConnection(const IpcConnection&) = delete;
|
|
IpcConnection& operator=(const IpcConnection&) = delete;
|
|
|
|
bool open() {
|
|
#ifdef _WIN32
|
|
wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0";
|
|
constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
|
|
|
for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) {
|
|
pipeName[kPipeDigit] = pipeNumber;
|
|
pipe = CreateFileW(
|
|
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
if (pipe != INVALID_HANDLE_VALUE) {
|
|
return true;
|
|
}
|
|
|
|
if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) {
|
|
--pipeNumber;
|
|
}
|
|
}
|
|
return false;
|
|
#else
|
|
const auto tempPaths = get_temp_paths();
|
|
for (const std::string& tempPath : tempPaths) {
|
|
for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) {
|
|
socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (socketFd == -1) {
|
|
return false;
|
|
}
|
|
|
|
sockaddr_un pipeAddress{};
|
|
pipeAddress.sun_family = AF_UNIX;
|
|
const std::string socketPath =
|
|
tempPath + "/discord-ipc-" + std::to_string(pipeNumber);
|
|
if (socketPath.size() >= sizeof(pipeAddress.sun_path)) {
|
|
close();
|
|
continue;
|
|
}
|
|
|
|
std::strncpy(
|
|
pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1);
|
|
if (connect(socketFd, reinterpret_cast<const sockaddr*>(&pipeAddress),
|
|
sizeof(pipeAddress)) == 0)
|
|
{
|
|
fcntl(socketFd, F_SETFL, O_NONBLOCK);
|
|
#ifdef SO_NOSIGPIPE
|
|
int optval = 1;
|
|
setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
|
#endif
|
|
return true;
|
|
}
|
|
close();
|
|
}
|
|
}
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void close() {
|
|
#ifdef _WIN32
|
|
if (pipe != INVALID_HANDLE_VALUE) {
|
|
CloseHandle(pipe);
|
|
pipe = INVALID_HANDLE_VALUE;
|
|
}
|
|
#else
|
|
if (socketFd != -1) {
|
|
::close(socketFd);
|
|
socketFd = -1;
|
|
}
|
|
#endif
|
|
pendingRead.clear();
|
|
}
|
|
|
|
bool is_open() const {
|
|
#ifdef _WIN32
|
|
return pipe != INVALID_HANDLE_VALUE;
|
|
#else
|
|
return socketFd != -1;
|
|
#endif
|
|
}
|
|
|
|
bool write_frame(const Frame& frame) {
|
|
std::array<uint8_t, kFrameHeaderSize> header{};
|
|
write_u32(header.data(), static_cast<uint32_t>(frame.opcode));
|
|
write_u32(header.data() + sizeof(uint32_t), static_cast<uint32_t>(frame.payload.size()));
|
|
|
|
return write_all(header.data(), header.size()) &&
|
|
write_all(
|
|
reinterpret_cast<const uint8_t*>(frame.payload.data()), frame.payload.size());
|
|
}
|
|
|
|
enum class ReadStatus {
|
|
None,
|
|
Frame,
|
|
Closed,
|
|
Corrupt,
|
|
};
|
|
|
|
ReadStatus read_frame(Frame& frame) {
|
|
if (!read_available()) {
|
|
return is_open() ? ReadStatus::None : ReadStatus::Closed;
|
|
}
|
|
|
|
if (pendingRead.size() < kFrameHeaderSize) {
|
|
return ReadStatus::None;
|
|
}
|
|
|
|
const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t));
|
|
if (payloadLength > kMaxFramePayloadSize) {
|
|
return ReadStatus::Corrupt;
|
|
}
|
|
|
|
const size_t frameLength = kFrameHeaderSize + payloadLength;
|
|
if (pendingRead.size() < frameLength) {
|
|
return ReadStatus::None;
|
|
}
|
|
|
|
frame.opcode = static_cast<Opcode>(read_u32(pendingRead.data()));
|
|
frame.payload.assign(
|
|
reinterpret_cast<const char*>(pendingRead.data() + kFrameHeaderSize), payloadLength);
|
|
pendingRead.erase(
|
|
pendingRead.begin(), pendingRead.begin() + static_cast<std::ptrdiff_t>(frameLength));
|
|
return ReadStatus::Frame;
|
|
}
|
|
|
|
private:
|
|
#ifndef _WIN32
|
|
static std::vector<std::string> get_temp_paths() {
|
|
std::vector<std::string> paths;
|
|
for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) {
|
|
if (const char* value = std::getenv(name); value && value[0] != '\0') {
|
|
if (std::find(paths.begin(), paths.end(), value) == paths.end()) {
|
|
paths.emplace_back(value);
|
|
}
|
|
}
|
|
}
|
|
if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) {
|
|
paths.emplace_back("/tmp");
|
|
}
|
|
return paths;
|
|
}
|
|
#endif
|
|
|
|
static void write_u32(uint8_t* out, uint32_t value) {
|
|
out[0] = static_cast<uint8_t>(value & 0xff);
|
|
out[1] = static_cast<uint8_t>((value >> 8) & 0xff);
|
|
out[2] = static_cast<uint8_t>((value >> 16) & 0xff);
|
|
out[3] = static_cast<uint8_t>((value >> 24) & 0xff);
|
|
}
|
|
|
|
static uint32_t read_u32(const uint8_t* in) {
|
|
return static_cast<uint32_t>(in[0]) | (static_cast<uint32_t>(in[1]) << 8) |
|
|
(static_cast<uint32_t>(in[2]) << 16) | (static_cast<uint32_t>(in[3]) << 24);
|
|
}
|
|
|
|
bool write_all(const uint8_t* data, size_t length) {
|
|
size_t written = 0;
|
|
while (written < length) {
|
|
#ifdef _WIN32
|
|
DWORD bytesWritten = 0;
|
|
if (WriteFile(pipe, data + written, static_cast<DWORD>(length - written), &bytesWritten,
|
|
nullptr) == FALSE ||
|
|
bytesWritten == 0)
|
|
{
|
|
close();
|
|
return false;
|
|
}
|
|
written += bytesWritten;
|
|
#else
|
|
#ifdef MSG_NOSIGNAL
|
|
constexpr int kMsgFlags = MSG_NOSIGNAL;
|
|
#else
|
|
constexpr int kMsgFlags = 0;
|
|
#endif
|
|
const ssize_t bytesWritten =
|
|
send(socketFd, data + written, length - written, kMsgFlags);
|
|
if (bytesWritten < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
continue;
|
|
}
|
|
close();
|
|
return false;
|
|
}
|
|
if (bytesWritten == 0) {
|
|
close();
|
|
return false;
|
|
}
|
|
written += static_cast<size_t>(bytesWritten);
|
|
#endif
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool read_available() {
|
|
std::array<uint8_t, 4096> buffer{};
|
|
bool readAny = false;
|
|
|
|
for (;;) {
|
|
#ifdef _WIN32
|
|
DWORD bytesAvailable = 0;
|
|
if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) {
|
|
close();
|
|
return readAny;
|
|
}
|
|
if (bytesAvailable == 0) {
|
|
return readAny;
|
|
}
|
|
|
|
const DWORD bytesToRead =
|
|
std::min<DWORD>(bytesAvailable, static_cast<DWORD>(buffer.size()));
|
|
DWORD bytesRead = 0;
|
|
if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) {
|
|
close();
|
|
return readAny;
|
|
}
|
|
if (bytesRead == 0) {
|
|
close();
|
|
return readAny;
|
|
}
|
|
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
|
|
readAny = true;
|
|
#else
|
|
#ifdef MSG_NOSIGNAL
|
|
constexpr int kMsgFlags = MSG_NOSIGNAL;
|
|
#else
|
|
constexpr int kMsgFlags = 0;
|
|
#endif
|
|
const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags);
|
|
if (bytesRead < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
return readAny;
|
|
}
|
|
close();
|
|
return readAny;
|
|
}
|
|
if (bytesRead == 0) {
|
|
close();
|
|
return readAny;
|
|
}
|
|
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
|
|
readAny = true;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
HANDLE pipe = INVALID_HANDLE_VALUE;
|
|
#else
|
|
int socketFd = -1;
|
|
#endif
|
|
std::vector<uint8_t> pendingRead;
|
|
};
|
|
|
|
int current_process_id() {
|
|
#ifdef _WIN32
|
|
return static_cast<int>(GetCurrentProcessId());
|
|
#else
|
|
return static_cast<int>(getpid());
|
|
#endif
|
|
}
|
|
|
|
std::string next_nonce() {
|
|
static std::atomic_uint64_t sNonce{1};
|
|
return std::to_string(sNonce.fetch_add(1));
|
|
}
|
|
|
|
const json* find_member(const json& object, const char* key) {
|
|
if (!object.is_object()) {
|
|
return nullptr;
|
|
}
|
|
|
|
const auto member = object.find(key);
|
|
if (member == object.end()) {
|
|
return nullptr;
|
|
}
|
|
return &*member;
|
|
}
|
|
|
|
std::string json_string_member(const json& object, const char* key) {
|
|
const json* member = find_member(object, key);
|
|
if (!member || !member->is_string()) {
|
|
return {};
|
|
}
|
|
return member->get<std::string>();
|
|
}
|
|
|
|
int json_int_member(const json& object, const char* key, int defaultValue) {
|
|
const json* member = find_member(object, key);
|
|
if (!member || !member->is_number_integer()) {
|
|
return defaultValue;
|
|
}
|
|
|
|
try {
|
|
return member->get<int>();
|
|
} catch (const json::exception&) {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
json make_presence_activity(const Presence& presence) {
|
|
json activity = json::object();
|
|
|
|
if (!presence.state.empty()) {
|
|
activity["state"] = presence.state;
|
|
}
|
|
if (!presence.details.empty()) {
|
|
activity["details"] = presence.details;
|
|
}
|
|
if (presence.startTimestamp != 0) {
|
|
activity["timestamps"] = {{"start", presence.startTimestamp}};
|
|
}
|
|
if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) {
|
|
json assets = json::object();
|
|
if (!presence.largeImageKey.empty()) {
|
|
assets["large_image"] = presence.largeImageKey;
|
|
}
|
|
if (!presence.largeImageText.empty()) {
|
|
assets["large_text"] = presence.largeImageText;
|
|
}
|
|
activity["assets"] = std::move(assets);
|
|
}
|
|
|
|
return activity;
|
|
}
|
|
|
|
Frame make_handshake_frame(std::string_view applicationId) {
|
|
return {
|
|
Opcode::Handshake,
|
|
json{
|
|
{"v", kRpcVersion},
|
|
{"client_id", std::string(applicationId)},
|
|
}
|
|
.dump(),
|
|
};
|
|
}
|
|
|
|
Frame make_set_activity_frame(std::string nonce, int pid, std::optional<Presence> presence) {
|
|
json args = {{"pid", pid}};
|
|
if (presence) {
|
|
args["activity"] = make_presence_activity(*presence);
|
|
} else {
|
|
args["activity"] = nullptr;
|
|
}
|
|
|
|
return {
|
|
Opcode::Frame,
|
|
json{
|
|
{"cmd", "SET_ACTIVITY"},
|
|
{"nonce", std::move(nonce)},
|
|
{"args", std::move(args)},
|
|
}
|
|
.dump(),
|
|
};
|
|
}
|
|
|
|
class Client {
|
|
public:
|
|
void initialize(std::string applicationId, EventHandlers handlers) {
|
|
shutdown();
|
|
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
this->applicationId = std::move(applicationId);
|
|
this->handlers = std::move(handlers);
|
|
shouldRun = true;
|
|
queuedPresence.reset();
|
|
hasQueuedPresence = false;
|
|
clearRequested = false;
|
|
sentInitialConnectLog = false;
|
|
}
|
|
|
|
ioThread = std::thread([this] { io_loop(); });
|
|
}
|
|
|
|
void run_callbacks() {
|
|
std::deque<QueuedEvent> events;
|
|
EventHandlers localHandlers;
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
events.swap(queuedEvents);
|
|
localHandlers = handlers;
|
|
}
|
|
|
|
for (const QueuedEvent& event : events) {
|
|
switch (event.type) {
|
|
case QueuedEvent::Type::Ready:
|
|
if (localHandlers.ready) {
|
|
localHandlers.ready(event.user);
|
|
}
|
|
break;
|
|
case QueuedEvent::Type::Disconnected:
|
|
if (localHandlers.disconnected) {
|
|
localHandlers.disconnected(event.code, event.message);
|
|
}
|
|
break;
|
|
case QueuedEvent::Type::Error:
|
|
if (localHandlers.error) {
|
|
localHandlers.error(event.code, event.message);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void update_presence(Presence presence) {
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (!shouldRun) {
|
|
return;
|
|
}
|
|
queuedPresence = std::move(presence);
|
|
hasQueuedPresence = true;
|
|
}
|
|
cv.notify_all();
|
|
}
|
|
|
|
void clear_presence() {
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (!shouldRun) {
|
|
return;
|
|
}
|
|
queuedPresence.reset();
|
|
hasQueuedPresence = false;
|
|
clearRequested = true;
|
|
}
|
|
cv.notify_all();
|
|
}
|
|
|
|
void shutdown() {
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (!shouldRun && !ioThread.joinable()) {
|
|
return;
|
|
}
|
|
shouldRun = false;
|
|
clearRequested = true;
|
|
}
|
|
cv.notify_all();
|
|
|
|
if (ioThread.joinable()) {
|
|
ioThread.join();
|
|
}
|
|
|
|
std::lock_guard lock(mutex);
|
|
queuedPresence.reset();
|
|
hasQueuedPresence = false;
|
|
clearRequested = false;
|
|
queuedEvents.clear();
|
|
handlers = {};
|
|
applicationId.clear();
|
|
}
|
|
|
|
private:
|
|
void io_loop() {
|
|
IpcConnection connection;
|
|
ConnectionState state = ConnectionState::Disconnected;
|
|
Backoff reconnectBackoff;
|
|
auto nextConnect = std::chrono::steady_clock::now();
|
|
const int pid = current_process_id();
|
|
std::string localApplicationId;
|
|
|
|
for (;;) {
|
|
{
|
|
std::unique_lock lock(mutex);
|
|
if (!shouldRun) {
|
|
break;
|
|
}
|
|
localApplicationId = applicationId;
|
|
}
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (state == ConnectionState::Disconnected && now >= nextConnect) {
|
|
if (connection.open()) {
|
|
if (connection.write_frame(make_handshake_frame(localApplicationId))) {
|
|
state = ConnectionState::SentHandshake;
|
|
} else {
|
|
connection.close();
|
|
}
|
|
}
|
|
|
|
if (state == ConnectionState::Disconnected) {
|
|
log_waiting_for_discord_once();
|
|
nextConnect = now + reconnectBackoff.next_delay();
|
|
}
|
|
}
|
|
|
|
if (state != ConnectionState::Disconnected) {
|
|
process_reads(connection, state, reconnectBackoff, nextConnect);
|
|
}
|
|
|
|
if (state == ConnectionState::Connected) {
|
|
flush_pending_presence(connection, pid);
|
|
}
|
|
|
|
std::unique_lock lock(mutex);
|
|
if (!shouldRun) {
|
|
break;
|
|
}
|
|
cv.wait_for(lock, kIoWait);
|
|
}
|
|
|
|
flush_shutdown_clear(connection, state, pid);
|
|
connection.close();
|
|
}
|
|
|
|
void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff,
|
|
std::chrono::steady_clock::time_point& nextConnect) {
|
|
for (;;) {
|
|
Frame frame;
|
|
const auto status = connection.read_frame(frame);
|
|
if (status == IpcConnection::ReadStatus::None) {
|
|
return;
|
|
}
|
|
if (status == IpcConnection::ReadStatus::Closed) {
|
|
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
|
static_cast<int>(DisconnectCode::PipeClosed), "Pipe closed");
|
|
return;
|
|
}
|
|
if (status == IpcConnection::ReadStatus::Corrupt) {
|
|
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
|
static_cast<int>(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame");
|
|
return;
|
|
}
|
|
|
|
switch (frame.opcode) {
|
|
case Opcode::Frame:
|
|
process_json_frame(frame.payload, state, reconnectBackoff);
|
|
break;
|
|
case Opcode::Close:
|
|
process_close_frame(
|
|
frame.payload, connection, state, reconnectBackoff, nextConnect);
|
|
return;
|
|
case Opcode::Ping:
|
|
connection.write_frame({Opcode::Pong, frame.payload});
|
|
break;
|
|
case Opcode::Pong:
|
|
break;
|
|
case Opcode::Handshake:
|
|
default:
|
|
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
|
static_cast<int>(DisconnectCode::BadFrame), "Bad Discord IPC frame");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void process_json_frame(
|
|
const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) {
|
|
json message;
|
|
try {
|
|
message = json::parse(payload);
|
|
} catch (const json::parse_error&) {
|
|
enqueue_error(
|
|
static_cast<int>(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON");
|
|
return;
|
|
}
|
|
|
|
const std::string cmd = json_string_member(message, "cmd");
|
|
const std::string evt = json_string_member(message, "evt");
|
|
|
|
if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") {
|
|
state = ConnectionState::Connected;
|
|
reconnectBackoff.reset();
|
|
enqueue_ready(message);
|
|
return;
|
|
}
|
|
|
|
if (evt == "ERROR") {
|
|
const json* data = find_member(message, "data");
|
|
enqueue_error(data ? json_int_member(*data, "code", 0) : 0,
|
|
data ? json_string_member(*data, "message") : std::string{});
|
|
}
|
|
}
|
|
|
|
void process_close_frame(const std::string& payload, IpcConnection& connection,
|
|
ConnectionState& state, Backoff& reconnectBackoff,
|
|
std::chrono::steady_clock::time_point& nextConnect) {
|
|
int code = static_cast<int>(DisconnectCode::PipeClosed);
|
|
std::string message = "Discord closed IPC connection";
|
|
|
|
try {
|
|
const json closePayload = json::parse(payload);
|
|
code = json_int_member(closePayload, "code", code);
|
|
const std::string closeMessage = json_string_member(closePayload, "message");
|
|
if (!closeMessage.empty()) {
|
|
message = closeMessage;
|
|
}
|
|
} catch (const json::exception&) {
|
|
}
|
|
|
|
handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message);
|
|
}
|
|
|
|
void handle_disconnect(IpcConnection& connection, ConnectionState& state,
|
|
Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code,
|
|
std::string_view message) {
|
|
const bool wasConnected =
|
|
state == ConnectionState::Connected || state == ConnectionState::SentHandshake;
|
|
connection.close();
|
|
state = ConnectionState::Disconnected;
|
|
nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay();
|
|
if (wasConnected) {
|
|
enqueue_disconnected(code, message);
|
|
}
|
|
}
|
|
|
|
void flush_pending_presence(IpcConnection& connection, int pid) {
|
|
std::optional<Presence> presence;
|
|
bool shouldClear = false;
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (hasQueuedPresence) {
|
|
presence = queuedPresence;
|
|
hasQueuedPresence = false;
|
|
} else if (clearRequested) {
|
|
shouldClear = true;
|
|
clearRequested = false;
|
|
}
|
|
}
|
|
|
|
if (presence) {
|
|
if (!connection.write_frame(
|
|
make_set_activity_frame(next_nonce(), pid, std::move(presence))))
|
|
{
|
|
requeue_presence();
|
|
}
|
|
} else if (shouldClear) {
|
|
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
|
|
}
|
|
}
|
|
|
|
void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) {
|
|
if (state != ConnectionState::Connected || !connection.is_open()) {
|
|
return;
|
|
}
|
|
|
|
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
|
|
const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout;
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
Frame frame;
|
|
const auto status = connection.read_frame(frame);
|
|
if (status == IpcConnection::ReadStatus::None) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
continue;
|
|
}
|
|
if (status != IpcConnection::ReadStatus::Frame) {
|
|
break;
|
|
}
|
|
if (frame.opcode == Opcode::Ping) {
|
|
connection.write_frame({Opcode::Pong, frame.payload});
|
|
}
|
|
}
|
|
}
|
|
|
|
void requeue_presence() {
|
|
std::lock_guard lock(mutex);
|
|
if (queuedPresence) {
|
|
hasQueuedPresence = true;
|
|
}
|
|
}
|
|
|
|
void enqueue_ready(const json& readyMessage) {
|
|
User user;
|
|
const auto data = readyMessage.find("data");
|
|
if (data != readyMessage.end() && data->is_object()) {
|
|
const auto userIt = data->find("user");
|
|
if (userIt != data->end() && userIt->is_object()) {
|
|
user.id = json_string_member(*userIt, "id");
|
|
user.username = json_string_member(*userIt, "username");
|
|
user.discriminator = json_string_member(*userIt, "discriminator");
|
|
user.avatar = json_string_member(*userIt, "avatar");
|
|
}
|
|
}
|
|
|
|
std::lock_guard lock(mutex);
|
|
queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)});
|
|
}
|
|
|
|
void enqueue_disconnected(int code, std::string_view message) {
|
|
std::lock_guard lock(mutex);
|
|
QueuedEvent event;
|
|
event.type = QueuedEvent::Type::Disconnected;
|
|
event.code = code;
|
|
event.message = message;
|
|
queuedEvents.push_back(std::move(event));
|
|
}
|
|
|
|
void enqueue_error(int code, std::string_view message) {
|
|
std::lock_guard lock(mutex);
|
|
QueuedEvent event;
|
|
event.type = QueuedEvent::Type::Error;
|
|
event.code = code;
|
|
event.message = message;
|
|
queuedEvents.push_back(std::move(event));
|
|
}
|
|
|
|
void log_waiting_for_discord_once() {
|
|
bool shouldLog = false;
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (!sentInitialConnectLog) {
|
|
sentInitialConnectLog = true;
|
|
shouldLog = true;
|
|
}
|
|
}
|
|
if (shouldLog) {
|
|
DuskLog.info("Discord: Waiting for local Discord IPC");
|
|
}
|
|
}
|
|
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
std::thread ioThread;
|
|
std::string applicationId;
|
|
EventHandlers handlers;
|
|
std::deque<QueuedEvent> queuedEvents;
|
|
std::optional<Presence> queuedPresence;
|
|
bool hasQueuedPresence = false;
|
|
bool clearRequested = false;
|
|
bool shouldRun = false;
|
|
bool sentInitialConnectLog = false;
|
|
};
|
|
|
|
Client& client() {
|
|
static Client sClient;
|
|
return sClient;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void initialize(std::string applicationId, EventHandlers handlers) {
|
|
client().initialize(std::move(applicationId), std::move(handlers));
|
|
}
|
|
|
|
void run_callbacks() {
|
|
client().run_callbacks();
|
|
}
|
|
|
|
void update_presence(Presence presence) {
|
|
client().update_presence(std::move(presence));
|
|
}
|
|
|
|
void clear_presence() {
|
|
client().clear_presence();
|
|
}
|
|
|
|
void shutdown() {
|
|
client().shutdown();
|
|
}
|
|
|
|
} // namespace dusk::discord::rpc
|
|
|
|
#endif // DUSK_DISCORD
|