discord-rpc: Port to Qt Json

This commit is contained in:
Jonas Kvinge
2025-04-12 13:12:24 +02:00
parent 21bdf88d09
commit 306709f498
14 changed files with 265 additions and 513 deletions

View File

@@ -37,5 +37,6 @@ if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32) target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif() endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS}) target_link_libraries(discord-rpc PRIVATE Qt${QT_VERSION_MAJOR}::Core)
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -24,11 +24,13 @@
#ifndef DISCORD_REGISTER_H #ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H #define DISCORD_REGISTER_H
#include <QString>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
void Discord_Register(const char *applicationId, const char *command); void Discord_Register(const QString &applicationId, const char *command);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -21,9 +21,6 @@
* *
*/ */
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio> #include <cstdio>
#include <errno.h> #include <errno.h>
#include <cstdlib> #include <cstdlib>
@@ -32,6 +29,11 @@
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <QString>
#include "discord_rpc.h"
#include "discord_register.h"
namespace { namespace {
static bool Mkdir(const char *path) { static bool Mkdir(const char *path) {
@@ -48,7 +50,7 @@ static bool Mkdir(const char *path) {
} // namespace } // namespace
// We want to register games so we can run them from Discord client as discord-<appid>:// // We want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const char *applicationId, const char *command) { extern "C" void Discord_Register(const QString &applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing. // Add a desktop file and update some mime handlers so that xdg-open does the right thing.
@@ -75,13 +77,13 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
"Categories=Discord;Games;\n" "Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n"; "MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{}; char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId); int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId.toUtf8().constData(), command, applicationId.toUtf8().constData());
if (fileLen <= 0) { if (fileLen <= 0) {
return; return;
} }
char desktopFilename[256]{}; char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId); (void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId.toUtf8().constData());
char desktopFilePath[1024]{}; char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home); (void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
@@ -111,8 +113,8 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
snprintf(xdgMimeCommand, snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand), sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s", "xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId, applicationId.toUtf8().constData(),
applicationId); applicationId.toUtf8().constData());
if (system(xdgMimeCommand) < 0) { if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n"); fprintf(stderr, "Failed to register mime handler\n");
} }

View File

