From 56e056eccae004a4b7071065bc2c82688d936ff7 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 7 Dec 2025 15:33:52 -0600 Subject: [PATCH] discord integration example working --- .cell/cell.toml | 6 + discord.cpp | 602 +++++++++++++++++++++++++++++++++++ examples/discord_example.ce | 182 +++++++++++ tests/config.cm | 15 + tests/discord.cm | 96 ++++++ tests/discord_integration.ce | 166 ++++++++++ 6 files changed, 1067 insertions(+) create mode 100644 .cell/cell.toml create mode 100644 discord.cpp create mode 100644 examples/discord_example.ce create mode 100644 tests/config.cm create mode 100644 tests/discord.cm create mode 100644 tests/discord_integration.ce diff --git a/.cell/cell.toml b/.cell/cell.toml new file mode 100644 index 0000000..01fa396 --- /dev/null +++ b/.cell/cell.toml @@ -0,0 +1,6 @@ +[compilation] +LDFLAGS = "-Ldiscord_social_sdk/lib/release" +CFLAGS = "-Idiscord_social_sdk/include" + +[compilation.macOS] +LDFLAGS = "-ldiscord_partner_sdk -Wl,-rpath,@loader_path" \ No newline at end of file diff --git a/discord.cpp b/discord.cpp new file mode 100644 index 0000000..d06f8fc --- /dev/null +++ b/discord.cpp @@ -0,0 +1,602 @@ +// Discord Social SDK bindings for Cell +#include "cell.h" +#include +#include +#include +#include "cdiscord.h" + +// Global state +static Discord_Client *discord_client = NULL; +static JSContext *stored_js_ctx = NULL; +static uint64_t discord_application_id = 0; + +// Stored JS callbacks +static JSValue js_status_cb = JS_UNINITIALIZED; +static JSValue js_log_cb = JS_UNINITIALIZED; +static JSValue js_auth_cb = JS_UNINITIALIZED; +static JSValue js_token_cb = JS_UNINITIALIZED; +static JSValue js_update_token_cb = JS_UNINITIALIZED; +static JSValue js_presence_cb = JS_UNINITIALIZED; + +// Stored code verifier for OAuth flow +static Discord_AuthorizationCodeVerifier *stored_verifier = NULL; + +// Helper: Discord_String to JS string +static JSValue dstr_to_js(JSContext *js, Discord_String *str) { + if (!str || !str->ptr || str->size == 0) return JS_NULL; + return JS_NewStringLen(js, (const char*)str->ptr, str->size); +} + +// Helper: JS string to Discord_String +static Discord_String js_to_dstr(JSContext *js, JSValue val) { + Discord_String str = {0}; + if (JS_IsNull(val)) return str; + size_t len; + const char *cstr = JS_ToCStringLen(js, &len, val); + if (cstr) { + str.ptr = (uint8_t*)Discord_Alloc(len + 1); + if (str.ptr) { + memcpy(str.ptr, cstr, len); + str.ptr[len] = 0; + str.size = len; + } + JS_FreeCString(js, cstr); + } + return str; +} + +// Helper: Free Discord_String +static void free_dstr(Discord_String *str) { + if (str && str->ptr) { Discord_Free(str->ptr); str->ptr = NULL; str->size = 0; } +} + +// Helper: JS value (string or number) to uint64_t +static uint64_t js_to_u64(JSContext *js, JSValueConst v) { + if (JS_IsString(v)) { + uint64_t out = 0; + const char *s = JS_ToCString(js, v); + if (s) { + out = strtoull(s, NULL, 10); + JS_FreeCString(js, s); + } + return out; + } + return (uint64_t)js2number(js, v); +} + +// Helper: free stored JS callback value +static void free_cb(JSContext *js, JSValue *cb) { + if (!JS_IsUninitialized(*cb)) { + JS_FreeValue(js, *cb); + *cb = JS_UNINITIALIZED; + } +} + +// Helper: UserHandle to JS object +static JSValue user_to_js(JSContext *js, Discord_UserHandle *h) { + if (!h) return JS_NULL; + JSValue u = JS_NewObject(js); + { + char buf[32]; + snprintf(buf, sizeof(buf), "%llu", (unsigned long long)Discord_UserHandle_Id(h)); + JS_SetPropertyStr(js, u, "id", JS_NewString(js, buf)); + } + Discord_String s; + Discord_UserHandle_Username(h, &s); + JS_SetPropertyStr(js, u, "username", dstr_to_js(js, &s)); + Discord_UserHandle_DisplayName(h, &s); + JS_SetPropertyStr(js, u, "display_name", dstr_to_js(js, &s)); + if (Discord_UserHandle_GlobalName(h, &s)) + JS_SetPropertyStr(js, u, "global_name", dstr_to_js(js, &s)); + Discord_UserHandle_AvatarUrl(h, Discord_UserHandle_AvatarType_Png, Discord_UserHandle_AvatarType_Png, &s); + JS_SetPropertyStr(js, u, "avatar_url", dstr_to_js(js, &s)); + Discord_StatusType st = Discord_UserHandle_Status(h); + const char *ss = "unknown"; + if (st == Discord_StatusType_Online) ss = "online"; + else if (st == Discord_StatusType_Offline) ss = "offline"; + else if (st == Discord_StatusType_Idle) ss = "idle"; + else if (st == Discord_StatusType_Dnd) ss = "dnd"; + JS_SetPropertyStr(js, u, "status", JS_NewString(js, ss)); + JS_SetPropertyStr(js, u, "is_provisional", JS_NewBool(js, Discord_UserHandle_IsProvisional(h))); + return u; +} + +// Helper: RelationshipHandle to JS object +static JSValue rel_to_js(JSContext *js, Discord_RelationshipHandle *h) { + if (!h) return JS_NULL; + JSValue r = JS_NewObject(js); + { + char buf[32]; + snprintf(buf, sizeof(buf), "%llu", (unsigned long long)Discord_RelationshipHandle_Id(h)); + JS_SetPropertyStr(js, r, "id", JS_NewString(js, buf)); + } + Discord_RelationshipType t = Discord_RelationshipHandle_DiscordRelationshipType(h); + const char *ts = "none"; + if (t == Discord_RelationshipType_Friend) ts = "friend"; + else if (t == Discord_RelationshipType_Blocked) ts = "blocked"; + else if (t == Discord_RelationshipType_PendingIncoming) ts = "pending_incoming"; + else if (t == Discord_RelationshipType_PendingOutgoing) ts = "pending_outgoing"; + JS_SetPropertyStr(js, r, "type", JS_NewString(js, ts)); + Discord_UserHandle uh; + if (Discord_RelationshipHandle_User(h, &uh)) + JS_SetPropertyStr(js, r, "user", user_to_js(js, &uh)); + return r; +} + +// Callback: Status changed +static void cb_status(Discord_Client_Status status, Discord_Client_Error error, int32_t detail, void* ud) { + if (!stored_js_ctx || JS_IsUninitialized(js_status_cb)) return; + JSContext *js = stored_js_ctx; + const char *ss = "unknown"; + if (status == Discord_Client_Status_Disconnected) ss = "disconnected"; + else if (status == Discord_Client_Status_Connecting) ss = "connecting"; + else if (status == Discord_Client_Status_Connected) ss = "connected"; + else if (status == Discord_Client_Status_Ready) ss = "ready"; + else if (status == Discord_Client_Status_Reconnecting) ss = "reconnecting"; + const char *es = "none"; + if (error == Discord_Client_Error_ConnectionFailed) es = "connection_failed"; + else if (error == Discord_Client_Error_UnexpectedClose) es = "unexpected_close"; + JSValue args[3] = { JS_NewString(js, ss), JS_NewString(js, es), JS_NewInt32(js, detail) }; + JSValue res = JS_Call(js, js_status_cb, JS_NULL, 3, args); + JS_FreeValue(js, args[0]); JS_FreeValue(js, args[1]); JS_FreeValue(js, args[2]); JS_FreeValue(js, res); +} + +// Callback: Log +static void cb_log(Discord_String msg, Discord_LoggingSeverity sev, void* ud) { + if (!stored_js_ctx || JS_IsUninitialized(js_log_cb)) return; + JSContext *js = stored_js_ctx; + const char *ss = "info"; + if (sev == Discord_LoggingSeverity_Verbose) ss = "verbose"; + else if (sev == Discord_LoggingSeverity_Warning) ss = "warning"; + else if (sev == Discord_LoggingSeverity_Error) ss = "error"; + JSValue args[2] = { dstr_to_js(js, &msg), JS_NewString(js, ss) }; + JSValue res = JS_Call(js, js_log_cb, JS_NULL, 2, args); + JS_FreeValue(js, args[0]); JS_FreeValue(js, args[1]); JS_FreeValue(js, res); +} + +// Callback: Authorization +static void cb_auth(Discord_ClientResult* result, Discord_String code, Discord_String uri, void* ud) { + if (!stored_js_ctx || JS_IsUninitialized(js_auth_cb)) return; + JSContext *js = stored_js_ctx; + bool ok = Discord_ClientResult_Successful(result); + JSValue args[4]; + args[0] = JS_NewBool(js, ok); + if (ok) { + args[1] = dstr_to_js(js, &code); + args[2] = dstr_to_js(js, &uri); + args[3] = JS_NULL; + } else { + args[1] = JS_NULL; args[2] = JS_NULL; + Discord_String e; Discord_ClientResult_Error(result, &e); + args[3] = dstr_to_js(js, &e); + } + JSValue res = JS_Call(js, js_auth_cb, JS_NULL, 4, args); + for (int i = 0; i < 4; i++) JS_FreeValue(js, args[i]); + JS_FreeValue(js, res); +} + +// Callback: Token exchange +static void cb_token(Discord_ClientResult* result, Discord_String access, Discord_String refresh, + Discord_AuthorizationTokenType type, int32_t expires, Discord_String scope, void* ud) { + if (!stored_js_ctx || JS_IsUninitialized(js_token_cb)) return; + JSContext *js = stored_js_ctx; + bool ok = Discord_ClientResult_Successful(result); + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "success", JS_NewBool(js, ok)); + if (ok) { + JS_SetPropertyStr(js, obj, "access_token", dstr_to_js(js, &access)); + JS_SetPropertyStr(js, obj, "refresh_token", dstr_to_js(js, &refresh)); + JS_SetPropertyStr(js, obj, "token_type", JS_NewString(js, type == Discord_AuthorizationTokenType_Bearer ? "bearer" : "user")); + JS_SetPropertyStr(js, obj, "expires_in", JS_NewInt32(js, expires)); + JS_SetPropertyStr(js, obj, "scope", dstr_to_js(js, &scope)); + } else { + Discord_String e; Discord_ClientResult_Error(result, &e); + JS_SetPropertyStr(js, obj, "error", dstr_to_js(js, &e)); + } + JSValue args[1] = { obj }; + JSValue res = JS_Call(js, js_token_cb, JS_NULL, 1, args); + JS_FreeValue(js, obj); JS_FreeValue(js, res); +} + +// Callback: Update token +static void cb_update_token(Discord_ClientResult* result, void* ud) { + if (!stored_js_ctx || JS_IsUninitialized(js_update_token_cb)) return; + JSContext *js = stored_js_ctx; + bool ok = Discord_ClientResult_Successful(result); + JSValue args[2]; + args[0] = JS_NewBool(js, ok); + if (!ok) { Discord_String e; Discord_ClientResult_Error(result, &e); args[1] = dstr_to_js(js, &e); } + else args[1] = JS_NULL; + JSValue res = JS_Call(js, js_update_token_cb, JS_NULL, 2, args); + JS_FreeValue(js, args[0]); JS_FreeValue(js, args[1]); JS_FreeValue(js, res); +} + +// Callback: Rich presence +static void cb_presence(Discord_ClientResult* result, void* ud) { + if (!stored_js_ctx || JS_IsUninitialized(js_presence_cb)) return; + JSContext *js = stored_js_ctx; + bool ok = Discord_ClientResult_Successful(result); + JSValue args[2]; + args[0] = JS_NewBool(js, ok); + if (!ok) { Discord_String e; Discord_ClientResult_Error(result, &e); args[1] = dstr_to_js(js, &e); } + else args[1] = JS_NULL; + JSValue res = JS_Call(js, js_presence_cb, JS_NULL, 2, args); + JS_FreeValue(js, args[0]); JS_FreeValue(js, args[1]); JS_FreeValue(js, res); +} + +extern "C" { + +// ============================================================================ +// CORE API +// ============================================================================ + +// discord.init(application_id) - Initialize the Discord client +JSC_CCALL(discord_init, + if (discord_client) return JS_ThrowInternalError(js, "Discord client already initialized"); + uint64_t app_id = 0; + if (argc > 0) app_id = js_to_u64(js, argv[0]); + if (app_id == 0) return JS_ThrowTypeError(js, "Application ID is required"); + discord_application_id = app_id; + stored_js_ctx = js; + discord_client = (Discord_Client*)Discord_Alloc(sizeof(Discord_Client)); + if (!discord_client) return JS_ThrowOutOfMemory(js); + Discord_Client_Init(discord_client); + Discord_Client_SetApplicationId(discord_client, app_id); + return JS_TRUE; +) + +// discord.run_callbacks() - Process Discord callbacks +JSC_CCALL(discord_run_callbacks, + Discord_RunCallbacks(); + return JS_NULL; +) + +// discord.shutdown() - Disconnect and clean up +JSC_CCALL(discord_shutdown, + if (discord_client) { + Discord_Client_Disconnect(discord_client); + Discord_Client_Drop(discord_client); + discord_client = NULL; + } + if (stored_verifier) { + Discord_AuthorizationCodeVerifier_Drop(stored_verifier); + Discord_Free(stored_verifier); + stored_verifier = NULL; + } + free_cb(js, &js_status_cb); + free_cb(js, &js_log_cb); + free_cb(js, &js_auth_cb); + free_cb(js, &js_token_cb); + free_cb(js, &js_update_token_cb); + free_cb(js, &js_presence_cb); + stored_js_ctx = NULL; + discord_application_id = 0; + return JS_NULL; +) + +// discord.get_status() - Get current connection status +JSC_CCALL(discord_get_status, + if (!discord_client) return JS_NewString(js, "not_initialized"); + Discord_Client_Status st = Discord_Client_GetStatus(discord_client); + const char *s = "unknown"; + if (st == Discord_Client_Status_Disconnected) s = "disconnected"; + else if (st == Discord_Client_Status_Connecting) s = "connecting"; + else if (st == Discord_Client_Status_Connected) s = "connected"; + else if (st == Discord_Client_Status_Ready) s = "ready"; + else if (st == Discord_Client_Status_Reconnecting) s = "reconnecting"; + return JS_NewString(js, s); +) + +// discord.is_authenticated() +JSC_CCALL(discord_is_authenticated, + if (!discord_client) return JS_FALSE; + return JS_NewBool(js, Discord_Client_IsAuthenticated(discord_client)); +) + +// discord.connect() +JSC_CCALL(discord_connect, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_Client_Connect(discord_client); + return JS_NULL; +) + +// discord.disconnect() +JSC_CCALL(discord_disconnect, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_Client_Disconnect(discord_client); + return JS_NULL; +) + +// ============================================================================ +// CALLBACK REGISTRATION +// ============================================================================ + +// discord.on_status_changed(callback) +JSC_CCALL(discord_on_status_changed, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + if (!JS_IsUninitialized(js_status_cb)) JS_FreeValue(js, js_status_cb); + if (JS_IsFunction(js, argv[0])) { + js_status_cb = JS_DupValue(js, argv[0]); + Discord_Client_SetStatusChangedCallback(discord_client, cb_status, NULL, NULL); + } else { + js_status_cb = JS_UNINITIALIZED; + Discord_Client_SetStatusChangedCallback(discord_client, NULL, NULL, NULL); + } + return JS_NULL; +) + +// discord.on_log(callback, min_severity) +JSC_CCALL(discord_on_log, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + if (!JS_IsUninitialized(js_log_cb)) JS_FreeValue(js, js_log_cb); + if (JS_IsFunction(js, argv[0])) { + js_log_cb = JS_DupValue(js, argv[0]); + Discord_LoggingSeverity sev = Discord_LoggingSeverity_Info; + if (argc > 1 && JS_IsString(argv[1])) { + const char *s = JS_ToCString(js, argv[1]); + if (strcmp(s, "verbose") == 0) sev = Discord_LoggingSeverity_Verbose; + else if (strcmp(s, "warning") == 0) sev = Discord_LoggingSeverity_Warning; + else if (strcmp(s, "error") == 0) sev = Discord_LoggingSeverity_Error; + JS_FreeCString(js, s); + } + Discord_Client_AddLogCallback(discord_client, cb_log, NULL, NULL, sev); + } else js_log_cb = JS_UNINITIALIZED; + return JS_NULL; +) + +// ============================================================================ +// AUTHENTICATION API +// ============================================================================ + +// discord.get_default_scopes() +JSC_CCALL(discord_get_default_scopes, + Discord_String scopes; + Discord_Client_GetDefaultPresenceScopes(&scopes); + return dstr_to_js(js, &scopes); +) + +// discord.authorize(callback) +JSC_CCALL(discord_authorize, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + if (!JS_IsUninitialized(js_auth_cb)) JS_FreeValue(js, js_auth_cb); + js_auth_cb = JS_DupValue(js, argv[0]); + if (stored_verifier) { Discord_AuthorizationCodeVerifier_Drop(stored_verifier); Discord_Free(stored_verifier); } + stored_verifier = (Discord_AuthorizationCodeVerifier*)Discord_Alloc(sizeof(Discord_AuthorizationCodeVerifier)); + Discord_Client_CreateAuthorizationCodeVerifier(discord_client, stored_verifier); + Discord_AuthorizationCodeChallenge challenge; + Discord_AuthorizationCodeVerifier_Challenge(stored_verifier, &challenge); + Discord_AuthorizationArgs args; + Discord_AuthorizationArgs_Init(&args); + Discord_AuthorizationArgs_SetClientId(&args, discord_application_id); + Discord_String scopes; + Discord_Client_GetDefaultPresenceScopes(&scopes); + Discord_AuthorizationArgs_SetScopes(&args, scopes); + Discord_AuthorizationArgs_SetCodeChallenge(&args, &challenge); + Discord_Client_Authorize(discord_client, &args, cb_auth, NULL, NULL); + Discord_AuthorizationArgs_Drop(&args); + return JS_NULL; +) + +// discord.get_token(code, redirect_uri, callback) +JSC_CCALL(discord_get_token, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + if (!stored_verifier) return JS_ThrowInternalError(js, "No code verifier - call authorize first"); + Discord_String code = js_to_dstr(js, argv[0]); + Discord_String uri = js_to_dstr(js, argv[1]); + if (!JS_IsUninitialized(js_token_cb)) JS_FreeValue(js, js_token_cb); + js_token_cb = JS_DupValue(js, argv[2]); + Discord_String verifier; + Discord_AuthorizationCodeVerifier_Verifier(stored_verifier, &verifier); + Discord_Client_GetToken(discord_client, discord_application_id, code, verifier, uri, cb_token, NULL, NULL); + free_dstr(&code); free_dstr(&uri); + return JS_NULL; +) + +// discord.update_token(token_type, token, callback) +JSC_CCALL(discord_update_token, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + const char *ts = JS_ToCString(js, argv[0]); + Discord_AuthorizationTokenType tt = Discord_AuthorizationTokenType_Bearer; + if (ts && strcmp(ts, "user") == 0) tt = Discord_AuthorizationTokenType_User; + JS_FreeCString(js, ts); + Discord_String token = js_to_dstr(js, argv[1]); + if (!JS_IsUninitialized(js_update_token_cb)) JS_FreeValue(js, js_update_token_cb); + js_update_token_cb = JS_DupValue(js, argv[2]); + Discord_Client_UpdateToken(discord_client, tt, token, cb_update_token, NULL, NULL); + free_dstr(&token); + return JS_NULL; +) + +// discord.refresh_token(refresh_token, callback) +JSC_CCALL(discord_refresh_token, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_String rt = js_to_dstr(js, argv[0]); + if (!JS_IsUninitialized(js_token_cb)) JS_FreeValue(js, js_token_cb); + js_token_cb = JS_DupValue(js, argv[1]); + Discord_Client_RefreshToken(discord_client, discord_application_id, rt, cb_token, NULL, NULL); + free_dstr(&rt); + return JS_NULL; +) + +// ============================================================================ +// USER API +// ============================================================================ + +// discord.get_current_user() +JSC_CCALL(discord_get_current_user, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_UserHandle user; + if (!Discord_Client_GetCurrentUserV2(discord_client, &user)) return JS_NULL; + return user_to_js(js, &user); +) + +// discord.get_user(user_id) +JSC_CCALL(discord_get_user, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + uint64_t uid = 0; + uid = js_to_u64(js, argv[0]); + Discord_UserHandle user; + if (!Discord_Client_GetUser(discord_client, uid, &user)) return JS_NULL; + return user_to_js(js, &user); +) + +// ============================================================================ +// RELATIONSHIPS API +// ============================================================================ + +// discord.get_relationships() +JSC_CCALL(discord_get_relationships, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_RelationshipHandleSpan rels; + Discord_Client_GetRelationships(discord_client, &rels); + JSValue arr = JS_NewArray(js); + for (size_t i = 0; i < rels.size; i++) + JS_SetPropertyUint32(js, arr, i, rel_to_js(js, &rels.ptr[i])); + return arr; +) + +// discord.get_friends_count() +JSC_CCALL(discord_get_friends_count, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_RelationshipHandleSpan rels; + Discord_Client_GetRelationships(discord_client, &rels); + return JS_NewUint32(js, (uint32_t)rels.size); +) + +// ============================================================================ +// RICH PRESENCE API +// ============================================================================ + +// discord.update_rich_presence(activity, callback) +JSC_CCALL(discord_update_rich_presence, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_Activity activity; + Discord_Activity_Init(&activity); + JSValue obj = argv[0]; + // Type + JSValue tv = JS_GetPropertyStr(js, obj, "type"); + if (!JS_IsNull(tv)) { + const char *ts = JS_ToCString(js, tv); + Discord_ActivityTypes t = Discord_ActivityTypes_Playing; + if (ts) { + if (strcmp(ts, "streaming") == 0) t = Discord_ActivityTypes_Streaming; + else if (strcmp(ts, "listening") == 0) t = Discord_ActivityTypes_Listening; + else if (strcmp(ts, "watching") == 0) t = Discord_ActivityTypes_Watching; + else if (strcmp(ts, "competing") == 0) t = Discord_ActivityTypes_Competing; + JS_FreeCString(js, ts); + } + Discord_Activity_SetType(&activity, t); + } + JS_FreeValue(js, tv); + // State + JSValue sv = JS_GetPropertyStr(js, obj, "state"); + if (JS_IsString(sv)) { Discord_String s = js_to_dstr(js, sv); Discord_Activity_SetState(&activity, &s); free_dstr(&s); } + JS_FreeValue(js, sv); + // Details + JSValue dv = JS_GetPropertyStr(js, obj, "details"); + if (JS_IsString(dv)) { Discord_String s = js_to_dstr(js, dv); Discord_Activity_SetDetails(&activity, &s); free_dstr(&s); } + JS_FreeValue(js, dv); + // Assets + JSValue li = JS_GetPropertyStr(js, obj, "large_image"); + JSValue lt = JS_GetPropertyStr(js, obj, "large_text"); + JSValue si = JS_GetPropertyStr(js, obj, "small_image"); + JSValue st = JS_GetPropertyStr(js, obj, "small_text"); + if (JS_IsString(li) || JS_IsString(si)) { + Discord_ActivityAssets assets; + Discord_ActivityAssets_Init(&assets); + if (JS_IsString(li)) { Discord_String s = js_to_dstr(js, li); Discord_ActivityAssets_SetLargeImage(&assets, &s); free_dstr(&s); } + if (JS_IsString(lt)) { Discord_String s = js_to_dstr(js, lt); Discord_ActivityAssets_SetLargeText(&assets, &s); free_dstr(&s); } + if (JS_IsString(si)) { Discord_String s = js_to_dstr(js, si); Discord_ActivityAssets_SetSmallImage(&assets, &s); free_dstr(&s); } + if (JS_IsString(st)) { Discord_String s = js_to_dstr(js, st); Discord_ActivityAssets_SetSmallText(&assets, &s); free_dstr(&s); } + Discord_Activity_SetAssets(&activity, &assets); + Discord_ActivityAssets_Drop(&assets); + } + JS_FreeValue(js, li); JS_FreeValue(js, lt); JS_FreeValue(js, si); JS_FreeValue(js, st); + // Timestamps + JSValue tsv = JS_GetPropertyStr(js, obj, "start_timestamp"); + JSValue tev = JS_GetPropertyStr(js, obj, "end_timestamp"); + if (!JS_IsNull(tsv) || !JS_IsNull(tev)) { + Discord_ActivityTimestamps ts; + Discord_ActivityTimestamps_Init(&ts); + if (!JS_IsNull(tsv)) Discord_ActivityTimestamps_SetStart(&ts, (uint64_t)js2number(js, tsv)); + if (!JS_IsNull(tev)) Discord_ActivityTimestamps_SetEnd(&ts, (uint64_t)js2number(js, tev)); + Discord_Activity_SetTimestamps(&activity, &ts); + Discord_ActivityTimestamps_Drop(&ts); + } + JS_FreeValue(js, tsv); JS_FreeValue(js, tev); + // Party + JSValue piv = JS_GetPropertyStr(js, obj, "party_id"); + if (JS_IsString(piv)) { + Discord_ActivityParty party; + Discord_ActivityParty_Init(&party); + Discord_String s = js_to_dstr(js, piv); + Discord_ActivityParty_SetId(&party, s); + free_dstr(&s); + JSValue psv = JS_GetPropertyStr(js, obj, "party_size"); + JSValue pmv = JS_GetPropertyStr(js, obj, "party_max"); + if (!JS_IsNull(psv)) Discord_ActivityParty_SetCurrentSize(&party, (int32_t)js2number(js, psv)); + if (!JS_IsNull(pmv)) Discord_ActivityParty_SetMaxSize(&party, (int32_t)js2number(js, pmv)); + JS_FreeValue(js, psv); JS_FreeValue(js, pmv); + Discord_Activity_SetParty(&activity, &party); + Discord_ActivityParty_Drop(&party); + } + JS_FreeValue(js, piv); + // Secrets + JSValue jsv = JS_GetPropertyStr(js, obj, "join_secret"); + if (JS_IsString(jsv)) { + Discord_ActivitySecrets secrets; + Discord_ActivitySecrets_Init(&secrets); + Discord_String s = js_to_dstr(js, jsv); + Discord_ActivitySecrets_SetJoin(&secrets, s); + free_dstr(&s); + Discord_Activity_SetSecrets(&activity, &secrets); + Discord_ActivitySecrets_Drop(&secrets); + } + JS_FreeValue(js, jsv); + // Callback + if (argc > 1 && JS_IsFunction(js, argv[1])) { + if (!JS_IsUninitialized(js_presence_cb)) JS_FreeValue(js, js_presence_cb); + js_presence_cb = JS_DupValue(js, argv[1]); + } + Discord_Client_UpdateRichPresence(discord_client, &activity, cb_presence, NULL, NULL); + Discord_Activity_Drop(&activity); + return JS_NULL; +) + +// discord.clear_rich_presence() +JSC_CCALL(discord_clear_rich_presence, + if (!discord_client) return JS_ThrowInternalError(js, "Discord client not initialized"); + Discord_Client_ClearRichPresence(discord_client); + return JS_NULL; +) + +// ============================================================================ +// FUNCTION LIST AND MODULE EXPORT +// ============================================================================ + +static const JSCFunctionListEntry js_discord_funcs[] = { + MIST_FUNC_DEF(discord, init, 1), + MIST_FUNC_DEF(discord, run_callbacks, 0), + MIST_FUNC_DEF(discord, shutdown, 0), + MIST_FUNC_DEF(discord, get_status, 0), + MIST_FUNC_DEF(discord, is_authenticated, 0), + MIST_FUNC_DEF(discord, connect, 0), + MIST_FUNC_DEF(discord, disconnect, 0), + MIST_FUNC_DEF(discord, on_status_changed, 1), + MIST_FUNC_DEF(discord, on_log, 2), + MIST_FUNC_DEF(discord, get_default_scopes, 0), + MIST_FUNC_DEF(discord, authorize, 1), + MIST_FUNC_DEF(discord, get_token, 3), + MIST_FUNC_DEF(discord, update_token, 3), + MIST_FUNC_DEF(discord, refresh_token, 2), + MIST_FUNC_DEF(discord, get_current_user, 0), + MIST_FUNC_DEF(discord, get_user, 1), + MIST_FUNC_DEF(discord, get_relationships, 0), + MIST_FUNC_DEF(discord, get_friends_count, 0), + MIST_FUNC_DEF(discord, update_rich_presence, 2), + MIST_FUNC_DEF(discord, clear_rich_presence, 0), +}; + +CELL_USE_FUNCS(js_discord_funcs) + +} // extern "C" \ No newline at end of file diff --git a/examples/discord_example.ce b/examples/discord_example.ce new file mode 100644 index 0000000..3977d30 --- /dev/null +++ b/examples/discord_example.ce @@ -0,0 +1,182 @@ +// Discord Social SDK Example +// Demonstrates: Authentication, Status Monitoring, Friends List, Rich Presence +// +// Configuration is loaded from config.cm +// Get your own app at: https://discord.com/developers/applications + +var discord = use("discord") +var config = use("tests/config") +var time = use('time') + +// ============================================================================ +// CONFIGURATION - Loaded from config.cm (or override here) +// ============================================================================ +def APPLICATION_ID = config.APPLICATION_ID +def CALLBACK_INTERVAL = config.CALLBACK_INTERVAL + +// ============================================================================ +// STATE +// ============================================================================ +var authenticated = false +var ready = false +var access_token = null +var refresh_token = null + +// ============================================================================ +// DISCORD CALLBACKS +// ============================================================================ + +function on_status_changed(status, error, detail) { + log.console(`Discord status: ${status}`) + + if (status == "ready") { + ready = true + log.console("Discord client is ready!") + on_ready() + } else if (error != "none") { + log.console(`Discord error: ${error} (detail: ${detail})`) + } +} + +function on_log(message, severity) { + log.console(`[Discord ${severity}] ${message}`) +} + +function on_ready() { + // Get and display current user info + var user = discord.get_current_user() + if (user) { + log.console(`Logged in as: ${user.display_name} (@${user.username})`) + log.console(`User ID: ${user.id}`) + log.console(`Status: ${user.status}`) + if (user.avatar_url) log.console(`Avatar: ${user.avatar_url}`) + } + + // Get friends count + var friends_count = discord.get_friends_count() + log.console(`Friends count: ${friends_count}`) + + // Get full relationships list + var relationships = discord.get_relationships() + log.console(`Total relationships: ${relationships.length}`) + for (var i = 0; i < relationships.length && i < 5; i++) { + var rel = relationships[i] + if (rel.user) { + log.console(` - ${rel.user.display_name} (${rel.type})`) + } + } + if (relationships.length > 5) { + log.console(` ... and ${relationships.length - 5} more`) + } + + // Set rich presence + set_rich_presence() +} + +function set_rich_presence() { + log.console("Setting rich presence...") + + discord.update_rich_presence({ + type: "playing", + state: "In Main Menu", + details: "Exploring the game", + start_timestamp: time.number() + }, function(success, error) { + if (success) { + log.console("Rich presence updated successfully!") + } else { + log.console(`Rich presence update failed: ${error}`) + } + }) +} + +// ============================================================================ +// AUTHENTICATION FLOW +// ============================================================================ + +function start_auth() { + log.console("Starting Discord authorization...") + log.console("A browser window should open for Discord login.") + + discord.authorize(function(success, code, redirect_uri, error) { + if (success) { + log.console("Authorization successful! Exchanging code for token...") + exchange_token(code, redirect_uri) + } else { + log.console(`Authorization failed: ${error}`) + } + }) +} + +function exchange_token(code, redirect_uri) { + discord.get_token(code, redirect_uri, function(result) { + if (result.success) { + log.console("Token received!") + access_token = result.access_token + refresh_token = result.refresh_token + log.console(`Token expires in: ${result.expires_in} seconds`) + + // Update the client with the token and connect + update_and_connect(result.access_token) + } else { + log.console(`Token exchange failed: ${result.error}`) + } + }) +} + +function update_and_connect(token) { + log.console("Updating token and connecting...") + + discord.update_token("bearer", token, function(success, error) { + if (success) { + log.console("Token updated, connecting to Discord...") + authenticated = true + discord.connect() + } else { + log.console(`Token update failed: ${error}`) + } + }) +} + +// ============================================================================ +// MAIN LOOP - Process Discord callbacks +// ============================================================================ + +function discord_tick() { + discord.run_callbacks() + $_.delay(discord_tick, CALLBACK_INTERVAL) +} + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +function main() { + log.console("=== Discord Social SDK Example ===") + log.console(`Application ID: ${APPLICATION_ID}`) + + // Initialize Discord client + var init_result = discord.init(APPLICATION_ID) + if (!init_result) { + log.console("Failed to initialize Discord client") + $_.stop() + return + } + log.console("Discord client initialized") + + // Set up callbacks + discord.on_status_changed(on_status_changed) + discord.on_log(on_log, "info") + + // Start the callback processing loop + discord_tick() + + // Start authentication + start_auth() + + log.console("Waiting for Discord events...") + log.console("Press Ctrl+C to exit") +} + +// Run main +main() diff --git a/tests/config.cm b/tests/config.cm new file mode 100644 index 0000000..da99dde --- /dev/null +++ b/tests/config.cm @@ -0,0 +1,15 @@ +// Discord Application Configuration +// Configure these values for your Discord application +// Get yours at: https://discord.com/developers/applications + +return { + // Tangle Tart application + APPLICATION_ID: "1446585686789586975", + PUBLIC_KEY: "18ce78765f6d57a6d25af9b088271c96f8d7357723d246080ae58e49c71e79bc", + + // OAuth2 redirect URI (configured in Discord Developer Portal) + REDIRECT_URI: "http://127.0.0.1/callback", + + // Callback processing interval (ms) - ~60fps + CALLBACK_INTERVAL: 16 +} diff --git a/tests/discord.cm b/tests/discord.cm new file mode 100644 index 0000000..5b9ec3c --- /dev/null +++ b/tests/discord.cm @@ -0,0 +1,96 @@ +// Discord SDK synchronous tests +// These tests check the basic API without requiring authentication +// For full integration tests with callbacks, see discord_integration.ce + +var discord = use('discord') + +// Test application ID (Tangle Tart) +def TEST_APP_ID = "1446585686789586975" + +return { + // Test that the module loads correctly + test_module_loads: function() { + if (typeof discord != 'object') throw "Discord module should be an object" + if (typeof discord.init != 'function') throw "discord.init should be a function" + if (typeof discord.run_callbacks != 'function') throw "discord.run_callbacks should be a function" + if (typeof discord.shutdown != 'function') throw "discord.shutdown should be a function" + }, + + // Test initialization + test_init: function() { + var result = discord.init(TEST_APP_ID) + if (!result) throw "discord.init should return true on success" + }, + + // Test status before connection + test_status_before_connect: function() { + var status = discord.get_status() + // Should be disconnected since we haven't connected yet + if (status != "disconnected" && status != "not_initialized") { + throw `Expected disconnected status, got: ${status}` + } + }, + + // Test is_authenticated before auth + test_not_authenticated: function() { + var auth = discord.is_authenticated() + if (auth) throw "Should not be authenticated before auth flow" + }, + + // Test get_default_scopes + test_get_default_scopes: function() { + var scopes = discord.get_default_scopes() + if (!scopes || scopes.length == 0) throw "Default scopes should not be empty" + log.console(`Default scopes: ${scopes}`) + }, + + // Test get_current_user before auth (should return null) + test_get_user_before_auth: function() { + var user = discord.get_current_user() + // Should be null since we're not authenticated + if (user != null) { + log.console("Note: User returned before auth - may be cached from previous session") + } + }, + + // Test get_relationships before auth + test_get_relationships_before_auth: function() { + var rels = discord.get_relationships() + if (!Array.isArray(rels)) throw "get_relationships should return an array" + log.console(`Relationships count: ${rels.length}`) + }, + + // Test callback registration + test_callback_registration: function() { + // These should not throw + discord.on_status_changed(function(status, error, detail) { + log.console(`Status: ${status}`) + }) + discord.on_log(function(msg, severity) { + log.console(`[${severity}] ${msg}`) + }, "info") + + // Clear callbacks + discord.on_status_changed(null) + discord.on_log(null) + }, + + // Test run_callbacks (should not throw even without connection) + test_run_callbacks: function() { + discord.run_callbacks() + }, + + // Test clear_rich_presence (should not throw) + test_clear_rich_presence: function() { + discord.clear_rich_presence() + }, + + // Test shutdown + test_shutdown: function() { + discord.shutdown() + var status = discord.get_status() + if (status != "not_initialized") { + throw `Expected not_initialized after shutdown, got: ${status}` + } + } +} diff --git a/tests/discord_integration.ce b/tests/discord_integration.ce new file mode 100644 index 0000000..79b604b --- /dev/null +++ b/tests/discord_integration.ce @@ -0,0 +1,166 @@ +// Discord SDK Integration Test (Actor-based) +// This test runs the full Discord integration flow with callbacks +// It sends results back to the test runner via $_.parent +// +// To run standalone: cell discord_integration +// As part of test suite: cell test + +var discord = use('discord') +var time = use('time') + +// Test application ID (Tangle Tart) +def TEST_APP_ID = "1446585686789586975" +def CALLBACK_INTERVAL = 50 +def TEST_TIMEOUT = 10000 // 10 seconds + +var test_start = time.number() +var test_passed = false +var test_error = null +var status_received = false +var connected = false + +// Report test result to parent (test runner) +function report_result(passed, error) { + if ($_.parent) { + $_.send($_.parent, { + type: "test_result", + passed: passed, + error: error + }) + } else { + // Running standalone + if (passed) { + log.console("TEST PASSED") + } else { + log.console(`TEST FAILED: ${error}`) + } + } + $_.stop() +} + +// Status change handler +function on_status(status, error, detail) { + log.console(`[Test] Status changed: ${status} (error: ${error}, detail: ${detail})`) + status_received = true + + if (status == "ready") { + connected = true + // Run post-connection tests + run_connected_tests() + } else if (error != "none") { + // Connection error - this is expected if Discord isn't running + log.console(`[Test] Connection error (expected if Discord app not running): ${error}`) + // Still pass the test - we verified the callback system works + report_result(true, null) + } +} + +// Log handler +function on_log(msg, severity) { + log.console(`[Discord ${severity}] ${msg}`) +} + +// Tests that run after connection +function run_connected_tests() { + log.console("[Test] Running connected tests...") + + try { + // Test get_current_user + var user = discord.get_current_user() + if (user) { + log.console(`[Test] Current user: ${user.display_name} (@${user.username})`) + log.console(`[Test] User ID: ${user.id}`) + } else { + log.console("[Test] No user (not authenticated)") + } + + // Test get_relationships + var rels = discord.get_relationships() + log.console(`[Test] Relationships: ${rels.length}`) + + // Test get_friends_count + var count = discord.get_friends_count() + log.console(`[Test] Friends count: ${count}`) + + // Test update_rich_presence + discord.update_rich_presence({ + type: "playing", + state: "Running Tests", + details: "Discord SDK Integration Test" + }, function(success, error) { + if (success) { + log.console("[Test] Rich presence updated") + } else { + log.console(`[Test] Rich presence failed: ${error}`) + } + }) + + // All tests passed + test_passed = true + + // Give time for presence update callback + $_.delay(function() { + discord.clear_rich_presence() + discord.shutdown() + report_result(true, null) + }, 1000) + + } catch (e) { + report_result(false, e.toString()) + } +} + +// Callback tick +function tick() { + discord.run_callbacks() + + // Check timeout + var elapsed = time.number() - test_start + if (elapsed > TEST_TIMEOUT) { + if (!status_received) { + // No status callback received - SDK might not be working + report_result(false, "Timeout: No status callback received") + } else if (!connected) { + // Status received but not connected - Discord app probably not running + // This is acceptable for CI environments + log.console("[Test] Timeout waiting for connection (Discord app may not be running)") + discord.shutdown() + report_result(true, null) // Pass - callbacks work, just no Discord app + } + return + } + + $_.delay(tick, CALLBACK_INTERVAL) +} + +// Main test +function main() { + log.console("=== Discord SDK Integration Test ===") + log.console(`Application ID: ${TEST_APP_ID}`) + + try { + // Initialize + var init_result = discord.init(TEST_APP_ID) + if (!init_result) { + report_result(false, "discord.init failed") + return + } + log.console("[Test] Discord initialized") + + // Set up callbacks + discord.on_status_changed(on_status) + discord.on_log(on_log, "info") + + // Start callback loop + tick() + + // Try to connect (will trigger status callbacks) + discord.connect() + log.console("[Test] Connection initiated, waiting for callbacks...") + + } catch (e) { + report_result(false, e.toString()) + } +} + +main()