From d1ca910b36f431676d8c02667f69eb2f1cd2ae47 Mon Sep 17 00:00:00 2001 From: George Hazan Date: Tue, 4 Jun 2024 19:22:10 +0300 Subject: Discord: voice call initialization --- protocols/Discord/src/dispatch.cpp | 3 + protocols/Discord/src/gateway.cpp | 10 +++ protocols/Discord/src/guilds.cpp | 15 +++- protocols/Discord/src/proto.cpp | 22 ++--- protocols/Discord/src/proto.h | 54 ++++++++--- protocols/Discord/src/voice.cpp | 178 +++++++++++++++++++++++++++++-------- 6 files changed, 215 insertions(+), 67 deletions(-) diff --git a/protocols/Discord/src/dispatch.cpp b/protocols/Discord/src/dispatch.cpp index 0abc80d607..e56d29eec1 100644 --- a/protocols/Discord/src/dispatch.cpp +++ b/protocols/Discord/src/dispatch.cpp @@ -67,6 +67,9 @@ static handlers[] = // these structures must me sorted alphabetically { L"USER_SETTINGS_UPDATE", &CDiscordProto::OnCommandUserSettingsUpdate }, { L"USER_UPDATE", &CDiscordProto::OnCommandUserUpdate }, + + { L"VOICE_SERVER_UPDATE", &CDiscordProto::OnCommandVoiceServerUpdate }, + { L"VOICE_STATE_UPDATE", &CDiscordProto::OnCommandVoiceStateUpdate }, }; static int __cdecl pSearchFunc(const void *p1, const void *p2) diff --git a/protocols/Discord/src/gateway.cpp b/protocols/Discord/src/gateway.cpp index 8367462159..dc7d1da26e 100644 --- a/protocols/Discord/src/gateway.cpp +++ b/protocols/Discord/src/gateway.cpp @@ -337,3 +337,13 @@ bool CDiscordProto::GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText JSONNode root; root << INT_PARAM("op", OPCODE_STATUS_UPDATE) << payload; return GatewaySend(root); } + +///////////////////////////////////////////////////////////////////////////////////////// + +bool CDiscordProto::GatewaySendVoice(JSONNode &payload) +{ + payload.set_name("d"); + + JSONNode root; root << INT_PARAM("op", OPCODE_VOICE_UPDATE) << payload; + return GatewaySend(root); +} diff --git a/protocols/Discord/src/guilds.cpp b/protocols/Discord/src/guilds.cpp index 322a4ae9ed..d19615ef93 100644 --- a/protocols/Discord/src/guilds.cpp +++ b/protocols/Discord/src/guilds.cpp @@ -160,6 +160,9 @@ void CDiscordProto::ProcessGuild(const JSONNode &pRoot) gm->iStatus = StrToStatus(it["status"].as_mstring()); } + for (auto &it : pRoot["voice_states"]) + pGuild->arVoiceStates.insert(new CDiscordVoiceState(it)); + for (auto &it : pGuild->arChatUsers) AddGuildUser(pGuild, *it); @@ -354,15 +357,23 @@ static int compareChatUsers(const CDiscordGuildMember *p1, const CDiscordGuildMe return compareInt64(p1->userId, p2->userId); } +static int compareVoiceState(const CDiscordVoiceState *p1, const CDiscordVoiceState *p2) +{ + return compareInt64(p1->m_userId, p2->m_userId); +} + CDiscordGuild::CDiscordGuild(SnowFlake _id) : m_id(_id), + arRoles(10, compareRoles), arChannels(10, compareUsers), arChatUsers(30, compareChatUsers), - arRoles(10, compareRoles) + arVoiceStates(10, compareVoiceState) {} CDiscordGuild::~CDiscordGuild() -{} +{ + delete pVoiceCall; +} CDiscordUser::~CDiscordUser() { diff --git a/protocols/Discord/src/proto.cpp b/protocols/Discord/src/proto.cpp index 5929b5ce19..cea528e5b7 100644 --- a/protocols/Discord/src/proto.cpp +++ b/protocols/Discord/src/proto.cpp @@ -37,6 +37,11 @@ static int compareGuilds(const CDiscordGuild *p1, const CDiscordGuild *p2) return compareInt64(p1->m_id, p2->m_id); } +static int compareCalls(const CDiscordVoiceCall *p1, const CDiscordVoiceCall *p2) +{ + return compareInt64(p1->channelId, p2->channelId); +} + CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : PROTO(proto_name, username), m_impl(*this), @@ -46,7 +51,7 @@ CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : arGuilds(1, compareGuilds), arMarkReadQueue(1, compareUsers), arOwnMessages(1, compareMessages), - arVoiceCalls(1), + arVoiceCalls(1, compareCalls), m_wszEmail(this, "Email", L""), m_wszDefaultGroup(this, "GroupName", DB_KEYVAL_GROUP), @@ -89,21 +94,6 @@ CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : // database db_set_resident(m_szModuleName, "XStatusMsg"); - // custom events - DBEVENTTYPEDESCR dbEventType = {}; - dbEventType.module = m_szModuleName; - dbEventType.flags = DETF_HISTORY | DETF_MSGWINDOW; - - dbEventType.eventType = EVENT_INCOMING_CALL; - dbEventType.descr = Translate("Incoming call"); - dbEventType.eventIcon = g_plugin.getIconHandle(IDI_VOICE_CALL); - DbEvent_RegisterType(&dbEventType); - - dbEventType.eventType = EVENT_CALL_FINISHED; - dbEventType.descr = Translate("Call ended"); - dbEventType.eventIcon = g_plugin.getIconHandle(IDI_VOICE_ENDED); - DbEvent_RegisterType(&dbEventType); - // Groupchat initialization GCREGISTER gcr = {}; gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR | GC_DATABASE; diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h index 844e6c8ef0..117e7a90ba 100644 --- a/protocols/Discord/src/proto.h +++ b/protocols/Discord/src/proto.h @@ -1,8 +1,5 @@ #pragma once -#define EVENT_INCOMING_CALL 10001 -#define EVENT_CALL_FINISHED 10002 - enum Permission : uint64_t { CREATE_INVITE = (1ll << 0), // Allows creation of instant invites @@ -148,6 +145,35 @@ struct CDiscordUser : public MZeroedObject ///////////////////////////////////////////////////////////////////////////////////////// +struct CDiscordVoiceState : public MZeroedObject +{ + explicit CDiscordVoiceState(const JSONNode &node); + + SnowFlake m_userId, m_channelId; + CMStringA m_sessionId; + + union { + int m_flags; + struct { + bool m_bDeaf : 1; // deafened by the server + bool m_bMute : 1; // muted by the server + bool m_bSelfMute : 1; // locally muted + bool m_bSelfDeaf : 1; // locally deafened + bool m_nSelfVideo : 1; // camera enabled + bool m_bSuppress : 1; // user can't speak + }; + }; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CDiscordVoiceCall : public MZeroedObject +{ + SnowFlake channelId, guildId; + CMStringA szSessionId, szToken, szEndpoint; + time_t startTime; +}; + struct CDiscordGuildMember : public MZeroedObject { CDiscordGuildMember(SnowFlake id) : @@ -191,6 +217,8 @@ struct CDiscordGuild : public MZeroedObject LIST arChannels; OBJLIST arChatUsers; OBJLIST arRoles; // guild roles + OBJLIST arVoiceStates; + CDiscordVoiceCall *pVoiceCall; uint64_t CalcPermissionOverride(SnowFlake myUserId, const JSONNode &json); void ProcessRole(const JSONNode &json); @@ -199,13 +227,6 @@ struct CDiscordGuild : public MZeroedObject void SaveToFile(); }; -struct CDiscordVoiceCall -{ - CMStringA szId; - SnowFlake channelId; - time_t startTime; -}; - ///////////////////////////////////////////////////////////////////////////////////////// #define OPCODE_DISPATCH 0 @@ -315,6 +336,7 @@ class CDiscordProto : public PROTO void GatewaySendIdentify(void); void GatewaySendResume(void); bool GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText); + bool GatewaySendVoice(JSONNode &node); GatewayHandlerFunc GetHandler(const wchar_t*); @@ -343,7 +365,6 @@ class CDiscordProto : public PROTO OBJLIST arUsers; OBJLIST arOwnMessages; - OBJLIST arVoiceCalls; CDiscordUser* FindUser(SnowFlake id); CDiscordUser* FindUser(const wchar_t *pwszUsername, int iDiscriminator); @@ -414,7 +435,14 @@ class CDiscordProto : public PROTO ////////////////////////////////////////////////////////////////////////////////////// // voice + mir_cs m_csVoice; + OBJLIST arVoiceCalls; + void InitVoip(bool bEnable); + void TryVoiceStart(CDiscordGuild *pGuild); + void VoiceChannelConnect(MCONTACT hContact); + + CDiscordVoiceCall* FindCall(SnowFlake channelId); INT_PTR __cdecl VoiceCaps(WPARAM, LPARAM); INT_PTR __cdecl VoiceCanCall(WPARAM, LPARAM); @@ -423,6 +451,8 @@ class CDiscordProto : public PROTO INT_PTR __cdecl VoiceCallCancel(WPARAM, LPARAM); int __cdecl OnVoiceState(WPARAM, LPARAM); + + void __cdecl VoiceClientThread(void *); ////////////////////////////////////////////////////////////////////////////////////// // misc methods @@ -526,6 +556,8 @@ public: void OnCommandTyping(const JSONNode &json); void OnCommandUserUpdate(const JSONNode &json); void OnCommandUserSettingsUpdate(const JSONNode &json); + void OnCommandVoiceServerUpdate(const JSONNode &json); + void OnCommandVoiceStateUpdate(const JSONNode &json); void OnLoggedIn(); void OnLoggedOut(); diff --git a/protocols/Discord/src/voice.cpp b/protocols/Discord/src/voice.cpp index 51899c57e3..b30d7b166a 100644 --- a/protocols/Discord/src/voice.cpp +++ b/protocols/Discord/src/voice.cpp @@ -17,34 +17,101 @@ along with this program. If not, see . #include "stdafx.h" +CDiscordVoiceState::CDiscordVoiceState(const JSONNode &node) +{ + m_userId = ::getId(node["user_id"]); + m_channelId = ::getId(node["channel_id"]); + m_sessionId = node["session_id"].as_mstring(); + m_bDeaf = node["deaf"].as_bool(); + m_bMute = node["mute"].as_bool(); + m_bSuppress = node["suppress"].as_bool(); + m_bSelfDeaf = node["self_deaf"].as_bool(); + m_bSelfMute = node["self_mute"].as_bool(); + m_nSelfVideo = node["self_video"].as_bool(); +} + +CDiscordVoiceCall* CDiscordProto::FindCall(SnowFlake channelId) +{ + mir_cslock lck(m_csVoice); + if (auto *pCall = arVoiceCalls.find((CDiscordVoiceCall *)&channelId)) + return pCall; + + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// voice server commands + +void CDiscordProto::VoiceChannelConnect(MCONTACT hContact) +{ + SnowFlake channelId = getId(hContact, DB_KEY_CHANNELID); + if (!hContact || !channelId) + return; + + if (auto *pUser = FindUserByChannel(channelId)) { + if (auto *pGuild = pUser->pGuild) { + { + mir_cslock lck(m_csVoice); + + // a voice call for this guild is already being establishing, exit + if (pGuild->pVoiceCall) + return; + + pGuild->pVoiceCall = new CDiscordVoiceCall(); + pGuild->pVoiceCall->guildId = pGuild->m_id; + pGuild->pVoiceCall->channelId = channelId; + } + JSONNode payload; + payload << INT64_PARAM("guild_id", pGuild->m_id) << INT64_PARAM("channel_id", channelId) + << BOOL_PARAM("self_mute", false) << BOOL_PARAM("self_deaf", false); + GatewaySendVoice(payload); + } + } +} + +void CDiscordProto::TryVoiceStart(CDiscordGuild *pGuild) +{ + if (auto *pCall = pGuild->pVoiceCall) { + // not enough data, waiting for the second command + if (pCall->szSessionId.IsEmpty() || pCall->szToken.IsEmpty() || pCall->szEndpoint.IsEmpty()) + return; + + // transfer a call from guild to the concrete channel + pGuild->pVoiceCall = nullptr; + if (auto *pUser = FindUserByChannel(pCall->channelId)) { + arVoiceCalls.insert(pCall); + ForkThread(&CDiscordProto::VoiceClientThread, pCall); + } + else { + delete pCall; + return; + } + } +} + +void CDiscordProto::VoiceClientThread(void *param) +{ + auto *pCall = (CDiscordVoiceCall *)param; + pCall->startTime = time(0); +} + ///////////////////////////////////////////////////////////////////////////////////////// // call operations (voice & video) void CDiscordProto::OnCommandCallCreated(const JSONNode &pRoot) { for (auto &it : pRoot["voice_states"]) { - SnowFlake channelId = ::getId(pRoot["channel_id"]); + SnowFlake channelId = ::getId(it["channel_id"]); auto *pUser = FindUserByChannel(channelId); if (pUser == nullptr) { debugLogA("Call from unknown channel %lld, skipping", channelId); continue; } - auto *pCall = new CDiscordVoiceCall(); - pCall->szId = it["session_id"].as_mstring(); - pCall->channelId = channelId; - pCall->startTime = time(0); - arVoiceCalls.insert(pCall); - - char *szMessage = TranslateU("Incoming call"); - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.timestamp = pCall->startTime; - dbei.eventType = EVENT_INCOMING_CALL; - dbei.cbBlob = uint32_t(mir_strlen(szMessage) + 1); - dbei.pBlob = szMessage; - dbei.flags = DBEF_UTF; - db_event_add(pUser->hContact, &dbei); + if (auto *pCall = FindCall(channelId)) { + pCall->startTime = time(0); + } + else debugLogA("Unregistered call received from channel %lld, skipping", channelId); } } @@ -57,34 +124,66 @@ void CDiscordProto::OnCommandCallDeleted(const JSONNode &pRoot) return; } - int elapsed = 0, currTime = time(0); - for (auto &call : arVoiceCalls.rev_iter()) - if (call->channelId == channelId) { - elapsed = currTime - call->startTime; - arVoiceCalls.removeItem(&call); - break; + if (auto *pCall = FindCall(channelId)) { + // int currTime = time(0); + for (auto &call : arVoiceCalls.rev_iter()) { + if (call->channelId == channelId) { + arVoiceCalls.removeItem(&call); + break; + } } - - if (!elapsed) { - debugLogA("Call from channel %lld isn't registered, skipping", channelId); - return; } - - CMStringA szMessage(FORMAT, TranslateU("Call ended, %d seconds long"), elapsed); - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.timestamp = currTime; - dbei.eventType = EVENT_CALL_FINISHED; - dbei.cbBlob = uint32_t(szMessage.GetLength() + 1); - dbei.pBlob = szMessage.GetBuffer(); - dbei.flags = DBEF_UTF; - db_event_add(pUser->hContact, &dbei); + else debugLogA("Unregistered call received from channel %lld, skipping", channelId); } void CDiscordProto::OnCommandCallUpdated(const JSONNode&) { } +void CDiscordProto::OnCommandVoiceServerUpdate(const JSONNode &pRoot) +{ + if (auto *pGuild = FindGuild(::getId(pRoot["guild_id"]))) { + mir_cslock lck(m_csVoice); + if (auto *pCall = pGuild->pVoiceCall) { + pCall->szToken = pRoot["token"].as_mstring(); + pCall->szEndpoint = pRoot["endpoint"].as_mstring(); + TryVoiceStart(pGuild); + } + } +} + +void CDiscordProto::OnCommandVoiceStateUpdate(const JSONNode &pRoot) +{ + CDiscordVoiceState vs(pRoot); + + if (auto *pGuild = FindGuild(::getId(pRoot["guild_id"]))) { + if (vs.m_channelId == 0) { + for (auto &it : pGuild->arVoiceStates.rev_iter()) + if (it->m_userId == vs.m_userId) + pGuild->arVoiceStates.removeItem(&it); + + // if (vs.m_userId == m_ownId) + // disconnect voiсe from guild + } + else { + auto *pVS = pGuild->arVoiceStates.find(&vs); + if (pVS) + *pVS = vs; + else + pGuild->arVoiceStates.insert(new CDiscordVoiceState(vs)); + + // if our voice call to this guild is in progress, assign session id & call it + if (vs.m_userId == m_ownId) { + mir_cslock lck(m_csVoice); + if (auto *pCall = pGuild->pVoiceCall) { + pCall->szSessionId = vs.m_sessionId; + TryVoiceStart(pGuild); + } + } + } + } +} + ///////////////////////////////////////////////////////////////////////////////////////// // Events & services @@ -102,8 +201,9 @@ INT_PTR CDiscordProto::VoiceCanCall(WPARAM hContact, LPARAM) return FALSE; } -INT_PTR CDiscordProto::VoiceCallCreate(WPARAM, LPARAM) +INT_PTR CDiscordProto::VoiceCallCreate(WPARAM hContact, LPARAM) { + VoiceChannelConnect(hContact); return 0; } @@ -125,7 +225,7 @@ int CDiscordProto::OnVoiceState(WPARAM wParam, LPARAM) CDiscordVoiceCall *pCall = nullptr; for (auto &it : arVoiceCalls) - if (it->szId == pVoice->id) { + if (it->szSessionId == pVoice->id) { pCall = it; break; } @@ -139,6 +239,8 @@ int CDiscordProto::OnVoiceState(WPARAM wParam, LPARAM) return 0; } +///////////////////////////////////////////////////////////////////////////////////////// + void CDiscordProto::InitVoip(bool bEnable) { if (!g_plugin.bVoiceService) -- cgit v1.2.3