@@ -84,15 +84,17 @@ static void RegisterURL(const char *applicationId) {
} }
void Discord_Register(const char *applicationId, const char *command) { void Discord_Register(const QString &applicationId, const char *command) {
const QByteArray applicationIdData = applicationId.toUtf8();
if (command) { if (command) {
RegisterCommand(applicationId, command); RegisterCommand(applicationIdData.constData(), command);
} }
else { else {
// raii lite // raii lite
@autoreleasepool { @autoreleasepool {
RegisterURL(applicationId); RegisterURL(applicationIdData.constData());
} }
} }

View File

@@ -147,10 +147,10 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
} }
extern "C" void Discord_Register(const char *applicationId, const char *command) { extern "C" void Discord_Register(const QString &applicationId, const char *command) {
wchar_t appId[32]{}; wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); MultiByteToWideChar(CP_UTF8, 0, applicationId.toUtf8().constData(), -1, appId, 32);
wchar_t openCommand[1024]{}; wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr; const wchar_t *wcommand = nullptr;

View File

@@ -27,6 +27,10 @@
#include <condition_variable> #include <condition_variable>
#include <thread> #include <thread>
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc.h" #include "discord_rpc.h"
#include "discord_backoff.h" #include "discord_backoff.h"
#include "discord_register.h" #include "discord_register.h"
@@ -34,6 +38,8 @@
#include "discord_rpc_connection.h" #include "discord_rpc_connection.h"
#include "discord_serialization.h" #include "discord_serialization.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc { namespace discord_rpc {
constexpr size_t MaxMessageSize { 16 * 1024 }; constexpr size_t MaxMessageSize { 16 * 1024 };
@@ -52,17 +58,19 @@ struct QueuedMessage {
} }
}; };
struct User { class User {
public:
explicit User() {}
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null // snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21 // terminator = 21
char userId[32]; QString userId;
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null // 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129 // terminator = 129
char username[344]; QString username;
// 4 decimal digits + 1 null terminator = 5 // 4 decimal digits + 1 null terminator = 5
char discriminator[8]; QString discriminator;
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35 // optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
char avatar[128]; QString avatar;
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes // Rounded way up because I'm paranoid about games breaking from future changes in these sizes
}; };
@@ -75,12 +83,12 @@ static std::atomic_bool GotErrorMessage { false };
static std::atomic_bool WasJoinGame { false }; static std::atomic_bool WasJoinGame { false };
static std::atomic_bool WasSpectateGame { false }; static std::atomic_bool WasSpectateGame { false };
static std::atomic_bool UpdatePresence { false }; static std::atomic_bool UpdatePresence { false };
static char JoinGameSecret[256]; static QString JoinGameSecret;
static char SpectateGameSecret[256]; static QString SpectateGameSecret;
static int LastErrorCode { 0 }; static int LastErrorCode { 0 };
static char LastErrorMessage[256]; static QString LastErrorMessage;
static int LastDisconnectErrorCode { 0 }; static int LastDisconnectErrorCode { 0 };
static char LastDisconnectErrorMessage[256]; static QString LastDisconnectErrorMessage;
static std::mutex PresenceMutex; static std::mutex PresenceMutex;
static std::mutex HandlerMutex; static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence {}; static QueuedMessage QueuedPresence {};
@@ -152,65 +160,63 @@ static void Discord_UpdateConnection() {
// reads // reads
for (;;) { for (;;) {
JsonDocument message; QJsonDocument json_document;
if (!Connection->Read(json_document)) {
if (!Connection->Read(message)) {
break; break;
} }
const char *evtName = GetStrMember(&message, "evt"); const QJsonObject json_object = json_document.object();
const char *nonce = GetStrMember(&message, "nonce"); const QString event_name = json_object["evt"_L1].toString();
const QString nonce = json_object["nonce"_L1].toString();
if (nonce) { if (json_object.contains("nonce"_L1)) {
// in responses only -- should use to match up response when needed. // in responses only -- should use to match up response when needed.
if (evtName && strcmp(evtName, "ERROR") == 0) { if (event_name == "ERROR"_L1) {
auto data = GetObjMember(&message, "data"); const QJsonObject data = json_object["data"_L1].toObject();
LastErrorCode = GetIntMember(data, "code"); LastErrorCode = data["code"_L1].toInt();
StringCopy(LastErrorMessage, GetStrMember(data, "message", "")); LastErrorMessage = data["message"_L1].toString();
GotErrorMessage.store(true); GotErrorMessage.store(true);
} }
} }
else { else {
// should have evt == name of event, optional data // should have evt == name of event, optional data
if (evtName == nullptr) { if (event_name.isEmpty()) {
continue; continue;
} }
auto data = GetObjMember(&message, "data"); const QJsonObject data = json_object["data"_L1].toObject();
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) { if (event_name == "ACTIVITY_JOIN"_L1) {
auto secret = GetStrMember(data, "secret"); if (data.contains("secret"_L1)) {
if (secret) { JoinGameSecret = data["secret"_L1].toString();
StringCopy(JoinGameSecret, secret);
WasJoinGame.store(true); WasJoinGame.store(true);
} }
} }
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) { else if (event_name == "ACTIVITY_SPECTATE"_L1) {
auto secret = GetStrMember(data, "secret"); if (data.contains("secret"_L1)) {
if (secret) { SpectateGameSecret = data["secret"_L1].toString();
StringCopy(SpectateGameSecret, secret);
WasSpectateGame.store(true); WasSpectateGame.store(true);
} }
} }
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) { else if (event_name == "ACTIVITY_JOIN_REQUEST"_L1) {
auto user = GetObjMember(data, "user"); const QJsonObject user = data["user"_L1].toObject();
auto userId = GetStrMember(user, "id"); const QString userId = user["id"_L1].toString();
auto username = GetStrMember(user, "username"); const QString username = user["username"_L1].toString();
auto avatar = GetStrMember(user, "avatar"); const QString avatar = user["avatar"_L1].toString();
auto joinReq = JoinAskQueue.GetNextAddMessage(); const auto joinReq = JoinAskQueue.GetNextAddMessage();
if (userId && username && joinReq) { if (!userId.isEmpty() && !username.isEmpty() && joinReq) {
StringCopy(joinReq->userId, userId); joinReq->userId = userId;
StringCopy(joinReq->username, username); joinReq->username = username;
auto discriminator = GetStrMember(user, "discriminator"); const QString discriminator = user["discriminator"_L1].toString();
if (discriminator) { if (!discriminator.isEmpty()) {
StringCopy(joinReq->discriminator, discriminator); joinReq->discriminator = discriminator;
} }
if (avatar) { if (!avatar.isEmpty()) {
StringCopy(joinReq->avatar, avatar); joinReq->avatar = avatar;
} }
else { else {
joinReq->avatar[0] = 0; joinReq->avatar.clear();
} }
JoinAskQueue.CommitAdd(); JoinAskQueue.CommitAdd();
} }
@@ -278,7 +284,7 @@ static bool DeregisterForEvent(const char *evtName) {
} }
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) { extern "C" void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
IoThread = new (std::nothrow) IoThreadHolder(); IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) { if (IoThread == nullptr) {
@@ -309,37 +315,38 @@ extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandle
} }
Connection = RpcConnection::Create(applicationId); Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](JsonDocument &readyMessage) { Connection->onConnect = [](QJsonDocument &readyMessage) {
Discord_UpdateHandlers(&QueuedHandlers); Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) { if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true); UpdatePresence.exchange(true);
SignalIOActivity(); SignalIOActivity();
} }
auto data = GetObjMember(&readyMessage, "data"); const QJsonValue json_object = readyMessage.object();
auto user = GetObjMember(data, "user"); auto data = json_object["data"_L1].toObject();
auto userId = GetStrMember(user, "id"); auto user = data["user"_L1].toObject();
auto username = GetStrMember(user, "username"); auto userId = user["id"_L1].toString();
auto avatar = GetStrMember(user, "avatar"); auto username = user["username"_L1].toString();
if (userId && username) { auto avatar = user["avatar"_L1].toString();
StringCopy(connectedUser.userId, userId); if (!userId.isEmpty() && !username.isEmpty()) {
StringCopy(connectedUser.username, username); connectedUser.userId = userId;
auto discriminator = GetStrMember(user, "discriminator"); connectedUser.username = username;
if (discriminator) { const QString discriminator = user["discriminator"_L1].toString();
StringCopy(connectedUser.discriminator, discriminator); if (!discriminator.isEmpty()) {
connectedUser.discriminator = discriminator;
} }
if (avatar) { if (!avatar.isEmpty()) {
StringCopy(connectedUser.avatar, avatar); connectedUser.avatar = avatar;
} }
else { else {
connectedUser.avatar[0] = 0; connectedUser = User();
} }
} }
WasJustConnected.exchange(true); WasJustConnected.exchange(true);
ReconnectTimeMs.reset(); ReconnectTimeMs.reset();
}; };
Connection->onDisconnect = [](int err, const char *message) { Connection->onDisconnect = [](int err, QString &message) {
LastDisconnectErrorCode = err; LastDisconnectErrorCode = err;
StringCopy(LastDisconnectErrorMessage, message); LastDisconnectErrorMessage = message;
WasJustDisconnected.exchange(true); WasJustDisconnected.exchange(true);
UpdateReconnectTime(); UpdateReconnectTime();
}; };
@@ -368,7 +375,7 @@ extern "C" void Discord_Shutdown(void) {
} }
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) { extern "C" void Discord_UpdatePresence(const DiscordRichPresence &presence) {
{ {
std::lock_guard<std::mutex> guard(PresenceMutex); std::lock_guard<std::mutex> guard(PresenceMutex);
@@ -380,8 +387,8 @@ extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
} }
extern "C" void Discord_ClearPresence(void) { extern "C" void Discord_ClearPresence() {
Discord_UpdatePresence(nullptr); Discord_UpdatePresence();
} }
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) { extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {

View File

@@ -25,6 +25,7 @@
#define DISCORD_RPC_H #define DISCORD_RPC_H
#include <cstdint> #include <cstdint>
#include <QString>
namespace discord_rpc { namespace discord_rpc {
@@ -32,40 +33,41 @@ namespace discord_rpc {
extern "C" { extern "C" {
#endif #endif
typedef struct DiscordRichPresence { class DiscordRichPresence {
public:
int type; int type;
const char *name; /* max 128 bytes */ QString name; /* max 128 bytes */
const char *state; /* max 128 bytes */ QString state; /* max 128 bytes */
const char *details; /* max 128 bytes */ QString details; /* max 128 bytes */
int64_t startTimestamp; qint64 startTimestamp;
int64_t endTimestamp; qint64 endTimestamp;
const char *largeImageKey; /* max 32 bytes */ QString largeImageKey; /* max 32 bytes */
const char *largeImageText; /* max 128 bytes */ QString largeImageText; /* max 128 bytes */
const char *smallImageKey; /* max 32 bytes */ QString smallImageKey; /* max 32 bytes */
const char *smallImageText; /* max 128 bytes */ QString smallImageText; /* max 128 bytes */
const char *partyId; /* max 128 bytes */ QString partyId; /* max 128 bytes */
int partySize; int partySize;
int partyMax; int partyMax;
int partyPrivacy; int partyPrivacy;
const char *matchSecret; /* max 128 bytes */ QString matchSecret; /* max 128 bytes */
const char *joinSecret; /* max 128 bytes */ QString joinSecret; /* max 128 bytes */
const char *spectateSecret; /* max 128 bytes */ QString spectateSecret; /* max 128 bytes */
int8_t instance; qint8 instance;
} DiscordRichPresence; };
typedef struct DiscordUser { typedef struct DiscordUser {
const char *userId; const QString userId;
const char *username; const QString username;
const char *discriminator; const QString discriminator;
const char *avatar; const QString avatar;
} DiscordUser; } DiscordUser;
typedef struct DiscordEventHandlers { typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request); void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const char *message); void (*disconnected)(int errorCode, const QString &message);
void (*errored)(int errorCode, const char *message); void (*errored)(int errorCode, const QString &message);
void (*joinGame)(const char *joinSecret); void (*joinGame)(const QString &joinSecret);
void (*spectateGame)(const char *spectateSecret); void (*spectateGame)(const QString &spectateSecret);
void (*joinRequest)(const DiscordUser *request); void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers; } DiscordEventHandlers;
@@ -75,14 +77,14 @@ typedef struct DiscordEventHandlers {
#define DISCORD_PARTY_PRIVATE 0 #define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1 #define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister); void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown(void); void Discord_Shutdown();
// checks for incoming messages, dispatches callbacks // checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks(void); void Discord_RunCallbacks();
void Discord_UpdatePresence(const DiscordRichPresence *presence); void Discord_UpdatePresence(const DiscordRichPresence &presence = DiscordRichPresence());
void Discord_ClearPresence(void); void Discord_ClearPresence();
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply); void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);

View File

@@ -21,18 +21,24 @@
* *
*/ */
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc_connection.h" #include "discord_rpc_connection.h"
#include "discord_serialization.h" #include "discord_serialization.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc { namespace discord_rpc {
static const int RpcVersion = 1; static const int RpcVersion = 1;
static RpcConnection Instance; static RpcConnection Instance;
RpcConnection *RpcConnection::Create(const char *applicationId) { RpcConnection *RpcConnection::Create(const QString &applicationId) {
Instance.connection = BaseConnection::Create(); Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId); Instance.appId = applicationId;
return &Instance; return &Instance;
} }
@@ -56,14 +62,15 @@ void RpcConnection::Open() {
} }
if (state == State::SentHandshake) { if (state == State::SentHandshake) {
JsonDocument message; QJsonDocument json_document;
if (Read(message)) { if (Read(json_document)) {
auto cmd = GetStrMember(&message, "cmd"); const QJsonObject json_object = json_document.object();
auto evt = GetStrMember(&message, "evt"); const QString cmd = json_object["cmd"_L1].toString();
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) { const QString evt = json_object["evt"_L1].toString();
if (cmd == "DISPATCH"_L1 && evt == "READY"_L1) {
state = State::Connected; state = State::Connected;
if (onConnect) { if (onConnect) {
onConnect(message); onConnect(json_document);
} }
} }
} }
@@ -106,7 +113,7 @@ bool RpcConnection::Write(const void *data, size_t length) {
} }
bool RpcConnection::Read(JsonDocument &message) { bool RpcConnection::Read(QJsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) { if (state != State::Connected && state != State::SentHandshake) {
return false; return false;
@@ -117,7 +124,7 @@ bool RpcConnection::Read(JsonDocument &message) {
if (!didRead) { if (!didRead) {
if (!connection->isOpen) { if (!connection->isOpen) {
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed); lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
StringCopy(lastErrorMessage, "Pipe closed"); lastErrorMessage = "Pipe closed"_L1;
Close(); Close();
} }
return false; return false;
@@ -127,7 +134,7 @@ bool RpcConnection::Read(JsonDocument &message) {
didRead = connection->Read(readFrame.message, readFrame.length); didRead = connection->Read(readFrame.message, readFrame.length);
if (!didRead) { if (!didRead) {
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt); lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Partial data in frame"); lastErrorMessage = "Partial data in frame"_L1;
Close(); Close();
return false; return false;
} }
@@ -136,14 +143,14 @@ bool RpcConnection::Read(JsonDocument &message) {
switch (readFrame.opcode) { switch (readFrame.opcode) {
case Opcode::Close: { case Opcode::Close: {
message.ParseInsitu(readFrame.message); message = QJsonDocument::fromJson(readFrame.message);
lastErrorCode = GetIntMember(&message, "code"); lastErrorCode = message["code"_L1].toInt();
StringCopy(lastErrorMessage, GetStrMember(&message, "message", "")); lastErrorMessage = message["message"_L1].toString();
Close(); Close();
return false; return false;
} }
case Opcode::Frame: case Opcode::Frame:
message.ParseInsitu(readFrame.message); message = QJsonDocument::fromJson(readFrame.message);
return true; return true;
case Opcode::Ping: case Opcode::Ping:
readFrame.opcode = Opcode::Pong; readFrame.opcode = Opcode::Pong;
@@ -157,7 +164,7 @@ bool RpcConnection::Read(JsonDocument &message) {
default: default:
// something bad happened // something bad happened
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt); lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Bad ipc frame"); lastErrorMessage = "Bad ipc frame"_L1;
Close(); Close();
return false; return false;
} }

View File

@@ -24,6 +24,9 @@
#ifndef DISCORD_RPC_CONNECTION_H #ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H #define DISCORD_RPC_CONNECTION_H
#include <QString>
#include <QJsonDocument>
#include "discord_connection.h" #include "discord_connection.h"
#include "discord_serialization.h" #include "discord_serialization.h"
@@ -65,14 +68,14 @@ struct RpcConnection {
BaseConnection *connection { nullptr }; BaseConnection *connection { nullptr };
State state { State::Disconnected }; State state { State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr }; void (*onConnect)(QJsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr }; void (*onDisconnect)(int errorCode, QString &message) { nullptr };
char appId[64] {}; QString appId;
int lastErrorCode { 0 }; int lastErrorCode { 0 };
char lastErrorMessage[256] {}; QString lastErrorMessage;
RpcConnection::MessageFrame sendFrame; RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId); static RpcConnection *Create(const QString &applicationId);
static void Destroy(RpcConnection *&); static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; } inline bool IsOpen() const { return state == State::Connected; }
@@ -80,7 +83,7 @@ struct RpcConnection {
void Open(); void Open();
void Close(); void Close();
bool Write(const void *data, size_t length); bool Write(const void *data, size_t length);
bool Read(JsonDocument &message); bool Read(QJsonDocument &message);
}; };
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -21,10 +21,15 @@
* *
*/ */
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_serialization.h" #include "discord_serialization.h"
#include "discord_connection.h"
#include "discord_rpc.h" #include "discord_rpc.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc { namespace discord_rpc {
template<typename T> template<typename T>
@@ -53,229 +58,129 @@ void NumberToString(char *dest, T number) {
} }
// it's ever so slightly faster to not have to strlen the key void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value);
template<typename T> void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value) {
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
struct WriteObject { if (!value.isEmpty()) {
JsonWriter &writer; json_object[key] = value;
WriteObject(JsonWriter &w)
: writer(w) {
writer.StartObject();
}
template<typename T>
WriteObject(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartObject();
}
~WriteObject() { writer.EndObject(); }
};
struct WriteArray {
JsonWriter &writer;
template<typename T>
WriteArray(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartArray();
}
~WriteArray() { writer.EndArray(); }
};
template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) {
w.Key(k, sizeof(T) - 1);
w.String(value);
} }
} }
static void JsonWriteNonce(JsonWriter &writer, const int nonce) { static QString JsonWriteNonce(const int nonce) {
WriteKey(writer, "nonce"); char nonce_buffer[32]{};
char nonceBuffer[32]; NumberToString(nonce_buffer, nonce);
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer); return QString::fromLatin1(nonce_buffer);
} }
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) { size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence) {
JsonWriter writer(dest, maxLen); QJsonObject json_object;
{ json_object["nonce"_L1] = JsonWriteNonce(nonce);
WriteObject top(writer); json_object["cmd"_L1] = "SET_ACTIVITY"_L1;
JsonWriteNonce(writer, nonce); QJsonObject args;
args["pid"_L1] = pid;
WriteKey(writer, "cmd"); QJsonObject activity;
writer.String("SET_ACTIVITY");
{ if (presence.type >= 0 && presence.type <= 5) {
WriteObject args(writer, "args"); activity["type"_L1] = presence.type;
}
WriteKey(writer, "pid"); activity["state"_L1] = presence.state;
writer.Int(pid); activity["details"_L1] = presence.details;
if (presence != nullptr) { if (presence.startTimestamp != 0 || presence.endTimestamp != 0) {
WriteObject activity(writer, "activity"); QJsonObject timestamps;
if (presence.startTimestamp != 0) {
if (presence->type >= 0 && presence->type <= 5) { timestamps["start"_L1] = presence.startTimestamp;
WriteKey(writer, "type");
writer.Int(presence->type);
}
WriteOptionalString(writer, "name", presence->name);
WriteOptionalString(writer, "state", presence->state);
WriteOptionalString(writer, "details", presence->details);
if (presence->startTimestamp || presence->endTimestamp) {
WriteObject timestamps(writer, "timestamps");
if (presence->startTimestamp) {
WriteKey(writer, "start");
writer.Int64(presence->startTimestamp);
}
if (presence->endTimestamp) {
WriteKey(writer, "end");
writer.Int64(presence->endTimestamp);
}
}
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
(presence->largeImageText && presence->largeImageText[0]) ||
(presence->smallImageKey && presence->smallImageKey[0]) ||
(presence->smallImageText && presence->smallImageText[0])) {
WriteObject assets(writer, "assets");
WriteOptionalString(writer, "large_image", presence->largeImageKey);
WriteOptionalString(writer, "large_text", presence->largeImageText);
WriteOptionalString(writer, "small_image", presence->smallImageKey);
WriteOptionalString(writer, "small_text", presence->smallImageText);
}
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) {
WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) {
WriteArray size(writer, "size");
writer.Int(presence->partySize);
writer.Int(presence->partyMax);
}
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
}
}
if ((presence->matchSecret && presence->matchSecret[0]) ||
(presence->joinSecret && presence->joinSecret[0]) ||
(presence->spectateSecret && presence->spectateSecret[0])) {
WriteObject secrets(writer, "secrets");
WriteOptionalString(writer, "match", presence->matchSecret);
WriteOptionalString(writer, "join", presence->joinSecret);
WriteOptionalString(writer, "spectate", presence->spectateSecret);
}
writer.Key("instance");
writer.Bool(presence->instance != 0);
}
} }
if (presence.endTimestamp != 0) {
timestamps["end"_L1] = presence.endTimestamp;
}
activity["timestamps"_L1] = timestamps;
} }
return writer.Size(); if (!presence.largeImageKey.isEmpty() || !presence.largeImageText.isEmpty() || !presence.smallImageKey.isEmpty() || !presence.smallImageText.isEmpty()) {
} QJsonObject assets;
WriteOptionalString(assets, "large_image"_L1, presence.largeImageKey);
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) { WriteOptionalString(assets, "large_text"_L1, presence.largeImageText);
WriteOptionalString(assets, "small_image"_L1, presence.smallImageKey);
JsonWriter writer(dest, maxLen); WriteOptionalString(assets, "small_text"_L1, presence.smallImageText);
activity["assets"_L1] = assets;
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
} }
return writer.Size(); activity["instance"_L1] = presence.instance != 0;
args["activity"_L1] = activity;
json_object["args"_L1] = args;
QJsonDocument json_document(json_object);
QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length();
} }
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) { size_t JsonWriteHandshakeObj(char *dest, const size_t maxLen, const int version, const QString &applicationId) {
JsonWriter writer(dest, maxLen); QJsonObject json_object;
json_object["v"_L1] = version;
json_object["client_id"_L1] = applicationId;
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
{ return data.length();
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
} }
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) { size_t JsonWriteSubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen); QJsonObject json_object;
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = "SUBSCRIBE"_L1;
json_object["evt"_L1] = QLatin1String(evtName);
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
{ return data.length();
WriteObject obj(writer);
JsonWriteNonce(writer, nonce); }
WriteKey(writer, "cmd"); size_t JsonWriteUnsubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) {
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt"); QJsonObject json_object;
writer.String(evtName); json_object["nonce"_L1] = JsonWriteNonce(nonce);
} json_object["cmd"_L1] = "UNSUBSCRIBE"_L1;
json_object["evt"_L1] = QLatin1String(evtName);
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return writer.Size(); return data.length();
} }
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) { size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
JsonWriter writer(dest, maxLen); QJsonObject json_object;
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = reply == DISCORD_REPLY_YES ? "SEND_ACTIVITY_JOIN_INVITE"_L1 : "CLOSE_ACTIVITY_JOIN_REQUEST"_L1;
QJsonObject args;
args["user_id"_L1] = QLatin1String(userId);
json_object["args"_L1] = args;
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
{ return data.length();
WriteObject obj(writer);
WriteKey(writer, "cmd");
if (reply == DISCORD_REPLY_YES) {
writer.String("SEND_ACTIVITY_JOIN_INVITE");
}
else {
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
}
WriteKey(writer, "args");
{
WriteObject args(writer);
WriteKey(writer, "user_id");
writer.String(userId);
}
JsonWriteNonce(writer, nonce);
}
return writer.Size();
} }

