Compare commits

...

8 Commits

Author SHA1 Message Date
Jonas Kvinge
306709f498 discord-rpc: Port to Qt Json 2025-04-13 12:24:59 +02:00
Jonas Kvinge
21bdf88d09 RichPresence: Remove unused variable 2025-04-13 12:16:57 +02:00
Jonas Kvinge
ff032c3cd7 RichPresence: Remove rate limit 2025-04-13 12:01:56 +02:00
Jonas Kvinge
c083110051 RichPresence: Move variable declaration
Fixes #1718
2025-04-13 11:52:16 +02:00
Jonas Kvinge
a7dbeb5d76 discord-rpc: Add copyright 2025-04-12 13:17:13 +02:00
Jonas Kvinge
634f6ea9f5 discord-rpc: Formatting 2025-04-12 13:17:00 +02:00
Jonas Kvinge
f9e4f9a09a discord-rpc: Formatting 2025-04-11 22:50:14 +02:00
Jonas Kvinge
aab9889174 Turn on git revision 2025-04-09 19:59:48 +02:00
32 changed files with 1252 additions and 1158 deletions

View File

@@ -1 +1,42 @@
add_subdirectory(src) set(DISCORD_RPC_SOURCES
discord_rpc.h
discord_register.h
discord_rpc.cpp
discord_rpc_connection.h
discord_rpc_connection.cpp
discord_serialization.h
discord_serialization.cpp
discord_connection.h
discord_backoff.h
discord_msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
if(APPLE)
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
add_definitions(-DDISCORD_OSX)
else()
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
add_definitions(-DDISCORD_LINUX)
endif()
endif()
if(WIN32)
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
add_definitions(-DDISCORD_WINDOWS)
endif()
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
if(APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif()
if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_link_libraries(discord-rpc PRIVATE Qt${QT_VERSION_MAJOR}::Core)
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

63
3rdparty/discord-rpc/discord_backoff.h vendored Normal file
View File

@@ -0,0 +1,63 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_BACKOFF_H
#define DISCORD_BACKOFF_H
#include <algorithm>
#include <random>
#include <cstdint>
#include <ctime>
namespace discord_rpc {
struct Backoff {
int64_t minAmount;
int64_t maxAmount;
int64_t current;
int fails;
std::mt19937_64 randGenerator;
std::uniform_real_distribution<> randDistribution;
double rand01() { return randDistribution(randGenerator); }
Backoff(int64_t min, int64_t max)
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
}
void reset() {
fails = 0;
current = minAmount;
}
int64_t nextDelay() {
++fails;
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
current = std::min(current + delay, maxAmount);
return current;
}
};
} // namespace discord_rpc
#endif // DISCORD_BACKOFF_H

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_CONNECTION_H
#define DISCORD_CONNECTION_H
// This is to wrap the platform specific kinds of connect/read/write.
#include <cstdlib>
namespace discord_rpc {
// not really connectiony, but need per-platform
int GetProcessId();
struct BaseConnection {
static BaseConnection *Create();
static void Destroy(BaseConnection *&);
bool isOpen = false;
bool Open();
bool Close();
bool Write(const void *data, size_t length);
bool Read(void *data, size_t length);
};
} // namespace discord_rpc
#endif // DISCORD_CONNECTION_H

View File

@@ -1,4 +1,27 @@
#include "connection.h" /*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_connection.h"
#include <cerrno> #include <cerrno>
#include <fcntl.h> #include <fcntl.h>
@@ -28,28 +51,34 @@ static int MsgFlags = 0;
#endif #endif
static const char *GetTempPath() { static const char *GetTempPath() {
const char *temp = getenv("XDG_RUNTIME_DIR"); const char *temp = getenv("XDG_RUNTIME_DIR");
temp = temp ? temp : getenv("TMPDIR"); temp = temp ? temp : getenv("TMPDIR");
temp = temp ? temp : getenv("TMP"); temp = temp ? temp : getenv("TMP");
temp = temp ? temp : getenv("TEMP"); temp = temp ? temp : getenv("TEMP");
temp = temp ? temp : "/tmp"; temp = temp ? temp : "/tmp";
return temp; return temp;
} }
/*static*/ BaseConnection *BaseConnection::Create() { BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX; PipeAddr.sun_family = AF_UNIX;
return &Connection; return &Connection;
} }
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) { void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionUnix *>(c);
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
self->Close(); self->Close();
c = nullptr; c = nullptr;
} }
bool BaseConnection::Open() { bool BaseConnection::Open() {
const char *tempPath = GetTempPath(); const char *tempPath = GetTempPath();
auto self = reinterpret_cast<BaseConnectionUnix *>(this); auto self = reinterpret_cast<BaseConnectionUnix*>(this);
self->sock = socket(AF_UNIX, SOCK_STREAM, 0); self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -61,8 +90,7 @@ bool BaseConnection::Open() {
#endif #endif
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) { for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
snprintf( snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr)); int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
if (err == 0) { if (err == 0) {
self->isOpen = true; self->isOpen = true;
@@ -70,10 +98,13 @@ bool BaseConnection::Open() {
} }
} }
self->Close(); self->Close();
return false; return false;
} }
bool BaseConnection::Close() { bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionUnix *>(this); auto self = reinterpret_cast<BaseConnectionUnix *>(this);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -81,11 +112,14 @@ bool BaseConnection::Close() {
close(self->sock); close(self->sock);
self->sock = -1; self->sock = -1;
self->isOpen = false; self->isOpen = false;
return true; return true;
} }
bool BaseConnection::Write(const void *data, size_t length) { bool BaseConnection::Write(const void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -95,11 +129,14 @@ bool BaseConnection::Write(const void *data, size_t length) {
if (sentBytes < 0) { if (sentBytes < 0) {
Close(); Close();
} }
return sentBytes == static_cast<ssize_t>(length); return sentBytes == static_cast<ssize_t>(length);
} }
bool BaseConnection::Read(void *data, size_t length) { bool BaseConnection::Read(void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -115,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
else if (res == 0) { else if (res == 0) {
Close(); Close();
} }
return static_cast<size_t>(res) == length; return static_cast<size_t>(res) == length;
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -1,9 +1,33 @@
#include "connection.h" /*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_connection.h"
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#define NOMCX #define NOMCX
#define NOSERVICE #define NOSERVICE
#define NOIME #define NOIME
#include <cassert> #include <cassert>
#include <windows.h> #include <windows.h>
@@ -19,24 +43,26 @@ struct BaseConnectionWin : public BaseConnection {
static BaseConnectionWin Connection; static BaseConnectionWin Connection;
/*static*/ BaseConnection *BaseConnection::Create() { BaseConnection *BaseConnection::Create() {
return &Connection; return &Connection;
} }
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) { void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionWin*>(c); auto self = reinterpret_cast<BaseConnectionWin*>(c);
self->Close(); self->Close();
c = nullptr; c = nullptr;
} }
bool BaseConnection::Open() { bool BaseConnection::Open() {
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" }; wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2; const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
pipeName[pipeDigit] = L'0'; pipeName[pipeDigit] = L'0';
auto self = reinterpret_cast<BaseConnectionWin *>(this); auto self = reinterpret_cast<BaseConnectionWin *>(this);
for (;;) { for (;;) {
self->pipe = ::CreateFileW( self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (self->pipe != INVALID_HANDLE_VALUE) { if (self->pipe != INVALID_HANDLE_VALUE) {
self->isOpen = true; self->isOpen = true;
return true; return true;
@@ -57,17 +83,22 @@ bool BaseConnection::Open() {
} }
return false; return false;
} }
} }
bool BaseConnection::Close() { bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionWin *>(this); auto self = reinterpret_cast<BaseConnectionWin *>(this);
::CloseHandle(self->pipe); ::CloseHandle(self->pipe);
self->pipe = INVALID_HANDLE_VALUE; self->pipe = INVALID_HANDLE_VALUE;
self->isOpen = false; self->isOpen = false;
return true; return true;
} }
bool BaseConnection::Write(const void *data, size_t length) { bool BaseConnection::Write(const void *data, size_t length) {
if (length == 0) { if (length == 0) {
return true; return true;
} }
@@ -85,11 +116,13 @@ bool BaseConnection::Write(const void *data, size_t length) {
} }
const DWORD bytesLength = static_cast<DWORD>(length); const DWORD bytesLength = static_cast<DWORD>(length);
DWORD bytesWritten = 0; DWORD bytesWritten = 0;
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE &&
bytesWritten == bytesLength; return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
} }
bool BaseConnection::Read(void *data, size_t length) { bool BaseConnection::Read(void *data, size_t length) {
assert(data); assert(data);
if (!data) { if (!data) {
return false; return false;
@@ -119,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
else { else {
Close(); Close();
} }
return false; return false;
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_MSG_QUEUE_H
#define DISCORD_MSG_QUEUE_H
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
namespace discord_rpc {
template<typename ElementType, std::size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize];
std::atomic_uint nextAdd_ { 0 };
std::atomic_uint nextSend_ { 0 };
std::atomic_uint pendingSends_ { 0 };
public:
MsgQueue() {}
ElementType *GetNextAddMessage() {
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType *GetNextSendMessage() {
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};
} // namespace discord_rpc
#endif // DISCORD_MSG_QUEUE_H

39
3rdparty/discord-rpc/discord_register.h vendored Normal file
View File

@@ -0,0 +1,39 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H
#include <QString>
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const QString &applicationId, const char *command);
#ifdef __cplusplus
}
#endif
#endif // DISCORD_REGISTER_H

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include <cstdio>
#include <errno.h>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <QString>
#include "discord_rpc.h"
#include "discord_register.h"
namespace {
static bool Mkdir(const char *path) {
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
} // namespace
// We want to register games so we can run them from Discord client as discord-<appid>://
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.
const char *home = getenv("HOME");
if (!home) {
return;
}
char exePath[1024]{};
if (!command || !command[0]) {
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
return;
}
exePath[size] = '\0';
command = exePath;
}
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
"Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n"
"NoDisplay=true\n"
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId.toUtf8().constData(), command, applicationId.toUtf8().constData());
if (fileLen <= 0) {
return;
}
char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId.toUtf8().constData());
char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/share");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/applications");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, desktopFilename);
FILE *fp = fopen(desktopFilePath, "w");
if (fp) {
fwrite(desktopFile, 1, fileLen, fp);
fclose(fp);
}
else {
return;
}
char xdgMimeCommand[1024]{};
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId.toUtf8().constData(),
applicationId.toUtf8().constData());
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static void RegisterCommand(const char *applicationId, const char *command) {
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
// the command therein (will pass to js's window.open, so requires a url-like thing)
// Note: will not work for sandboxed apps
NSString *home = NSHomeDirectory();
if (!home) {
return;
}
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Application Support"]
stringByAppendingPathComponent:@"discord"]
stringByAppendingPathComponent:@"games"]
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
stringByAppendingPathExtension:@"json"];
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
}
static void RegisterURL(const char *applicationId) {
char url[256];
snprintf(url, sizeof(url), "discord-%s", applicationId);
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
if (!myBundleId) {
fprintf(stderr, "No bundle id found\n");
return;
}
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
if (!myURL) {
fprintf(stderr, "No bundle url found\n");
return;
}
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
if (status != noErr) {
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
return;
}
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
if (status != noErr) {
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
}
}
void Discord_Register(const QString &applicationId, const char *command) {
const QByteArray applicationIdData = applicationId.toUtf8();
if (command) {
RegisterCommand(applicationIdData.constData(), command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationIdData.constData());
}
}
}

