remote auth impl. up to QR code display

This commit is contained in:
ouwou 2023-06-30 20:43:08 -04:00
parent ec6f18ff12
commit 3b206e1121
13 changed files with 567 additions and 7 deletions

3
.gitmodules vendored
View File

@ -13,3 +13,6 @@
[submodule "subprojects/miniaudio"]
path = subprojects/miniaudio
url = https://github.com/mackron/miniaudio
[submodule "subprojects/qrcodegen"]
path = subprojects/qrcodegen
url = https://github.com/nayuki/QR-Code-generator

View File

@ -61,6 +61,12 @@ target_include_directories(abaddon PUBLIC ${ZLIB_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
add_library(qrcodegen subprojects/qrcodegen/cpp/qrcodegen.hpp subprojects/qrcodegen/cpp/qrcodegen.cpp)
target_include_directories(qrcodegen PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/subprojects/qrcodegen/cpp")
target_link_libraries(abaddon qrcodegen)
target_include_directories(abaddon PUBLIC "subprojects/qrcodegen/cpp")
target_precompile_headers(abaddon PRIVATE <gtkmm.h> src/abaddon.hpp src/util.hpp)
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
@ -151,11 +157,11 @@ if (ENABLE_VOICE)
target_link_libraries(abaddon ${CMAKE_DL_LIBS})
if(APPLE)
target_link_libraries(abaddon "-framework CoreFoundation")
target_link_libraries(abaddon "-framework CoreAudio")
target_link_libraries(abaddon "-framework AudioToolbox")
endif()
if (APPLE)
target_link_libraries(abaddon "-framework CoreFoundation")
target_link_libraries(abaddon "-framework CoreAudio")
target_link_libraries(abaddon "-framework AudioToolbox")
endif ()
endif ()

View File

@ -21,6 +21,7 @@
#include "windows/voicewindow.hpp"
#include "startup.hpp"
#include "notifications/notifications.hpp"
#include "remoteauth/remoteauthdialog.hpp"
#ifdef WITH_LIBHANDY
#include <handy.h>
@ -267,6 +268,7 @@ int Abaddon::StartGTK() {
m_main_window->signal_action_connect().connect(sigc::mem_fun(*this, &Abaddon::ActionConnect));
m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect));
m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken));
m_main_window->signal_action_login_qr().connect(sigc::mem_fun(*this, &Abaddon::ActionLoginQR));
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
@ -834,6 +836,16 @@ void Abaddon::ActionSetToken() {
m_main_window->UpdateMenus();
}
void Abaddon::ActionLoginQR() {
RemoteAuthDialog dlg(*m_main_window);
auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) {
m_discord_token = dlg.GetToken();
m_main_window->UpdateComponents();
}
m_main_window->UpdateMenus();
}
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
if (!id.IsValid()) {
m_discord.SetReferringChannel(Snowflake::Invalid);
@ -1142,6 +1154,7 @@ int main(int argc, char **argv) {
auto log_audio = spdlog::stdout_color_mt("audio");
auto log_voice = spdlog::stdout_color_mt("voice");
auto log_discord = spdlog::stdout_color_mt("discord");
auto log_ra = spdlog::stdout_color_mt("remote-auth");
Gtk::Main::init_gtkmm_internals(); // why???
return Abaddon::Get().StartGTK();

View File

@ -41,6 +41,7 @@ public:
void ActionConnect();
void ActionDisconnect();
void ActionSetToken();
void ActionLoginQR();
void ActionJoinGuildDialog();
void ActionChannelOpened(Snowflake id, bool expand_to = true);
void ActionChatInputSubmit(ChatSubmitParams data);

View File

@ -26,7 +26,7 @@ void Websocket::StartConnection(const std::string &url) {
m_websocket->disableAutomaticReconnection();
m_websocket->setUrl(url);
m_websocket->setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward<decltype(msg)>(msg)); });
m_websocket->setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent } }); // idk if this actually works
m_websocket->setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent }, { "Origin", "https://discord.com" } }); // idk if this actually works
m_websocket->start();
}
@ -81,6 +81,9 @@ void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) {
case ix::WebSocketMessageType::Message: {
m_signal_message.emit(msg->str);
} break;
case ix::WebSocketMessageType::Error: {
m_log->error("Websocket error: Status: {} Reason: {}", msg->errorInfo.http_status, msg->errorInfo.reason);
} break;
default:
break;
}

