diff options
author | ouwou <26526779+ouwou@users.noreply.github.com> | 2022-08-31 01:51:02 -0400 |
---|---|---|
committer | ouwou <26526779+ouwou@users.noreply.github.com> | 2022-08-31 01:51:02 -0400 |
commit | 0fa33915da6255cf7460758197eaea7e43353543 (patch) | |
tree | 15a92a3aae2cd2647c24ce4c44f1aaca01fcf422 /src/discord | |
parent | 634f51fb4117c0870399e73560ac313d68d281e8 (diff) | |
download | abaddon-portaudio-0fa33915da6255cf7460758197eaea7e43353543.tar.gz abaddon-portaudio-0fa33915da6255cf7460758197eaea7e43353543.zip |
rudimentary voice implementation
Diffstat (limited to 'src/discord')
-rw-r--r-- | src/discord/discord.cpp | 37 | ||||
-rw-r--r-- | src/discord/discord.hpp | 39 | ||||
-rw-r--r-- | src/discord/objects.cpp | 21 | ||||
-rw-r--r-- | src/discord/objects.hpp | 28 | ||||
-rw-r--r-- | src/discord/voiceclient.cpp | 372 | ||||
-rw-r--r-- | src/discord/voiceclient.hpp | 205 | ||||
-rw-r--r-- | src/discord/waiter.hpp | 29 |
7 files changed, 703 insertions, 28 deletions
diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 561b25b..ed9b999 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1169,6 +1169,16 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI }); } +void DiscordClient::ConnectToVoice(Snowflake channel_id) { + auto channel = GetChannel(channel_id); + if (!channel.has_value() || !channel->GuildID.has_value()) return; + VoiceStateUpdateMessage m; + m.GuildID = *channel->GuildID; + m.ChannelID = channel_id; + m.PreferredRegion = "newark"; + m_websocket.Send(m); +} + void DiscordClient::SetReferringChannel(Snowflake id) { if (!id.IsValid()) { m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me"); @@ -1488,6 +1498,12 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::GUILD_MEMBERS_CHUNK: { HandleGatewayGuildMembersChunk(m); } break; + case GatewayEvent::VOICE_STATE_UPDATE: { + HandleGatewayVoiceStateUpdate(m); + } break; + case GatewayEvent::VOICE_SERVER_UPDATE: { + HandleGatewayVoiceServerUpdate(m); + } break; } } break; default: @@ -2098,6 +2114,25 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { m_store.EndTransaction(); } +void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { + VoiceStateUpdateData data = msg.Data; + if (data.UserID == m_user_data.ID) { + printf("voice session id: %s\n", data.SessionID.c_str()); + m_voice.SetSessionID(data.SessionID); + } +} + +void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { + VoiceServerUpdateData data = msg.Data; + printf("endpoint: %s\n", data.Endpoint.c_str()); + printf("token: %s\n", data.Token.c_str()); + m_voice.SetEndpoint(data.Endpoint); + m_voice.SetToken(data.Token); + m_voice.SetServerID(data.GuildID); + m_voice.SetUserID(m_user_data.ID); + m_voice.Start(); +} + void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { ReadySupplementalData data = msg.Data; for (const auto &p : data.MergedPresences.Friends) { @@ -2589,6 +2624,8 @@ void DiscordClient::LoadEventMap() { m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK; m_event_map["USER_GUILD_SETTINGS_UPDATE"] = GatewayEvent::USER_GUILD_SETTINGS_UPDATE; m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK; + m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE; + m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE; } DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 70c2d82..a6eabd9 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -1,9 +1,11 @@ #pragma once -#include "websocket.hpp" +#include "chatsubmitparams.hpp" +#include "waiter.hpp" #include "httpclient.hpp" #include "objects.hpp" #include "store.hpp" -#include "chatsubmitparams.hpp" +#include "voiceclient.hpp" +#include "websocket.hpp" #include <sigc++/sigc++.h> #include <nlohmann/json.hpp> #include <thread> @@ -18,31 +20,6 @@ #undef GetMessage #endif -class HeartbeatWaiter { -public: - template<class R, class P> - bool wait_for(std::chrono::duration<R, P> const &time) const { - std::unique_lock<std::mutex> lock(m); - return !cv.wait_for(lock, time, [&] { return terminate; }); - } - - void kill() { - std::unique_lock<std::mutex> lock(m); - terminate = true; - cv.notify_all(); - } - - void revive() { - std::unique_lock<std::mutex> lock(m); - terminate = false; - } - -private: - mutable std::condition_variable cv; - mutable std::mutex m; - bool terminate = false; -}; - class Abaddon; class DiscordClient { friend class Abaddon; @@ -204,6 +181,8 @@ public: void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback); void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback); + void ConnectToVoice(Snowflake channel_id); + void SetReferringChannel(Snowflake id); void SetBuildNumber(uint32_t build_number); @@ -283,6 +262,8 @@ private: void HandleGatewayMessageAck(const GatewayMessage &msg); void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg); void HandleGatewayGuildMembersChunk(const GatewayMessage &msg); + void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg); + void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg); @@ -338,13 +319,15 @@ private: std::thread m_heartbeat_thread; std::atomic<int> m_last_sequence = -1; std::atomic<int> m_heartbeat_msec = 0; - HeartbeatWaiter m_heartbeat_waiter; + Waiter m_heartbeat_waiter; std::atomic<bool> m_heartbeat_acked = true; bool m_reconnecting = false; // reconnecting either to resume or reidentify bool m_wants_resume = false; // reconnecting specifically to resume std::string m_session_id; + DiscordVoiceClient m_voice; + mutable std::mutex m_msg_mutex; Glib::Dispatcher m_msg_dispatch; std::queue<std::string> m_msg_queue; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index e43e05a..e4c61c5 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -640,3 +640,24 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) { JS_D("members", m.Members); JS_D("guild_id", m.GuildID); } + +void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { + j["op"] = GatewayOp::VoiceStateUpdate; + j["d"]["guild_id"] = m.GuildID; + j["d"]["channel_id"] = m.ChannelID; + j["d"]["self_mute"] = m.SelfMute; + j["d"]["self_deaf"] = m.SelfDeaf; + j["d"]["self_video"] = m.SelfVideo; + j["d"]["preferred_region"] = m.PreferredRegion; +} + +void from_json(const nlohmann::json &j, VoiceStateUpdateData &m) { + JS_ON("user_id", m.UserID); + JS_ON("session_id", m.SessionID); +} + +void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) { + JS_D("token", m.Token); + JS_D("guild_id", m.GuildID); + JS_D("endpoint", m.Endpoint); +} diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index 9db9369..240b4c5 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -100,6 +100,8 @@ enum class GatewayEvent : int { MESSAGE_ACK, USER_GUILD_SETTINGS_UPDATE, GUILD_MEMBERS_CHUNK, + VOICE_STATE_UPDATE, + VOICE_SERVER_UPDATE, }; enum class GatewayCloseCode : uint16_t { @@ -864,3 +866,29 @@ struct GuildMembersChunkData { friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m); }; + +struct VoiceStateUpdateMessage { + Snowflake GuildID; + Snowflake ChannelID; + bool SelfMute = false; + bool SelfDeaf = false; + bool SelfVideo = false; + std::string PreferredRegion; + + friend void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m); +}; + +struct VoiceStateUpdateData { + Snowflake UserID; + std::string SessionID; + + friend void from_json(const nlohmann::json &j, VoiceStateUpdateData &m); +}; + +struct VoiceServerUpdateData { + std::string Token; + Snowflake GuildID; + std::string Endpoint; + + friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m); +}; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp new file mode 100644 index 0000000..162a5a1 --- /dev/null +++ b/src/discord/voiceclient.cpp @@ -0,0 +1,372 @@ +#include "voiceclient.hpp" +#include "json.hpp" +#include <sodium.h> +#include "abaddon.hpp" +#include "audio/manager.hpp" + +UDPSocket::UDPSocket() { + m_socket = socket(AF_INET, SOCK_DGRAM, 0); +} + +UDPSocket::~UDPSocket() { + Stop(); +} + +void UDPSocket::Connect(std::string_view ip, uint16_t port) { + std::memset(&m_server, 0, sizeof(m_server)); + m_server.sin_family = AF_INET; + m_server.sin_addr.S_un.S_addr = inet_addr(ip.data()); + m_server.sin_port = htons(port); + bind(m_socket, reinterpret_cast<sockaddr *>(&m_server), sizeof(m_server)); +} + +void UDPSocket::Run() { + m_running = true; + m_thread = std::thread(&UDPSocket::ReadThread, this); +} + +void UDPSocket::SetSecretKey(std::array<uint8_t, 32> key) { + m_secret_key = key; +} + +void UDPSocket::SetSSRC(uint32_t ssrc) { + m_ssrc = ssrc; +} + +void UDPSocket::SendEncrypted(const std::vector<uint8_t> &data) { + m_sequence++; + m_timestamp += (48000 / 100) * 2; + + std::vector<uint8_t> rtp(12, 0); + rtp[0] = 0x80; // ver 2 + rtp[1] = 0x78; // payload type 0x78 + rtp[2] = (m_sequence >> 8) & 0xFF; + rtp[3] = (m_sequence >> 0) & 0xFF; + rtp[4] = (m_timestamp >> 24) & 0xFF; + rtp[5] = (m_timestamp >> 16) & 0xFF; + rtp[6] = (m_timestamp >> 8) & 0xFF; + rtp[7] = (m_timestamp >> 0) & 0xFF; + rtp[8] = (m_ssrc >> 24) & 0xFF; + rtp[9] = (m_ssrc >> 16) & 0xFF; + rtp[10] = (m_ssrc >> 8) & 0xFF; + rtp[11] = (m_ssrc >> 0) & 0xFF; + + static std::array<uint8_t, 24> nonce = {}; + std::memcpy(nonce.data(), rtp.data(), 12); + + std::vector<uint8_t> ciphertext(crypto_secretbox_MACBYTES + rtp.size(), 0); + crypto_secretbox_easy(ciphertext.data(), rtp.data(), rtp.size(), nonce.data(), m_secret_key.data()); + rtp.insert(rtp.end(), ciphertext.begin(), ciphertext.end()); + + Send(rtp.data(), rtp.size()); +} + +void UDPSocket::Send(const uint8_t *data, size_t len) { + sendto(m_socket, reinterpret_cast<const char *>(data), static_cast<int>(len), 0, reinterpret_cast<sockaddr *>(&m_server), sizeof(m_server)); +} + +std::vector<uint8_t> UDPSocket::Receive() { + while (true) { + sockaddr_in from; + int fromlen = sizeof(from); + static std::array<uint8_t, 4096> buf; + int n = recvfrom(m_socket, reinterpret_cast<char *>(buf.data()), sizeof(buf), 0, reinterpret_cast<sockaddr *>(&from), &fromlen); + if (n < 0) { + return {}; + } else if (from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) { + return { buf.begin(), buf.begin() + n }; + } + } +} + +void UDPSocket::Stop() { + m_running = false; + shutdown(m_socket, SD_BOTH); + if (m_thread.joinable()) m_thread.join(); +} + +void UDPSocket::ReadThread() { + while (m_running) { + static std::array<uint8_t, 4096> buf; + sockaddr_in from; + int addrlen = sizeof(from); + int n = recvfrom(m_socket, reinterpret_cast<char *>(buf.data()), sizeof(buf), 0, reinterpret_cast<sockaddr *>(&from), &addrlen); + if (n > 0 && from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) { + m_signal_data.emit({ buf.begin(), buf.begin() + n }); + } + } +} + +UDPSocket::type_signal_data UDPSocket::signal_data() { + return m_signal_data; +} + +DiscordVoiceClient::DiscordVoiceClient() { + sodium_init(); + + m_ws.signal_open().connect([this]() { + puts("vws open"); + }); + + m_ws.signal_close().connect([this](uint16_t code) { + printf("vws close %u\n", code); + }); + + m_ws.signal_message().connect([this](const std::string &str) { + std::lock_guard<std::mutex> _(m_dispatch_mutex); + m_message_queue.push(str); + m_dispatcher.emit(); + }); + + m_udp.signal_data().connect([this](const std::vector<uint8_t> &data) { + std::lock_guard<std::mutex> _(m_udp_dispatch_mutex); + m_udp_message_queue.push(data); + m_udp_dispatcher.emit(); + }); + + m_dispatcher.connect([this]() { + m_dispatch_mutex.lock(); + if (m_message_queue.empty()) { + m_dispatch_mutex.unlock(); + return; + } + auto msg = std::move(m_message_queue.front()); + m_message_queue.pop(); + m_dispatch_mutex.unlock(); + OnGatewayMessage(msg); + }); + + m_udp_dispatcher.connect([this]() { + m_udp_dispatch_mutex.lock(); + if (m_udp_message_queue.empty()) { + m_udp_dispatch_mutex.unlock(); + return; + } + auto data = std::move(m_udp_message_queue.front()); + m_udp_message_queue.pop(); + m_udp_dispatch_mutex.unlock(); + OnUDPData(data); + }); +} + +DiscordVoiceClient::~DiscordVoiceClient() { + m_ws.Stop(); + m_udp.Stop(); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); +} + +void DiscordVoiceClient::Start() { + m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); +} + +void DiscordVoiceClient::SetSessionID(std::string_view session_id) { + m_session_id = session_id; +} + +void DiscordVoiceClient::SetEndpoint(std::string_view endpoint) { + m_endpoint = endpoint; +} + +void DiscordVoiceClient::SetToken(std::string_view token) { + m_token = token; +} + +void DiscordVoiceClient::SetServerID(Snowflake id) { + m_server_id = id; +} + +void DiscordVoiceClient::SetUserID(Snowflake id) { + m_user_id = id; +} + +void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { + VoiceGatewayMessage msg = nlohmann::json::parse(str); + puts(msg.Data.dump(4).c_str()); + switch (msg.Opcode) { + case VoiceGatewayOp::Hello: { + HandleGatewayHello(msg); + } break; + case VoiceGatewayOp::Ready: { + HandleGatewayReady(msg); + } break; + case VoiceGatewayOp::SessionDescription: { + HandleGatewaySessionDescription(msg); + } break; + default: break; + } +} + +void DiscordVoiceClient::HandleGatewayHello(const VoiceGatewayMessage &m) { + VoiceHelloData d = m.Data; + m_heartbeat_msec = d.HeartbeatInterval; + m_heartbeat_thread = std::thread(&DiscordVoiceClient::HeartbeatThread, this); + + Identify(); +} + +void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { + VoiceReadyData d = m.Data; + m_ip = d.IP; + m_port = d.Port; + m_ssrc = d.SSRC; + if (std::find(d.Modes.begin(), d.Modes.end(), "xsalsa20_poly1305") == d.Modes.end()) { + puts("xsalsa20_poly1305 not in encryption modes"); + } + printf("connect to %s:%u ssrc %u\n", m_ip.c_str(), m_port, m_ssrc); + + m_udp.Connect(m_ip, m_port); + + Discovery(); +} + +void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessage &m) { + VoiceSessionDescriptionData d = m.Data; + printf("receiving with %s secret key: ", d.Mode.c_str()); + for (auto b : d.SecretKey) { + printf("%02X", b); + } + printf("\n"); + m_secret_key = d.SecretKey; + m_udp.SetSSRC(m_ssrc); + m_udp.SetSecretKey(m_secret_key); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.Run(); +} + +void DiscordVoiceClient::Identify() { + VoiceIdentifyMessage msg; + msg.ServerID = m_server_id; + msg.UserID = m_user_id; + msg.SessionID = m_session_id; + msg.Token = m_token; + msg.Video = true; + m_ws.Send(msg); +} + +void DiscordVoiceClient::Discovery() { + std::vector<uint8_t> payload; + // 2 bytes = 1, request + payload.push_back(0x00); + payload.push_back(0x01); + // 2 bytes = 70, pl length + payload.push_back(0x00); + payload.push_back(70); + // 4 bytes = ssrc + payload.push_back((m_ssrc >> 24) & 0xFF); + payload.push_back((m_ssrc >> 16) & 0xFF); + payload.push_back((m_ssrc >> 8) & 0xFF); + payload.push_back((m_ssrc >> 0) & 0xFF); + // address and port + for (int i = 0; i < 66; i++) + payload.push_back(0); + m_udp.Send(payload.data(), payload.size()); + auto response = m_udp.Receive(); + if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) { + const char *our_ip = reinterpret_cast<const char *>(&response[8]); + uint16_t our_port = (response[73] << 8) | response[74]; + printf("we are %s:%u\n", our_ip, our_port); + SelectProtocol(our_ip, our_port); + } else { + puts("received non-discovery packet after discovery"); + } +} + +void DiscordVoiceClient::SelectProtocol(std::string_view ip, uint16_t port) { + VoiceSelectProtocolMessage msg; + msg.Mode = "xsalsa20_poly1305"; + msg.Address = ip; + msg.Port = port; + msg.Protocol = "udp"; + m_ws.Send(msg); +} + +void DiscordVoiceClient::OnUDPData(std::vector<uint8_t> data) { + uint8_t *payload = data.data() + 12; + static std::array<uint8_t, 24> nonce = {}; + std::memcpy(nonce.data(), data.data(), 12); + if (crypto_secretbox_open_easy(payload, payload, data.size() - 12, nonce.data(), m_secret_key.data())) { + puts("decrypt fail"); + } else { + Abaddon::Get().GetAudio().FeedMeOpus({ payload, payload + data.size() - 12 - crypto_box_MACBYTES }); + } +} + +void DiscordVoiceClient::HeartbeatThread() { + while (true) { + if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) + break; + + const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + VoiceHeartbeatMessage msg; + msg.Nonce = static_cast<uint64_t>(ms); + m_ws.Send(msg); + } +} + +void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) { + JS_D("op", m.Opcode); + m.Data = j.at("d"); +} + +void from_json(const nlohmann::json &j, VoiceHelloData &m) { + JS_D("heartbeat_interval", m.HeartbeatInterval); +} + +void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m) { + j["op"] = VoiceGatewayOp::Heartbeat; + j["d"] = m.Nonce; +} + +void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m) { + j["op"] = VoiceGatewayOp::Identify; + j["d"]["server_id"] = m.ServerID; + j["d"]["user_id"] = m.UserID; + j["d"]["session_id"] = m.SessionID; + j["d"]["token"] = m.Token; + j["d"]["video"] = m.Video; + j["d"]["streams"][0]["type"] = "video"; + j["d"]["streams"][0]["rid"] = "100"; + j["d"]["streams"][0]["quality"] = 100; +} + +void from_json(const nlohmann::json &j, VoiceReadyData::VoiceStream &m) { + JS_D("active", m.IsActive); + JS_D("quality", m.Quality); + JS_D("rid", m.RID); + JS_D("rtx_ssrc", m.RTXSSRC); + JS_D("ssrc", m.SSRC); + JS_D("type", m.Type); +} + +void from_json(const nlohmann::json &j, VoiceReadyData &m) { + JS_ON("experiments", m.Experiments); + JS_D("ip", m.IP); + JS_D("modes", m.Modes); + JS_D("port", m.Port); + JS_D("ssrc", m.SSRC); + JS_ON("streams", m.Streams); +} + +void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m) { + j["op"] = VoiceGatewayOp::SelectProtocol; + j["d"]["address"] = m.Address; + j["d"]["port"] = m.Port; + j["d"]["protocol"] = m.Protocol; + j["d"]["mode"] = m.Mode; + j["d"]["data"]["address"] = m.Address; + j["d"]["data"]["port"] = m.Port; + j["d"]["data"]["mode"] = m.Mode; +} + +void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m) { + JS_D("mode", m.Mode); + JS_D("secret_key", m.SecretKey); +} diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp new file mode 100644 index 0000000..615bbde --- /dev/null +++ b/src/discord/voiceclient.hpp @@ -0,0 +1,205 @@ +#pragma once +#include "snowflake.hpp" +#include "waiter.hpp" +#include "websocket.hpp" +#include <mutex> +#include <queue> +#include <string> +#include <glibmm/dispatcher.h> + +enum class VoiceGatewayCloseCode : uint16_t { + UnknownOpcode = 4001, + InvalidPayload = 4002, + NotAuthenticated = 4003, + AuthenticationFailed = 4004, + AlreadyAuthenticated = 4005, + SessionInvalid = 4006, + SessionTimedOut = 4009, + ServerNotFound = 4011, + UnknownProtocol = 4012, + Disconnected = 4014, + ServerCrashed = 4015, + UnknownEncryption = 4016, +}; + +enum class VoiceGatewayOp : int { + Identify = 0, + SelectProtocol = 1, + Ready = 2, + Heartbeat = 3, + SessionDescription = 4, + Speaking = 5, + HeartbeatAck = 6, + Resume = 7, + Hello = 8, + Resumed = 9, + ClientDisconnect = 13, +}; + +struct VoiceGatewayMessage { + VoiceGatewayOp Opcode; + nlohmann::json Data; + + friend void from_json(const nlohmann::json &j, VoiceGatewayMessage &m); +}; + +struct VoiceHelloData { + int HeartbeatInterval; + + friend void from_json(const nlohmann::json &j, VoiceHelloData &m); +}; + +struct VoiceHeartbeatMessage { + uint64_t Nonce; + + friend void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m); +}; + +struct VoiceIdentifyMessage { + Snowflake ServerID; + Snowflake UserID; + std::string SessionID; + std::string Token; + bool Video; + // todo streams i guess? + + friend void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m); +}; + +struct VoiceReadyData { + struct VoiceStream { + bool IsActive; + int Quality; + std::string RID; + int RTXSSRC; + int SSRC; + std::string Type; + + friend void from_json(const nlohmann::json &j, VoiceStream &m); + }; + + std::vector<std::string> Experiments; + std::string IP; + std::vector<std::string> Modes; + uint16_t Port; + uint32_t SSRC; + std::vector<VoiceStream> Streams; + + friend void from_json(const nlohmann::json &j, VoiceReadyData &m); +}; + +struct VoiceSelectProtocolMessage { + std::string Address; + uint16_t Port; + std::string Mode; + std::string Protocol; + + friend void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m); +}; + +struct VoiceSessionDescriptionData { + // std::string AudioCodec; + // std::string VideoCodec; + // std::string MediaSessionID; + std::string Mode; + std::array<uint8_t, 32> SecretKey; + + friend void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m); +}; + +class UDPSocket { +public: + UDPSocket(); + ~UDPSocket(); + + void Connect(std::string_view ip, uint16_t port); + void Run(); + void SetSecretKey(std::array<uint8_t, 32> key); + void SetSSRC(uint32_t ssrc); + void SendEncrypted(const std::vector<uint8_t> &data); + void Send(const uint8_t *data, size_t len); + std::vector<uint8_t> Receive(); + void Stop(); + +private: + void ReadThread(); + +#ifdef _WIN32 + SOCKET m_socket; +#else + int m_socket; +#endif + sockaddr_in m_server; + + std::atomic<bool> m_running = false; + + std::thread m_thread; + + std::array<uint8_t, 32> m_secret_key; + uint32_t m_ssrc; + + uint16_t m_sequence = 0; + uint32_t m_timestamp = 0; + +public: + using type_signal_data = sigc::signal<void, std::vector<uint8_t>>; + type_signal_data signal_data(); + +private: + type_signal_data m_signal_data; +}; + +class DiscordVoiceClient { +public: + DiscordVoiceClient(); + ~DiscordVoiceClient(); + + void Start(); + + void SetSessionID(std::string_view session_id); + void SetEndpoint(std::string_view endpoint); + void SetToken(std::string_view token); + void SetServerID(Snowflake id); + void SetUserID(Snowflake id); + +private: + void OnGatewayMessage(const std::string &str); + void HandleGatewayHello(const VoiceGatewayMessage &m); + void HandleGatewayReady(const VoiceGatewayMessage &m); + void HandleGatewaySessionDescription(const VoiceGatewayMessage &m); + + void Identify(); + void Discovery(); + void SelectProtocol(std::string_view ip, uint16_t port); + + void OnUDPData(std::vector<uint8_t> data); + + void HeartbeatThread(); + + std::string m_session_id; + std::string m_endpoint; + std::string m_token; + Snowflake m_server_id; + Snowflake m_user_id; + + std::string m_ip; + uint16_t m_port; + uint32_t m_ssrc; + + std::array<uint8_t, 32> m_secret_key; + + Websocket m_ws; + UDPSocket m_udp; + + Glib::Dispatcher m_dispatcher; + std::queue<std::string> m_message_queue; + std::mutex m_dispatch_mutex; + + Glib::Dispatcher m_udp_dispatcher; + std::queue<std::vector<uint8_t>> m_udp_message_queue; + std::mutex m_udp_dispatch_mutex; + + int m_heartbeat_msec; + Waiter m_heartbeat_waiter; + std::thread m_heartbeat_thread; +}; diff --git a/src/discord/waiter.hpp b/src/discord/waiter.hpp new file mode 100644 index 0000000..0d5ae92 --- /dev/null +++ b/src/discord/waiter.hpp @@ -0,0 +1,29 @@ +#pragma once +#include <chrono> +#include <condition_variable> +#include <mutex> + +class Waiter { +public: + template<class R, class P> + bool wait_for(std::chrono::duration<R, P> const &time) const { + std::unique_lock<std::mutex> lock(m); + return !cv.wait_for(lock, time, [&] { return terminate; }); + } + + void kill() { + std::unique_lock<std::mutex> lock(m); + terminate = true; + cv.notify_all(); + } + + void revive() { + std::unique_lock<std::mutex> lock(m); + terminate = false; + } + +private: + mutable std::condition_variable cv; + mutable std::mutex m; + bool terminate = false; +}; |