/* RetroArch - A frontend for libretro. * Copyright (C) 2018-2019 - Andrés Suárez * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include #include #include #include #include #include #include #include "discord.h" #include "discord_register.h" #include "../deps/discord-rpc/include/discord_rpc.h" #include "../retroarch.h" #include "../core.h" #include "../core_info.h" #include "../paths.h" #include "../playlist.h" #include "../verbosity.h" #include "../msg_hash.h" #include "../tasks/task_file_transfer.h" #ifdef HAVE_NETWORKING #include "../../network/netplay/netplay.h" #include "../../network/netplay/netplay_discovery.h" #include "../../tasks/tasks_internal.h" #endif #ifdef HAVE_CHEEVOS #include "../cheevos-new/cheevos.h" #endif #ifdef HAVE_MENU #include "../../menu/menu_cbs.h" #endif #include "../network/net_http_special.h" #include "../tasks/tasks_internal.h" #include "../file_path_special.h" /* The discord API specifies these variables: - userId --------- char[24] - the userId of the player asking to join - username ------- char[344] - the username of the player asking to join - discriminator -- char[8] - the discriminator of the player asking to join - spectateSecret - char[128] - secret used for spectatin matches - joinSecret - char[128] - secret used to join matches - partyId - char[128] - the party you would be joining */ struct discord_state { bool ready; bool avatar_ready; bool connecting; unsigned status; int64_t start_time; int64_t pause_time; int64_t elapsed_time; char user_name[344]; char self_party_id[128]; char peer_party_id[128]; char user_avatar[PATH_MAX_LENGTH]; DiscordRichPresence presence; }; typedef struct discord_state discord_state_t; static discord_state_t discord_st; #define CDN_URL "https://cdn.discordapp.com/avatars" static discord_state_t *discord_get_ptr(void) { return &discord_st; } bool discord_is_ready(void) { discord_state_t *discord_st = discord_get_ptr(); return discord_st->ready; } char* discord_get_own_username(void) { discord_state_t *discord_st = discord_get_ptr(); if (discord_st->ready) return discord_st->user_name; return NULL; } char *discord_get_own_avatar(void) { discord_state_t *discord_st = discord_get_ptr(); if (discord_st->ready) return discord_st->user_avatar; return NULL; } bool discord_avatar_is_ready(void) { return false; } void discord_avatar_set_ready(bool ready) { discord_state_t *discord_st = discord_get_ptr(); discord_st->avatar_ready = ready; } #ifdef HAVE_MENU static bool discord_download_avatar( const char* user_id, const char* avatar_id) { static char url[PATH_MAX_LENGTH]; static char url_encoded[PATH_MAX_LENGTH]; static char full_path[PATH_MAX_LENGTH]; static char buf[PATH_MAX_LENGTH]; file_transfer_t *transf = NULL; discord_state_t *discord_st = discord_get_ptr(); RARCH_LOG("[DISCORD] user avatar id: %s\n", user_id); fill_pathname_application_special(buf, sizeof(buf), APPLICATION_SPECIAL_DIRECTORY_THUMBNAILS_DISCORD_AVATARS); fill_pathname_join(full_path, buf, avatar_id, sizeof(full_path)); strlcpy(discord_st->user_avatar, avatar_id, sizeof(discord_st->user_avatar)); if (path_is_valid(full_path)) return true; if (string_is_empty(avatar_id)) return false; snprintf(url, sizeof(url), "%s/%s/%s.png", CDN_URL, user_id, avatar_id); net_http_urlencode_full(url_encoded, url, sizeof(url_encoded)); snprintf(buf, sizeof(buf), "%s.png", avatar_id); transf = (file_transfer_t*)calloc(1, sizeof(*transf)); transf->enum_idx = MENU_ENUM_LABEL_CB_DISCORD_AVATAR; strlcpy(transf->path, buf, sizeof(transf->path)); RARCH_LOG("[DISCORD] downloading avatar from: %s\n", url_encoded); task_push_http_transfer_file(url_encoded, true, NULL, cb_generic_download, transf); return false; } #endif static void handle_discord_ready(const DiscordUser* connectedUser) { discord_state_t *discord_st = discord_get_ptr(); strlcpy(discord_st->user_name, connectedUser->username, sizeof(discord_st->user_name)); RARCH_LOG("[DISCORD] connected to user: %s#%s\n", connectedUser->username, connectedUser->discriminator); #ifdef HAVE_MENU discord_download_avatar(connectedUser->userId, connectedUser->avatar); #endif } static void handle_discord_disconnected(int errcode, const char* message) { RARCH_LOG("[DISCORD] disconnected (%d: %s)\n", errcode, message); } static void handle_discord_error(int errcode, const char* message) { RARCH_LOG("[DISCORD] error (%d: %s)\n", errcode, message); } static void handle_discord_join_cb(retro_task_t *task, void *task_data, void *user_data, const char *err) { char join_hostname[PATH_MAX_LENGTH]; struct netplay_room *room = NULL; http_transfer_data_t *data = (http_transfer_data_t*)task_data; discord_state_t *discord_st = discord_get_ptr(); if (!data || err) goto finish; data->data = (char*)realloc(data->data, data->len + 1); data->data[data->len] = '\0'; netplay_rooms_parse(data->data); room = netplay_room_get(0); if (room) { bool host_method_is_mitm = room->host_method == NETPLAY_HOST_METHOD_MITM; const char *srv_address = host_method_is_mitm ? room->mitm_address : room->address; unsigned srv_port = host_method_is_mitm ? room->mitm_port : room->port; if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_DATA_INITED, NULL)) deinit_netplay(); netplay_driver_ctl(RARCH_NETPLAY_CTL_ENABLE_CLIENT, NULL); snprintf(join_hostname, sizeof(join_hostname), "%s|%d", srv_address, srv_port); RARCH_LOG("[DISCORD] joining lobby at: %s\n", join_hostname); task_push_netplay_crc_scan(room->gamecrc, room->gamename, join_hostname, room->corename, room->subsystem_name); discord_st->connecting = true; discord_update(DISCORD_PRESENCE_NETPLAY_CLIENT, false); } finish: if (err) RARCH_ERR("%s: %s\n", msg_hash_to_str(MSG_DOWNLOAD_FAILED), err); if (data) { if (data->data) free(data->data); free(data); } if (user_data) free(user_data); } static void handle_discord_join(const char* secret) { char url[2048] = "http://lobby.libretro.com/"; struct string_list *list = string_split(secret, "|"); discord_state_t *discord_st = discord_get_ptr(); RARCH_LOG("[DISCORD] join secret: (%s)\n", secret); strlcpy(discord_st->peer_party_id, list->elems[0].data, sizeof(discord_st->peer_party_id)); strlcat(url, discord_st->peer_party_id, sizeof(url)); strlcat(url, "/", sizeof(url)); RARCH_LOG("[DISCORD] querying lobby id: %s at %s\n", discord_st->peer_party_id, url); task_push_http_transfer(url, true, NULL, handle_discord_join_cb, NULL); } static void handle_discord_spectate(const char* secret) { RARCH_LOG("[DISCORD] spectate (%s)\n", secret); } #ifdef HAVE_MENU static void handle_discord_join_response(void *ignore, const char *line) { #if 0 /* TODO/FIXME: needs in-game widgets */ if (strstr(line, "yes")) Discord_Respond(user_id, DISCORD_REPLY_YES); #ifdef HAVE_MENU menu_input_dialog_end(); retroarch_menu_running_finished(false); #endif #endif } #endif static void handle_discord_join_request(const DiscordUser* request) { static char url[PATH_MAX_LENGTH]; static char url_encoded[PATH_MAX_LENGTH]; static char filename[PATH_MAX_LENGTH]; char buf[PATH_MAX_LENGTH]; #ifdef HAVE_MENU menu_input_ctx_line_t line; #endif RARCH_LOG("[DISCORD] join request from %s#%s - %s %s\n", request->username, request->discriminator, request->userId, request->avatar); #ifdef HAVE_MENU discord_download_avatar(request->userId, request->avatar); /* To-Do: needs in-game widgets retroarch_menu_running(); */ memset(&line, 0, sizeof(line)); snprintf(buf, sizeof(buf), "%s %s?", msg_hash_to_str(MSG_DISCORD_CONNECTION_REQUEST), request->username); line.label = buf; line.label_setting = "no_setting"; line.cb = handle_discord_join_response; /* To-Do: needs in-game widgets To-Do: bespoke dialog, should show while in-game and have a hotkey to accept To-Do: show avatar of the user connecting if (!menu_input_dialog_start(&line)) return; */ #endif } /* TODO/FIXME - replace last parameter with struct type to allow for more * arguments to be passed later */ void discord_update(enum discord_presence presence, bool fuzzy_archive_match) { core_info_t *core_info = NULL; discord_state_t *discord_st = discord_get_ptr(); core_info_get_current_core(&core_info); if (!discord_st->ready) return; if (presence == discord_st->status) return; if (!discord_st->connecting && ( presence == DISCORD_PRESENCE_NONE || presence == DISCORD_PRESENCE_MENU)) { memset(&discord_st->presence, 0, sizeof(discord_st->presence)); discord_st->peer_party_id[0] = '\0'; } switch (presence) { case DISCORD_PRESENCE_MENU: discord_st->presence.details = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_DISCORD_IN_MENU); discord_st->presence.largeImageKey = "base"; discord_st->presence.largeImageText = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_NO_CORE); discord_st->presence.instance = 0; break; case DISCORD_PRESENCE_GAME_PAUSED: discord_st->presence.smallImageKey = "paused"; discord_st->presence.smallImageText = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_DISCORD_STATUS_PAUSED); discord_st->presence.details = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_DISCORD_IN_GAME_PAUSED); discord_st->pause_time = time(0); discord_st->elapsed_time = difftime(time(0), discord_st->start_time); discord_st->presence.startTimestamp = discord_st->pause_time; break; case DISCORD_PRESENCE_GAME: if (core_info) { const char *system_id = core_info->system_id ? core_info->system_id : "core"; const char *label = NULL; const struct playlist_entry *entry = NULL; playlist_t *current_playlist = playlist_get_cached(); if (current_playlist) { playlist_get_index_by_path( current_playlist, path_get(RARCH_PATH_CONTENT), &entry, fuzzy_archive_match); if (entry && !string_is_empty(entry->label)) label = entry->label; } if (!label) label = path_basename(path_get(RARCH_PATH_BASENAME)); #if 0 RARCH_LOG("[DISCORD] current core: %s\n", system_id); RARCH_LOG("[DISCORD] current content: %s\n", label); #endif discord_st->presence.largeImageKey = system_id; if (core_info->display_name) discord_st->presence.largeImageText = core_info->display_name; discord_st->start_time = time(0); if (discord_st->pause_time != 0) discord_st->start_time = time(0) - discord_st->elapsed_time; discord_st->pause_time = 0; discord_st->elapsed_time = 0; discord_st->presence.smallImageKey = "playing"; discord_st->presence.smallImageText = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_DISCORD_STATUS_PLAYING); discord_st->presence.startTimestamp = discord_st->start_time; discord_st->presence.details = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_DISCORD_IN_GAME); discord_st->presence.state = label; discord_st->presence.instance = 0; if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL)) { discord_st->peer_party_id[0] = '\0'; discord_st->connecting = false; discord_st->presence.partyId = NULL; discord_st->presence.partyMax = 0; discord_st->presence.partySize = 0; discord_st->presence.joinSecret = (const char*)'\0'; } } break; case DISCORD_PRESENCE_NETPLAY_HOSTING: { char join_secret[128]; struct netplay_room *room = netplay_get_host_room(); bool host_method_is_mitm = room->host_method == NETPLAY_HOST_METHOD_MITM; const char *srv_address = host_method_is_mitm ? room->mitm_address : room->address; unsigned srv_port = host_method_is_mitm ? room->mitm_port : room->port; if (room->id == 0) return; RARCH_LOG("[DISCORD] netplay room details: id=%d" ", nick=%s IP=%s port=%d\n", room->id, room->nickname, srv_address, srv_port); snprintf(discord_st->self_party_id, sizeof(discord_st->self_party_id), "%d", room->id); snprintf(join_secret, sizeof(join_secret), "%d|%" PRId64, room->id, cpu_features_get_time_usec()); discord_st->presence.joinSecret = strdup(join_secret); #if 0 discord_st->presence.spectateSecret = "SPECSPECSPEC"; #endif discord_st->presence.partyId = strdup(discord_st->self_party_id); discord_st->presence.partyMax = 2; discord_st->presence.partySize = 1; RARCH_LOG("[DISCORD] join secret: %s\n", join_secret); RARCH_LOG("[DISCORD] party id: %s\n", discord_st->self_party_id); } break; case DISCORD_PRESENCE_NETPLAY_CLIENT: RARCH_LOG("[DISCORD] party id: %s\n", discord_st->peer_party_id); discord_st->presence.partyId = strdup(discord_st->peer_party_id); break; case DISCORD_PRESENCE_NETPLAY_NETPLAY_STOPPED: { if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL) && !netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_CONNECTED, NULL)) { discord_st->peer_party_id[0] = '\0'; discord_st->connecting = false; discord_st->presence.partyId = NULL; discord_st->presence.partyMax = 0; discord_st->presence.partySize = 0; discord_st->presence.joinSecret = (const char*)'\0'; } } break; #ifdef HAVE_CHEEVOS case DISCORD_PRESENCE_RETROACHIEVEMENTS: discord_st->presence.details = rcheevos_get_richpresence(); presence = DISCORD_PRESENCE_GAME; break; #endif case DISCORD_PRESENCE_SHUTDOWN: discord_st->presence.partyId = NULL; discord_st->presence.partyMax = 0; discord_st->presence.partySize = 0; discord_st->presence.joinSecret = (const char*)'\0'; discord_st->connecting = false; default: break; } RARCH_LOG("[DISCORD] updating (%d)\n", presence); Discord_UpdatePresence(&discord_st->presence); discord_st->status = presence; } void discord_init(const char *discord_app_id, char *args) { DiscordEventHandlers handlers; char full_path[PATH_MAX_LENGTH]; char command[PATH_MAX_LENGTH]; discord_state_t *discord_st = discord_get_ptr(); discord_st->start_time = time(0); memset(&handlers, 0, sizeof(handlers)); handlers.ready = handle_discord_ready; handlers.disconnected = handle_discord_disconnected; handlers.errored = handle_discord_error; handlers.joinGame = handle_discord_join; handlers.spectateGame = handle_discord_spectate; handlers.joinRequest = handle_discord_join_request; RARCH_LOG("[DISCORD] initializing ..\n"); Discord_Initialize(discord_app_id, &handlers, 0, NULL); #ifdef _WIN32 fill_pathname_application_path(full_path, sizeof(full_path)); if (strstr(args, full_path)) strlcpy(command, args, sizeof(command)); else { path_basedir(full_path); snprintf(command, sizeof(command), "%s%s", full_path, args); } #else snprintf(command, sizeof(command), "sh -c %s", args); #endif RARCH_LOG("[DISCORD] registering startup command: %s\n", command); Discord_Register(discord_app_id, command); discord_st->ready = true; } void discord_shutdown(void) { discord_state_t *discord_st = discord_get_ptr(); RARCH_LOG("[DISCORD] shutting down ..\n"); Discord_ClearPresence(); Discord_Shutdown(); discord_st->ready = false; }