View File

@ -0,0 +1,321 @@
#include "remoteauthclient.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/fmt/bin_to_hex.h>
struct RemoteAuthGatewayMessage {
std::string Opcode;
friend void from_json(const nlohmann::json &j, RemoteAuthGatewayMessage &m) {
j.at("op").get_to(m.Opcode);
}
};
struct RemoteAuthHelloMessage {
int TimeoutMS;
int HeartbeatInterval;
friend void from_json(const nlohmann::json &j, RemoteAuthHelloMessage &m) {
j.at("timeout_ms").get_to(m.TimeoutMS);
j.at("heartbeat_interval").get_to(m.HeartbeatInterval);
}
};
struct RemoteAuthHeartbeatMessage {
friend void to_json(nlohmann::json &j, const RemoteAuthHeartbeatMessage &m) {
j["op"] = "heartbeat";
}
};
struct RemoteAuthInitMessage {
std::string EncodedPublicKey;
friend void to_json(nlohmann::json &j, const RemoteAuthInitMessage &m) {
j["op"] = "init";
j["encoded_public_key"] = m.EncodedPublicKey;
}
};
struct RemoteAuthNonceProofMessage {
std::string EncryptedNonce;
std::string Nonce;
friend void from_json(const nlohmann::json &j, RemoteAuthNonceProofMessage &m) {
j.at("encrypted_nonce").get_to(m.EncryptedNonce);
}
friend void to_json(nlohmann::json &j, const RemoteAuthNonceProofMessage &m) {
j["op"] = "nonce_proof";
j["nonce"] = m.Nonce;
}
};
struct RemoteAuthFingerprintMessage {
std::string Fingerprint;
friend void from_json(const nlohmann::json &j, RemoteAuthFingerprintMessage &m) {
j.at("fingerprint").get_to(m.Fingerprint);
}
};
struct RemoteAuthPendingTicketMessage {
std::string EncryptedUserPayload;
friend void from_json(const nlohmann::json &j, RemoteAuthPendingTicketMessage &m) {
j.at("encrypted_user_payload").get_to(m.EncryptedUserPayload);
}
};
RemoteAuthClient::RemoteAuthClient()
: m_ws("remote-auth-ws")
, m_log(spdlog::get("remote-auth")) {
m_ws.signal_open().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnWebsocketOpen));
m_ws.signal_close().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnWebsocketClose));
m_ws.signal_message().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnWebsocketMessage));
m_dispatcher.connect(sigc::mem_fun(*this, &RemoteAuthClient::OnDispatch));
}
RemoteAuthClient::~RemoteAuthClient() {
Stop();
}
void RemoteAuthClient::Start() {
if (IsConnected()) {
Stop();
}
m_connected = true;
m_heartbeat_waiter.revive();
m_ws.StartConnection("wss://remote-auth-gateway.discord.gg/?v=2");
}
void RemoteAuthClient::Stop() {
if (!IsConnected()) {
m_log->warn("Requested stop while not connected");
return;
}
m_connected = false;
m_ws.Stop(1000);
m_heartbeat_waiter.kill();
if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join();
}
bool RemoteAuthClient::IsConnected() const noexcept {
return m_connected;
}
void RemoteAuthClient::OnGatewayMessage(const std::string &str) {
auto j = nlohmann::json::parse(str);
RemoteAuthGatewayMessage msg = j;
if (msg.Opcode == "hello") {
HandleGatewayHello(j);
} else if (msg.Opcode == "nonce_proof") {
HandleGatewayNonceProof(j);
} else if (msg.Opcode == "pending_remote_init") {
HandleGatewayPendingRemoteInit(j);
} else if (msg.Opcode == "pending_ticket") {
HandleGatewayPendingTicket(j);
}
}
void RemoteAuthClient::HandleGatewayHello(const nlohmann::json &j) {
RemoteAuthHelloMessage msg = j;
m_log->debug("Timeout: {}, Heartbeat: {}", msg.TimeoutMS, msg.HeartbeatInterval);
m_heartbeat_msec = msg.HeartbeatInterval;
m_heartbeat_thread = std::thread(&RemoteAuthClient::HeartbeatThread, this);
Init();
}
void RemoteAuthClient::HandleGatewayNonceProof(const nlohmann::json &j) {
RemoteAuthNonceProofMessage msg = j;
m_log->debug("Received encrypted nonce");
const auto encrypted_nonce = Glib::Base64::decode(msg.EncryptedNonce);
const auto proof = Decrypt(reinterpret_cast<const unsigned char *>(encrypted_nonce.data()), encrypted_nonce.size());
auto proof_encoded = Glib::Base64::encode(std::string(proof.begin(), proof.end()));
std::replace(proof_encoded.begin(), proof_encoded.end(), '/', '_');
std::replace(proof_encoded.begin(), proof_encoded.end(), '+', '-');
proof_encoded.erase(std::remove(proof_encoded.begin(), proof_encoded.end(), '='), proof_encoded.end());
RemoteAuthNonceProofMessage reply;
reply.Nonce = proof_encoded;
m_ws.Send(reply);
}
void RemoteAuthClient::HandleGatewayPendingRemoteInit(const nlohmann::json &j) {
RemoteAuthFingerprintMessage msg = j;
m_log->debug("Received fingerprint");
m_signal_fingerprint.emit(msg.Fingerprint);
}
void RemoteAuthClient::HandleGatewayPendingTicket(const nlohmann::json &j) {
RemoteAuthPendingTicketMessage msg = j;
const auto encrypted_payload = Glib::Base64::decode(msg.EncryptedUserPayload);
const auto payload = Decrypt(reinterpret_cast<const unsigned char *>(encrypted_payload.data()), encrypted_payload.size());
m_log->trace("User payload: {}", std::string(payload.begin(), payload.end()));
}
void RemoteAuthClient::Init() {
RemoteAuthInitMessage msg;
GenerateKey();
msg.EncodedPublicKey = GetEncodedPublicKey();
if (msg.EncodedPublicKey.empty()) {
m_log->error("Something went wrong");
// todo disconnect
return;
}
m_ws.Send(msg);
}
void RemoteAuthClient::GenerateKey() {
// you javascript people have it so easy
// check out this documentation https://www.openssl.org/docs/man1.1.1/man3/PEM_write_bio_PUBKEY.html
m_pkey_ctx.reset(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr));
if (!m_pkey_ctx) {
m_log->error("Failed to create RSA context");
return;
}
if (EVP_PKEY_keygen_init(m_pkey_ctx.get()) <= 0) {
m_log->error("Failed to initialize RSA context");
return;
}
if (EVP_PKEY_CTX_set_rsa_keygen_bits(m_pkey_ctx.get(), 2048) <= 0) {
m_log->error("Failed to set keygen bits");
return;
}
EVP_PKEY *pkey_tmp = nullptr;
if (EVP_PKEY_keygen(m_pkey_ctx.get(), &pkey_tmp) <= 0) {
m_log->error("Failed to generate keypair");
return;
}
m_pkey.reset(pkey_tmp);
m_dec_ctx.reset(EVP_PKEY_CTX_new(m_pkey.get(), nullptr));
if (EVP_PKEY_decrypt_init(m_dec_ctx.get()) <= 0) {
m_log->error("Failed to initialize RSA decrypt context");
return;
}
if (EVP_PKEY_CTX_set_rsa_padding(m_dec_ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) {
m_log->error("EVP_PKEY_CTX_set_rsa_padding failed");
return;
}
if (EVP_PKEY_CTX_set_rsa_oaep_md(m_dec_ctx.get(), EVP_sha256()) <= 0) {
m_log->error("EVP_PKEY_CTX_set_rsa_oaep_md failed");
return;
}
if (EVP_PKEY_CTX_set_rsa_mgf1_md(m_dec_ctx.get(), EVP_sha256()) <= 0) {
m_log->error("EVP_PKEY_CTX_set_rsa_mgf1_md");
return;
}
}
std::string RemoteAuthClient::GetEncodedPublicKey() const {
auto bio = BIO_ptr(BIO_new(BIO_s_mem()), BIO_free);
if (!bio) {
m_log->error("Failed to create BIO");
return {};
}
if (PEM_write_bio_PUBKEY(bio.get(), m_pkey.get()) <= 0) {
m_log->error("Failed to write public key to BIO");
return {};
}
// i think this is freed when the bio is too
BUF_MEM *mem = nullptr;
if (BIO_get_mem_ptr(bio.get(), &mem) <= 0) {
m_log->error("Failed to get BIO mem buf");
return {};
}
if (mem->data == nullptr || mem->length == 0) {
m_log->error("BIO mem buf is null or of zero length");
return {};
}
std::string pem_pubkey(mem->data, mem->length);
// isolate key
pem_pubkey.erase(0, pem_pubkey.find("\n") + 1);
pem_pubkey.erase(pem_pubkey.rfind("\n-"));
size_t pos;
while ((pos = pem_pubkey.find("\n")) != std::string::npos) {
pem_pubkey.erase(pos, 1);
}
return pem_pubkey;
}
std::vector<uint8_t> RemoteAuthClient::Decrypt(const unsigned char *in, size_t inlen) const {
// get length
size_t outlen;
if (EVP_PKEY_decrypt(m_dec_ctx.get(), nullptr, &outlen, in, inlen) <= 0) {
m_log->error("Failed to get length when decrypting");
return {};
}
std::vector<uint8_t> ret(outlen);
if (EVP_PKEY_decrypt(m_dec_ctx.get(), ret.data(), &outlen, in, inlen) <= 0) {
m_log->error("Failed to decrypt");
return {};
}
ret.resize(outlen);
return ret;
}
void RemoteAuthClient::OnWebsocketOpen() {
m_log->info("Websocket opened");
}
void RemoteAuthClient::OnWebsocketClose(const ix::WebSocketCloseInfo &info) {
if (info.remote) {
m_log->debug("Websocket closed (remote): {} ({})", info.code, info.reason);
} else {
m_log->debug("Websocket closed (local): {} ({})", info.code, info.reason);
}
}
void RemoteAuthClient::OnWebsocketMessage(const std::string &data) {
m_dispatch_mutex.lock();
m_dispatch_queue.push(data);
m_dispatcher.emit();
m_dispatch_mutex.unlock();
}
void RemoteAuthClient::HeartbeatThread() {
while (true) {
if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) break;
m_ws.Send(RemoteAuthHeartbeatMessage());
}
}
void RemoteAuthClient::OnDispatch() {
m_dispatch_mutex.lock();
if (m_dispatch_queue.empty()) {
m_dispatch_mutex.unlock();
return;
}
auto msg = std::move(m_dispatch_queue.front());
m_dispatch_queue.pop();
m_dispatch_mutex.unlock();
OnGatewayMessage(msg);
}
RemoteAuthClient::type_signal_fingerprint RemoteAuthClient::signal_fingerprint() {
return m_signal_fingerprint;
}

