#ifdef DUSK_DISCORD #ifndef NOMINMAX #define NOMINMAX #endif #include "discord.hpp" #include "dusk/logging.h" #include "nlohmann/json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define NOMCX #define NOSERVICE #define NOIME #include #else #include #include #include #include #include #include #include #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(&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 header{}; write_u32(header.data(), static_cast(frame.opcode)); write_u32(header.data() + sizeof(uint32_t), static_cast(frame.payload.size())); return write_all(header.data(), header.size()) && write_all( reinterpret_cast(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(read_u32(pendingRead.data())); frame.payload.assign( reinterpret_cast(pendingRead.data() + kFrameHeaderSize), payloadLength); pendingRead.erase( pendingRead.begin(), pendingRead.begin() + static_cast(frameLength)); return ReadStatus::Frame; } private: #ifndef _WIN32 static std::vector get_temp_paths() { std::vector 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(value & 0xff); out[1] = static_cast((value >> 8) & 0xff); out[2] = static_cast((value >> 16) & 0xff); out[3] = static_cast((value >> 24) & 0xff); } static uint32_t read_u32(const uint8_t* in) { return static_cast(in[0]) | (static_cast(in[1]) << 8) | (static_cast(in[2]) << 16) | (static_cast(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(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(bytesWritten); #endif } return true; } bool read_available() { std::array 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(bytesAvailable, static_cast(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 pendingRead; }; int current_process_id() { #ifdef _WIN32 return static_cast(GetCurrentProcessId()); #else return static_cast(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(); } 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(); } 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) { 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 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(DisconnectCode::PipeClosed), "Pipe closed"); return; } if (status == IpcConnection::ReadStatus::Corrupt) { handle_disconnect(connection, state, reconnectBackoff, nextConnect, static_cast(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(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(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(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; 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 queuedEvents; std::optional 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