From 18af78e6af49821f8c7adb5b4325d75c8bf4fd03 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 17 Aug 2020 02:40:03 -0400 Subject: [PATCH] connect and heartbeat --- .clang-format | 64 ++++++++++++++++ Abaddon.sln | 31 ++++++++ Abaddon.vcxproj | 160 ++++++++++++++++++++++++++++++++++++++++ Abaddon.vcxproj.filters | 51 +++++++++++++ abaddon.cpp | 44 +++++++++++ abaddon.hpp | 14 ++++ components/channels.cpp | 12 +++ components/channels.hpp | 12 +++ discord/discord.cpp | 85 +++++++++++++++++++++ discord/discord.hpp | 75 +++++++++++++++++++ discord/websocket.cpp | 30 ++++++++ discord/websocket.hpp | 22 ++++++ windows/mainwindow.cpp | 31 ++++++++ windows/mainwindow.hpp | 22 ++++++ 14 files changed, 653 insertions(+) create mode 100644 .clang-format create mode 100644 Abaddon.sln create mode 100644 Abaddon.vcxproj create mode 100644 Abaddon.vcxproj.filters create mode 100644 abaddon.cpp create mode 100644 abaddon.hpp create mode 100644 components/channels.cpp create mode 100644 components/channels.hpp create mode 100644 discord/discord.cpp create mode 100644 discord/discord.hpp create mode 100644 discord/websocket.cpp create mode 100644 discord/websocket.hpp create mode 100644 windows/mainwindow.cpp create mode 100644 windows/mainwindow.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..074ecf0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,64 @@ +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveDeclarations: 'false' +AlignOperands: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'false' +AllowAllConstructorInitializersOnNextLine: 'false' +AllowAllParametersOfDeclarationOnNextLine: 'false' +AllowShortBlocksOnASingleLine: 'true' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Always +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: 'true' +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: 'false' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackArguments: 'true' +BinPackParameters: 'true' +BreakAfterJavaFieldAnnotations: 'true' +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: 'true' +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakStringLiterals: 'true' +ColumnLimit: '0' +CompactNamespaces: 'false' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' +ContinuationIndentWidth: '4' +Cpp11BracedListStyle: 'false' +DerivePointerAlignment: 'false' +FixNamespaceComments: 'true' +IncludeBlocks: Merge +IndentCaseLabels: 'true' +IndentPPDirectives: BeforeHash +IndentWidth: '4' +IndentWrappedFunctionNames: 'false' +JavaScriptQuotes: Double +KeepEmptyLinesAtTheStartOfBlocks: 'false' +Language: Cpp +NamespaceIndentation: Inner +PointerAlignment: Right +SortIncludes: 'false' +SpaceAfterCStyleCast: 'false' +SpaceAfterLogicalNot: 'false' +SpaceAfterTemplateKeyword: 'false' +SpaceBeforeAssignmentOperators: 'true' +SpaceBeforeCpp11BracedList: 'true' +SpaceBeforeCtorInitializerColon: 'true' +SpaceBeforeInheritanceColon: 'true' +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: 'true' +SpaceInEmptyParentheses: 'false' +SpacesInAngles: 'false' +SpacesInCStyleCastParentheses: 'false' +SpacesInParentheses: 'false' +SpacesInSquareBrackets: 'false' +Standard: Auto +TabWidth: '4' + +... diff --git a/Abaddon.sln b/Abaddon.sln new file mode 100644 index 0000000..e9641b0 --- /dev/null +++ b/Abaddon.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30204.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Abaddon", "Abaddon.vcxproj", "{A2A67504-F7F1-4DD3-B86B-68033416FFF5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Debug|x64.ActiveCfg = Debug|x64 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Debug|x64.Build.0 = Debug|x64 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Debug|x86.ActiveCfg = Debug|Win32 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Debug|x86.Build.0 = Debug|Win32 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Release|x64.ActiveCfg = Release|x64 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Release|x64.Build.0 = Release|x64 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Release|x86.ActiveCfg = Release|Win32 + {A2A67504-F7F1-4DD3-B86B-68033416FFF5}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CA30A7C6-E05A-4D4C-BC71-1D4C80E5647E} + EndGlobalSection +EndGlobal diff --git a/Abaddon.vcxproj b/Abaddon.vcxproj new file mode 100644 index 0000000..bbc3ef1 --- /dev/null +++ b/Abaddon.vcxproj @@ -0,0 +1,160 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {a2a67504-f7f1-4dd3-b86b-68033416fff5} + Abaddon + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + + + Console + true + true + true + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Abaddon.vcxproj.filters b/Abaddon.vcxproj.filters new file mode 100644 index 0000000..00f0697 --- /dev/null +++ b/Abaddon.vcxproj.filters @@ -0,0 +1,51 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/abaddon.cpp b/abaddon.cpp new file mode 100644 index 0000000..6c170e0 --- /dev/null +++ b/abaddon.cpp @@ -0,0 +1,44 @@ +#include +#include "discord/discord.hpp" +#include "windows/mainwindow.hpp" +#include +#include "abaddon.hpp" + +#ifdef _WIN32 + #pragma comment(lib, "crypt32.lib") +#endif + +int Abaddon::DoMainLoop() { + m_gtk_app = Gtk::Application::create("com.github.lorpus.abaddon"); + + MainWindow main; + main.SetAbaddon(this); + main.set_title("Abaddon"); + main.show(); + + m_gtk_app->signal_shutdown().connect([&]() { + m_discord.Stop(); + }); + + /*sigc::connection draw_signal_handler = main.signal_draw().connect([&](const Cairo::RefPtr &ctx) -> bool { + draw_signal_handler.disconnect(); + + return false; + });*/ + + return m_gtk_app->run(main); +} + +void Abaddon::StartDiscordThread() { + m_discord.Start(); +} + +void Abaddon::ActionConnect() { + if (!m_discord.IsStarted()) + StartDiscordThread(); +} + +int main(int argc, char **argv) { + Abaddon abaddon; + return abaddon.DoMainLoop(); +} diff --git a/abaddon.hpp b/abaddon.hpp new file mode 100644 index 0000000..6842d20 --- /dev/null +++ b/abaddon.hpp @@ -0,0 +1,14 @@ +#include +#include "discord/discord.hpp" + +class Abaddon { +public: + int DoMainLoop(); + void StartDiscordThread(); + + void ActionConnect(); + +private: + Glib::RefPtr m_gtk_app; + DiscordClient m_discord; +}; \ No newline at end of file diff --git a/components/channels.cpp b/components/channels.cpp new file mode 100644 index 0000000..364f18b --- /dev/null +++ b/components/channels.cpp @@ -0,0 +1,12 @@ +#include "channels.hpp" + +ChannelList::ChannelList() { + m_main = Gtk::manage(new Gtk::ScrolledWindow); + m_list = Gtk::manage(new Gtk::ListBox); + + m_main->add(*m_list); +} + +Gtk::Widget* ChannelList::GetRoot() const { + return m_main; +} diff --git a/components/channels.hpp b/components/channels.hpp new file mode 100644 index 0000000..0bc1855 --- /dev/null +++ b/components/channels.hpp @@ -0,0 +1,12 @@ +#pragma once +#include + +class ChannelList { +public: + ChannelList(); + Gtk::Widget *GetRoot() const; + +protected: + Gtk::ListBox *m_list; + Gtk::ScrolledWindow *m_main; +}; diff --git a/discord/discord.cpp b/discord/discord.cpp new file mode 100644 index 0000000..cd1e723 --- /dev/null +++ b/discord/discord.cpp @@ -0,0 +1,85 @@ +#include "discord.hpp" + +DiscordClient::DiscordClient() {} + +void DiscordClient::Start() { + if (m_client_connected) + throw std::runtime_error("attempt to start client twice consecutively"); + + m_client_connected = true; + m_websocket.StartConnection(DiscordGateway); + m_websocket.SetJSONCallback(std::bind(&DiscordClient::HandleGatewayMessage, this, std::placeholders::_1)); +} + +void DiscordClient::Stop() { + if (!m_client_connected) return; + m_heartbeat_waiter.kill(); + m_heartbeat_thread.join(); + m_client_connected = false; +} + +bool DiscordClient::IsStarted() const { + return m_client_connected; +} + +void DiscordClient::HandleGatewayMessage(nlohmann::json j) { + GatewayMessage m; + try { + m = j; + } catch (std::exception &e) { + printf("Error decoding JSON. Discarding message: %s\n", e.what()); + return; + } + + switch (m.Opcode) { + case GatewayOp::Hello: { + HelloMessageData d = m.Data; + m_heartbeat_msec = d.HeartbeatInterval; + m_heartbeat_thread = std::thread(std::bind(&DiscordClient::HeartbeatThread, this)); + } break; + case GatewayOp::HeartbeatAck: { + m_heartbeat_acked = true; + } break; + default: + printf("Unknown opcode %d\n", m.Opcode); + break; + } +} + +void DiscordClient::HeartbeatThread() { + while (m_client_connected) { + if (!m_heartbeat_acked) { + printf("wow! a heartbeat wasn't acked! how could this happen?"); + } + + m_heartbeat_acked = false; + + HeartbeatMessage msg; + msg.Sequence = m_last_sequence; + nlohmann::json j = msg; + m_websocket.Send(j.dump()); + + if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) + break; + } +} + +void from_json(const nlohmann::json &j, GatewayMessage &m) { + j.at("op").get_to(m.Opcode); + m.Data = j.at("d"); + + if (j.contains("t") && !j.at("t").is_null()) + j.at("t").get_to(m.Type); +} + +void from_json(const nlohmann::json &j, HelloMessageData &m) { + j.at("heartbeat_interval").get_to(m.HeartbeatInterval); +} + +void to_json(nlohmann::json &j, const HeartbeatMessage &m) { + j["op"] = GatewayOp::Heartbeat; + if (m.Sequence == -1) + j["d"] = nullptr; + else + j["d"] = m.Sequence; +} diff --git a/discord/discord.hpp b/discord/discord.hpp new file mode 100644 index 0000000..692d57b --- /dev/null +++ b/discord/discord.hpp @@ -0,0 +1,75 @@ +#pragma once +#include "websocket.hpp" +#include +#include + +enum class GatewayOp : int { + Heartbeat = 1, + Hello = 10, + HeartbeatAck = 11, +}; + +struct GatewayMessage { + GatewayOp Opcode; + nlohmann::json Data; + std::string Type; + + friend void from_json(const nlohmann::json &j, GatewayMessage &m); +}; + +struct HelloMessageData { + int HeartbeatInterval; + + friend void from_json(const nlohmann::json &j, HelloMessageData &m); +}; + +struct HeartbeatMessage : GatewayMessage { + int Sequence; + + friend void to_json(nlohmann::json &j, const HeartbeatMessage &m); +}; + +// https://stackoverflow.com/questions/29775153/stopping-long-sleep-threads/29775639#29775639 +class HeartbeatWaiter { +public: + template + bool wait_for(std::chrono::duration const &time) const { + std::unique_lock lock(m); + return !cv.wait_for(lock, time, [&] { return terminate; }); + } + void kill() { + std::unique_lock lock(m); + terminate = true; + cv.notify_all(); + } + +private: + mutable std::condition_variable cv; + mutable std::mutex m; + bool terminate = false; +}; + +class DiscordClient { +public: + static const constexpr char *DiscordGateway = "wss://gateway.discord.gg/?v=6&encoding=json"; + static const constexpr char *DiscordAPI = "https://discord.com/api"; + +public: + DiscordClient(); + void Start(); + void Stop(); + bool IsStarted() const; + +private: + void HandleGatewayMessage(nlohmann::json msg); + void HeartbeatThread(); + + Websocket m_websocket; + bool m_client_connected = false; + + std::thread m_heartbeat_thread; + int m_last_sequence = -1; + int m_heartbeat_msec = 0; + HeartbeatWaiter m_heartbeat_waiter; + bool m_heartbeat_acked = true; +}; diff --git a/discord/websocket.cpp b/discord/websocket.cpp new file mode 100644 index 0000000..3590db3 --- /dev/null +++ b/discord/websocket.cpp @@ -0,0 +1,30 @@ +#include "websocket.hpp" +#include +#include + +Websocket::Websocket() {} + +void Websocket::StartConnection(std::string url) { + m_websocket.setUrl(url); + m_websocket.setOnMessageCallback(std::bind(&Websocket::OnMessage, this, std::placeholders::_1)); + m_websocket.start(); +} + +void Websocket::SetJSONCallback(JSONCallback_t func) { + m_json_callback = func; +} + +void Websocket::Send(const std::string &str) { + m_websocket.sendText(str); +} + +void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) { + switch (msg->type) { + case ix::WebSocketMessageType::Message: + printf("%s\n", msg->str.c_str()); + auto obj = nlohmann::json::parse(msg->str); + if (m_json_callback) + m_json_callback(obj); + break; + } +} diff --git a/discord/websocket.hpp b/discord/websocket.hpp new file mode 100644 index 0000000..47a60d5 --- /dev/null +++ b/discord/websocket.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include +#include +#include + +class Websocket { +public: + Websocket(); + void StartConnection(std::string url); + + using JSONCallback_t = std::function; + void SetJSONCallback(JSONCallback_t func); + void Send(const std::string &str); + +private: + void OnMessage(const ix::WebSocketMessagePtr &msg); + + JSONCallback_t m_json_callback; + ix::WebSocket m_websocket; +}; diff --git a/windows/mainwindow.cpp b/windows/mainwindow.cpp new file mode 100644 index 0000000..d085362 --- /dev/null +++ b/windows/mainwindow.cpp @@ -0,0 +1,31 @@ +#include "mainwindow.hpp" +#include "../abaddon.hpp" + +MainWindow::MainWindow() + : m_main_box(Gtk::ORIENTATION_VERTICAL) { + set_default_size(800, 600); + + m_menu_discord.set_label("Discord"); + m_menu_discord.set_submenu(m_menu_discord_sub); + m_menu_discord_connect.set_label("Connect"); + m_menu_discord_sub.append(m_menu_discord_connect); + m_menu_discord.set_submenu(m_menu_discord_sub); + m_menu_bar.append(m_menu_discord); + + m_menu_discord_connect.signal_activate().connect([&] { + m_abaddon->ActionConnect(); // this feels maybe not too smart + }); + + m_main_box.add(m_menu_bar); + + auto *channel_list = m_channel_list.GetRoot(); + m_main_box.add(*channel_list); + + add(m_main_box); + + show_all_children(); +} + +void MainWindow::SetAbaddon(Abaddon* ptr) { + m_abaddon = ptr; +} diff --git a/windows/mainwindow.hpp b/windows/mainwindow.hpp new file mode 100644 index 0000000..6e7ab16 --- /dev/null +++ b/windows/mainwindow.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "../components/channels.hpp" +#include + +class Abaddon; +class MainWindow : public Gtk::Window { +public: + MainWindow(); + void SetAbaddon(Abaddon *ptr); + +protected: + Gtk::Box m_main_box; + + ChannelList m_channel_list; + + Gtk::MenuBar m_menu_bar; + Gtk::MenuItem m_menu_discord; + Gtk::Menu m_menu_discord_sub; + Gtk::MenuItem m_menu_discord_connect; + + Abaddon *m_abaddon = nullptr; +};