View File

@ -0,0 +1,64 @@
#pragma once
#include <string>
#include <queue>
#include <spdlog/logger.h>
#include "ssl.hpp"
#include "discord/waiter.hpp"
#include "discord/websocket.hpp"
class RemoteAuthClient {
public:
RemoteAuthClient();
~RemoteAuthClient();
void Start();
void Stop();
[[nodiscard]] bool IsConnected() const noexcept;
private:
void OnGatewayMessage(const std::string &str);
void HandleGatewayHello(const nlohmann::json &j);
void HandleGatewayNonceProof(const nlohmann::json &j);
void HandleGatewayPendingRemoteInit(const nlohmann::json &j);
void HandleGatewayPendingTicket(const nlohmann::json &j);
void Init();
void GenerateKey();
std::string GetEncodedPublicKey() const;
std::vector<uint8_t> Decrypt(const unsigned char *in, size_t inlen) const;
void OnWebsocketOpen();
void OnWebsocketClose(const ix::WebSocketCloseInfo &info);
void OnWebsocketMessage(const std::string &str);
void HeartbeatThread();
int m_heartbeat_msec;
Waiter m_heartbeat_waiter;
std::thread m_heartbeat_thread;
Glib::Dispatcher m_dispatcher;
std::queue<std::string> m_dispatch_queue;
std::mutex m_dispatch_mutex;
void OnDispatch();
Websocket m_ws;
bool m_connected = false;
std::shared_ptr<spdlog::logger> m_log;
EVP_PKEY_CTX_ptr m_pkey_ctx;
EVP_PKEY_CTX_ptr m_dec_ctx;
EVP_PKEY_ptr m_pkey;
public:
using type_signal_fingerprint = sigc::signal<void(std::string)>;
type_signal_fingerprint signal_fingerprint();
private:
type_signal_fingerprint m_signal_fingerprint;
};

