From 8c8158ad5b2f6248a5ec1af71902df453b8185d3 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 28 Dec 2024 10:36:36 +1300 Subject: [PATCH] initial commit --- .gitignore | 3 + Makefile | 25 +++++ discord_rpc.h | 87 +++++++++++++++++ prezzyc.c | 146 ++++++++++++++++++++++++++++ prezzyd.c | 258 ++++++++++++++++++++++++++++++++++++++++++++++++++ prezzyipc.c | 132 ++++++++++++++++++++++++++ prezzyipc.h | 73 ++++++++++++++ 7 files changed, 724 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 discord_rpc.h create mode 100644 prezzyc.c create mode 100644 prezzyd.c create mode 100644 prezzyipc.c create mode 100644 prezzyipc.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5efa054 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +prezzyd +prezzyc +*.o \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f89505c --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CC = cc +CFLAGS = -std=c99 -O2 -pedantic -Wall -Wextra +LDFLAGS = -ldiscord-rpc +OBJS = prezzyipc.o + +all: prezzyd prezzyc + +prezzyd: prezzyd.o ${OBJS} + cc ${CFLAGS} -o $@ prezzyd.o ${OBJS} ${LDFLAGS} +prezzyc: prezzyc.o ${OBJS} + cc ${CFLAGS} -o $@ prezzyc.o ${OBJS} ${LDFLAGS} + +.SUFFIXES: .c .o + +.c.o: + ${CC} ${CFLAGS} -DBUILD_DATE=\"$$(date -I)\" -c $< + +clean: + rm -f prezzyd prezzyc prezzyd.o prezzyc.o ${OBJS} + +.PHONY: all clean + +prezzyd.o: prezzyd.c prezzyipc.h discord_rpc.h +prezzyc.o: prezzyc.c prezzyipc.h +prezzyipc.o: prezzyipc.c prezzyipc.h diff --git a/discord_rpc.h b/discord_rpc.h new file mode 100644 index 0000000..3e1441e --- /dev/null +++ b/discord_rpc.h @@ -0,0 +1,87 @@ +#pragma once +#include + +// clang-format off + +#if defined(DISCORD_DYNAMIC_LIB) +# if defined(_WIN32) +# if defined(DISCORD_BUILDING_SDK) +# define DISCORD_EXPORT __declspec(dllexport) +# else +# define DISCORD_EXPORT __declspec(dllimport) +# endif +# else +# define DISCORD_EXPORT __attribute__((visibility("default"))) +# endif +#else +# define DISCORD_EXPORT +#endif + +// clang-format on + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct DiscordRichPresence { + 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; + 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 + +DISCORD_EXPORT void Discord_Initialize(const char* applicationId, + DiscordEventHandlers* handlers, + int autoRegister, + const char* optionalSteamId); +DISCORD_EXPORT void Discord_Shutdown(void); + +/* checks for incoming messages, dispatches callbacks */ +DISCORD_EXPORT void Discord_RunCallbacks(void); + +/* If you disable the lib starting its own io thread, you'll need to call this from your own */ +#ifdef DISCORD_DISABLE_IO_THREAD +DISCORD_EXPORT void Discord_UpdateConnection(void); +#endif + +DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence); +DISCORD_EXPORT void Discord_ClearPresence(void); + +DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); + +DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/prezzyc.c b/prezzyc.c new file mode 100644 index 0000000..72178f7 --- /dev/null +++ b/prezzyc.c @@ -0,0 +1,146 @@ +/* + * prezzyc + * Copyright (c) 2024 Jeremy Baxter. + */ + +#define _PREZZYC_C_ +#define _DEFAULT_SOURCE + +#ifndef BUILD_DATE +#define BUILD_DATE "1970-01-01" +#endif + +#include +#include +#include +#include +#include + +#include "prezzyipc.h" +#include "discord_rpc.h" + +static PrezzyIpcPacket **packets; +static int packetCount; + +void +addPacket(PrezzyProperty property, PrezzyPayloadType type, + char *pString, int64_t pInteger) +{ + PrezzyIpcPacket *p; + size_t len; + + p = malloc(sizeof(PrezzyIpcPacket)); + p->property = property; + p->type = type; + if (type == PLT_STRING) { + len = strlen(pString) + 1; + p->pString = malloc(len * sizeof(char)); + strbcpy(p->pString, pString, len); + } else + p->pInteger = pInteger; + + packetCount++; + packets = realloc(packets, + packetCount * sizeof(PrezzyIpcPacket *)); + packets[packetCount - 1] = p; +} + +int +getServer(const char *socketPath) +{ + struct sockaddr_un addr; + int fd; + + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + strbcpy(addr.sun_path, socketPath, sizeof(addr.sun_path) - 1); + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + err(1, "failed to create IPC client socket"); + if (connect(fd, (struct sockaddr *)&addr, + sizeof(struct sockaddr_un)) == -1) + err(1, "failed to establish connection to IPC daemon"); + + return fd; +} + +void +usage(void) +{ + fputs( + "usage: prezzyc [-aV] [-d details] [-I icon] [-i tooltip]\n" + " [-L large-image] [-l tooltip] [-s state]\n" + " [-P limit] [-p size] [-T end] [-t start]\n", + stderr); +} + +int +main(int argc, char **argv) +{ + char *buffer; + ssize_t sz; + int ch, i; + int server; + + if (argc < 2) { /* no options? */ + usage(); + return 1; + } + + while ((ch = getopt(argc, argv, "ad:I:i:L:l:P:p:s:T:t:V")) != -1) { + switch (ch) { + case 'a': addPacket(PREZZY_TIMESTAMP_START, PLT_INT64, NULL, time(NULL)); break; + case 'd': addPacket(PREZZY_DETAILS, PLT_STRING, optarg, 0); break; + case 'I': addPacket(PREZZY_ICON, PLT_STRING, optarg, 0); break; + case 'i': addPacket(PREZZY_ICON_TOOLTIP, PLT_STRING, optarg, 0); break; + case 'L': addPacket(PREZZY_LARGE_IMAGE, PLT_STRING, optarg, 0); break; + case 'l': addPacket(PREZZY_LARGE_TOOLTIP, PLT_STRING, optarg, 0); break; + case 's': addPacket(PREZZY_STATE, PLT_STRING, optarg, 0); break; + case 'P': addPacket(PREZZY_PARTY_LIMIT, PLT_INT, NULL, + squashLong(strtol(optarg, NULL, 10))); break; + case 'p': addPacket(PREZZY_PARTY_SIZE, PLT_INT, NULL, + squashLong(strtol(optarg, NULL, 10))); break; + case 'T': addPacket(PREZZY_TIMESTAMP_END, PLT_INT64, NULL, + strtol(optarg, NULL, 10)); break; + case 't': addPacket(PREZZY_TIMESTAMP_START, PLT_INT64, NULL, + strtol(optarg, NULL, 10)); break; + case 'V': + printf("prezzyc version 0.1 built on " BUILD_DATE "\n"); + return 0; + break; + } + } + + if (argc - optind > 0) { /* extra arguments? */ + usage(); + return 1; + } + + server = getServer(PREZZY_SOCK); + + for (i = 0; i < packetCount; i++) { + buffer = fromIpcPacket(*packets[i]); + write(server, buffer, strlen(buffer)); + free(buffer); + } + write(server, "\0", 1); + + buffer = malloc(PREZZY_PACKET_MAX * sizeof(char)); + memset(buffer, 0, PREZZY_PACKET_MAX); + while ((sz = read(server, buffer, PREZZY_PACKET_MAX - 1)) > 0) { + if (buffer[0] != 0) + errx(1, "IPC error %d: %s", buffer[0], buffer + 1); + } + + if (sz == -1) + err(1, "failed to read from IPC server"); + + for (i = 0; i < packetCount; i++) { + if (packets[i]->type == PLT_STRING) + free(packets[i]->pString); + free(packets[i]); + } + free(packets); + + return 0; +} diff --git a/prezzyd.c b/prezzyd.c new file mode 100644 index 0000000..832af8e --- /dev/null +++ b/prezzyd.c @@ -0,0 +1,258 @@ +/* + * prezzyd + * Copyright (c) 2024 Jeremy Baxter. + */ + +#define _PREZZYD_C_ +#define _DEFAULT_SOURCE + +#ifndef BUILD_DATE +#define BUILD_DATE "1970-01-01" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "prezzyipc.h" +#include "discord_rpc.h" + +void +connectHandler(const DiscordUser *user) +{ + warnx("established connection to Discord (%s/%s)", + user->username, user->userId); +} + +void +disconnectHandler(int code, const char *message) +{ + warnx("disconnected from client (%d: %s)", code, message); +} + +void +errorHandler(int code, const char *message) +{ + errx(1, "rpc error %d: %s", code, message); +} + +int +canReadFrom(int fd) +{ + struct pollfd *p; + int ret; + + p = malloc(sizeof(struct pollfd)); + p->fd = fd; + p->events = POLLIN; + + if (poll(p, 1, 5000) == -1) + err(1, "failed to poll client"); + + ret = p->revents & POLLIN; + free(p); + return ret; +} + +int +ipcClose(int fd, byte code, const char *message) +{ + write(fd, (char []){code}, 1); + write(fd, message, strlen(message)); + if (close(fd) == -1) + err(1, "failed to close IPC connection"); + + return code; +} + +void +ipcSucceed(int fd) +{ + ipcClose(fd, 0, "OK"); +} + +int +makeIpcServer(const char *bindPath, int backlog) +{ + struct sockaddr_un addr; + int fd; + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + err(1, "failed to create IPC socket"); + + if (remove(bindPath) == -1) { + if (errno != ENOENT) + err(1, "could not remove %s", bindPath); + } + + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + strbcpy(addr.sun_path, bindPath, sizeof(addr.sun_path) - 1); + + if (bind(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) + err(1, "failed to bind to %s", bindPath); + if (listen(fd, backlog) == -1) + err(1, "failed to listen on IPC socket %s", bindPath); + + if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) == -1) + err(1, "failed to create non-blocking IPC socket"); + + return fd; +} + +int +main(int argc, char **argv) +{ + void *propertyMapping[PREZZY_PROPERTY_MAX]; + DiscordRichPresence presence; + DiscordEventHandlers handlers; + const char *clientId; + int ch, sd; + + while ((ch = getopt(argc, argv, "V")) != -1) { + switch (ch) { + case 'V': + printf("prezzyd version 0.1 built on " BUILD_DATE "\n"); + return 0; + break; + } + } + + if (argc - optind != 1) { + fputs("usage: prezzyd [-V] client-id\n", stderr); + return 1; + } + + clientId = argv[optind]; + + memset(&presence, 0, sizeof(DiscordRichPresence)); + memset(&handlers, 0, sizeof(DiscordEventHandlers)); + handlers.ready = connectHandler; + handlers.disconnected = disconnectHandler; + handlers.errored = errorHandler; + + Discord_Initialize(clientId, &handlers, 1, NULL); + + propertyMapping[PREZZY_STATE] = &presence.state; + propertyMapping[PREZZY_DETAILS] = &presence.details; + propertyMapping[PREZZY_LARGE_IMAGE] = &presence.largeImageKey; + propertyMapping[PREZZY_LARGE_TOOLTIP] = &presence.largeImageText; + propertyMapping[PREZZY_ICON] = &presence.smallImageKey; + propertyMapping[PREZZY_ICON_TOOLTIP] = &presence.smallImageText; + propertyMapping[PREZZY_PARTY_SIZE] = &presence.partySize; + propertyMapping[PREZZY_PARTY_LIMIT] = &presence.partyMax; + propertyMapping[PREZZY_TIMESTAMP_START] = &presence.startTimestamp; + propertyMapping[PREZZY_TIMESTAMP_END] = &presence.endTimestamp; + + /* start ipc server */ + sd = makeIpcServer(PREZZY_SOCK, 8); + + for (;;) { + PrezzyIpcPacket **packets; + char buffer[PREZZY_PACKET_MAX]; + ssize_t sz; + int client, i, packetCount; + char ch; + + packets = NULL; + packetCount = 0; + client = accept(sd, NULL, NULL); + + if (client == -1) { + switch (errno) { + case EWOULDBLOCK: + goto update; + default: + err(1, "failed to accept incoming IPC connection"); + break; + } + } + + /* + * IPC error codes: + * 1: error during initial read + * 2: bad request format + */ + + if (!canReadFrom(client)) + ipcClose(client, 1, "Timed out"); + + for (i = 0; (sz = read(client, &ch, 1)) > 0; i++) { + if (i == PREZZY_PACKET_MAX) { + ipcClose(client, 0, "OK (truncated)"); + buffer[i - 1] = '\0'; + break; + } + + switch (ch) { + case '\n': + case '\0': + /* end current packet */ + if (i + 1 < 2 && /* are we before the payload? */ + /* accept null bytes after newlines */ + !(ch == '\0' && i == 0)) { + ipcClose(client, 2, "Request missing payload"); + goto update; + } + if (buffer[0] < 0 || buffer[0] >= PREZZY_PROPERTY_MAX) { + ipcClose(client, 2, "Invalid property byte"); + goto update; + } + buffer[i] = '\0'; + + packetCount++; + packets = realloc(packets, packetCount * + sizeof(PrezzyIpcPacket *)); + packets[packetCount - 1] = makeIpcPacket(buffer); + + if (ch == '\0') { + ipcSucceed(client); + goto process; + } + /* '\n'; more requests later */ + i = -1; + continue; + default: + buffer[i] = ch; + break; + } + } + + if (sz == -1) + err(1, "failed to read from IPC client"); + + process: + for (i = 0; i < packetCount; i++) { + size_t len; + + if (packets[i]->type == PLT_STRING) { + len = strlen(packets[i]->pString) + 1; + *(char **)propertyMapping[packets[i]->property] = + malloc(len * sizeof(char)); + strlcpy(*(char **)propertyMapping[packets[i]->property], + packets[i]->pString, len); + } else { + if (packets[i]->type == PLT_INT64) { + *(int64_t *)propertyMapping[packets[i]->property] = + packets[i]->pInteger; + } else { + *(int *)propertyMapping[packets[i]->property] = + squashLong(packets[i]->pInteger); + } + } + } + + update: + Discord_UpdatePresence(&presence); + Discord_RunCallbacks(); + usleep(64000); + } + + Discord_Shutdown(); + + return 0; +} diff --git a/prezzyipc.c b/prezzyipc.c new file mode 100644 index 0000000..f96951c --- /dev/null +++ b/prezzyipc.c @@ -0,0 +1,132 @@ +/* + * prezzyipc.c + * Copyright (c) 2024 Jeremy Baxter. + */ + +#define _PREZZYIPC_C_ + +#include +#include +#include + +#include "prezzyipc.h" + +void +dumpPacket(PrezzyIpcPacket p) +{ + fprintf(stderr, + "[packet]\n" + "property = %d\n" + "type = %d\n" + "payload = ", + p.property, p.type); + if (p.type == PLT_STRING) + fprintf(stderr, "%s\n", p.pString); + else + fprintf(stderr, "%ld\n", p.pInteger); +} + +char * +fromIpcPacket(PrezzyIpcPacket p) +{ + char *buffer; + + buffer = malloc(PREZZY_PACKET_MAX * sizeof(char)); + + if (p.type == PLT_STRING) { + snprintf(buffer, PREZZY_PACKET_MAX, "%c%s\n", + p.property, p.pString); + } else { + snprintf(buffer, PREZZY_PACKET_MAX, "%c%ld\n", + p.property, p.pInteger); + } + + return buffer; +} + +PrezzyIpcPacket * +makeIpcPacket(const char *buffer) +{ + PrezzyIpcPacket *m; + int i; + + m = malloc(sizeof(PrezzyIpcPacket)); + + /* extract property */ + m->property = buffer[0]; + if (m->property < 0 || m->property >= PREZZY_PROPERTY_MAX) + return NULL; + + /* determine payload type */ + m->type = payloadTypeMapping[m->property]; + + switch (m->type) { + case PLT_STRING: + m->pString = NULL; + for (i = 1; buffer[i] != '\n' && buffer[i] != '\0' + && i < PREZZY_PACKET_MAX; i++) { + m->pString = realloc(m->pString, (i + 1) * sizeof(char)); + m->pString[i - 1] = buffer[i]; + m->pString[i] = '\0'; + } + break; + case PLT_INT64: + m->pInteger = strtol(buffer + 1, NULL, 10); + break; + case PLT_INT: + m->pInteger = squashLong(strtol(buffer + 1, NULL, 10)); + break; + default: break; + } + + return m; +} + +int +squashLong(long l) +{ + return (l >= INT_MIN && l <= INT_MAX) ? l : INT_MAX ; +} + +/* + * Copy string src to buffer dst of size dsize. At most dsize-1 + * chars will be copied. Always NUL terminates (unless dsize == 0). + * Returns strlen(src); if retval >= dsize, truncation occurred. + */ +size_t +strbcpy(char *dst, const char *src, size_t dsize) +{ + const char *osrc = src; + size_t nleft = dsize; + + /* Copy as many bytes as will fit. */ + if (nleft != 0) { + while (--nleft != 0) { + if ((*dst++ = *src++) == '\0') + break; + } + } + + /* Not enough room in dst, add NUL and traverse rest of src. */ + if (nleft == 0) { + if (dsize != 0) + *dst = '\0'; /* NUL-terminate dst */ + while (*src++) + ; + } + + return (src - osrc - 1); /* count does not include NUL */ +} + +char * +stringFrom(const char *reference) +{ + char *buffer; + size_t len; + + len = strlen(reference) + 1; + buffer = malloc(len * sizeof(char)); + strbcpy(buffer, reference, len); + + return buffer; +} diff --git a/prezzyipc.h b/prezzyipc.h new file mode 100644 index 0000000..86f7201 --- /dev/null +++ b/prezzyipc.h @@ -0,0 +1,73 @@ +/* + * prezzyipc.h + * Copyright (c) 2024 Jeremy Baxter. + */ + +#ifndef _PREZZYIPC_H_ +#define _PREZZYIPC_H_ + +#include +#include + +#include +#include +#include + +#define PREZZY_PACKET_MAX 1024 +#define PREZZY_SOCK "/tmp/prezzyd.sock" + +typedef char byte; + +typedef enum { + PREZZY_STATE = 1, + PREZZY_DETAILS = 2, + PREZZY_LARGE_IMAGE = 3, + PREZZY_LARGE_TOOLTIP = 4, + PREZZY_ICON = 5, + PREZZY_ICON_TOOLTIP = 6, + PREZZY_PARTY_SIZE = 7, + PREZZY_PARTY_LIMIT = 8, + PREZZY_TIMESTAMP_START = 9, + PREZZY_TIMESTAMP_END = 10, + PREZZY_PROPERTY_MAX = 11 +} PrezzyProperty; + +typedef enum { + PLT_STRING = 1, + PLT_INT64 = 2, + PLT_INT = 3, + PLT_MAX = 4 +} PrezzyPayloadType; + +#ifdef _PREZZYIPC_C_ +PrezzyPayloadType payloadTypeMapping[PREZZY_PROPERTY_MAX] = { + [PREZZY_STATE] = PLT_STRING, + [PREZZY_DETAILS] = PLT_STRING, + [PREZZY_LARGE_IMAGE] = PLT_STRING, + [PREZZY_LARGE_TOOLTIP] = PLT_STRING, + [PREZZY_ICON] = PLT_STRING, + [PREZZY_ICON_TOOLTIP] = PLT_STRING, + [PREZZY_PARTY_SIZE] = PLT_INT, + [PREZZY_PARTY_LIMIT] = PLT_INT, + [PREZZY_TIMESTAMP_START] = PLT_INT64, + [PREZZY_TIMESTAMP_END] = PLT_INT64 +}; +#else + extern PrezzyPayloadType payloadTypeMapping[PREZZY_PROPERTY_MAX]; +#endif + +typedef struct { + PrezzyProperty property; + PrezzyPayloadType type; + char *pString; + int64_t pInteger; +} PrezzyIpcPacket; + +void dumpPacket(PrezzyIpcPacket); +char *fromIpcPacket(PrezzyIpcPacket); +PrezzyIpcPacket *makeIpcPacket(const char *); +int squashLong(long); +size_t strbcpy(char *, const char *, size_t); +char *stringFrom(const char *); + +#endif