View File

@@ -1,3 +1,26 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_rpc.h" #include "discord_rpc.h"
#include "discord_register.h" #include "discord_register.h"
@@ -5,6 +28,7 @@
#define NOMCX #define NOMCX
#define NOSERVICE #define NOSERVICE
#define NOIME #define NOIME
#include <windows.h> #include <windows.h>
#include <psapi.h> #include <psapi.h>
#include <cstdio> #include <cstdio>
@@ -46,12 +70,8 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
#endif #endif
#define RegSetKeyValueW regset #define RegSetKeyValueW regset
static LSTATUS regset(HKEY hkey, static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
LPCWSTR subkey,
LPCWSTR name,
DWORD type,
const void *data,
DWORD len) {
HKEY htkey = hkey, hsubkey = nullptr; HKEY htkey = hkey, hsubkey = nullptr;
LSTATUS ret; LSTATUS ret;
if (subkey && subkey[0]) { if (subkey && subkey[0]) {
@@ -64,16 +84,18 @@ static LSTATUS regset(HKEY hkey,
if (hsubkey && hsubkey != hkey) if (hsubkey && hsubkey != hkey)
RegCloseKey(hsubkey); RegCloseKey(hsubkey);
return ret; return ret;
} }
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) { static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
// we want to register games so we can run them as discord-<appid>:// // we want to register games so we can run them as discord-<appid>://
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions. // Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
wchar_t exeFilePath[MAX_PATH]; wchar_t exeFilePath[MAX_PATH]{};
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH); DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
wchar_t openCommand[1024]; wchar_t openCommand[1024]{};
if (command && command[0]) { if (command && command[0]) {
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command); StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
@@ -83,18 +105,16 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath); StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
} }
wchar_t protocolName[64]; wchar_t protocolName[64]{};
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId); StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
wchar_t protocolDescription[128]; wchar_t protocolDescription[128]{};
StringCbPrintfW( StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
wchar_t urlProtocol = 0; wchar_t urlProtocol = 0;
wchar_t keyName[256]; wchar_t keyName[256]{};
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName); StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
HKEY key; HKEY key;
auto status = auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) { if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error creating key\n"); fprintf(stderr, "Error creating key\n");
return; return;
@@ -102,8 +122,7 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
DWORD len; DWORD len;
LSTATUS result; LSTATUS result;
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1); len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
if (FAILED(result)) { if (FAILED(result)) {
fprintf(stderr, "Error writing description\n"); fprintf(stderr, "Error writing description\n");
} }
@@ -114,26 +133,26 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
fprintf(stderr, "Error writing description\n"); fprintf(stderr, "Error writing description\n");
} }
result = RegSetKeyValueW( result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
if (FAILED(result)) { if (FAILED(result)) {
fprintf(stderr, "Error writing icon\n"); fprintf(stderr, "Error writing icon\n");
} }
len = static_cast<DWORD>(lstrlenW(openCommand) + 1); len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
result = RegSetKeyValueW( result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
if (FAILED(result)) { if (FAILED(result)) {
fprintf(stderr, "Error writing command\n"); fprintf(stderr, "Error writing command\n");
} }
RegCloseKey(key); RegCloseKey(key);
} }
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];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t openCommand[1024]; wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId.toUtf8().constData(), -1, appId, 32);
wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr; const wchar_t *wcommand = nullptr;
if (command && command[0]) { if (command && command[0]) {
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand); const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
@@ -142,42 +161,6 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
} }
Discord_RegisterW(appId, wcommand); Discord_RegisterW(appId, wcommand);
}
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
const char *steamId) {
wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t wSteamId[32];
MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32);
HKEY key;
auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error opening Steam key\n");
return;
}
wchar_t steamPath[MAX_PATH];
DWORD pathBytes = sizeof(steamPath);
status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE *)steamPath, &pathBytes);
RegCloseKey(key);
if (status != ERROR_SUCCESS || pathBytes < 1) {
fprintf(stderr, "Error reading SteamExe key\n");
return;
}
DWORD pathChars = pathBytes / sizeof(wchar_t);
for (DWORD i = 0; i < pathChars; ++i) {
if (steamPath[i] == L'/') {
steamPath[i] = L'\\';
}
}
wchar_t command[1024];
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId);
Discord_RegisterW(appId, command);
} }