View File

@ -0,0 +1,79 @@
#include "remoteauthdialog.hpp"
#include <qrcodegen.hpp>
RemoteAuthDialog::RemoteAuthDialog(Gtk::Window &parent)
: Gtk::Dialog("Login with QR Code", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
, m_ok("OK")
, m_cancel("Cancel")
, m_bbox(Gtk::ORIENTATION_HORIZONTAL) {
set_default_size(300, 50);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
m_ok.signal_clicked().connect([&]() {
response(Gtk::RESPONSE_OK);
});
m_cancel.signal_clicked().connect([&]() {
response(Gtk::RESPONSE_CANCEL);
});
m_bbox.pack_start(m_ok, Gtk::PACK_SHRINK);
m_bbox.pack_start(m_cancel, Gtk::PACK_SHRINK);
m_bbox.set_layout(Gtk::BUTTONBOX_END);
m_ra.signal_fingerprint().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnFingerprint));
m_ra.Start();
m_image.set_size_request(256, 256);
m_layout.add(m_image);
m_layout.add(m_bbox);
get_content_area()->add(m_layout);
show_all_children();
}
void RemoteAuthDialog::OnFingerprint(const std::string &fingerprint) {
const auto url = "https://discord.com/ra/" + fingerprint;
const auto level = qrcodegen::QrCode::Ecc::QUARTILE;
const auto qr = qrcodegen::QrCode::encodeText(url.c_str(), level);
int size = qr.getSize();
const int border = 4;
std::ostringstream sb;
sb << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
sb << "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
sb << "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 ";
sb << (size + border * 2) << " " << (size + border * 2) << "\" stroke=\"none\">\n";
sb << "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
sb << "\t<path d=\"";
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
if (qr.getModule(x, y)) {
if (x != 0 || y != 0)
sb << " ";
sb << "M" << (x + border) << "," << (y + border) << "h1v1h-1z";
}
}
}
sb << "\" fill=\"#000000\"/>\n";
sb << "</svg>\n";
const auto svg = sb.str();
auto loader = Gdk::PixbufLoader::create();
loader->write(reinterpret_cast<const guint8 *>(svg.data()), svg.size());
loader->close();
const auto pb = loader->get_pixbuf()->scale_simple(256, 256, Gdk::INTERP_NEAREST);
m_image.property_pixbuf() = pb;
}
std::string RemoteAuthDialog::GetToken() {
return m_token;
}

