diff --git a/Abaddon.vcxproj b/Abaddon.vcxproj index 87a56fd..10c6072 100644 --- a/Abaddon.vcxproj +++ b/Abaddon.vcxproj @@ -114,7 +114,7 @@ Level3 true - _DEBUG;_CONSOLE;USE_LOCAL_PROXY;%(PreprocessorDefinitions) + _DEBUG;_CONSOLE;USE_LOCAL_PROXY;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) true stdcpp17 true @@ -130,7 +130,7 @@ true true true - NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) true stdcpp17 true @@ -146,6 +146,7 @@ + @@ -157,6 +158,7 @@ + diff --git a/Abaddon.vcxproj.filters b/Abaddon.vcxproj.filters index 10ecb62..4ff8b0a 100644 --- a/Abaddon.vcxproj.filters +++ b/Abaddon.vcxproj.filters @@ -42,6 +42,9 @@ Source Files + + Source Files + @@ -71,5 +74,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ea86a9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Built using: +* [gtkmm](https://www.gtkmm.org/en/) +* [JSON for Modern C++](https://github.com/nlohmann/json) +* [IXWebSocket](https://github.com/machinezone/IXWebSocket) +* [C++ Requests: Curl for People](https://github.com/whoshuu/cpr/) diff --git a/abaddon.cpp b/abaddon.cpp index 74d980d..04d80ac 100644 --- a/abaddon.cpp +++ b/abaddon.cpp @@ -107,37 +107,49 @@ void Abaddon::ActionSetToken() { } void Abaddon::ActionMoveGuildUp(Snowflake id) { - UserSettingsData d = m_discord.GetUserSettings(); - std::vector &pos = d.GuildPositions; - if (pos.size() == 0) { - auto x = m_discord.GetUserSortedGuilds(); - for (const auto &pair : x) - pos.push_back(pair.first); + auto order = m_discord.GetUserSortedGuilds(); + // get iter to target + decltype(order)::iterator target_iter; + for (auto it = order.begin(); it != order.end(); it++) { + if (it->first == id) { + target_iter = it; + break; + } } - auto it = std::find(pos.begin(), pos.end(), id); - assert(it != pos.end()); - std::vector::iterator left = it - 1; - std::swap(*left, *it); + decltype(order)::iterator left = target_iter - 1; + std::swap(*left, *target_iter); - m_discord.UpdateSettingsGuildPositions(pos); + std::vector new_sort; + for (const auto& x : order) + new_sort.push_back(x.first); + + m_discord.UpdateSettingsGuildPositions(new_sort); } void Abaddon::ActionMoveGuildDown(Snowflake id) { - UserSettingsData d = m_discord.GetUserSettings(); - std::vector &pos = d.GuildPositions; - if (pos.size() == 0) { - auto x = m_discord.GetUserSortedGuilds(); - for (const auto &pair : x) - pos.push_back(pair.first); + auto order = m_discord.GetUserSortedGuilds(); + // get iter to target + decltype(order)::iterator target_iter; + for (auto it = order.begin(); it != order.end(); it++) { + if (it->first == id) { + target_iter = it; + break; + } } - auto it = std::find(pos.begin(), pos.end(), id); - assert(it != pos.end()); - std::vector::iterator right = it + 1; - std::swap(*right, *it); + decltype(order)::iterator right = target_iter + 1; + std::swap(*right, *target_iter); - m_discord.UpdateSettingsGuildPositions(pos); + std::vector new_sort; + for (const auto &x : order) + new_sort.push_back(x.first); + + m_discord.UpdateSettingsGuildPositions(new_sort); +} + +void Abaddon::ActionCopyGuildID(Snowflake id) { + Gtk::Clipboard::get()->set_text(std::to_string(id)); } void Abaddon::ActionListChannelItemClick(Snowflake id) { diff --git a/abaddon.hpp b/abaddon.hpp index 9d58358..87aebcf 100644 --- a/abaddon.hpp +++ b/abaddon.hpp @@ -23,6 +23,7 @@ public: void ActionSetToken(); void ActionMoveGuildUp(Snowflake id); void ActionMoveGuildDown(Snowflake id); + void ActionCopyGuildID(Snowflake id); void ActionListChannelItemClick(Snowflake id); void ActionChatInputSubmit(std::string msg, Snowflake channel); diff --git a/components/channels.cpp b/components/channels.cpp index d556d40..b913919 100644 --- a/components/channels.cpp +++ b/components/channels.cpp @@ -16,6 +16,10 @@ ChannelList::ChannelList() { m_guild_menu_down->signal_activate().connect(sigc::mem_fun(*this, &ChannelList::on_menu_move_down)); m_guild_menu.append(*m_guild_menu_down); + m_guild_menu_copyid = Gtk::manage(new Gtk::MenuItem("_Copy ID", true)); + m_guild_menu_copyid->signal_activate().connect(sigc::mem_fun(*this, &ChannelList::on_menu_copyid)); + m_guild_menu.append(*m_guild_menu_copyid); + m_guild_menu.show_all(); m_list->set_activate_on_single_click(true); @@ -269,6 +273,11 @@ void ChannelList::on_menu_move_down() { m_abaddon->ActionMoveGuildDown(m_infos[row].ID); } +void ChannelList::on_menu_copyid() { + auto row = m_list->get_selected_row(); + m_abaddon->ActionCopyGuildID(m_infos[row].ID); +} + void ChannelList::AttachMenuHandler(Gtk::ListBoxRow *row) { row->signal_button_press_event().connect([&, row](GdkEventButton *e) -> bool { if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) { diff --git a/components/channels.hpp b/components/channels.hpp index 1576783..fce1ce9 100644 --- a/components/channels.hpp +++ b/components/channels.hpp @@ -43,8 +43,10 @@ protected: Gtk::Menu m_guild_menu; Gtk::MenuItem *m_guild_menu_up; Gtk::MenuItem *m_guild_menu_down; + Gtk::MenuItem *m_guild_menu_copyid; void on_menu_move_up(); void on_menu_move_down(); + void on_menu_copyid(); Glib::Dispatcher m_update_dispatcher; mutable std::mutex m_update_mutex; diff --git a/components/memberlist.cpp b/components/memberlist.cpp new file mode 100644 index 0000000..f1553b8 --- /dev/null +++ b/components/memberlist.cpp @@ -0,0 +1,9 @@ +#include "memberlist.hpp" + +MemberList::MemberList() { + m_main = Gtk::manage(new Gtk::Box); +} + +Gtk::Widget *MemberList::GetRoot() const { + return m_main; +} diff --git a/components/memberlist.hpp b/components/memberlist.hpp new file mode 100644 index 0000000..5434324 --- /dev/null +++ b/components/memberlist.hpp @@ -0,0 +1,11 @@ +#pragma once +#include + +class MemberList { +public: + MemberList(); + Gtk::Widget *GetRoot() const; + +private: + Gtk::Box *m_main; +}; diff --git a/discord/discord.cpp b/discord/discord.cpp index 9aa8eed..15a719c 100644 --- a/discord/discord.cpp +++ b/discord/discord.cpp @@ -3,7 +3,11 @@ #include DiscordClient::DiscordClient() - : m_http(DiscordAPI) { + : m_http(DiscordAPI) +#ifdef ABADDON_USE_COMPRESSED_SOCKET + , m_decompress_buf(InflateChunkSize) +#endif +{ LoadEventMap(); } @@ -17,7 +21,7 @@ void DiscordClient::Start() { m_client_connected = true; m_websocket.StartConnection(DiscordGateway); - m_websocket.SetJSONCallback(std::bind(&DiscordClient::HandleGatewayMessage, this, std::placeholders::_1)); + m_websocket.SetMessageCallback(std::bind(&DiscordClient::HandleGatewayMessageRaw, this, std::placeholders::_1)); } void DiscordClient::Stop() { @@ -25,7 +29,7 @@ void DiscordClient::Stop() { if (!m_client_connected) return; m_heartbeat_waiter.kill(); - m_heartbeat_thread.join(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); m_client_connected = false; m_websocket.Stop(); @@ -51,9 +55,21 @@ std::vector> DiscordClient::GetUserSortedGuilds( std::vector> sorted_guilds; if (m_user_settings.GuildPositions.size()) { + std::unordered_set positioned_guilds(m_user_settings.GuildPositions.begin(), m_user_settings.GuildPositions.end()); + // guilds not in the guild_positions object are at the top of the list, descending by guild ID + std::set unpositioned_guilds; + for (const auto &[id, guild] : m_guilds) { + if (positioned_guilds.find(id) == positioned_guilds.end()) + unpositioned_guilds.insert(id); + } + + // unpositioned_guilds now has unpositioned guilds in ascending order + for (auto it = unpositioned_guilds.rbegin(); it != unpositioned_guilds.rend(); it++) + sorted_guilds.push_back(std::make_pair(*it, m_guilds.at(*it))); + + // now the rest go at the end in the order they are sorted for (const auto &id : m_user_settings.GuildPositions) { - auto &guild = m_guilds.at(id); - sorted_guilds.push_back(std::make_pair(id, guild)); + sorted_guilds.push_back(std::make_pair(id, m_guilds.at(id))); } } else { // default sort is alphabetic for (auto &it : m_guilds) @@ -130,10 +146,62 @@ void DiscordClient::UpdateToken(std::string token) { m_http.SetAuth(token); } -void DiscordClient::HandleGatewayMessage(nlohmann::json j) { +std::string DiscordClient::DecompressGatewayMessage(std::string str) { + return std::string(); +} + +void DiscordClient::HandleGatewayMessageRaw(std::string str) { +#ifdef ABADDON_USE_COMPRESSED_SOCKET // fuck you work + // handles multiple zlib compressed messages, calling HandleGatewayMessage when a full message is received + std::vector buf(str.begin(), str.end()); + int len = buf.size(); + bool has_suffix = buf[len - 4] == 0x00 && buf[len - 3] == 0x00 && buf[len - 2] == 0xFF && buf[len - 1] == 0xFF; + + m_compressed_buf.insert(m_compressed_buf.begin(), buf.begin(), buf.end()); + + if (!has_suffix) return; + + z_stream z; + std::memset(&z, 0, sizeof(z)); + + assert(inflateInit2(&z, 15) == 0); + + z.next_in = m_compressed_buf.data(); + z.avail_in = m_compressed_buf.size(); + + // loop in case of really big messages (e.g. READY) + while (true) { + z.next_out = m_decompress_buf.data() + z.total_out; + z.avail_out = m_decompress_buf.size() - z.total_out; + + int err = inflate(&z, Z_SYNC_FLUSH); + if ((err == Z_OK || err == Z_BUF_ERROR) && z.avail_in > 0) { + m_decompress_buf.resize(m_decompress_buf.size() + InflateChunkSize); + } else { + if (err != Z_OK) { + fprintf(stderr, "Error decompressing input buffer %d (%d/%d)\n", err, z.avail_in, z.avail_out); + } else { + HandleGatewayMessage(std::string(m_decompress_buf.begin(), m_decompress_buf.begin() + z.total_out)); + if (m_decompress_buf.size() > InflateChunkSize) + m_decompress_buf.resize(InflateChunkSize); + } + + inflateEnd(&z); + + break; + } + } + + m_compressed_buf.clear(); +#else + HandleGatewayMessage(str); +#endif +} + +void DiscordClient::HandleGatewayMessage(std::string str) { GatewayMessage m; try { - m = j; + m = nlohmann::json::parse(str); } catch (std::exception &e) { printf("Error decoding JSON. Discarding message: %s\n", e.what()); return; diff --git a/discord/discord.hpp b/discord/discord.hpp index ab390dc..3a98b40 100644 --- a/discord/discord.hpp +++ b/discord/discord.hpp @@ -4,8 +4,12 @@ #include #include #include +#include #include #include +#ifdef ABADDON_USE_COMPRESSED_SOCKET + #include +#endif // bruh #ifdef GetMessage @@ -372,7 +376,11 @@ class DiscordClient { friend class Abaddon; public: +#ifdef ABADDON_USE_COMPRESSED_SOCKET + static const constexpr char *DiscordGateway = "wss://gateway.discord.gg/?v=6&encoding=json&compress=zlib-stream"; +#else static const constexpr char *DiscordGateway = "wss://gateway.discord.gg/?v=6&encoding=json"; +#endif static const constexpr char *DiscordAPI = "https://discord.com/api"; static const constexpr char *GatewayIdentity = "Discord"; @@ -400,7 +408,14 @@ public: void UpdateToken(std::string token); private: - void HandleGatewayMessage(nlohmann::json msg); +#ifdef ABADDON_USE_COMPRESSED_SOCKET + static const constexpr int InflateChunkSize = 0x10000; + std::vector m_compressed_buf; + std::vector m_decompress_buf; +#endif + std::string DecompressGatewayMessage(std::string str); + void HandleGatewayMessageRaw(std::string str); + void HandleGatewayMessage(std::string str); void HandleGatewayReady(const GatewayMessage &msg); void HandleGatewayMessageCreate(const GatewayMessage &msg); void HeartbeatThread(); diff --git a/discord/websocket.cpp b/discord/websocket.cpp index 8232ac6..2251a01 100644 --- a/discord/websocket.cpp +++ b/discord/websocket.cpp @@ -1,10 +1,10 @@ #include "websocket.hpp" #include -#include Websocket::Websocket() {} void Websocket::StartConnection(std::string url) { + m_websocket.disableAutomaticReconnection(); m_websocket.setUrl(url); m_websocket.setOnMessageCallback(std::bind(&Websocket::OnMessage, this, std::placeholders::_1)); m_websocket.start(); @@ -19,8 +19,8 @@ bool Websocket::IsOpen() const { return state == ix::ReadyState::Open; } -void Websocket::SetJSONCallback(JSONCallback_t func) { - m_json_callback = func; +void Websocket::SetMessageCallback(MessageCallback_t func) { + m_callback = func; } void Websocket::Send(const std::string &str) { @@ -39,15 +39,8 @@ void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) { // printf("%s\n", msg->str.substr(0, 1000).c_str()); //else // printf("%s\n", msg->str.c_str()); - nlohmann::json obj; - try { - obj = nlohmann::json::parse(msg->str); - } catch (std::exception &e) { - printf("Error decoding JSON. Discarding message: %s\n", e.what()); - return; - } - if (m_json_callback) - m_json_callback(obj); + if (m_callback) + m_callback(msg->str); } break; } } diff --git a/discord/websocket.hpp b/discord/websocket.hpp index dc8cbec..8e3aa94 100644 --- a/discord/websocket.hpp +++ b/discord/websocket.hpp @@ -10,8 +10,8 @@ public: Websocket(); void StartConnection(std::string url); - using JSONCallback_t = std::function; - void SetJSONCallback(JSONCallback_t func); + using MessageCallback_t = std::function; + void SetMessageCallback(MessageCallback_t func); void Send(const std::string &str); void Send(const nlohmann::json &j); void Stop(); @@ -20,6 +20,6 @@ public: private: void OnMessage(const ix::WebSocketMessagePtr &msg); - JSONCallback_t m_json_callback; + MessageCallback_t m_callback; ix::WebSocket m_websocket; }; diff --git a/windows/mainwindow.cpp b/windows/mainwindow.cpp index 41f7ee8..3404799 100644 --- a/windows/mainwindow.cpp +++ b/windows/mainwindow.cpp @@ -4,9 +4,10 @@ MainWindow::MainWindow() : m_main_box(Gtk::ORIENTATION_VERTICAL) , m_content_box(Gtk::ORIENTATION_HORIZONTAL) - , m_chan_chat_paned(Gtk::ORIENTATION_HORIZONTAL) { + , m_chan_chat_paned(Gtk::ORIENTATION_HORIZONTAL) + , m_chat_members_paned(Gtk::ORIENTATION_HORIZONTAL) { set_default_size(1200, 800); - + m_menu_discord.set_label("Discord"); m_menu_discord.set_submenu(m_menu_discord_sub); m_menu_discord_connect.set_label("Connect"); @@ -39,18 +40,32 @@ MainWindow::MainWindow() m_main_box.add(m_content_box); auto *channel_list = m_channel_list.GetRoot(); - channel_list->set_vexpand(true); - channel_list->set_size_request(-1, -1); - m_chan_chat_paned.pack1(*channel_list); + auto *member_list = m_members.GetRoot(); auto *chat = m_chat.GetRoot(); + chat->set_vexpand(true); chat->set_hexpand(true); - m_chan_chat_paned.pack2(*chat); - m_chan_chat_paned.set_position(200); + + channel_list->set_vexpand(true); + channel_list->set_size_request(-1, -1); + + member_list->set_vexpand(true); + + m_chan_chat_paned.pack1(*channel_list); + m_chan_chat_paned.pack2(m_chat_members_paned); m_chan_chat_paned.child_property_shrink(*channel_list) = true; m_chan_chat_paned.child_property_resize(*channel_list) = true; + m_chan_chat_paned.set_position(200); m_content_box.add(m_chan_chat_paned); + m_chat_members_paned.pack1(*chat); + m_chat_members_paned.pack2(*member_list); + m_chat_members_paned.child_property_shrink(*member_list) = true; + m_chat_members_paned.child_property_resize(*member_list) = true; + int w, h; + get_default_size(w, h); // :s + m_chat_members_paned.set_position(w - m_chan_chat_paned.get_position() - 150); + add(m_main_box); show_all_children(); diff --git a/windows/mainwindow.hpp b/windows/mainwindow.hpp index 240d744..f3085f5 100644 --- a/windows/mainwindow.hpp +++ b/windows/mainwindow.hpp @@ -1,6 +1,7 @@ #pragma once #include "../components/channels.hpp" #include "../components/chatwindow.hpp" +#include "../components/memberlist.hpp" #include class Abaddon; @@ -20,9 +21,11 @@ protected: Gtk::Box m_main_box; Gtk::Box m_content_box; Gtk::Paned m_chan_chat_paned; + Gtk::Paned m_chat_members_paned; ChannelList m_channel_list; ChatWindow m_chat; + MemberList m_members; Gtk::MenuBar m_menu_bar; Gtk::MenuItem m_menu_discord;