View File

@@ -1,18 +1,45 @@
#include "discord_rpc.h" /*
* Copyright 2017 Discord, Inc.
#include "backoff.h" *
#include "discord_register.h" * Permission is hereby granted, free of charge, to any person obtaining a copy of
#include "msg_queue.h" * this software and associated documentation files (the "Software"), to deal in
#include "rpc_connection.h" * the Software without restriction, including without limitation the rights to
#include "serialization.h" * 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.
*
*/
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <condition_variable> #include <condition_variable>
#include <thread> #include <thread>
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc.h"
#include "discord_backoff.h"
#include "discord_register.h"
#include "discord_msg_queue.h"
#include "discord_rpc_connection.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 };
@@ -31,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
}; };
@@ -54,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 {};
@@ -67,8 +96,7 @@ static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<User, JoinQueueSize> JoinAskQueue; static MsgQueue<User, JoinQueueSize> JoinAskQueue;
static User connectedUser; static User connectedUser;
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential // We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
// backoff from 0.5 seconds to 1 minute
static Backoff ReconnectTimeMs(500, 60 * 1000); static Backoff ReconnectTimeMs(500, 60 * 1000);
static auto NextConnect = std::chrono::system_clock::now(); static auto NextConnect = std::chrono::system_clock::now();
static int Pid { 0 }; static int Pid { 0 };
@@ -111,11 +139,13 @@ class IoThreadHolder {
static IoThreadHolder *IoThread { nullptr }; static IoThreadHolder *IoThread { nullptr };
static void UpdateReconnectTime() { static void UpdateReconnectTime() {
NextConnect = std::chrono::system_clock::now() +
std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() }; NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
} }
static void Discord_UpdateConnection(void) { static void Discord_UpdateConnection() {
if (!Connection) { if (!Connection) {
return; return;
} }
@@ -130,65 +160,63 @@ static void Discord_UpdateConnection(void) {
// 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();
} }
@@ -217,54 +245,54 @@ static void Discord_UpdateConnection(void) {
SendQueue.CommitSend(); SendQueue.CommitSend();
} }
} }
} }
static void SignalIOActivity() { static void SignalIOActivity() {
if (IoThread != nullptr) { if (IoThread != nullptr) {
IoThread->Notify(); IoThread->Notify();
} }
} }
static bool RegisterForEvent(const char *evtName) { static bool RegisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage(); auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) { if (qmessage) {
qmessage->length = qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd(); SendQueue.CommitAdd();
SignalIOActivity(); SignalIOActivity();
return true; return true;
} }
return false; return false;
} }
static bool DeregisterForEvent(const char *evtName) { static bool DeregisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage(); auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) { if (qmessage) {
qmessage->length = qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd(); SendQueue.CommitAdd();
SignalIOActivity(); SignalIOActivity();
return true; return true;
} }
return false; return false;
} }
extern "C" void Discord_Initialize(const char *applicationId, extern "C" void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
DiscordEventHandlers *handlers,
int autoRegister,
const char *optionalSteamId) {
IoThread = new (std::nothrow) IoThreadHolder(); IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) { if (IoThread == nullptr) {
return; return;
} }
if (autoRegister) { if (autoRegister) {
if (optionalSteamId && optionalSteamId[0]) { Discord_Register(applicationId, nullptr);
Discord_RegisterSteamGame(applicationId, optionalSteamId);
}
else {
Discord_Register(applicationId, nullptr);
}
} }
Pid = GetProcessId(); Pid = GetProcessId();
@@ -287,45 +315,48 @@ extern "C" void Discord_Initialize(const char *applicationId,
} }
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();
}; };
IoThread->Start(); IoThread->Start();
} }
extern "C" void Discord_Shutdown(void) { extern "C" void Discord_Shutdown(void) {
if (!Connection) { if (!Connection) {
return; return;
} }
@@ -341,37 +372,42 @@ extern "C" void Discord_Shutdown(void) {
} }
RpcConnection::Destroy(Connection); RpcConnection::Destroy(Connection);
} }
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);
QueuedPresence.length = JsonWriteRichPresenceObj( QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
UpdatePresence.exchange(true); UpdatePresence.exchange(true);
} }
SignalIOActivity(); SignalIOActivity();
} }
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) {
// if we are not connected, let's not batch up stale messages for later // if we are not connected, let's not batch up stale messages for later
if (!Connection || !Connection->IsOpen()) { if (!Connection || !Connection->IsOpen()) {
return; return;
} }
auto qmessage = SendQueue.GetNextAddMessage(); auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) { if (qmessage) {
qmessage->length = qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
SendQueue.CommitAdd(); SendQueue.CommitAdd();
SignalIOActivity(); SignalIOActivity();
} }
} }
extern "C" void Discord_RunCallbacks(void) { extern "C" void Discord_RunCallbacks() {
// Note on some weirdness: internally we might connect, get other signals, disconnect any number // Note on some weirdness: internally we might connect, get other signals, disconnect any number
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other // of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
// signals are book-ended by calls to ready and disconnect. // signals are book-ended by calls to ready and disconnect.
@@ -380,8 +416,8 @@ extern "C" void Discord_RunCallbacks(void) {
return; return;
} }
bool wasDisconnected = WasJustDisconnected.exchange(false); const bool wasDisconnected = WasJustDisconnected.exchange(false);
bool isConnected = Connection->IsOpen(); const bool isConnected = Connection->IsOpen();
if (isConnected) { if (isConnected) {
// if we are connected, disconnect cb first // if we are connected, disconnect cb first
@@ -394,10 +430,7 @@ extern "C" void Discord_RunCallbacks(void) {
if (WasJustConnected.exchange(false)) { if (WasJustConnected.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex); std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.ready) { if (Handlers.ready) {
DiscordUser du { connectedUser.userId, DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
connectedUser.username,
connectedUser.discriminator,
connectedUser.avatar };
Handlers.ready(&du); Handlers.ready(&du);
} }
} }
@@ -429,7 +462,7 @@ extern "C" void Discord_RunCallbacks(void) {
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if // maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
// not it should be trivial for the implementer to make a queue themselves. // not it should be trivial for the implementer to make a queue themselves.
while (JoinAskQueue.HavePendingSends()) { while (JoinAskQueue.HavePendingSends()) {
auto req = JoinAskQueue.GetNextSendMessage(); const auto req = JoinAskQueue.GetNextSendMessage();
{ {
std::lock_guard<std::mutex> guard(HandlerMutex); std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) { if (Handlers.joinRequest) {
@@ -447,9 +480,11 @@ extern "C" void Discord_RunCallbacks(void) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
} }
} }
} }
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) { extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
if (newHandlers) { if (newHandlers) {
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \ #define HANDLE_EVENT_REGISTRATION(handler_name, event) \
if (!Handlers.handler_name && newHandlers->handler_name) { \ if (!Handlers.handler_name && newHandlers->handler_name) { \
@@ -472,8 +507,7 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
std::lock_guard<std::mutex> guard(HandlerMutex); std::lock_guard<std::mutex> guard(HandlerMutex);
Handlers = {}; Handlers = {};
} }
return;
} }
} // namespace discord_rpc } // namespace discord_rpc

99
3rdparty/discord-rpc/discord_rpc.h vendored Normal file
View File

@@ -0,0 +1,99 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_RPC_H
#define DISCORD_RPC_H
#include <cstdint>
#include <QString>
namespace discord_rpc {
#ifdef __cplusplus
extern "C" {
#endif
class DiscordRichPresence {
public:
int type;
QString name; /* max 128 bytes */
QString state; /* max 128 bytes */
QString details; /* max 128 bytes */
qint64 startTimestamp;
qint64 endTimestamp;
QString largeImageKey; /* max 32 bytes */
QString largeImageText; /* max 128 bytes */
QString smallImageKey; /* max 32 bytes */
QString smallImageText; /* max 128 bytes */
QString partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
QString matchSecret; /* max 128 bytes */
QString joinSecret; /* max 128 bytes */
QString spectateSecret; /* max 128 bytes */
qint8 instance;
};
typedef struct DiscordUser {
const QString userId;
const QString username;
const QString discriminator;
const QString avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const QString &message);
void (*errored)(int errorCode, const QString &message);
void (*joinGame)(const QString &joinSecret);
void (*spectateGame)(const QString &spectateSecret);
void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown();
// checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks();
void Discord_UpdatePresence(const DiscordRichPresence &presence = DiscordRichPresence());
void Discord_ClearPresence();
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
#ifdef __cplusplus
} /* extern "C" */
#endif
} // namespace discord_rpc
#endif // DISCORD_RPC_H

View File

@@ -1,24 +1,58 @@
#include "rpc_connection.h" /*
#include "serialization.h" * Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc_connection.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;
/*static*/ 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;
} }
/*static*/ void RpcConnection::Destroy(RpcConnection *&c) { void RpcConnection::Destroy(RpcConnection *&c) {
c->Close(); c->Close();
BaseConnection::Destroy(c->connection); BaseConnection::Destroy(c->connection);
c = nullptr; c = nullptr;
} }
void RpcConnection::Open() { void RpcConnection::Open() {
if (state == State::Connected) { if (state == State::Connected) {
return; return;
} }
@@ -28,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);
} }
} }
} }
@@ -51,17 +86,21 @@ void RpcConnection::Open() {
Close(); Close();
} }
} }
} }
void RpcConnection::Close() { void RpcConnection::Close() {
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) { if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
onDisconnect(lastErrorCode, lastErrorMessage); onDisconnect(lastErrorCode, lastErrorMessage);
} }
connection->Close(); connection->Close();
state = State::Disconnected; state = State::Disconnected;
} }
bool RpcConnection::Write(const void *data, size_t length) { bool RpcConnection::Write(const void *data, size_t length) {
sendFrame.opcode = Opcode::Frame; sendFrame.opcode = Opcode::Frame;
memcpy(sendFrame.message, data, length); memcpy(sendFrame.message, data, length);
sendFrame.length = static_cast<uint32_t>(length); sendFrame.length = static_cast<uint32_t>(length);
@@ -69,20 +108,23 @@ bool RpcConnection::Write(const void *data, size_t length) {
Close(); Close();
return false; return false;
} }
return true; return true;
} }
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;
} }
MessageFrame readFrame; MessageFrame readFrame{};
for (;;) { for (;;) {
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader)); bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
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;
@@ -92,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;
} }
@@ -101,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;
@@ -122,12 +164,12 @@ 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;
} }
} }
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H
#include <QString>
#include <QJsonDocument>
#include "discord_connection.h"
#include "discord_serialization.h"
namespace discord_rpc {
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
constexpr size_t MaxRpcFrameSize = 64 * 1024;
struct RpcConnection {
enum class ErrorCode : int {
Success = 0,
PipeClosed = 1,
ReadCorrupt = 2,
};
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
struct MessageFrameHeader {
Opcode opcode;
uint32_t length;
};
struct MessageFrame : public MessageFrameHeader {
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
};
enum class State : uint32_t {
Disconnected,
SentHandshake,
AwaitingResponse,
Connected,
};
BaseConnection *connection { nullptr };
State state { State::Disconnected };
void (*onConnect)(QJsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, QString &message) { nullptr };
QString appId;
int lastErrorCode { 0 };
QString lastErrorMessage;
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const QString &applicationId);
static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; }
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(QJsonDocument &message);
};
} // namespace discord_rpc
#endif // DISCORD_RPC_CONNECTION_H

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_serialization.h"
#include "discord_rpc.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc {
template<typename T>
void NumberToString(char *dest, T number) {
if (!number) {
*dest++ = '0';
*dest++ = 0;
return;
}
if (number < 0) {
*dest++ = '-';
number = -number;
}
char temp[32];
int place = 0;
while (number) {
auto digit = number % 10;
number = number / 10;
temp[place++] = '0' + static_cast<char>(digit);
}
for (--place; place >= 0; --place) {
*dest++ = temp[place];
}
*dest = 0;
}
void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value);
void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value) {
if (!value.isEmpty()) {
json_object[key] = value;
}
}
static QString JsonWriteNonce(const int nonce) {
char nonce_buffer[32]{};
NumberToString(nonce_buffer, nonce);
return QString::fromLatin1(nonce_buffer);
}
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence) {
QJsonObject json_object;
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = "SET_ACTIVITY"_L1;
QJsonObject args;
args["pid"_L1] = pid;
QJsonObject activity;
if (presence.type >= 0 && presence.type <= 5) {
activity["type"_L1] = presence.type;
}
activity["state"_L1] = presence.state;
activity["details"_L1] = presence.details;
if (presence.startTimestamp != 0 || presence.endTimestamp != 0) {
QJsonObject timestamps;
if (presence.startTimestamp != 0) {
timestamps["start"_L1] = presence.startTimestamp;
}
if (presence.endTimestamp != 0) {
timestamps["end"_L1] = presence.endTimestamp;
}
activity["timestamps"_L1] = timestamps;
}
if (!presence.largeImageKey.isEmpty() || !presence.largeImageText.isEmpty() || !presence.smallImageKey.isEmpty() || !presence.smallImageText.isEmpty()) {
QJsonObject assets;
WriteOptionalString(assets, "large_image"_L1, presence.largeImageKey);
WriteOptionalString(assets, "large_text"_L1, presence.largeImageText);
WriteOptionalString(assets, "small_image"_L1, presence.smallImageKey);
WriteOptionalString(assets, "small_text"_L1, presence.smallImageText);
activity["assets"_L1] = assets;
}
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 JsonWriteHandshakeObj(char *dest, const size_t maxLen, const int version, const QString &applicationId) {
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();
}
size_t JsonWriteSubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) {
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();
}
size_t JsonWriteUnsubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) {
QJsonObject json_object;
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 data.length();
}
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
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();
}
} // namespace discord_rpc

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H
#include <cstddef>
#include <QString>
namespace discord_rpc {
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const QString &applicationId);
class 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 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);
} // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H