View File

@ -0,0 +1,23 @@
#pragma once
#include <gtkmm/dialog.h>
#include "remoteauthclient.hpp"
class RemoteAuthDialog : public Gtk::Dialog {
public:
RemoteAuthDialog(Gtk::Window &parent);
std::string GetToken();
protected:
Gtk::Image m_image;
Gtk::Box m_layout;
Gtk::Button m_ok;
Gtk::Button m_cancel;
Gtk::ButtonBox m_bbox;
private:
RemoteAuthClient m_ra;
void OnFingerprint(const std::string &fingerprint);
std::string m_token;
};

31
src/remoteauth/ssl.hpp Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <memory>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>
struct EVP_PKEY_CTX_deleter {
void operator()(EVP_PKEY_CTX *ptr) const {
EVP_PKEY_CTX_free(ptr);
}
};
struct EVP_PKEY_deleter {
void operator()(EVP_PKEY *ptr) const {
EVP_PKEY_free(ptr);
}
};
struct EVP_MD_CTX_deleter {
void operator()(EVP_MD_CTX *ptr) const {
EVP_MD_CTX_free(ptr);
}
};
using EVP_PKEY_CTX_ptr = std::unique_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_deleter>;
using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, EVP_PKEY_deleter>;
using EVP_MD_CTX_ptr = std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_deleter>;
using BIO_ptr = std::unique_ptr<BIO, decltype(&BIO_free)>;
using BUF_MEM_ptr = std::unique_ptr<BUF_MEM, decltype(&BUF_MEM_free)>;