View File

@@ -24,189 +24,18 @@
#ifndef DISCORD_SERIALIZATION_H #ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H #define DISCORD_SERIALIZATION_H
#include <rapidjson/document.h> #include <cstddef>
#include <rapidjson/stringbuffer.h> #include <QString>
#include <rapidjson/writer.h>
namespace discord_rpc { namespace discord_rpc {
// if only there was a standard library function for this size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const QString &applicationId);
template<size_t Len> class DiscordRichPresence;
inline size_t StringCopy(char (&dest)[Len], const char *src) { size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence);
if (!src || !Len) {
return 0;
}
size_t copied;
char *out = dest;
for (copied = 1; *src && copied < Len; ++copied) {
*out++ = *src++;
}
*out = 0;
return copied - 1;
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands
struct DiscordRichPresence;
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName); size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName); size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce); size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
// to supply some of your own allocators for stuff rather than use the defaults
class LinearAllocator {
public:
char *buffer_;
char *end_;
LinearAllocator() {
assert(0); // needed for some default case in rapidjson, should not use
}
LinearAllocator(char *buffer, size_t size)
: buffer_(buffer), end_(buffer + size) {
}
static const bool kNeedFree = false;
void *Malloc(size_t size) {
char *res = buffer_;
buffer_ += size;
if (buffer_ > end_) {
buffer_ = res;
return nullptr;
}
return res;
}
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
if (newSize == 0) {
return nullptr;
}
// allocate how much you need in the first place
assert(!originalPtr && !originalSize);
// unused parameter warning
(void)(originalPtr);
(void)(originalSize);
return Malloc(newSize);
}
static void Free(void *ptr) {
/* shrug */
(void)ptr;
}
};
template<size_t Size>
class FixedLinearAllocator : public LinearAllocator {
public:
char fixedBuffer_[Size];
FixedLinearAllocator()
: LinearAllocator(fixedBuffer_, Size) {
}
static const bool kNeedFree = false;
};
// wonder why this isn't a thing already, maybe I missed it
class DirectStringBuffer {
public:
using Ch = char;
char *buffer_;
char *end_;
char *current_;
DirectStringBuffer(char *buffer, size_t maxLen)
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
}
void Put(char c) {
if (current_ < end_) {
*current_++ = c;
}
}
void Flush() {}
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
using UTF8 = rapidjson::UTF8<char>;
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
using StackAllocator = FixedLinearAllocator<2048>;
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
using JsonWriterBase =
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
class JsonWriter : public JsonWriterBase {
public:
DirectStringBuffer stringBuffer_;
StackAllocator stackAlloc_;
JsonWriter(char *dest, size_t maxLen)
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
}
size_t Size() const { return stringBuffer_.GetSize(); }
};
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
class JsonDocument : public JsonDocumentBase {
public:
static const int kDefaultChunkCapacity = 32 * 1024;
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
// send any messages that would use all of this, though.
char parseBuffer_[32 * 1024];
MallocAllocator mallocAllocator_;
PoolAllocator poolAllocator_;
StackAllocator stackAllocator_;
JsonDocument()
: JsonDocumentBase(rapidjson::kObjectType,
&poolAllocator_,
sizeof(stackAllocator_.fixedBuffer_),
&stackAllocator_),
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
}
};
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value;
}
}
return nullptr;
}
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt();
}
}
return notFoundDefault;
}
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString();
}
}
return notFoundDefault;
}
} // namespace discord_rpc } // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H #endif // DISCORD_SERIALIZATION_H