View File

@@ -1,12 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char* applicationId, const char* command);
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
#ifdef __cplusplus
}
#endif

View File

@@ -1,77 +0,0 @@
#pragma once
#include <stdint.h>
// clang-format off
// clang-format on
namespace discord_rpc {
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
int type;
const char* name; /* max 128 bytes */
const char* state; /* max 128 bytes */
const char* details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char* largeImageKey; /* max 32 bytes */
const char* largeImageText; /* max 128 bytes */
const char* smallImageKey; /* max 32 bytes */
const char* smallImageText; /* max 128 bytes */
const char* partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
const char* matchSecret; /* max 128 bytes */
const char* joinSecret; /* max 128 bytes */
const char* spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char* userId;
const char* username;
const char* discriminator;
const char* avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser* request);
void (*disconnected)(int errorCode, const char* message);
void (*errored)(int errorCode, const char* message);
void (*joinGame)(const char* joinSecret);
void (*spectateGame)(const char* spectateSecret);
void (*joinRequest)(const DiscordUser* request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId);
void Discord_Shutdown(void);
/* checks for incoming messages, dispatches callbacks */
void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence* presence);
void Discord_ClearPresence(void);
void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
#ifdef __cplusplus
} /* extern "C" */
} // namespace discord_rpc
#endif