View File

@ -206,6 +206,7 @@ void MainWindow::OnDiscordSubmenuPopup() {
m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active);
m_menu_discord_disconnect.set_sensitive(discord_active);
m_menu_discord_set_token.set_sensitive(!discord_active);
m_menu_discord_login_qr.set_sensitive(!discord_active);
m_menu_discord_set_status.set_sensitive(discord_active);
}
@ -247,12 +248,14 @@ void MainWindow::SetupMenu() {
m_menu_discord_disconnect.set_label("Disconnect");
m_menu_discord_disconnect.set_sensitive(false);
m_menu_discord_set_token.set_label("Set Token");
m_menu_discord_login_qr.set_label("Login with QR Code");
m_menu_discord_set_status.set_label("Set Status");
m_menu_discord_set_status.set_sensitive(false);
m_menu_discord_add_recipient.set_label("Add user to DM");
m_menu_discord_sub.append(m_menu_discord_connect);
m_menu_discord_sub.append(m_menu_discord_disconnect);
m_menu_discord_sub.append(m_menu_discord_set_token);
m_menu_discord_sub.append(m_menu_discord_login_qr);
m_menu_discord_sub.append(m_menu_discord_set_status);
m_menu_discord_sub.append(m_menu_discord_add_recipient);
m_menu_discord.set_submenu(m_menu_discord_sub);
@ -331,6 +334,10 @@ void MainWindow::SetupMenu() {
m_signal_action_set_token.emit();
});
m_menu_discord_login_qr.signal_activate().connect([this] {
m_signal_action_login_qr.emit();
});
m_menu_file_reload_css.signal_activate().connect([this] {
m_signal_action_reload_css.emit();
});
@ -421,6 +428,10 @@ MainWindow::type_signal_action_set_token MainWindow::signal_action_set_token() {
return m_signal_action_set_token;
}
MainWindow::type_signal_action_login_qr MainWindow::signal_action_login_qr() {
return m_signal_action_login_qr;
}
MainWindow::type_signal_action_reload_css MainWindow::signal_action_reload_css() {
return m_signal_action_reload_css;
}

View File

@ -74,6 +74,7 @@ private:
Gtk::MenuItem m_menu_discord_connect;
Gtk::MenuItem m_menu_discord_disconnect;
Gtk::MenuItem m_menu_discord_set_token;
Gtk::MenuItem m_menu_discord_login_qr;
Gtk::MenuItem m_menu_discord_set_status;
Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day
void OnDiscordSubmenuPopup();
@ -103,6 +104,7 @@ public:
typedef sigc::signal<void> type_signal_action_connect;
typedef sigc::signal<void> type_signal_action_disconnect;
typedef sigc::signal<void> type_signal_action_set_token;
typedef sigc::signal<void> type_signal_action_login_qr;
typedef sigc::signal<void> type_signal_action_reload_css;
typedef sigc::signal<void> type_signal_action_set_status;
// this should probably be removed
@ -113,6 +115,7 @@ public:
type_signal_action_connect signal_action_connect();
type_signal_action_disconnect signal_action_disconnect();
type_signal_action_set_token signal_action_set_token();
type_signal_action_login_qr signal_action_login_qr();
type_signal_action_reload_css signal_action_reload_css();
type_signal_action_set_status signal_action_set_status();
type_signal_action_add_recipient signal_action_add_recipient();
@ -123,6 +126,7 @@ private:
type_signal_action_connect m_signal_action_connect;
type_signal_action_disconnect m_signal_action_disconnect;
type_signal_action_set_token m_signal_action_set_token;
type_signal_action_login_qr m_signal_action_login_qr;
type_signal_action_reload_css m_signal_action_reload_css;
type_signal_action_set_status m_signal_action_set_status;
type_signal_action_add_recipient m_signal_action_add_recipient;

1
subprojects/qrcodegen Submodule

@ -0,0 +1 @@
Subproject commit 22fac31bdf81da68730c177c0e931c93234d2a30