forked from OpenGamers/abaddon
fix guild order, add copy id guild, add broken zlib, start member list
This commit is contained in:
parent
6b72931ba7
commit
82a21bd085
@ -114,7 +114,7 @@
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;USE_LOCAL_PROXY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;USE_LOCAL_PROXY;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
@ -130,7 +130,7 @@
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
@ -146,6 +146,7 @@
|
||||
<ClCompile Include="abaddon.cpp" />
|
||||
<ClCompile Include="components\channels.cpp" />
|
||||
<ClCompile Include="components\chatwindow.cpp" />
|
||||
<ClCompile Include="components\memberlist.cpp" />
|
||||
<ClCompile Include="dialogs\token.cpp" />
|
||||
<ClCompile Include="discord\discord.cpp" />
|
||||
<ClCompile Include="discord\http.cpp" />
|
||||
@ -157,6 +158,7 @@
|
||||
<ClInclude Include="components\channels.hpp" />
|
||||
<ClInclude Include="abaddon.hpp" />
|
||||
<ClInclude Include="components\chatwindow.hpp" />
|
||||
<ClInclude Include="components\memberlist.hpp" />
|
||||
<ClInclude Include="dialogs\token.hpp" />
|
||||
<ClInclude Include="discord\discord.hpp" />
|
||||
<ClInclude Include="discord\http.hpp" />
|
||||
|
@ -42,6 +42,9 @@
|
||||
<ClCompile Include="components\chatwindow.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="components\memberlist.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="windows\mainwindow.hpp">
|
||||
@ -71,5 +74,8 @@
|
||||
<ClInclude Include="components\chatwindow.hpp">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="components\memberlist.hpp">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
5
README.md
Normal file
5
README.md
Normal file
@ -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/)
|
56
abaddon.cpp
56
abaddon.cpp
@ -107,37 +107,49 @@ void Abaddon::ActionSetToken() {
|
||||
}
|
||||
|
||||
void Abaddon::ActionMoveGuildUp(Snowflake id) {
|
||||
UserSettingsData d = m_discord.GetUserSettings();
|
||||
std::vector<Snowflake> &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<Snowflake>::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<Snowflake> 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<Snowflake> &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<Snowflake>::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<Snowflake> 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) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
9
components/memberlist.cpp
Normal file
9
components/memberlist.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include "memberlist.hpp"
|
||||
|
||||
MemberList::MemberList() {
|
||||
m_main = Gtk::manage(new Gtk::Box);
|
||||
}
|
||||
|
||||
Gtk::Widget *MemberList::GetRoot() const {
|
||||
return m_main;
|
||||
}
|
11
components/memberlist.hpp
Normal file
11
components/memberlist.hpp
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
|
||||
class MemberList {
|
||||
public:
|
||||
MemberList();
|
||||
Gtk::Widget *GetRoot() const;
|
||||
|
||||
private:
|
||||
Gtk::Box *m_main;
|
||||
};
|
@ -3,7 +3,11 @@
|
||||
#include <cassert>
|
||||
|
||||
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<std::pair<Snowflake, GuildData>> DiscordClient::GetUserSortedGuilds(
|
||||
std::vector<std::pair<Snowflake, GuildData>> sorted_guilds;
|
||||
|
||||
if (m_user_settings.GuildPositions.size()) {
|
||||
std::unordered_set<Snowflake> 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<Snowflake> 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<uint8_t> 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;
|
||||
|
@ -4,8 +4,12 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <mutex>
|
||||
#ifdef ABADDON_USE_COMPRESSED_SOCKET
|
||||
#include <zlib.h>
|
||||
#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<uint8_t> m_compressed_buf;
|
||||
std::vector<uint8_t> 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();
|
||||
|
@ -1,10 +1,10 @@
|
||||
#include "websocket.hpp"
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ public:
|
||||
Websocket();
|
||||
void StartConnection(std::string url);
|
||||
|
||||
using JSONCallback_t = std::function<void(nlohmann::json)>;
|
||||
void SetJSONCallback(JSONCallback_t func);
|
||||
using MessageCallback_t = std::function<void(std::string data)>;
|
||||
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;
|
||||
};
|
||||
|
@ -4,7 +4,8 @@
|
||||
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");
|
||||
@ -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();
|
||||
|
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "../components/channels.hpp"
|
||||
#include "../components/chatwindow.hpp"
|
||||
#include "../components/memberlist.hpp"
|
||||
#include <gtkmm.h>
|
||||
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user