View File

@@ -1,41 +0,0 @@
set(DISCORD_RPC_SOURCES
../include/discord_rpc.h
../include/discord_register.h
discord_rpc.cpp
rpc_connection.h
rpc_connection.cpp
serialization.h
serialization.cpp
connection.h
backoff.h
msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES connection_unix.cpp)
if(APPLE)
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
add_definitions(-DDISCORD_OSX)
else()
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
add_definitions(-DDISCORD_LINUX)
endif()
endif()
if(WIN32)
list(APPEND DISCORD_RPC_SOURCES connection_win.cpp discord_register_win.cpp)
add_definitions(-DDISCORD_WINDOWS)
endif()
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
if(APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif()
if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)

View File

@@ -1,44 +0,0 @@
#pragma once
#include <algorithm>
#include <random>
#include <cstdint>
#include <ctime>
namespace discord_rpc {
struct Backoff {
int64_t minAmount;
int64_t maxAmount;
int64_t current;
int fails;
std::mt19937_64 randGenerator;
std::uniform_real_distribution<> randDistribution;
double rand01() { return randDistribution(randGenerator); }
Backoff(int64_t min, int64_t max)
: minAmount(min)
, maxAmount(max)
, current(min)
, fails(0)
, randGenerator(static_cast<uint64_t>(time(0)))
{
}
void reset()
{
fails = 0;
current = minAmount;
}
int64_t nextDelay()
{
++fails;
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
current = std::min(current + delay, maxAmount);
return current;
}
};
} // namespace discord_rpc