View File

@@ -212,8 +212,6 @@ find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash) pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
find_package(RapidJSON)
set(QT_VERSION_MAJOR 6) set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0) set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR}) set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
@@ -366,9 +364,7 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
DEPENDS "sparsehash" LIBSPARSEHASH_FOUND DEPENDS "sparsehash" LIBSPARSEHASH_FOUND
) )
optional_component(DISCORD_RPC ON "Discord Rich Presence" optional_component(DISCORD_RPC ON "Discord Rich Presence")
DEPENDS "RapidJSON" RapidJSON_FOUND
)
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ) if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON) set(HAVE_CHROMAPRINT ON)

View File

@@ -94,7 +94,6 @@ Optional dependencies:
* MTP devices: [libmtp](http://libmtp.sourceforge.net/) * MTP devices: [libmtp](http://libmtp.sourceforge.net/)
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/) * iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128) * EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
* Discord rich presence [RapidJSON](https://rapidjson.org/)
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats. You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.

View File

@@ -50,7 +50,7 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
playlist_manager_(playlist_manager), playlist_manager_(playlist_manager),
enabled_(false) { enabled_(false) {
Discord_Initialize(kDiscordApplicationId, nullptr, 1); Discord_Initialize(QLatin1String(kDiscordApplicationId), nullptr, 1);
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged); QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged); QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
@@ -109,36 +109,33 @@ void RichPresence::SendPresenceUpdate() {
return; return;
} }
::DiscordRichPresence presence_data{}; DiscordRichPresence presence_data{};
memset(&presence_data, 0, sizeof(presence_data));
presence_data.type = 2; // Listening presence_data.type = 2; // Listening
presence_data.largeImageKey = kStrawberryIconResourceName; presence_data.largeImageKey = QLatin1String(kStrawberryIconResourceName);
presence_data.smallImageKey = kStrawberryIconResourceName; presence_data.smallImageKey = QLatin1String(kStrawberryIconResourceName);
presence_data.smallImageText = kStrawberryIconDescription; presence_data.smallImageText = QLatin1String(kStrawberryIconDescription);
presence_data.instance = 0; presence_data.instance = 0;
QByteArray artist;
if (!activity_.artist.isEmpty()) { if (!activity_.artist.isEmpty()) {
artist = activity_.artist.toUtf8(); QString artist = activity_.artist;
artist.prepend(tr("by ").toUtf8()); artist.prepend(tr("by "));
presence_data.state = artist.constData(); presence_data.state = artist;
} }
QByteArray album;
if (!activity_.album.isEmpty() && activity_.album != activity_.title) { if (!activity_.album.isEmpty() && activity_.album != activity_.title) {
album = activity_.album.toUtf8(); QString album = activity_.album;
album.prepend(tr("on ").toUtf8()); album.prepend(tr("on "));
presence_data.largeImageText = album.constData(); presence_data.largeImageText = album;
} }
const QByteArray title = activity_.title.toUtf8(); const QString title = activity_.title;
presence_data.details = title.constData(); presence_data.details = title;
const qint64 start_timestamp = activity_.start_timestamp - activity_.seek_secs; const qint64 start_timestamp = activity_.start_timestamp - activity_.seek_secs;
presence_data.startTimestamp = start_timestamp; presence_data.startTimestamp = start_timestamp;
presence_data.endTimestamp = start_timestamp + activity_.length_secs; presence_data.endTimestamp = start_timestamp + activity_.length_secs;
Discord_UpdatePresence(&presence_data); Discord_UpdatePresence(presence_data);
} }