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;
+};