View File

@@ -1,22 +0,0 @@
#pragma once
// This is to wrap the platform specific kinds of connect/read/write.
#include <cstdlib>
namespace discord_rpc {
// not really connectiony, but need per-platform
int GetProcessId();
struct BaseConnection {
static BaseConnection *Create();
static void Destroy(BaseConnection *&);
bool isOpen { false };
bool Open();
bool Close();
bool Write(const void *data, size_t length);
bool Read(void *data, size_t length);
};
} // namespace discord_rpc

View File

@@ -1,104 +0,0 @@
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio>
#include <errno.h>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace {
static bool Mkdir(const char *path) {
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
} // namespace
// 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) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
const char *home = getenv("HOME");
if (!home) {
return;
}
char exePath[1024];
if (!command || !command[0]) {
ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
return;
}
exePath[size] = '\0';
command = exePath;
}
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
"Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n"
"NoDisplay=true\n"
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048];
int fileLen = snprintf(
desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) {
return;
}
char desktopFilename[256];
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
char desktopFilePath[1024];
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/share");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/applications");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, desktopFilename);
FILE *fp = fopen(desktopFilePath, "w");
if (fp) {
fwrite(desktopFile, 1, fileLen, fp);
fclose(fp);
}
else {
return;
}
char xdgMimeCommand[1024];
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId,
applicationId);
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}
}
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
const char *steamId) {
char command[256];
sprintf(command, "xdg-open steam://rungameid/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@@ -1,80 +0,0 @@
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static void RegisterCommand(const char* applicationId, const char* command)
{
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
// the command therein (will pass to js's window.open, so requires a url-like thing)
// Note: will not work for sandboxed apps
NSString *home = NSHomeDirectory();
if (!home) {
return;
}
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Application Support"]
stringByAppendingPathComponent:@"discord"]
stringByAppendingPathComponent:@"games"]
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
stringByAppendingPathExtension:@"json"];
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
}
static void RegisterURL(const char* applicationId)
{
char url[256];
snprintf(url, sizeof(url), "discord-%s", applicationId);
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
if (!myBundleId) {
fprintf(stderr, "No bundle id found\n");
return;
}
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
if (!myURL) {
fprintf(stderr, "No bundle url found\n");
return;
}
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
if (status != noErr) {
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
return;
}
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
if (status != noErr) {
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
}
}
void Discord_Register(const char* applicationId, const char* command)
{
if (command) {
RegisterCommand(applicationId, command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationId);
}
}
}
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
{
char command[256];
snprintf(command, 256, "steam://rungameid/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@@ -1,40 +0,0 @@
#pragma once
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
namespace discord_rpc {
template <typename ElementType, std::size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize];
std::atomic_uint nextAdd_{0};
std::atomic_uint nextSend_{0};
std::atomic_uint pendingSends_{0};
public:
MsgQueue() {}
ElementType* GetNextAddMessage()
{
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType* GetNextSendMessage()
{
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};
} // namespace discord_rpc

View File

@@ -1,64 +0,0 @@
#pragma once
#include "connection.h"
#include "serialization.h"
namespace discord_rpc {
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much
// smaller.
constexpr size_t MaxRpcFrameSize = 64 * 1024;
struct RpcConnection {
enum class ErrorCode : int {
Success = 0,
PipeClosed = 1,
ReadCorrupt = 2,
};
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
struct MessageFrameHeader {
Opcode opcode;
uint32_t length;
};
struct MessageFrame : public MessageFrameHeader {
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
};
enum class State : uint32_t {
Disconnected,
SentHandshake,
AwaitingResponse,
Connected,
};
BaseConnection *connection { nullptr };
State state { State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
char appId[64] {};
int lastErrorCode { 0 };
char lastErrorMessage[256] {};
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId);
static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; }
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(JsonDocument &message);
};
} // namespace discord_rpc

View File

@@ -1,249 +0,0 @@
#include "serialization.h"
#include "connection.h"
#include "discord_rpc.h"
namespace discord_rpc {
template<typename T>
void NumberToString(char *dest, T number) {
if (!number) {
*dest++ = '0';
*dest++ = 0;
return;
}
if (number < 0) {
*dest++ = '-';
number = -number;
}
char temp[32];
int place = 0;
while (number) {
auto digit = number % 10;
number = number / 10;
temp[place++] = '0' + static_cast<char>(digit);
}
for (--place; place >= 0; --place) {
*dest++ = temp[place];
}
*dest = 0;
}
// it's ever so slightly faster to not have to strlen the key
template<typename T>
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
struct WriteObject {
JsonWriter &writer;
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, int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
}
size_t JsonWriteRichPresenceObj(char *dest,
size_t maxLen,
int nonce,
int pid,
const DiscordRichPresence *presence) {
JsonWriter writer(dest, maxLen);
{
WriteObject top(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SET_ACTIVITY");
{
WriteObject args(writer, "args");
WriteKey(writer, "pid");
writer.Int(pid);
if (presence != nullptr) {
WriteObject activity(writer, "activity");
if (presence->type >= 0 && presence->type <= 5) {
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);
}
}
}
return writer.Size();
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
}
return writer.Size();
}
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
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) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce) {
JsonWriter writer(dest, maxLen);
{
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();
}
} // namespace discord_rpc

View File

@@ -1,183 +0,0 @@
#pragma once
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
namespace discord_rpc {
// if only there was a standard library function for this
template<size_t Len>
inline size_t StringCopy(char (&dest)[Len], const char *src) {
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,
size_t maxLen,
int nonce,
int pid,
const DiscordRichPresence *presence);
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 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

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)
@@ -1494,7 +1490,7 @@ endif()
if(HAVE_DISCORD_RPC) if(HAVE_DISCORD_RPC)
add_subdirectory(3rdparty/discord-rpc) add_subdirectory(3rdparty/discord-rpc)
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc/include) target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
endif() endif()
if(HAVE_TRANSLATIONS) if(HAVE_TRANSLATIONS)

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

@@ -3,7 +3,7 @@ set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 9) set(STRAWBERRY_VERSION_PATCH 9)
#set(STRAWBERRY_VERSION_PRERELEASE rc1) #set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF) set(INCLUDE_GIT_REVISION ON)
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}") set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")

View File

@@ -36,7 +36,6 @@ namespace {
constexpr char kDiscordApplicationId[] = "1352351827206733974"; constexpr char kDiscordApplicationId[] = "1352351827206733974";
constexpr char kStrawberryIconResourceName[] = "embedded_cover"; constexpr char kStrawberryIconResourceName[] = "embedded_cover";
constexpr char kStrawberryIconDescription[] = "Strawberry Music Player"; constexpr char kStrawberryIconDescription[] = "Strawberry Music Player";
constexpr qint64 kDiscordPresenceUpdateRateLimitMs = 2000;
} // namespace } // namespace
using namespace discord_rpc; using namespace discord_rpc;
@@ -49,10 +48,9 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
: QObject(parent), : QObject(parent),
player_(player), player_(player),
playlist_manager_(playlist_manager), playlist_manager_(playlist_manager),
send_presence_timestamp_(0),
enabled_(false) { enabled_(false) {
Discord_Initialize(kDiscordApplicationId, nullptr, 1, nullptr); 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);
@@ -111,42 +109,33 @@ void RichPresence::SendPresenceUpdate() {
return; return;
} }
const qint64 current_timestamp = QDateTime::currentMSecsSinceEpoch(); DiscordRichPresence presence_data{};
if (current_timestamp - send_presence_timestamp_ < kDiscordPresenceUpdateRateLimitMs) {
qLog(Info) << "Not sending rich presence due to rate limit of" << kDiscordPresenceUpdateRateLimitMs << "ms";
return;
}
send_presence_timestamp_ = current_timestamp;
::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;
if (!activity_.artist.isEmpty()) { if (!activity_.artist.isEmpty()) {
QByteArray 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;
} }
if (!activity_.album.isEmpty() && activity_.album != activity_.title) { if (!activity_.album.isEmpty() && activity_.album != activity_.title) {
QByteArray 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);
} }

View File

@@ -69,7 +69,6 @@ class RichPresence : public QObject {
qint64 seek_secs; qint64 seek_secs;
}; };
Activity activity_; Activity activity_;
qint64 send_presence_timestamp_;
bool enabled_; bool enabled_;
}; };