Merge branch 'master' into rnnoise

This commit is contained in:
ouwou 2023-07-17 21:37:24 -04:00
commit d04e101800
67 changed files with 1473 additions and 379 deletions

View File

@ -67,7 +67,7 @@ jobs:
with: with:
cond: ${{ matrix.mindeps == true }} cond: ${{ matrix.mindeps == true }}
if_true: | if_true: |
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} -DUSE_LIBHANDY=OFF -DENABLE_VOICE=OFF -DENABLE_NOTIFICATION_SOUNDS=OFF cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} -DUSE_LIBHANDY=OFF -DENABLE_VOICE=OFF -DENABLE_NOTIFICATION_SOUNDS=OFF -DENABLE_QRCODE_LOGIN=OFF
cmake --build build cmake --build build
if_false: | if_false: |
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} -DCMAKE_CXX_FLAGS="-Wl,--default-image-base-low" cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} -DCMAKE_CXX_FLAGS="-Wl,--default-image-base-low"
@ -95,7 +95,6 @@ jobs:
mkdir -p 16x16/devices 24x24/devices 32x32/devices 48x48/devices 64x64/devices 96x96/devices scalable/devices mkdir -p 16x16/devices 24x24/devices 32x32/devices 48x48/devices 64x64/devices 96x96/devices scalable/devices
mkdir -p 16x16/status 24x24/status 32x32/status 48x48/status 64x64/status 96x96/status scalable/status mkdir -p 16x16/status 24x24/status 32x32/status 48x48/status 64x64/status 96x96/status scalable/status
cd ${GITHUB_WORKSPACE} cd ${GITHUB_WORKSPACE}
cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/icon-theme.cache ${artifact_dir}/share/icons/Adwaita/icon-theme.cache
cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/index.theme ${artifact_dir}/share/icons/Adwaita/index.theme cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/index.theme ${artifact_dir}/share/icons/Adwaita/index.theme
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/16x16/%.symbolic.png || : cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/16x16/%.symbolic.png || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/24x24/%.symbolic.png || : cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/24x24/%.symbolic.png || :
@ -104,6 +103,8 @@ jobs:
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/64x64/%.symbolic.png || : cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/64x64/%.symbolic.png || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/96x96/%.symbolic.png || : cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/96x96/%.symbolic.png || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/%.svg ${artifact_dir}/share/icons/Adwaita/scalable/%.svg || : cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/%.svg ${artifact_dir}/share/icons/Adwaita/scalable/%.svg || :
cd ${artifact_dir}/share/icons/Adwaita
gtk-update-icon-cache .
- name: Upload build (1) - name: Upload build (1)
uses: haya14busa/action-cond@v1 uses: haya14busa/action-cond@v1

3
.gitmodules vendored
View File

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

View File

@ -12,6 +12,7 @@ option(ENABLE_VOICE "Enable voice suppport" ON)
option(USE_KEYCHAIN "Store the token in the keychain (default)" ON) option(USE_KEYCHAIN "Store the token in the keychain (default)" ON)
option(ENABLE_NOTIFICATION_SOUNDS "Enable notification sounds (default)" ON) option(ENABLE_NOTIFICATION_SOUNDS "Enable notification sounds (default)" ON)
option(ENABLE_RNNOISE "Enable RNNoise for voice activity detection (default)" ON) option(ENABLE_RNNOISE "Enable RNNoise for voice activity detection (default)" ON)
option(ENABLE_QRCODE_LOGIN "Enable QR code login (default)" ON)
find_package(nlohmann_json REQUIRED) find_package(nlohmann_json REQUIRED)
find_package(CURL) find_package(CURL)
@ -62,6 +63,15 @@ target_include_directories(abaddon PUBLIC ${ZLIB_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS}) target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS}) target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
if (ENABLE_QRCODE_LOGIN)
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_compile_definitions(abaddon PRIVATE WITH_QRLOGIN)
endif ()
target_precompile_headers(abaddon PRIVATE <gtkmm.h> src/abaddon.hpp src/util.hpp) target_precompile_headers(abaddon PRIVATE <gtkmm.h> src/abaddon.hpp src/util.hpp)
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
@ -180,6 +190,12 @@ if (ENABLE_VOICE)
target_link_libraries(abaddon rnnoise) target_link_libraries(abaddon rnnoise)
endif () endif ()
endif () endif ()
if (APPLE)
target_link_libraries(abaddon "-framework CoreFoundation")
target_link_libraries(abaddon "-framework CoreAudio")
target_link_libraries(abaddon "-framework AudioToolbox")
endif ()
endif () endif ()
if (${ENABLE_NOTIFICATION_SOUNDS}) if (${ENABLE_NOTIFICATION_SOUNDS})
@ -188,6 +204,11 @@ if (${ENABLE_NOTIFICATION_SOUNDS})
endif () endif ()
if (USE_MINIAUDIO) if (USE_MINIAUDIO)
target_include_directories(abaddon PUBLIC subprojects/miniaudio) find_path(MINIAUDIO_INCLUDE_DIR
NAMES miniaudio.h
HINTS subprojects
PATH_SUFFIXES miniaudio
REQUIRED)
target_include_directories(abaddon PUBLIC ${MINIAUDIO_INCLUDE_DIR})
target_compile_definitions(abaddon PRIVATE WITH_MINIAUDIO) target_compile_definitions(abaddon PRIVATE WITH_MINIAUDIO)
endif () endif ()

View File

@ -226,32 +226,33 @@ Used in guild settings popup:
Used in profile popup: Used in profile popup:
| Selector | Description | | Selector | Description |
|------------------------------|---------------------------------------------------------| |--------------------------------|------------------------------------------------------------|
| `.mutual-friend-item` | Applied to every item in the mutual friends list | | `.mutual-friend-item` | Applied to every item in the mutual friends list |
| `.mutual-friend-item-name` | Name in mutual friend item | | `.mutual-friend-item-name` | Name in mutual friend item |
| `.mutual-friend-item-avatar` | Avatar in mutual friend item | | `.mutual-friend-item-avatar` | Avatar in mutual friend item |
| `.mutual-guild-item` | Applied to every item in the mutual guilds list | | `.mutual-guild-item` | Applied to every item in the mutual guilds list |
| `.mutual-guild-item-name` | Name in mutual guild item | | `.mutual-guild-item-name` | Name in mutual guild item |
| `.mutual-guild-item-icon` | Icon in mutual guild item | | `.mutual-guild-item-icon` | Icon in mutual guild item |
| `.mutual-guild-item-nick` | User nickname in mutual guild item | | `.mutual-guild-item-nick` | User nickname in mutual guild item |
| `.profile-connection` | Applied to every item in the user connections list | | `.profile-connection` | Applied to every item in the user connections list |
| `.profile-connection-label` | Label in profile connection item | | `.profile-connection-label` | Label in profile connection item |
| `.profile-connection-check` | Checkmark in verified profile connection items | | `.profile-connection-check` | Checkmark in verified profile connection items |
| `.profile-connections` | Container for profile connections | | `.profile-connections` | Container for profile connections |
| `.profile-notes` | Container for notes in profile window | | `.profile-notes` | Container for notes in profile window |
| `.profile-notes-label` | Label that says "NOTE" | | `.profile-notes-label` | Label that says "NOTE" |
| `.profile-notes-text` | Actual note text | | `.profile-notes-text` | Actual note text |
| `.profile-info-pane` | Applied to container for info section of profile popup | | `.profile-info-pane` | Applied to container for info section of profile popup |
| `.profile-info-created` | Label for creation date of profile | | `.profile-info-created` | Label for creation date of profile |
| `.user-profile-window` | | | `.user-profile-window` | |
| `.profile-main-container` | Inner container for profile | | `.profile-main-container` | Inner container for profile |
| `.profile-avatar` | | | `.profile-avatar` | |
| `.profile-username` | | | `.profile-username` | User's display name (username for backwards compatibility) |
| `.profile-switcher` | Buttons used to switch viewed section of profile | | `.profile-username-nondisplay` | User's actual username |
| `.profile-stack` | Container for profile info that can be switched between | | `.profile-switcher` | Buttons used to switch viewed section of profile |
| `.profile-badges` | Container for badges | | `.profile-stack` | Container for profile info that can be switched between |
| `.profile-badge` | | | `.profile-badges` | Container for badges |
| `.profile-badge` | |
### Settings ### Settings

View File

@ -114,6 +114,10 @@
border: 1px solid #026FB9; border: 1px solid #026FB9;
} }
.message-input.editing {
border: 1px solid #b9026f;
}
.message-input.bad-input { .message-input.bad-input {
border: 1px solid #dd3300; border: 1px solid #dd3300;
} }
@ -237,6 +241,10 @@
font-size: 20px; font-size: 20px;
} }
.profile-username-nondisplay {
margin-left: 10px;
}
.profile-badge { .profile-badge {
margin-right: 10px; margin-right: 10px;
} }
@ -382,3 +390,8 @@
.voice-state-server { .voice-state-server {
color: red; color: red;
} }
spinbutton {
color: @text_color;
margin-top: 10px;
}

View File

@ -8,7 +8,6 @@
#include "audio/manager.hpp" #include "audio/manager.hpp"
#include "discord/discord.hpp" #include "discord/discord.hpp"
#include "dialogs/token.hpp" #include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp"
#include "dialogs/confirm.hpp" #include "dialogs/confirm.hpp"
#include "dialogs/setstatus.hpp" #include "dialogs/setstatus.hpp"
#include "dialogs/friendpicker.hpp" #include "dialogs/friendpicker.hpp"
@ -21,6 +20,7 @@
#include "windows/voicewindow.hpp" #include "windows/voicewindow.hpp"
#include "startup.hpp" #include "startup.hpp"
#include "notifications/notifications.hpp" #include "notifications/notifications.hpp"
#include "remoteauth/remoteauthdialog.hpp"
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
#include <handy.h> #include <handy.h>
@ -252,6 +252,12 @@ int Abaddon::StartGTK() {
} }
#endif #endif
#ifdef _WIN32
if (m_settings.GetSettings().HideConsole) {
ShowWindow(GetConsoleWindow(), SW_HIDE);
}
#endif
// store must be checked before this can be called // store must be checked before this can be called
m_main_window->UpdateComponents(); m_main_window->UpdateComponents();
@ -261,6 +267,7 @@ int Abaddon::StartGTK() {
m_main_window->signal_action_connect().connect(sigc::mem_fun(*this, &Abaddon::ActionConnect)); 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_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_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_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_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient)); m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
@ -828,6 +835,21 @@ void Abaddon::ActionSetToken() {
m_main_window->UpdateMenus(); m_main_window->UpdateMenus();
} }
void Abaddon::ActionLoginQR() {
#ifdef WITH_QRLOGIN
RemoteAuthDialog dlg(*m_main_window);
auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) {
m_discord_token = dlg.GetToken();
m_discord.UpdateToken(m_discord_token);
m_main_window->UpdateComponents();
GetSettings().DiscordToken = m_discord_token;
ActionConnect();
}
m_main_window->UpdateMenus();
#endif
}
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) { void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
if (!id.IsValid()) { if (!id.IsValid()) {
m_discord.SetReferringChannel(Snowflake::Invalid); m_discord.SetReferringChannel(Snowflake::Invalid);
@ -914,24 +936,26 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
} }
void Abaddon::ActionChatInputSubmit(ChatSubmitParams data) { void Abaddon::ActionChatInputSubmit(ChatSubmitParams data) {
if (data.Message.substr(0, 7) == "/shrug " || data.Message == "/shrug") if (data.Message.substr(0, 7) == "/shrug " || data.Message == "/shrug") {
data.Message = data.Message.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important data.Message = data.Message.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
}
if (data.Message.substr(0, 8) == "@silent " || (data.Message.substr(0, 7) == "@silent" && !data.Attachments.empty())) {
data.Silent = true;
data.Message = data.Message.substr(7);
}
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, data.ChannelID, Permission::VIEW_CHANNEL)) return; if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, data.ChannelID, Permission::VIEW_CHANNEL)) return;
m_discord.SendChatMessage(data, NOOP_CALLBACK); if (data.EditingID.IsValid()) {
m_discord.EditMessage(data.ChannelID, data.EditingID, data.Message);
} else {
m_discord.SendChatMessage(data, NOOP_CALLBACK);
}
} }
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) { void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
const auto msg = m_discord.GetMessage(id); m_main_window->EditMessage(id);
if (!msg.has_value()) return;
EditMessageDialog dlg(*m_main_window);
dlg.SetContent(msg->Content);
auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) {
auto new_content = dlg.GetContent();
m_discord.EditMessage(channel_id, id, new_content);
}
} }
void Abaddon::ActionInsertMention(Snowflake id) { void Abaddon::ActionInsertMention(Snowflake id) {
@ -952,7 +976,7 @@ void Abaddon::ActionKickMember(Snowflake user_id, Snowflake guild_id) {
ConfirmDialog dlg(*m_main_window); ConfirmDialog dlg(*m_main_window);
const auto user = m_discord.GetUser(user_id); const auto user = m_discord.GetUser(user_id);
if (user.has_value()) if (user.has_value())
dlg.SetConfirmText("Are you sure you want to kick " + user->Username + "#" + user->Discriminator + "?"); dlg.SetConfirmText("Are you sure you want to kick " + user->GetUsername() + "?");
auto response = dlg.run(); auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) if (response == Gtk::RESPONSE_OK)
m_discord.KickUser(user_id, guild_id); m_discord.KickUser(user_id, guild_id);
@ -962,7 +986,7 @@ void Abaddon::ActionBanMember(Snowflake user_id, Snowflake guild_id) {
ConfirmDialog dlg(*m_main_window); ConfirmDialog dlg(*m_main_window);
const auto user = m_discord.GetUser(user_id); const auto user = m_discord.GetUser(user_id);
if (user.has_value()) if (user.has_value())
dlg.SetConfirmText("Are you sure you want to ban " + user->Username + "#" + user->Discriminator + "?"); dlg.SetConfirmText("Are you sure you want to ban " + user->GetUsername() + "?");
auto response = dlg.run(); auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) if (response == Gtk::RESPONSE_OK)
m_discord.BanUser(user_id, guild_id); m_discord.BanUser(user_id, guild_id);
@ -1127,9 +1151,11 @@ int main(int argc, char **argv) {
#endif #endif
spdlog::cfg::load_env_levels(); spdlog::cfg::load_env_levels();
auto log_ui = spdlog::stdout_color_mt("ui");
auto log_audio = spdlog::stdout_color_mt("audio"); auto log_audio = spdlog::stdout_color_mt("audio");
auto log_voice = spdlog::stdout_color_mt("voice"); auto log_voice = spdlog::stdout_color_mt("voice");
auto log_discord = spdlog::stdout_color_mt("discord"); auto log_discord = spdlog::stdout_color_mt("discord");
auto log_ra = spdlog::stdout_color_mt("remote-auth");
Gtk::Main::init_gtkmm_internals(); // why??? Gtk::Main::init_gtkmm_internals(); // why???
return Abaddon::Get().StartGTK(); return Abaddon::Get().StartGTK();

View File

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

View File

@ -1,4 +1,7 @@
#ifdef WITH_MINIAUDIO #ifdef WITH_MINIAUDIO
#define MINIAUDIO_IMPLEMENTATION #define MINIAUDIO_IMPLEMENTATION
#ifdef __APPLE__
#define MA_NO_RUNTIME_LINKING
#endif
#include <miniaudio.h> #include <miniaudio.h>
#endif #endif

View File

@ -96,19 +96,23 @@ AudioManager::AudioManager() {
if (const auto playback_id = m_devices.GetDefaultPlayback(); playback_id.has_value()) { if (const auto playback_id = m_devices.GetDefaultPlayback(); playback_id.has_value()) {
m_playback_id = *playback_id; m_playback_id = *playback_id;
m_playback_config.playback.pDeviceID = &m_playback_id; m_playback_config.playback.pDeviceID = &m_playback_id;
}
if (ma_device_init(&m_context, &m_playback_config, &m_playback_device) != MA_SUCCESS) { if (ma_device_init(&m_context, &m_playback_config, &m_playback_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to initialize playback device"); spdlog::get("audio")->error("failed to initialize playback device");
m_ok = false; m_ok = false;
return; return;
} }
if (ma_device_start(&m_playback_device) != MA_SUCCESS) { if (ma_device_start(&m_playback_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to start playback"); spdlog::get("audio")->error("failed to start playback");
ma_device_uninit(&m_playback_device); ma_device_uninit(&m_playback_device);
m_ok = false; m_ok = false;
return; return;
}
char playback_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1];
ma_device_get_name(&m_playback_device, ma_device_type_playback, playback_device_name, sizeof(playback_device_name), nullptr);
spdlog::get("audio")->info("using {} as playback device", playback_device_name);
} }
m_capture_config = ma_device_config_init(ma_device_type_capture); m_capture_config = ma_device_config_init(ma_device_type_capture);
@ -122,22 +126,18 @@ AudioManager::AudioManager() {
if (const auto capture_id = m_devices.GetDefaultCapture(); capture_id.has_value()) { if (const auto capture_id = m_devices.GetDefaultCapture(); capture_id.has_value()) {
m_capture_id = *capture_id; m_capture_id = *capture_id;
m_capture_config.capture.pDeviceID = &m_capture_id; m_capture_config.capture.pDeviceID = &m_capture_id;
if (ma_device_init(&m_context, &m_capture_config, &m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to initialize capture device");
m_ok = false;
return;
}
char capture_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1];
ma_device_get_name(&m_capture_device, ma_device_type_capture, capture_device_name, sizeof(capture_device_name), nullptr);
spdlog::get("audio")->info("using {} as capture device", capture_device_name);
} }
if (ma_device_init(&m_context, &m_capture_config, &m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to initialize capture device");
m_ok = false;
return;
}
char playback_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1];
ma_device_get_name(&m_playback_device, ma_device_type_playback, playback_device_name, sizeof(playback_device_name), nullptr);
spdlog::get("audio")->info("using {} as playback device", playback_device_name);
char capture_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1];
ma_device_get_name(&m_capture_device, ma_device_type_capture, capture_device_name, sizeof(capture_device_name), nullptr);
spdlog::get("audio")->info("using {} as capture device", capture_device_name);
Glib::signal_timeout().connect(sigc::mem_fun(*this, &AudioManager::DecayVolumeMeters), 40); Glib::signal_timeout().connect(sigc::mem_fun(*this, &AudioManager::DecayVolumeMeters), 40);
} }

View File

@ -42,7 +42,7 @@ ChannelList::ChannelList()
const auto type = row[m_columns.m_type]; const auto type = row[m_columns.m_type];
// text channels should not be allowed to be collapsed // text channels should not be allowed to be collapsed
// maybe they should be but it seems a little difficult to handle expansion to permit this // maybe they should be but it seems a little difficult to handle expansion to permit this
if (type != RenderType::TextChannel) { if (type != RenderType::TextChannel && type != RenderType::DM) {
if (row[m_columns.m_expanded]) { if (row[m_columns.m_expanded]) {
m_view.collapse_row(path); m_view.collapse_row(path);
row[m_columns.m_expanded] = false; row[m_columns.m_expanded] = false;
@ -527,6 +527,7 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) {
#ifdef WITH_VOICE #ifdef WITH_VOICE
void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) {
auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel);
if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM);
if (!parent_iter) return; if (!parent_iter) return;
const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id);
if (!user.has_value()) return; if (!user.has_value()) return;
@ -863,7 +864,7 @@ Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &
auto row = *m_model->append(parent); auto row = *m_model->append(parent);
row[m_columns.m_type] = RenderType::VoiceParticipant; row[m_columns.m_type] = RenderType::VoiceParticipant;
row[m_columns.m_id] = user.ID; row[m_columns.m_id] = user.ID;
row[m_columns.m_name] = user.GetEscapedName(); row[m_columns.m_name] = user.GetDisplayNameEscaped();
const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID); const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID);
if (voice_state.has_value()) { if (voice_state.has_value()) {
@ -1015,20 +1016,17 @@ void ChannelList::AddPrivateChannels() {
row[m_columns.m_name] = Glib::Markup::escape_text(dm->GetDisplayName()); row[m_columns.m_name] = Glib::Markup::escape_text(dm->GetDisplayName());
row[m_columns.m_sort] = static_cast<int64_t>(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id)); row[m_columns.m_sort] = static_cast<int64_t>(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id));
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
row[m_columns.m_expanded] = true;
if (dm->HasIcon()) { #ifdef WITH_VOICE
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) { for (auto user_id : discord.GetUsersInVoiceChannel(dm_id)) {
if (iter) if (const auto user = discord.GetUser(user_id); user.has_value()) {
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); CreateVoiceParticipantRow(*user, row->children());
}; }
img.LoadFromURL(dm->GetIconURL(), sigc::track_obj(cb, *this));
} else if (top_recipient.has_value()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter)
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
} }
#endif
SetDMChannelIcon(iter, *dm);
} }
} }
@ -1036,11 +1034,6 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
auto header_row = m_model->get_iter(m_dm_header); auto header_row = m_model->get_iter(m_dm_header);
auto &img = Abaddon::Get().GetImageManager(); auto &img = Abaddon::Get().GetImageManager();
std::optional<UserData> top_recipient;
const auto recipients = dm.GetDMRecipients();
if (!recipients.empty())
top_recipient = recipients[0];
auto iter = m_model->append(header_row->children()); auto iter = m_model->append(header_row->children());
auto row = *iter; auto row = *iter;
row[m_columns.m_type] = RenderType::DM; row[m_columns.m_type] = RenderType::DM;
@ -1049,12 +1042,74 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
row[m_columns.m_sort] = static_cast<int64_t>(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID)); row[m_columns.m_sort] = static_cast<int64_t>(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID));
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
if (top_recipient.has_value()) { SetDMChannelIcon(iter, dm);
}
void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) {
auto &img = Abaddon::Get().GetImageManager();
std::optional<UserData> top_recipient;
const auto recipients = dm.GetDMRecipients();
if (!recipients.empty())
top_recipient = recipients[0];
if (dm.HasIcon()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter)
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(dm.GetIconURL(), sigc::track_obj(cb, *this));
} else if (dm.Type == ChannelType::DM && top_recipient.has_value()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) { const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter) if (iter)
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
}; };
img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
} else { // GROUP_DM
std::string hash;
switch (dm.ID.GetUnixMilliseconds() % 8) {
case 0:
hash = "ee9275c5a437f7dc7f9430ba95f12ebd";
break;
case 1:
hash = "9baf45aac2a0ec2e2dab288333acb9d9";
break;
case 2:
hash = "7ba11ffb1900fa2b088cb31324242047";
break;
case 3:
hash = "f90fca70610c4898bc57b58bce92f587";
break;
case 4:
hash = "e2779af34b8d9126b77420e5f09213ce";
break;
case 5:
hash = "c6851bd0b03f1cca5a8c1e720ea6ea17";
break;
case 6:
hash = "f7e38ac976a2a696161c923502a8345b";
break;
case 7:
default:
hash = "3cb840d03313467838d658bbec801fcd";
break;
}
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter)
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL("https://discord.com/assets/" + hash + ".png", sigc::track_obj(cb, *this));
}
}
void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) {
if (channel.GuildID.has_value()) {
auto iter = GetIteratorForGuildFromID(*channel.GuildID);
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
}
if (channel.ParentID.has_value()) {
auto iter = GetIteratorForRowFromIDOfType(*channel.ParentID, RenderType::Category);
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
} }
} }
@ -1064,9 +1119,8 @@ void ChannelList::OnMessageAck(const MessageAckData &data) {
auto iter = GetIteratorForRowFromID(data.ChannelID); auto iter = GetIteratorForRowFromID(data.ChannelID);
if (iter) m_model->row_changed(m_model->get_path(iter), iter); if (iter) m_model->row_changed(m_model->get_path(iter), iter);
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID);
if (channel.has_value() && channel->GuildID.has_value()) { if (channel.has_value()) {
iter = GetIteratorForGuildFromID(*channel->GuildID); RedrawUnreadIndicatorsForChannel(*channel);
if (iter) m_model->row_changed(m_model->get_path(iter), iter);
} }
} }
@ -1079,9 +1133,7 @@ void ChannelList::OnMessageCreate(const Message &msg) {
if (iter) if (iter)
(*iter)[m_columns.m_sort] = static_cast<int64_t>(-msg.ID); (*iter)[m_columns.m_sort] = static_cast<int64_t>(-msg.ID);
} }
if (channel->GuildID.has_value()) RedrawUnreadIndicatorsForChannel(*channel);
if ((iter = GetIteratorForGuildFromID(*channel->GuildID)))
m_model->row_changed(m_model->get_path(iter), iter);
} }
bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {

View File

@ -114,10 +114,12 @@ protected:
void AddPrivateChannels(); void AddPrivateChannels();
void UpdateCreateDMChannel(const ChannelData &channel); void UpdateCreateDMChannel(const ChannelData &channel);
void SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm);
void RedrawUnreadIndicatorsForChannel(const ChannelData& channel);
void OnMessageAck(const MessageAckData &data); void OnMessageAck(const MessageAckData &data);
void OnMessageCreate(const Message &msg); void OnMessageCreate(const Message &msg);
Gtk::TreeModel::Path m_path_for_menu; Gtk::TreeModel::Path m_path_for_menu;
// cant be recovered through selection // cant be recovered through selection

View File

@ -410,6 +410,17 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Wi
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
} }
void AddUnreadIndicator(const Cairo::RefPtr<Cairo::Context> &cr, const Gdk::Rectangle &background_area) {
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor);
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
const auto x = background_area.get_x();
const auto y = background_area.get_y();
const auto w = background_area.get_width();
const auto h = background_area.get_height();
cr->rectangle(x, y, 3, h);
cr->fill();
}
void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
// todo: figure out how Gtk::Arrow is rendered because i like it better :^) // todo: figure out how Gtk::Arrow is rendered because i like it better :^)
constexpr static int len = 5; constexpr static int len = 5;
@ -447,13 +458,18 @@ void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Cont
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { auto &discord = Abaddon::Get().GetDiscordClient();
const auto id = m_property_id.get_value();
if (discord.IsChannelMuted(m_property_id.get_value())) {
auto muted = color; auto muted = color;
muted.set_red(muted.get_red() * 0.5); muted.set_red(muted.get_red() * 0.5);
muted.set_green(muted.get_green() * 0.5); muted.set_green(muted.get_green() * 0.5);
muted.set_blue(muted.get_blue() * 0.5); muted.set_blue(muted.get_blue() * 0.5);
m_renderer_text.property_foreground_rgba() = muted; m_renderer_text.property_foreground_rgba() = muted;
} else { } else {
if (discord.GetUnreadChannelsCountForCategory(id) > 0) {
AddUnreadIndicator(cr, background_area);
}
m_renderer_text.property_foreground_rgba() = color; m_renderer_text.property_foreground_rgba() = color;
} }
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
@ -519,14 +535,7 @@ void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Conte
if (unread_state < 0) return; if (unread_state < 0) return;
if (!is_muted) { if (!is_muted) {
static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); AddUnreadIndicator(cr, background_area);
cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
const auto x = background_area.get_x();
const auto y = background_area.get_y();
const auto w = background_area.get_width();
const auto h = background_area.get_height();
cr->rectangle(x, y, 3, h);
cr->fill();
} }
if (unread_state < 1) return; if (unread_state < 1) return;

View File

@ -41,9 +41,15 @@ bool ChatInputText::ProcessKeyPress(GdkEventKey *event) {
return true; return true;
} }
#ifdef __APPLE__
if ((event->state & GDK_MOD2_MASK) && event->keyval == GDK_KEY_v) {
return CheckHandleClipboardPaste();
}
#else
if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) { if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) {
return CheckHandleClipboardPaste(); return CheckHandleClipboardPaste();
} }
#endif
if (event->keyval == GDK_KEY_Return) { if (event->keyval == GDK_KEY_Return) {
if (event->state & GDK_SHIFT_MASK) if (event->state & GDK_SHIFT_MASK)
@ -491,6 +497,10 @@ void ChatInput::InsertText(const Glib::ustring &text) {
m_input.Get().InsertText(text); m_input.Get().InsertText(text);
} }
void ChatInput::Clear() {
GetBuffer()->set_text("");
}
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() { Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
return m_input.Get().GetBuffer(); return m_input.Get().GetBuffer();
} }
@ -559,6 +569,24 @@ void ChatInput::StopReplying() {
m_input.Get().get_style_context()->remove_class("replying"); m_input.Get().get_style_context()->remove_class("replying");
} }
void ChatInput::StartEditing(const Message &message) {
m_is_editing = true;
m_input.Get().grab_focus();
m_input.Get().get_style_context()->add_class("editing");
GetBuffer()->set_text(message.Content);
m_attachments.Clear();
m_attachments_revealer.set_reveal_child(false);
}
void ChatInput::StopEditing() {
m_is_editing = false;
m_input.Get().get_style_context()->remove_class("editing");
}
bool ChatInput::IsEmpty() {
return GetBuffer()->get_char_count() == 0;
}
bool ChatInput::AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file) { bool ChatInput::AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file) {
try { try {
const auto read_stream = file->read(); const auto read_stream = file->read();
@ -571,7 +599,7 @@ bool ChatInput::AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file) {
} }
bool ChatInput::CanAttachFiles() { bool ChatInput::CanAttachFiles() {
return Abaddon::Get().GetDiscordClient().HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES | Permission::SEND_MESSAGES); return !m_is_editing && Abaddon::Get().GetDiscordClient().HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES | Permission::SEND_MESSAGES);
} }
ChatInput::type_signal_submit ChatInput::signal_submit() { ChatInput::type_signal_submit ChatInput::signal_submit() {

View File

@ -129,6 +129,7 @@ public:
ChatInput(); ChatInput();
void InsertText(const Glib::ustring &text); void InsertText(const Glib::ustring &text);
void Clear();
Glib::RefPtr<Gtk::TextBuffer> GetBuffer(); Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
bool ProcessKeyPress(GdkEventKey *event); bool ProcessKeyPress(GdkEventKey *event);
void AddAttachment(const Glib::RefPtr<Gio::File> &file); void AddAttachment(const Glib::RefPtr<Gio::File> &file);
@ -139,6 +140,11 @@ public:
void StartReplying(); void StartReplying();
void StopReplying(); void StopReplying();
void StartEditing(const Message &message);
void StopEditing();
bool IsEmpty();
private: private:
bool AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file); bool AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file);
bool CanAttachFiles(); bool CanAttachFiles();
@ -149,6 +155,8 @@ private:
Snowflake m_active_channel; Snowflake m_active_channel;
bool m_is_editing = false;
public: public:
using type_signal_submit = sigc::signal<bool, ChatSubmitParams>; using type_signal_submit = sigc::signal<bool, ChatSubmitParams>;
using type_signal_escape = sigc::signal<void>; using type_signal_escape = sigc::signal<void>;

View File

@ -54,6 +54,12 @@ void ChatInputIndicator::AddUser(Snowflake channel_id, const UserData &user, int
void ChatInputIndicator::SetActiveChannel(Snowflake id) { void ChatInputIndicator::SetActiveChannel(Snowflake id) {
m_active_channel = id; m_active_channel = id;
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (channel.has_value()) {
m_active_guild = channel->GuildID;
} else {
m_active_guild = std::nullopt;
}
ComputeTypingString(); ComputeTypingString();
} }
@ -105,14 +111,14 @@ void ChatInputIndicator::ComputeTypingString() {
if (typers.empty()) { if (typers.empty()) {
SetTypingString(""); SetTypingString("");
} else if (typers.size() == 1) { } else if (typers.size() == 1) {
SetTypingString(typers[0].Username + " is typing..."); SetTypingString(typers[0].GetDisplayName(m_active_guild) + " is typing...");
} else if (typers.size() == 2) { } else if (typers.size() == 2) {
SetTypingString(typers[0].Username + " and " + typers[1].Username + " are typing..."); SetTypingString(typers[0].GetDisplayName(m_active_guild) + " and " + typers[1].GetDisplayName(m_active_guild) + " are typing...");
} else if (typers.size() > 2 && typers.size() <= MaxUsersInIndicator) { } else if (typers.size() > 2 && typers.size() <= MaxUsersInIndicator) {
Glib::ustring str; Glib::ustring str;
for (size_t i = 0; i < typers.size() - 1; i++) for (size_t i = 0; i < typers.size() - 1; i++)
str += typers[i].Username + ", "; str += typers[i].GetDisplayName(m_active_guild) + ", ";
SetTypingString(str + "and " + typers[typers.size() - 1].Username + " are typing..."); SetTypingString(str + "and " + typers[typers.size() - 1].GetDisplayName(m_active_guild) + " are typing...");
} else { // size() > MaxUsersInIndicator } else { // size() > MaxUsersInIndicator
SetTypingString("Several people are typing..."); SetTypingString("Several people are typing...");
} }

View File

@ -23,5 +23,6 @@ private:
Glib::ustring m_custom_markup; Glib::ustring m_custom_markup;
Snowflake m_active_channel; Snowflake m_active_channel;
std::optional<Snowflake> m_active_guild;
std::unordered_map<Snowflake, std::unordered_map<Snowflake, sigc::connection>> m_typers; // channel id -> [user id -> connection] std::unordered_map<Snowflake, std::unordered_map<Snowflake, sigc::connection>> m_typers; // channel id -> [user id -> connection]
}; };

View File

@ -133,10 +133,9 @@ void ChatList::ProcessNewMessage(const Message &data, bool prepend) {
m_menu_delete_message->set_sensitive(false); m_menu_delete_message->set_sensitive(false);
m_menu_edit_message->set_sensitive(false); m_menu_edit_message->set_sensitive(false);
} else { } else {
const bool can_edit = client.GetUserData().ID == data->Author.ID; const bool can_delete = (client.GetUserData().ID == data->Author.ID) || has_manage;
const bool can_delete = can_edit || has_manage;
m_menu_delete_message->set_sensitive(can_delete); m_menu_delete_message->set_sensitive(can_delete);
m_menu_edit_message->set_sensitive(can_edit); m_menu_edit_message->set_sensitive(data->IsEditable());
} }
m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
@ -253,6 +252,25 @@ void ChatList::ActuallyRemoveMessage(Snowflake id) {
RemoveMessageAndHeader(it->second); RemoveMessageAndHeader(it->second);
} }
std::optional<Snowflake> ChatList::GetLastSentEditableMessage() {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto self_id = discord.GetUserData().ID;
std::map<Snowflake, Gtk::Widget *> ordered(m_id_to_widget.begin(), m_id_to_widget.end());
for (auto it = ordered.crbegin(); it != ordered.crend(); it++) {
const auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second);
if (widget == nullptr) continue;
const auto msg = discord.GetMessage(widget->ID);
if (!msg.has_value()) continue;
if (msg->Author.ID != self_id) continue;
if (!msg->IsEditable()) continue;
return msg->ID;
}
return std::nullopt;
}
void ChatList::SetupMenu() { void ChatList::SetupMenu() {
m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID")); m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_menu_copy_id->signal_activate().connect([this] { m_menu_copy_id->signal_activate().connect([this] {

View File

@ -23,6 +23,7 @@ public:
void SetSeparateAll(bool separate); void SetSeparateAll(bool separate);
void SetUsePinnedMenu(); // i think i need a better way to do menus void SetUsePinnedMenu(); // i think i need a better way to do menus
void ActuallyRemoveMessage(Snowflake id); // perhaps not the best method name void ActuallyRemoveMessage(Snowflake id); // perhaps not the best method name
std::optional<Snowflake> GetLastSentEditableMessage();
private: private:
void SetupMenu(); void SetupMenu();

View File

@ -221,36 +221,36 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
if (data->Mentions.empty()) break; if (data->Mentions.empty()) break;
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
const auto &added = data->Mentions[0]; const auto &added = data->Mentions[0];
b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->Username + "</span> added <span color='#eeeeee'>" + added.Username + "</span></span></i>"); b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->GetUsername() + "</span> added <span color='#eeeeee'>" + added.GetUsername() + "</span></span></i>");
} break; } break;
case MessageType::RECIPIENT_REMOVE: { case MessageType::RECIPIENT_REMOVE: {
if (data->Mentions.empty()) break; if (data->Mentions.empty()) break;
const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
const auto &added = data->Mentions[0]; const auto &added = data->Mentions[0];
if (adder->ID == added.ID) if (adder->ID == added.ID)
b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->Username + "</span> left</span></i>"); b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->GetUsername() + "</span> left</span></i>");
else else
b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->Username + "</span> removed <span color='#eeeeee'>" + added.Username + "</span></span></i>"); b->insert_markup(s, "<i><span color='#999999'><span color='#eeeeee'>" + adder->GetUsername() + "</span> removed <span color='#eeeeee'>" + added.GetUsername() + "</span></span></i>");
} break; } break;
case MessageType::CHANNEL_NAME_CHANGE: { case MessageType::CHANNEL_NAME_CHANGE: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " changed the name to <b>" + Glib::Markup::escape_text(data->Content) + "</b></span></i>"); b->insert_markup(s, "<i><span color='#999999'>" + author->GetDisplayNameEscapedBold() + " changed the name to <b>" + Glib::Markup::escape_text(data->Content) + "</b></span></i>");
} break; } break;
case MessageType::CHANNEL_ICON_CHANGE: { case MessageType::CHANNEL_ICON_CHANGE: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " changed the channel icon</span></i>"); b->insert_markup(s, "<i><span color='#999999'>" + author->GetDisplayNameEscapedBold() + " changed the channel icon</span></i>");
} break; } break;
case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1: case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1:
case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2: case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2:
case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3: { case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data->GuildID); const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data->GuildID);
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " just boosted the server <b>" + Glib::Markup::escape_text(data->Content) + "</b> times! " + b->insert_markup(s, "<i><span color='#999999'>" + author->GetDisplayNameEscapedBold() + " just boosted the server <b>" + Glib::Markup::escape_text(data->Content) + "</b> times! " +
Glib::Markup::escape_text(guild->Name) + " has achieved <b>Level " + std::to_string(static_cast<int>(data->Type) - 8) + "!</b></span></i>"); // oo cheeky me !!! Glib::Markup::escape_text(guild->Name) + " has achieved <b>Level " + std::to_string(static_cast<int>(data->Type) - 8) + "!</b></span></i>"); // oo cheeky me !!!
} break; } break;
case MessageType::CHANNEL_FOLLOW_ADD: { case MessageType::CHANNEL_FOLLOW_ADD: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " has added <b>" + Glib::Markup::escape_text(data->Content) + "</b> to this channel. Its most important updates will show up here.</span></i>"); b->insert_markup(s, "<i><span color='#999999'>" + author->GetDisplayNameEscapedBold() + " has added <b>" + Glib::Markup::escape_text(data->Content) + "</b> to this channel. Its most important updates will show up here.</span></i>");
} break; } break;
case MessageType::CALL: { case MessageType::CALL: {
b->insert_markup(s, "<span color='#999999'><i>[started a call]</i></span>"); b->insert_markup(s, "<span color='#999999'><i>[started a call]</i></span>");
@ -270,13 +270,13 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
case MessageType::THREAD_CREATED: { case MessageType::THREAD_CREATED: {
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
if (data->MessageReference.has_value() && data->MessageReference->ChannelID.has_value()) { if (data->MessageReference.has_value() && data->MessageReference->ChannelID.has_value()) {
auto iter = b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span></i>"); auto iter = b->insert_markup(s, "<i><span color='#999999'>" + author->GetDisplayNameEscapedBold() + " started a thread: </span></i>");
auto tag = b->create_tag(); auto tag = b->create_tag();
tag->property_weight() = Pango::WEIGHT_BOLD; tag->property_weight() = Pango::WEIGHT_BOLD;
m_channel_tagmap[tag] = *data->MessageReference->ChannelID; m_channel_tagmap[tag] = *data->MessageReference->ChannelID;
b->insert_with_tag(iter, data->Content, tag); b->insert_with_tag(iter, data->Content, tag);
} else { } else {
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>"); b->insert_markup(s, "<i><span color='#999999'>" + author->GetDisplayNameEscapedBold() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>");
} }
} break; } break;
default: break; default: break;
@ -656,7 +656,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
if (role.has_value()) { if (role.has_value()) {
const auto author = discord.GetUser(author_id); const auto author = discord.GetUser(author_id);
if (author.has_value()) { if (author.has_value()) {
return "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">" + author->GetEscapedString() + "</span></b>"; return "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">" + author->GetDisplayNameEscaped(guild_id) + "</span></b>";
} }
} }
} }
@ -664,7 +664,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
const auto author = discord.GetUser(author_id); const auto author = discord.GetUser(author_id);
if (author.has_value()) { if (author.has_value()) {
return author->GetEscapedBoldString<false>(); return author->GetDisplayNameEscapedBold(guild_id);
} }
return "<b>Unknown User</b>"; return "<b>Unknown User</b>";
@ -674,8 +674,9 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
std::optional<std::shared_ptr<Message>> referenced_message = data.ReferencedMessage; std::optional<std::shared_ptr<Message>> referenced_message = data.ReferencedMessage;
if (data.MessageReference.has_value() && data.MessageReference->MessageID.has_value() && !referenced_message.has_value()) { if (data.MessageReference.has_value() && data.MessageReference->MessageID.has_value() && !referenced_message.has_value()) {
auto refd = discord.GetMessage(*data.MessageReference->MessageID); auto refd = discord.GetMessage(*data.MessageReference->MessageID);
if (refd.has_value()) if (refd.has_value()) {
referenced_message = std::make_shared<Message>(std::move(*refd)); referenced_message = std::make_shared<Message>(std::move(*refd));
}
} }
if (data.Interaction.has_value()) { if (data.Interaction.has_value()) {
@ -685,7 +686,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
Glib::Markup::escape_text(data.Interaction->Name) + Glib::Markup::escape_text(data.Interaction->Name) +
"</span>"); "</span>");
} else if (const auto user = discord.GetUser(data.Interaction->User.ID); user.has_value()) { } else if (const auto user = discord.GetUser(data.Interaction->User.ID); user.has_value()) {
lbl->set_markup(user->GetEscapedBoldString<false>()); lbl->set_markup(user->GetDisplayNameEscapedBold());
} else { } else {
lbl->set_markup("<b>Unknown User</b>"); lbl->set_markup("<b>Unknown User</b>");
} }
@ -1011,6 +1012,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
add(m_main_box); add(m_main_box);
set_margin_bottom(8); set_margin_bottom(8);
set_focus_on_click(false);
show_all(); show_all();
@ -1042,7 +1044,7 @@ void ChatMessageHeader::UpdateName() {
else else
m_author.set_markup("<span weight='bold'>" + name + "</span>"); m_author.set_markup("<span weight='bold'>" + name + "</span>");
} else } else
m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</span>"); m_author.set_markup("<span weight='bold'>" + user->GetDisplayNameEscaped() + "</span>");
} }
std::vector<Gtk::Widget *> ChatMessageHeader::GetChildContent() { std::vector<Gtk::Widget *> ChatMessageHeader::GetChildContent() {
@ -1068,7 +1070,7 @@ Glib::ustring ChatMessageHeader::GetEscapedDisplayName(const UserData &user, con
if (member.has_value() && !member->Nickname.empty()) if (member.has_value() && !member->Nickname.empty())
return Glib::Markup::escape_text(member->Nickname); return Glib::Markup::escape_text(member->Nickname);
else else
return Glib::Markup::escape_text(user.GetEscapedName()); return Glib::Markup::escape_text(user.GetDisplayNameEscaped());
} }
bool ChatMessageHeader::on_author_button_press(GdkEventButton *ev) { bool ChatMessageHeader::on_author_button_press(GdkEventButton *ev) {

View File

@ -50,8 +50,8 @@ ChatWindow::ChatWindow() {
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit)); m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() { m_input->signal_escape().connect([this]() {
if (m_is_replying) if (m_is_replying) StopReplying();
StopReplying(); if (m_is_editing) StopEditing();
}); });
m_input->signal_key_press_event().connect(sigc::mem_fun(*this, &ChatWindow::OnKeyPressEvent), false); m_input->signal_key_press_event().connect(sigc::mem_fun(*this, &ChatWindow::OnKeyPressEvent), false);
m_input->show(); m_input->show();
@ -132,8 +132,8 @@ void ChatWindow::SetActiveChannel(Snowflake id) {
m_input->SetActiveChannel(id); m_input->SetActiveChannel(id);
m_input_indicator->SetActiveChannel(id); m_input_indicator->SetActiveChannel(id);
m_rate_limit_indicator->SetActiveChannel(id); m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying) if (m_is_replying) StopReplying();
StopReplying(); if (m_is_editing) StopEditing();
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
m_tab_switcher->ReplaceActiveTab(id); m_tab_switcher->ReplaceActiveTab(id);
@ -274,15 +274,31 @@ bool ChatWindow::OnInputSubmit(ChatSubmitParams data) {
data.ChannelID = m_active_channel; data.ChannelID = m_active_channel;
data.InReplyToID = m_replying_to; data.InReplyToID = m_replying_to;
data.EditingID = m_editing_id;
if (m_active_channel.IsValid()) if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(data); // m_replying_to is checked for invalid in the handler m_signal_action_chat_submit.emit(data); // m_replying_to is checked for invalid in the handler
if (m_is_replying)
StopReplying(); if (m_is_replying) StopReplying();
if (m_is_editing) StopEditing();
return true; return true;
} }
bool ChatWindow::ProcessKeyEvent(GdkEventKey *e) {
if (e->type != GDK_KEY_PRESS) return false;
if (e->keyval == GDK_KEY_Up && !(e->state & GDK_SHIFT_MASK) && m_input->IsEmpty()) {
const auto edit_id = m_chat->GetLastSentEditableMessage();
if (edit_id.has_value()) {
StartEditing(*edit_id);
}
return true;
}
return false;
}
bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) { bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) {
if (m_completer.ProcessKeyPress(e)) if (m_completer.ProcessKeyPress(e))
return true; return true;
@ -290,6 +306,9 @@ bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) {
if (m_input->ProcessKeyPress(e)) if (m_input->ProcessKeyPress(e))
return true; return true;
if (ProcessKeyEvent(e))
return true;
return false; return false;
} }
@ -300,10 +319,11 @@ void ChatWindow::StartReplying(Snowflake message_id) {
m_replying_to = message_id; m_replying_to = message_id;
m_is_replying = true; m_is_replying = true;
m_input->StartReplying(); m_input->StartReplying();
if (author.has_value()) if (author.has_value()) {
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>()); m_input_indicator->SetCustomMarkup("Replying to " + author->GetUsernameEscapedBold());
else } else {
m_input_indicator->SetCustomMarkup("Replying..."); m_input_indicator->SetCustomMarkup("Replying...");
}
} }
void ChatWindow::StopReplying() { void ChatWindow::StopReplying() {
@ -313,6 +333,26 @@ void ChatWindow::StopReplying() {
m_input_indicator->ClearCustom(); m_input_indicator->ClearCustom();
} }
void ChatWindow::StartEditing(Snowflake message_id) {
const auto message = Abaddon::Get().GetDiscordClient().GetMessage(message_id);
if (!message.has_value()) {
spdlog::get("ui")->warn("ChatWindow::StartEditing message is nullopt");
return;
}
m_is_editing = true;
m_editing_id = message_id;
m_input->StartEditing(*message);
m_input_indicator->SetCustomMarkup("Editing...");
}
void ChatWindow::StopEditing() {
m_is_editing = false;
m_editing_id = Snowflake::Invalid;
m_input->StopEditing();
m_input->Clear();
m_input_indicator->ClearCustom();
}
void ChatWindow::OnScrollEdgeOvershot(Gtk::PositionType pos) { void ChatWindow::OnScrollEdgeOvershot(Gtk::PositionType pos) {
if (pos == Gtk::POS_TOP) if (pos == Gtk::POS_TOP)
m_signal_action_chat_load_history.emit(m_active_channel); m_signal_action_chat_load_history.emit(m_active_channel);

View File

@ -37,6 +37,9 @@ public:
void SetTopic(const std::string &text); void SetTopic(const std::string &text);
void AddAttachment(const Glib::RefPtr<Gio::File> &file); void AddAttachment(const Glib::RefPtr<Gio::File> &file);
void StartEditing(Snowflake message_id);
void StopEditing();
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
void OpenNewTab(Snowflake id); void OpenNewTab(Snowflake id);
TabsState GetTabsState(); TabsState GetTabsState();
@ -55,10 +58,14 @@ protected:
void StartReplying(Snowflake message_id); void StartReplying(Snowflake message_id);
void StopReplying(); void StopReplying();
bool m_is_editing = false;
Snowflake m_editing_id;
Snowflake m_active_channel; Snowflake m_active_channel;
bool OnInputSubmit(ChatSubmitParams data); bool OnInputSubmit(ChatSubmitParams data);
bool ProcessKeyEvent(GdkEventKey *e);
bool OnKeyPressEvent(GdkEventKey *e); bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos); void OnScrollEdgeOvershot(Gtk::PositionType pos);

View File

@ -117,12 +117,13 @@ void Completer::CompleteMentions(const Glib::ustring &term) {
if (id == me) continue; if (id == me) continue;
const auto author = discord.GetUser(id); const auto author = discord.GetUser(id);
if (!author.has_value()) continue; if (!author.has_value()) continue;
// todo improve the predicate here
if (!StringContainsCaseless(author->Username, term)) continue; if (!StringContainsCaseless(author->Username, term)) continue;
if (i++ > 15) break; if (i++ > 15) break;
auto entry = CreateEntry(author->GetMention()); auto entry = CreateEntry(author->GetMention());
entry->SetText(author->Username + "#" + author->Discriminator); entry->SetText(author->GetUsername());
if (channel_id.IsValid()) { if (channel_id.IsValid()) {
const auto chan = discord.GetChannel(channel_id); const auto chan = discord.GetChannel(channel_id);

View File

@ -117,16 +117,16 @@ void FriendsList::OnActionRemove(Snowflake id) {
Glib::ustring str; Glib::ustring str;
switch (*discord.GetRelationship(id)) { switch (*discord.GetRelationship(id)) {
case RelationshipType::Blocked: case RelationshipType::Blocked:
str = "Are you sure you want to unblock " + user->Username + "#" + user->Discriminator + "?"; str = "Are you sure you want to unblock " + user->GetUsername() + "?";
break; break;
case RelationshipType::Friend: case RelationshipType::Friend:
str = "Are you sure you want to remove " + user->Username + "#" + user->Discriminator + "?"; str = "Are you sure you want to remove " + user->GetUsername() + "?";
break; break;
case RelationshipType::PendingIncoming: case RelationshipType::PendingIncoming:
str = "Are you sure you want to ignore " + user->Username + "#" + user->Discriminator + "?"; str = "Are you sure you want to ignore " + user->GetUsername() + "?";
break; break;
case RelationshipType::PendingOutgoing: case RelationshipType::PendingOutgoing:
str = "Are you sure you want to cancel your request to " + user->Username + "#" + user->Discriminator + "?"; str = "Are you sure you want to cancel your request to " + user->GetUsername() + "?";
break; break;
default: default:
break; break;
@ -244,7 +244,7 @@ bool FriendsListAddComponent::OnKeyPress(GdkEventKey *e) {
FriendsListFriendRow::FriendsListFriendRow(RelationshipType type, const UserData &data) FriendsListFriendRow::FriendsListFriendRow(RelationshipType type, const UserData &data)
: ID(data.ID) : ID(data.ID)
, Type(type) , Type(type)
, Name(data.Username + "#" + data.Discriminator) , Name(data.GetUsername())
, Status(Abaddon::Get().GetDiscordClient().GetUserStatus(data.ID)) , Status(Abaddon::Get().GetDiscordClient().GetUserStatus(data.ID))
, m_accept("Accept") { , m_accept("Accept") {
auto *ev = Gtk::manage(new Gtk::EventBox); auto *ev = Gtk::manage(new Gtk::EventBox);
@ -265,7 +265,7 @@ FriendsListFriendRow::FriendsListFriendRow(RelationshipType type, const UserData
img->SetURL(data.GetAvatarURL("png", "32")); img->SetURL(data.GetAvatarURL("png", "32"));
} }
namelbl->set_markup(data.GetEscapedBoldName()); namelbl->set_markup(data.GetDisplayNameEscapedBold());
UpdatePresenceLabel(); UpdatePresenceLabel();

View File

@ -37,9 +37,17 @@ MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, cons
m_label->set_single_line_mode(true); m_label->set_single_line_mode(true);
m_label->set_ellipsize(Pango::ELLIPSIZE_END); m_label->set_ellipsize(Pango::ELLIPSIZE_END);
std::string display = data.Username; // todo remove after migration complete
if (Abaddon::Get().GetSettings().ShowMemberListDiscriminators) std::string display;
display += "#" + data.Discriminator; if (data.IsPomelo()) {
display = data.GetDisplayName(guild.has_value() ? guild->ID : Snowflake::Invalid);
} else {
display = data.Username;
if (Abaddon::Get().GetSettings().ShowMemberListDiscriminators) {
display += "#" + data.Discriminator;
}
}
if (guild.has_value()) { if (guild.has_value()) {
if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) { if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) {
auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color; auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color;

View File

@ -4,7 +4,7 @@
constexpr static uint64_t SnowflakeSplitDifference = 600; constexpr static uint64_t SnowflakeSplitDifference = 600;
constexpr static int MaxMessagesForChatCull = 50; // this has to be 50 (for now) cuz that magic number is used in a couple other places and i dont feel like replacing them constexpr static int MaxMessagesForChatCull = 50; // this has to be 50 (for now) cuz that magic number is used in a couple other places and i dont feel like replacing them
constexpr static int AttachmentItemSize = 120; constexpr static int AttachmentItemSize = 120;
constexpr static int BaseAttachmentSizeLimit = 8 * 1024 * 1024; constexpr static int BaseAttachmentSizeLimit = 25 * 1024 * 1024;
constexpr static int NitroClassicAttachmentSizeLimit = 50 * 1024 * 1024; constexpr static int NitroClassicAttachmentSizeLimit = 50 * 1024 * 1024;
constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024; constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024;
constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024; constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;

View File

@ -1,45 +0,0 @@
#include "editmessage.hpp"
EditMessageDialog::EditMessageDialog(Gtk::Window &parent)
: Gtk::Dialog("Edit Message", 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([&]() {
m_content = m_text.get_buffer()->get_text();
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_text.set_hexpand(true);
m_scroll.set_hexpand(true);
m_scroll.set_vexpand(true);
m_scroll.add(m_text);
m_layout.add(m_scroll);
m_layout.add(m_bbox);
get_content_area()->add(m_layout);
show_all_children();
}
Glib::ustring EditMessageDialog::GetContent() {
return m_content;
}
void EditMessageDialog::SetContent(const Glib::ustring &str) {
m_text.get_buffer()->set_text(str);
}

View File

@ -1,20 +0,0 @@
#pragma once
#include <string>
class EditMessageDialog : public Gtk::Dialog {
public:
EditMessageDialog(Gtk::Window &parent);
Glib::ustring GetContent();
void SetContent(const Glib::ustring &str);
protected:
Gtk::Box m_layout;
Gtk::Button m_ok;
Gtk::Button m_cancel;
Gtk::ButtonBox m_bbox;
Gtk::ScrolledWindow m_scroll;
Gtk::TextView m_text;
private:
Glib::ustring m_content;
};

View File

@ -62,7 +62,7 @@ FriendPickerDialogItem::FriendPickerDialogItem(Snowflake user_id)
, m_layout(Gtk::ORIENTATION_HORIZONTAL) { , m_layout(Gtk::ORIENTATION_HORIZONTAL) {
auto user = *Abaddon::Get().GetDiscordClient().GetUser(user_id); auto user = *Abaddon::Get().GetDiscordClient().GetUser(user_id);
m_name.set_markup(user.GetEscapedBoldString<false>()); m_name.set_markup(user.GetUsernameEscapedBold());
m_name.set_single_line_mode(true); m_name.set_single_line_mode(true);
m_avatar.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(32); m_avatar.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(32);

View File

@ -147,13 +147,13 @@ std::string ChannelData::GetRecipientsDisplay() const {
const auto recipients = GetDMRecipients(); const auto recipients = GetDMRecipients();
if (Type == ChannelType::DM && !recipients.empty()) { if (Type == ChannelType::DM && !recipients.empty()) {
return recipients[0].Username; return recipients[0].GetDisplayName();
} }
Glib::ustring r; Glib::ustring r;
for (size_t i = 0; i < recipients.size(); i++) { for (size_t i = 0; i < recipients.size(); i++) {
const auto &recipient = recipients[i]; const auto &recipient = recipients[i];
r += recipient.Username; r += recipient.GetDisplayName();
if (i < recipients.size() - 1) { if (i < recipients.size() - 1) {
r += ", "; r += ", ";
} }

View File

@ -17,8 +17,10 @@ struct ChatSubmitParams {
std::string Filename; std::string Filename;
}; };
bool Silent = false;
Snowflake ChannelID; Snowflake ChannelID;
Snowflake InReplyToID; Snowflake InReplyToID;
Snowflake EditingID;
Glib::ustring Message; Glib::ustring Message;
std::vector<Attachment> Attachments; std::vector<Attachment> Attachments;
}; };

View File

@ -461,8 +461,13 @@ void DiscordClient::SendChatMessageNoAttachments(const ChatSubmitParams &params,
CreateMessageObject obj; CreateMessageObject obj;
obj.Content = params.Message; obj.Content = params.Message;
obj.Nonce = nonce; obj.Nonce = nonce;
if (params.InReplyToID.IsValid()) if (params.Silent) {
obj.Flags |= MessageFlags::SUPPRESS_NOTIFICATIONS;
}
if (params.InReplyToID.IsValid()) {
obj.MessageReference.emplace().MessageID = params.InReplyToID; obj.MessageReference.emplace().MessageID = params.InReplyToID;
}
m_http.MakePOST("/channels/" + std::to_string(params.ChannelID) + "/messages", m_http.MakePOST("/channels/" + std::to_string(params.ChannelID) + "/messages",
nlohmann::json(obj).dump(), nlohmann::json(obj).dump(),
@ -494,8 +499,13 @@ void DiscordClient::SendChatMessageAttachments(const ChatSubmitParams &params, c
CreateMessageObject obj; CreateMessageObject obj;
obj.Content = params.Message; obj.Content = params.Message;
obj.Nonce = nonce; obj.Nonce = nonce;
if (params.InReplyToID.IsValid()) if (params.Silent) {
obj.Flags |= MessageFlags::SUPPRESS_NOTIFICATIONS;
}
if (params.InReplyToID.IsValid()) {
obj.MessageReference.emplace().MessageID = params.InReplyToID; obj.MessageReference.emplace().MessageID = params.InReplyToID;
}
auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(params.ChannelID) + "/messages"); auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(params.ChannelID) + "/messages");
m_progress_cb_timer.start(); m_progress_cb_timer.start();
@ -545,10 +555,11 @@ void DiscordClient::SendChatMessageAttachments(const ChatSubmitParams &params, c
} }
void DiscordClient::SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) { void DiscordClient::SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
if (params.Attachments.empty()) if (params.Attachments.empty()) {
SendChatMessageNoAttachments(params, callback); SendChatMessageNoAttachments(params, callback);
else } else {
SendChatMessageAttachments(params, callback); SendChatMessageAttachments(params, callback);
}
} }
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) { void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
@ -1186,6 +1197,27 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI
}); });
} }
void DiscordClient::RemoteAuthLogin(const std::string &ticket, const sigc::slot<void(std::optional<std::string>, DiscordError code)> &callback) {
http::request req(http::REQUEST_POST, "https://discord.com/api/v9/users/@me/remote-auth/login");
req.set_header("Content-Type", "application/json");
req.set_user_agent(Abaddon::Get().GetSettings().UserAgent);
req.set_body("{\"ticket\":\"" + ticket + "\"}");
m_http.Execute(std::move(req), [this, callback](const http::response_type &r) {
if (CheckCode(r)) {
callback(nlohmann::json::parse(r.text).at("encrypted_token").get<std::string>(), DiscordError::NONE);
} else {
try {
const auto j = nlohmann::json::parse(r.text);
if (j.contains("captcha_service")) {
callback(std::nullopt, DiscordError::CAPTCHA_REQUIRED);
return;
}
} catch (...) {}
callback(std::nullopt, GetCodeFromResponse(r));
}
});
}
#ifdef WITH_VOICE #ifdef WITH_VOICE
void DiscordClient::ConnectToVoice(Snowflake channel_id) { void DiscordClient::ConnectToVoice(Snowflake channel_id) {
auto channel = GetChannel(channel_id); auto channel = GetChannel(channel_id);
@ -1306,6 +1338,17 @@ int DiscordClient::GetUnreadStateForChannel(Snowflake id) const noexcept {
return iter->second; return iter->second;
} }
int DiscordClient::GetUnreadChannelsCountForCategory(Snowflake id) const noexcept {
int result = 0;
for (Snowflake channel_id : m_store.GetChannelIDsWithParentID(id)) {
if (IsChannelMuted(channel_id)) continue;
const auto iter = m_unread.find(channel_id);
if (iter == m_unread.end()) continue;
result += 1;
}
return result;
}
bool DiscordClient::GetUnreadStateForGuild(Snowflake id, int &total_mentions) const noexcept { bool DiscordClient::GetUnreadStateForGuild(Snowflake id, int &total_mentions) const noexcept {
total_mentions = 0; total_mentions = 0;
bool has_any_unread = false; bool has_any_unread = false;
@ -1579,6 +1622,9 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::VOICE_SERVER_UPDATE: { case GatewayEvent::VOICE_SERVER_UPDATE: {
HandleGatewayVoiceServerUpdate(m); HandleGatewayVoiceServerUpdate(m);
} break; } break;
case GatewayEvent::CALL_CREATE: {
HandleGatewayCallCreate(m);
} break;
#endif #endif
} }
} break; } break;
@ -1801,9 +1847,12 @@ void DiscordClient::HandleGatewayPresenceUpdate(const GatewayMessage &msg) {
void DiscordClient::HandleGatewayChannelDelete(const GatewayMessage &msg) { void DiscordClient::HandleGatewayChannelDelete(const GatewayMessage &msg) {
const auto id = msg.Data.at("id").get<Snowflake>(); const auto id = msg.Data.at("id").get<Snowflake>();
const auto channel = GetChannel(id); const auto channel = GetChannel(id);
auto it = m_guild_to_channels.find(*channel->GuildID); if (channel.has_value() && channel->GuildID.has_value()) {
if (it != m_guild_to_channels.end()) auto it = m_guild_to_channels.find(*channel->GuildID);
it->second.erase(id); if (it != m_guild_to_channels.end()) {
it->second.erase(id);
}
}
m_store.ClearChannel(id); m_store.ClearChannel(id);
m_signal_channel_delete.emit(id); m_signal_channel_delete.emit(id);
m_signal_channel_accessibility_changed.emit(id, false); m_signal_channel_accessibility_changed.emit(id, false);
@ -2253,8 +2302,39 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) {
void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) {
spdlog::get("discord")->trace("VOICE_STATE_UPDATE"); spdlog::get("discord")->trace("VOICE_STATE_UPDATE");
VoiceState data = msg.Data; CheckVoiceState(msg.Data);
}
void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) {
spdlog::get("discord")->trace("VOICE_SERVER_UPDATE");
VoiceServerUpdateData data = msg.Data;
spdlog::get("discord")->debug("Voice server endpoint: {}", data.Endpoint);
spdlog::get("discord")->debug("Voice token: {}", data.Token);
m_voice.SetEndpoint(data.Endpoint);
m_voice.SetToken(data.Token);
if (data.GuildID.has_value()) {
m_voice.SetServerID(*data.GuildID);
} else if (data.ChannelID.has_value()) {
m_voice.SetServerID(*data.ChannelID);
} else {
spdlog::get("discord")->error("No guild or channel ID in voice server?");
}
m_voice.SetUserID(m_user_data.ID);
m_voice.Start();
}
void DiscordClient::HandleGatewayCallCreate(const GatewayMessage &msg) {
CallCreateData data = msg.Data;
spdlog::get("discord")->debug("CALL_CREATE: {}", data.ChannelID);
for (const auto &state : data.VoiceStates) {
CheckVoiceState(state);
}
}
void DiscordClient::CheckVoiceState(const VoiceState &data) {
if (data.UserID == m_user_data.ID) { if (data.UserID == m_user_data.ID) {
spdlog::get("discord")->debug("Voice session ID: {}", data.SessionID); spdlog::get("discord")->debug("Voice session ID: {}", data.SessionID);
m_voice.SetSessionID(data.SessionID); m_voice.SetSessionID(data.SessionID);
@ -2292,25 +2372,6 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) {
} }
} }
} }
void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) {
spdlog::get("discord")->trace("VOICE_SERVER_UPDATE");
VoiceServerUpdateData data = msg.Data;
spdlog::get("discord")->debug("Voice server endpoint: {}", data.Endpoint);
spdlog::get("discord")->debug("Voice token: {}", data.Token);
m_voice.SetEndpoint(data.Endpoint);
m_voice.SetToken(data.Token);
if (data.GuildID.has_value()) {
m_voice.SetServerID(*data.GuildID);
} else if (data.ChannelID.has_value()) {
m_voice.SetServerID(*data.ChannelID);
} else {
spdlog::get("discord")->error("No guild or channel ID in voice server?");
}
m_voice.SetUserID(m_user_data.ID);
m_voice.Start();
}
#endif #endif
void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) {
@ -2556,7 +2617,7 @@ void DiscordClient::HeartbeatThread() {
void DiscordClient::SendIdentify() { void DiscordClient::SendIdentify() {
IdentifyMessage msg; IdentifyMessage msg;
msg.Token = m_token; msg.Token = m_token;
msg.Capabilities = 509; // no idea what this is msg.Capabilities = 4605; // bit 12 is necessary for CALL_CREATE... apparently? need to get this in sync with official client
msg.Properties.OS = "Windows"; msg.Properties.OS = "Windows";
msg.Properties.Browser = "Chrome"; msg.Properties.Browser = "Chrome";
msg.Properties.Device = ""; msg.Properties.Device = "";
@ -2575,9 +2636,6 @@ void DiscordClient::SendIdentify() {
msg.Presence.Since = 0; msg.Presence.Since = 0;
msg.Presence.IsAFK = false; msg.Presence.IsAFK = false;
msg.DoesSupportCompression = false; msg.DoesSupportCompression = false;
msg.ClientState.HighestLastMessageID = "0";
msg.ClientState.ReadStateVersion = 0;
msg.ClientState.UserGuildSettingsVersion = -1;
SetSuperPropertiesFromIdentity(msg); SetSuperPropertiesFromIdentity(msg);
const bool b = m_websocket.GetPrintMessages(); const bool b = m_websocket.GetPrintMessages();
m_websocket.SetPrintMessages(false); m_websocket.SetPrintMessages(false);
@ -2893,6 +2951,7 @@ void DiscordClient::LoadEventMap() {
m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK; m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK;
m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE; m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE;
m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE; m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE;
m_event_map["CALL_CREATE"] = GatewayEvent::CALL_CREATE;
} }
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {

View File

@ -184,6 +184,8 @@ public:
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback); void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback); void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
void RemoteAuthLogin(const std::string &ticket, const sigc::slot<void(std::optional<std::string>, DiscordError code)> &callback);
#ifdef WITH_VOICE #ifdef WITH_VOICE
void ConnectToVoice(Snowflake channel_id); void ConnectToVoice(Snowflake channel_id);
void DisconnectFromVoice(); void DisconnectFromVoice();
@ -214,6 +216,7 @@ public:
bool IsChannelMuted(Snowflake id) const noexcept; bool IsChannelMuted(Snowflake id) const noexcept;
bool IsGuildMuted(Snowflake id) const noexcept; bool IsGuildMuted(Snowflake id) const noexcept;
int GetUnreadStateForChannel(Snowflake id) const noexcept; int GetUnreadStateForChannel(Snowflake id) const noexcept;
int GetUnreadChannelsCountForCategory(Snowflake id) const noexcept;
bool GetUnreadStateForGuild(Snowflake id, int &total_mentions) const noexcept; bool GetUnreadStateForGuild(Snowflake id, int &total_mentions) const noexcept;
int GetUnreadDMsCount() const; int GetUnreadDMsCount() const;
@ -291,6 +294,9 @@ private:
#ifdef WITH_VOICE #ifdef WITH_VOICE
void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg); void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg);
void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg); void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg);
void HandleGatewayCallCreate(const GatewayMessage &msg);
void CheckVoiceState(const VoiceState &data);
#endif #endif
void HeartbeatThread(); void HeartbeatThread();

View File

@ -11,6 +11,7 @@ enum class DiscordError {
RELATIONSHIP_ALREADY_FRIENDS = 80007, RELATIONSHIP_ALREADY_FRIENDS = 80007,
NONE = -1, NONE = -1,
CAPTCHA_REQUIRED = -2,
}; };
constexpr const char *GetDiscordErrorDisplayString(DiscordError error) { constexpr const char *GetDiscordErrorDisplayString(DiscordError error) {

View File

@ -265,6 +265,10 @@ bool Message::IsEdited() const {
return m_edited; return m_edited;
} }
bool Message::IsEditable() const noexcept {
return (Abaddon::Get().GetDiscordClient().GetUserData().ID == Author.ID) && !IsDeleted() && !IsPending && (Type == MessageType::DEFAULT || Type == MessageType::INLINE_REPLY);
}
bool Message::DoesMentionEveryoneOrUser(Snowflake id) const noexcept { bool Message::DoesMentionEveryoneOrUser(Snowflake id) const noexcept {
if (DoesMentionEveryone) return true; if (DoesMentionEveryone) return true;
return std::any_of(Mentions.begin(), Mentions.end(), [id](const UserData &user) { return std::any_of(Mentions.begin(), Mentions.end(), [id](const UserData &user) {

View File

@ -8,6 +8,7 @@
#include "emoji.hpp" #include "emoji.hpp"
#include "member.hpp" #include "member.hpp"
#include "interactions.hpp" #include "interactions.hpp"
#include "misc/bitwise.hpp"
enum class MessageType { enum class MessageType {
DEFAULT = 0, // yep DEFAULT = 0, // yep
@ -35,14 +36,23 @@ enum class MessageType {
enum class MessageFlags { enum class MessageFlags {
NONE = 0, NONE = 0,
CROSSPOSTED = 1 << 0, // this message has been published to subscribed channels (via Channel Following) CROSSPOSTED = 1 << 0, // this message has been published to subscribed channels (via Channel Following)
IS_CROSSPOST = 1 << 1, // this message originated from a message in another channel (via Channel Following) IS_CROSSPOST = 1 << 1, // this message originated from a message in another channel (via Channel Following)
SUPPRESS_EMBEDS = 1 << 2, // do not include any embeds when serializing this message SUPPRESS_EMBEDS = 1 << 2, // do not include any embeds when serializing this message
SOURCE_MESSAGE_DELETE = 1 << 3, // the source message for this crosspost has been deleted (via Channel Following) SOURCE_MESSAGE_DELETE = 1 << 3, // the source message for this crosspost has been deleted (via Channel Following)
URGENT = 1 << 4, // this message came from the urgent message system URGENT = 1 << 4, // this message came from the urgent message system
HAS_THREAD = 1 << 5, // this message has an associated thread, with the same id as the message HAS_THREAD = 1 << 5, // this message has an associated thread, with the same id as the message
EPHEMERAL = 1 << 6, // this message is only visible to the user who invoked the Interaction EPHEMERAL = 1 << 6, // this message is only visible to the user who invoked the Interaction
LOADING = 1 << 7, // this message is an Interaction Response and the bot is "thinking" LOADING = 1 << 7, // this message is an Interaction Response and the bot is "thinking"
FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8, // this message failed to mention some roles and add their members to the thread
SHOULD_SHOW_LINK_NOT_DISCORD_WARNING = 1 << 10, //
SUPPRESS_NOTIFICATIONS = 1 << 12, // this message will not trigger push and desktop notifications
IS_VOICE_MESSAGE = 1 << 13, // this message is a voice message
};
template<>
struct Bitwise<MessageFlags> {
static const bool enable = true;
}; };
struct EmbedFooterData { struct EmbedFooterData {
@ -209,11 +219,13 @@ struct Message {
void SetDeleted(); void SetDeleted();
void SetEdited(); void SetEdited();
[[nodiscard]] bool IsDeleted() const; bool IsDeleted() const;
[[nodiscard]] bool IsEdited() const; bool IsEdited() const;
[[nodiscard]] bool DoesMentionEveryoneOrUser(Snowflake id) const noexcept; bool IsEditable() const noexcept;
[[nodiscard]] bool DoesMention(Snowflake id) const noexcept;
bool DoesMentionEveryoneOrUser(Snowflake id) const noexcept;
bool DoesMention(Snowflake id) const noexcept;
private: private:
bool m_deleted = false; bool m_deleted = false;

View File

@ -251,7 +251,7 @@ void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m) {
void from_json(const nlohmann::json &j, SupplementalGuildEntry &m) { void from_json(const nlohmann::json &j, SupplementalGuildEntry &m) {
JS_D("id", m.ID); JS_D("id", m.ID);
JS_D("voice_states", m.VoiceStates); JS_ON("voice_states", m.VoiceStates);
} }
void from_json(const nlohmann::json &j, ReadySupplementalData &m) { void from_json(const nlohmann::json &j, ReadySupplementalData &m) {
@ -308,6 +308,7 @@ void to_json(nlohmann::json &j, const HeartbeatMessage &m) {
void to_json(nlohmann::json &j, const CreateMessageObject &m) { void to_json(nlohmann::json &j, const CreateMessageObject &m) {
j["content"] = m.Content; j["content"] = m.Content;
j["flags"] = m.Flags;
JS_IF("message_reference", m.MessageReference); JS_IF("message_reference", m.MessageReference);
JS_IF("nonce", m.Nonce); JS_IF("nonce", m.Nonce);
} }
@ -463,6 +464,7 @@ void from_json(const nlohmann::json &j, UserProfileData &m) {
JS_D("mutual_guilds", m.MutualGuilds); JS_D("mutual_guilds", m.MutualGuilds);
JS_ON("premium_guild_since", m.PremiumGuildSince); JS_ON("premium_guild_since", m.PremiumGuildSince);
JS_ON("premium_since", m.PremiumSince); JS_ON("premium_since", m.PremiumSince);
JS_ON("legacy_username", m.LegacyUsername);
JS_D("user", m.User); JS_D("user", m.User);
} }
@ -686,6 +688,11 @@ void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) {
JS_ON("guild_id", m.GuildID); JS_ON("guild_id", m.GuildID);
JS_ON("channel_id", m.ChannelID); JS_ON("channel_id", m.ChannelID);
} }
void from_json(const nlohmann::json &j, CallCreateData &m) {
JS_D("channel_id", m.ChannelID);
JS_ON("voice_states", m.VoiceStates);
}
#endif #endif
void from_json(const nlohmann::json &j, VoiceState &m) { void from_json(const nlohmann::json &j, VoiceState &m) {

View File

@ -102,6 +102,7 @@ enum class GatewayEvent : int {
GUILD_MEMBERS_CHUNK, GUILD_MEMBERS_CHUNK,
VOICE_STATE_UPDATE, VOICE_STATE_UPDATE,
VOICE_SERVER_UPDATE, VOICE_SERVER_UPDATE,
CALL_CREATE,
}; };
enum class GatewayCloseCode : uint16_t { enum class GatewayCloseCode : uint16_t {
@ -427,6 +428,7 @@ struct HeartbeatMessage : GatewayMessage {
struct CreateMessageObject { struct CreateMessageObject {
std::string Content; std::string Content;
MessageFlags Flags = MessageFlags::NONE;
std::optional<MessageReferenceData> MessageReference; std::optional<MessageReferenceData> MessageReference;
std::optional<std::string> Nonce; std::optional<std::string> Nonce;
@ -617,6 +619,7 @@ struct UserProfileData {
std::vector<MutualGuildData> MutualGuilds; std::vector<MutualGuildData> MutualGuilds;
std::optional<std::string> PremiumGuildSince; // null std::optional<std::string> PremiumGuildSince; // null
std::optional<std::string> PremiumSince; // null std::optional<std::string> PremiumSince; // null
std::optional<std::string> LegacyUsername; // null
UserData User; UserData User;
friend void from_json(const nlohmann::json &j, UserProfileData &m); friend void from_json(const nlohmann::json &j, UserProfileData &m);
@ -886,6 +889,23 @@ struct GuildMembersChunkData {
friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m); friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m);
}; };
struct VoiceState {
std::optional<Snowflake> ChannelID;
bool IsDeafened;
bool IsMuted;
std::optional<Snowflake> GuildID;
std::optional<GuildMember> Member;
bool IsSelfDeafened;
bool IsSelfMuted;
bool IsSelfVideo;
bool IsSelfStream = false;
std::string SessionID;
bool IsSuppressed;
Snowflake UserID;
friend void from_json(const nlohmann::json &j, VoiceState &m);
};
#ifdef WITH_VOICE #ifdef WITH_VOICE
struct VoiceStateUpdateMessage { struct VoiceStateUpdateMessage {
std::optional<Snowflake> GuildID; std::optional<Snowflake> GuildID;
@ -906,21 +926,15 @@ struct VoiceServerUpdateData {
friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m); friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m);
}; };
#endif
struct VoiceState { struct CallCreateData {
std::optional<Snowflake> ChannelID; Snowflake ChannelID;
bool IsDeafened; std::vector<VoiceState> VoiceStates;
bool IsMuted; // Snowflake MessageID;
std::optional<Snowflake> GuildID; // std::string Region;
std::optional<GuildMember> Member; // std::vector<?> Ringing;
bool IsSelfDeafened; // std::vector<?> EmbeddedActivities;
bool IsSelfMuted;
bool IsSelfVideo;
bool IsSelfStream = false;
std::string SessionID;
bool IsSuppressed;
Snowflake UserID;
friend void from_json(const nlohmann::json &j, VoiceState &m); friend void from_json(const nlohmann::json &j, CallCreateData &m);
}; };
#endif

View File

@ -60,6 +60,10 @@ Glib::ustring Snowflake::GetLocalTimestamp() const {
return tmp.data(); return tmp.data();
} }
uint64_t Snowflake::GetUnixMilliseconds() const noexcept {
return (m_num >> 22) + DiscordEpochSeconds * 1000;
}
void from_json(const nlohmann::json &j, Snowflake &s) { void from_json(const nlohmann::json &j, Snowflake &s) {
if (j.is_string()) { if (j.is_string()) {
std::string tmp; std::string tmp;

View File

@ -16,6 +16,7 @@ struct Snowflake {
[[nodiscard]] bool IsValid() const; [[nodiscard]] bool IsValid() const;
[[nodiscard]] Glib::ustring GetLocalTimestamp() const; [[nodiscard]] Glib::ustring GetLocalTimestamp() const;
[[nodiscard]] uint64_t GetUnixMilliseconds() const noexcept;
bool operator==(const Snowflake &s) const noexcept { bool operator==(const Snowflake &s) const noexcept {
return m_num == s.m_num; return m_num == s.m_num;

View File

@ -438,6 +438,7 @@ void Store::SetUser(Snowflake id, const UserData &user) {
s->Bind(7, user.IsMFAEnabled); s->Bind(7, user.IsMFAEnabled);
s->Bind(8, user.PremiumType); s->Bind(8, user.PremiumType);
s->Bind(9, user.PublicFlags); s->Bind(9, user.PublicFlags);
s->Bind(10, user.GlobalName);
if (!s->Insert()) if (!s->Insert())
fprintf(stderr, "user insert failed for %" PRIu64 ": %s\n", static_cast<uint64_t>(id), m_db.ErrStr()); fprintf(stderr, "user insert failed for %" PRIu64 ": %s\n", static_cast<uint64_t>(id), m_db.ErrStr());
@ -558,8 +559,9 @@ std::vector<Message> Store::GetMessagesBefore(Snowflake channel_id, Snowflake me
for (auto &msg : msgs) { for (auto &msg : msgs) {
if (msg.MessageReference.has_value() && msg.MessageReference->MessageID.has_value()) { if (msg.MessageReference.has_value() && msg.MessageReference->MessageID.has_value()) {
auto ref = GetMessage(*msg.MessageReference->MessageID); auto ref = GetMessage(*msg.MessageReference->MessageID);
if (ref.has_value()) if (ref.has_value()) {
msg.ReferencedMessage = std::make_shared<Message>(std::move(*ref)); msg.ReferencedMessage = std::make_shared<Message>(std::move(*ref));
}
} }
} }
@ -1109,6 +1111,7 @@ std::optional<UserData> Store::GetUser(Snowflake id) const {
s->Get(6, r.IsMFAEnabled); s->Get(6, r.IsMFAEnabled);
s->Get(7, r.PremiumType); s->Get(7, r.PremiumType);
s->Get(8, r.PublicFlags); s->Get(8, r.PublicFlags);
s->Get(9, r.GlobalName);
s->Reset(); s->Reset();
@ -1233,7 +1236,8 @@ bool Store::CreateTables() {
system BOOL, system BOOL,
mfa BOOL, mfa BOOL,
premium INTEGER, premium INTEGER,
pubflags INTEGER pubflags INTEGER,
global_name TEXT
) )
)"; )";
@ -1797,7 +1801,7 @@ bool Store::CreateStatements() {
m_stmt_set_user = std::make_unique<Statement>(m_db, R"( m_stmt_set_user = std::make_unique<Statement>(m_db, R"(
REPLACE INTO users VALUES ( REPLACE INTO users VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) )
)"); )");
if (!m_stmt_set_user->OK()) { if (!m_stmt_set_user->OK()) {
@ -2057,26 +2061,22 @@ bool Store::CreateStatements() {
message_interactions.name, message_interactions.name,
message_interactions.type, message_interactions.type,
message_interactions.user_id, message_interactions.user_id,
attachments.id, message_references.message,
attachments.filename, message_references.channel,
attachments.size, message_references.guild,
attachments.url, COUNT(attachments.id)
attachments.proxy,
attachments.height,
attachments.width,
message_references.message
FROM messages FROM messages
LEFT OUTER JOIN LEFT OUTER JOIN
message_interactions message_interactions
ON messages.id = message_interactions.message_id ON messages.id = message_interactions.message_id
LEFT OUTER JOIN
attachments
ON messages.id = attachments.message
LEFT OUTER JOIN LEFT OUTER JOIN
message_references message_references
ON messages.id = message_references.id ON messages.id = message_references.id
LEFT OUTER JOIN
attachments
ON messages.id = attachments.message
WHERE channel_id = ? AND pending = 0 AND messages.id < ? ORDER BY id DESC LIMIT ? WHERE channel_id = ? AND pending = 0 AND messages.id < ? ORDER BY id DESC LIMIT ?
) ORDER BY id ASC ) WHERE id IS NOT NULL ORDER BY id ASC
)"); )");
if (!m_stmt_get_messages_before->OK()) { if (!m_stmt_get_messages_before->OK()) {
fprintf(stderr, "failed to prepare get messages before statement: %s\n", m_db.ErrStr()); fprintf(stderr, "failed to prepare get messages before statement: %s\n", m_db.ErrStr());
@ -2084,32 +2084,28 @@ bool Store::CreateStatements() {
} }
m_stmt_get_pins = std::make_unique<Statement>(m_db, R"( m_stmt_get_pins = std::make_unique<Statement>(m_db, R"(
SELECT messages.*, SELECT * FROM (
message_interactions.interaction_id, SELECT messages.*,
message_interactions.name, message_interactions.interaction_id,
message_interactions.type, message_interactions.name,
message_interactions.user_id, message_interactions.type,
attachments.id, message_interactions.user_id,
attachments.filename, message_references.message,
attachments.size, message_references.channel,
attachments.url, message_references.guild,
attachments.proxy, COUNT(attachments.id)
attachments.height, FROM messages
attachments.width, LEFT OUTER JOIN
message_references.message, message_interactions
message_references.channel, ON messages.id = message_interactions.message_id
message_references.guild LEFT OUTER JOIN
FROM messages message_references
LEFT OUTER JOIN ON messages.id = message_references.id
message_interactions LEFT OUTER JOIN
ON messages.id = message_interactions.message_id attachments
LEFT OUTER JOIN ON messages.id = attachments.message
attachments WHERE channel_id = ? AND pinned = 1 ORDER BY id ASC
ON messages.id = attachments.message ) WHERE id IS NOT NULL
LEFT OUTER JOIN
message_references
ON messages.id = message_references.id
WHERE channel_id = ? AND pinned = 1 ORDER BY id ASC
)"); )");
if (!m_stmt_get_pins->OK()) { if (!m_stmt_get_pins->OK()) {
fprintf(stderr, "failed to prepare get pins statement: %s\n", m_db.ErrStr()); fprintf(stderr, "failed to prepare get pins statement: %s\n", m_db.ErrStr());

View File

@ -1,5 +1,9 @@
#include "user.hpp" #include "user.hpp"
bool UserData::IsPomelo() const noexcept {
return Discriminator.size() == 1 && Discriminator[0] == '0';
}
bool UserData::IsABot() const noexcept { bool UserData::IsABot() const noexcept {
return IsBot.has_value() && *IsBot; return IsBot.has_value() && *IsBot;
} }
@ -18,49 +22,54 @@ bool UserData::HasAnimatedAvatar() const noexcept {
bool UserData::HasAnimatedAvatar(Snowflake guild_id) const { bool UserData::HasAnimatedAvatar(Snowflake guild_id) const {
const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id); const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id);
if (member.has_value() && member->Avatar.has_value() && member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_') if (member.has_value() && member->Avatar.has_value() && member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_') {
return true; return true;
else if (member.has_value() && !member->Avatar.has_value()) } else if (member.has_value() && !member->Avatar.has_value()) {
return HasAnimatedAvatar(); return HasAnimatedAvatar();
}
return false; return false;
} }
bool UserData::HasAnimatedAvatar(const std::optional<Snowflake> &guild_id) const { bool UserData::HasAnimatedAvatar(const std::optional<Snowflake> &guild_id) const {
if (guild_id.has_value()) if (guild_id.has_value()) {
return HasAnimatedAvatar(*guild_id); return HasAnimatedAvatar(*guild_id);
else }
return HasAnimatedAvatar();
return HasAnimatedAvatar();
} }
std::string UserData::GetAvatarURL(Snowflake guild_id, const std::string &ext, std::string size) const { std::string UserData::GetAvatarURL(Snowflake guild_id, const std::string &ext, std::string size) const {
const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id); const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id);
if (member.has_value() && member->Avatar.has_value()) { if (member.has_value() && member->Avatar.has_value()) {
if (ext == "gif" && !(member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_')) if (ext == "gif" && !(member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_')) {
return GetAvatarURL(ext, size); return GetAvatarURL(ext, size);
}
return "https://cdn.discordapp.com/guilds/" + return "https://cdn.discordapp.com/guilds/" +
std::to_string(guild_id) + "/users/" + std::to_string(ID) + std::to_string(guild_id) + "/users/" + std::to_string(ID) +
"/avatars/" + *member->Avatar + "." + "/avatars/" + *member->Avatar + "." +
ext + "?" + "size=" + size; ext + "?" + "size=" + size;
} else {
return GetAvatarURL(ext, size);
} }
return GetAvatarURL(ext, size);
} }
std::string UserData::GetAvatarURL(const std::optional<Snowflake> &guild_id, const std::string &ext, std::string size) const { std::string UserData::GetAvatarURL(const std::optional<Snowflake> &guild_id, const std::string &ext, std::string size) const {
if (guild_id.has_value()) if (guild_id.has_value()) {
return GetAvatarURL(*guild_id, ext, size); return GetAvatarURL(*guild_id, ext, size);
else }
return GetAvatarURL(ext, size); return GetAvatarURL(ext, size);
} }
std::string UserData::GetAvatarURL(const std::string &ext, std::string size) const { std::string UserData::GetAvatarURL(const std::string &ext, std::string size) const {
if (HasAvatar()) if (HasAvatar()) {
return "https://cdn.discordapp.com/avatars/" + std::to_string(ID) + "/" + Avatar + "." + ext + "?size=" + size; return "https://cdn.discordapp.com/avatars/" + std::to_string(ID) + "/" + Avatar + "." + ext + "?size=" + size;
else }
return GetDefaultAvatarURL(); return GetDefaultAvatarURL();
} }
std::string UserData::GetDefaultAvatarURL() const { std::string UserData::GetDefaultAvatarURL() const {
if (IsPomelo()) {
return "https://cdn.discordapp.com/embed/avatars/" + std::to_string((static_cast<uint64_t>(ID) >> 22) % 6) + ".png";
}
return "https://cdn.discordapp.com/embed/avatars/" + std::to_string(std::stoul(Discriminator) % 5) + ".png"; // size isn't respected by the cdn return "https://cdn.discordapp.com/embed/avatars/" + std::to_string(std::stoul(Discriminator) % 5) + ".png"; // size isn't respected by the cdn
} }
@ -72,18 +81,75 @@ std::string UserData::GetMention() const {
return "<@" + std::to_string(ID) + ">"; return "<@" + std::to_string(ID) + ">";
} }
std::string UserData::GetEscapedName() const { std::string UserData::GetDisplayName() const {
return Glib::Markup::escape_text(Username); if (IsPomelo() && GlobalName.has_value()) {
return *GlobalName;
}
return Username;
} }
std::string UserData::GetEscapedBoldName() const { std::string UserData::GetDisplayName(Snowflake guild_id) const {
return "<b>" + Glib::Markup::escape_text(Username) + "</b>"; const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id);
if (member.has_value() && !member->Nickname.empty()) {
return member->Nickname;
}
return GetDisplayName();
} }
std::string UserData::GetEscapedString() const { std::string UserData::GetDisplayName(const std::optional<Snowflake> &guild_id) const {
if (guild_id.has_value()) {
return GetDisplayName(*guild_id);
}
return GetDisplayName();
}
std::string UserData::GetDisplayNameEscaped() const {
return Glib::Markup::escape_text(GetDisplayName());
}
std::string UserData::GetDisplayNameEscaped(Snowflake guild_id) const {
return Glib::Markup::escape_text(GetDisplayName(guild_id));
}
std::string UserData::GetDisplayNameEscapedBold() const {
return "<b>" + Glib::Markup::escape_text(GetDisplayName()) + "</b>";
}
std::string UserData::GetDisplayNameEscapedBold(Snowflake guild_id) const {
return "<b>" + Glib::Markup::escape_text(GetDisplayName(guild_id)) + "</b>";
}
std::string UserData::GetUsername() const {
if (IsPomelo()) {
return Username;
}
return Username + "#" + Discriminator;
}
std::string UserData::GetUsernameEscaped() const {
if (IsPomelo()) {
return Glib::Markup::escape_text(Username);
}
return Glib::Markup::escape_text(Username) + "#" + Discriminator; return Glib::Markup::escape_text(Username) + "#" + Discriminator;
} }
std::string UserData::GetUsernameEscapedBold() const {
if (IsPomelo()) {
return "<b>" + Glib::Markup::escape_text(Username) + "</b>";
}
return "<b>" + Glib::Markup::escape_text(Username) + "</b>#" + Discriminator;
}
std::string UserData::GetUsernameEscapedBoldAt() const {
if (IsPomelo()) {
return "<b>@" + Glib::Markup::escape_text(Username) + "</b>";
}
return "<b>@" + Glib::Markup::escape_text(Username) + "</b>#" + Discriminator;
}
void from_json(const nlohmann::json &j, UserData &m) { void from_json(const nlohmann::json &j, UserData &m) {
JS_D("id", m.ID); JS_D("id", m.ID);
JS_D("username", m.Username); JS_D("username", m.Username);
@ -104,6 +170,7 @@ void from_json(const nlohmann::json &j, UserData &m) {
JS_ON("phone", m.Phone); JS_ON("phone", m.Phone);
JS_ON("bio", m.Bio); JS_ON("bio", m.Bio);
JS_ON("banner", m.BannerHash); JS_ON("banner", m.BannerHash);
JS_ON("global_name", m.GlobalName);
} }
void to_json(nlohmann::json &j, const UserData &m) { void to_json(nlohmann::json &j, const UserData &m) {
@ -127,6 +194,7 @@ void to_json(nlohmann::json &j, const UserData &m) {
JS_IF("mobile", m.IsMobile); JS_IF("mobile", m.IsMobile);
JS_IF("nsfw_allowed", m.IsNSFWAllowed); JS_IF("nsfw_allowed", m.IsNSFWAllowed);
JS_IF("phone", m.Phone); JS_IF("phone", m.Phone);
JS_IF("global_name", m.GlobalName);
} }
void UserData::update_from_json(const nlohmann::json &j) { void UserData::update_from_json(const nlohmann::json &j) {
@ -146,6 +214,7 @@ void UserData::update_from_json(const nlohmann::json &j) {
JS_RD("mobile", IsMobile); JS_RD("mobile", IsMobile);
JS_RD("nsfw_allowed", IsNSFWAllowed); JS_RD("nsfw_allowed", IsNSFWAllowed);
JS_RD("phone", Phone); JS_RD("phone", Phone);
JS_RD("global_name", GlobalName);
} }
const char *UserData::GetFlagName(uint64_t flag) { const char *UserData::GetFlagName(uint64_t flag) {

View File

@ -25,6 +25,11 @@ struct UserData {
VerifiedBot = 1 << 16, VerifiedBot = 1 << 16,
EarlyVerifiedBotDeveloper = 1 << 17, EarlyVerifiedBotDeveloper = 1 << 17,
CertifiedModerator = 1 << 18, CertifiedModerator = 1 << 18,
BotHTTPInteractions = 1 << 19,
Spammer = 1 << 20,
DisablePremium = 1 << 21,
ActiveDeveloper = 1 << 22,
Quarantined = 1ULL << 44,
MaxFlag_PlusOne, MaxFlag_PlusOne,
MaxFlag = MaxFlag_PlusOne - 1, MaxFlag = MaxFlag_PlusOne - 1,
@ -37,6 +42,7 @@ struct UserData {
std::string Username; std::string Username;
std::string Discriminator; std::string Discriminator;
std::string Avatar; // null std::string Avatar; // null
std::optional<std::string> GlobalName;
std::optional<bool> IsBot; std::optional<bool> IsBot;
std::optional<bool> IsSystem; std::optional<bool> IsSystem;
std::optional<bool> IsMFAEnabled; std::optional<bool> IsMFAEnabled;
@ -60,6 +66,7 @@ struct UserData {
friend void to_json(nlohmann::json &j, const UserData &m); friend void to_json(nlohmann::json &j, const UserData &m);
void update_from_json(const nlohmann::json &j); void update_from_json(const nlohmann::json &j);
[[nodiscard]] bool IsPomelo() const noexcept;
[[nodiscard]] bool IsABot() const noexcept; [[nodiscard]] bool IsABot() const noexcept;
[[nodiscard]] bool IsDeleted() const; [[nodiscard]] bool IsDeleted() const;
[[nodiscard]] bool HasAvatar() const; [[nodiscard]] bool HasAvatar() const;
@ -72,14 +79,15 @@ struct UserData {
[[nodiscard]] std::string GetDefaultAvatarURL() const; [[nodiscard]] std::string GetDefaultAvatarURL() const;
[[nodiscard]] Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const; [[nodiscard]] Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const;
[[nodiscard]] std::string GetMention() const; [[nodiscard]] std::string GetMention() const;
[[nodiscard]] std::string GetEscapedName() const; [[nodiscard]] std::string GetDisplayName() const;
[[nodiscard]] std::string GetEscapedBoldName() const; [[nodiscard]] std::string GetDisplayName(Snowflake guild_id) const;
[[nodiscard]] std::string GetEscapedString() const; [[nodiscard]] std::string GetDisplayName(const std::optional<Snowflake> &guild_id) const;
template<bool with_at> [[nodiscard]] std::string GetDisplayNameEscaped() const;
[[nodiscard]] inline std::string GetEscapedBoldString() const { [[nodiscard]] std::string GetDisplayNameEscaped(Snowflake guild_id) const;
if constexpr (with_at) [[nodiscard]] std::string GetDisplayNameEscapedBold() const;
return "<b>@" + Glib::Markup::escape_text(Username) + "</b>#" + Discriminator; [[nodiscard]] std::string GetDisplayNameEscapedBold(Snowflake guild_id) const;
else [[nodiscard]] std::string GetUsername() const;
return "<b>" + Glib::Markup::escape_text(Username) + "</b>#" + Discriminator; [[nodiscard]] std::string GetUsernameEscaped() const;
} [[nodiscard]] std::string GetUsernameEscapedBold() const;
[[nodiscard]] std::string GetUsernameEscapedBoldAt() const;
}; };

View File

@ -112,8 +112,8 @@ void UDPSocket::ReadThread() {
sockaddr_in from; sockaddr_in from;
socklen_t addrlen = sizeof(from); socklen_t addrlen = sizeof(from);
tv.tv_sec = 0; tv.tv_sec = 1;
tv.tv_usec = 1000000; tv.tv_usec = 0;
fd_set read_fds; fd_set read_fds;
FD_ZERO(&read_fds); FD_ZERO(&read_fds);

View File

@ -26,7 +26,7 @@ void Websocket::StartConnection(const std::string &url) {
m_websocket->disableAutomaticReconnection(); m_websocket->disableAutomaticReconnection();
m_websocket->setUrl(url); m_websocket->setUrl(url);
m_websocket->setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward<decltype(msg)>(msg)); }); 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(); m_websocket->start();
} }
@ -81,6 +81,9 @@ void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) {
case ix::WebSocketMessageType::Message: { case ix::WebSocketMessageType::Message: {
m_signal_message.emit(msg->str); m_signal_message.emit(msg->str);
} break; } break;
case ix::WebSocketMessageType::Error: {
m_log->error("Websocket error: Status: {} Reason: {}", msg->errorInfo.http_status, msg->errorInfo.reason);
} break;
default: default:
break; break;
} }

View File

@ -74,17 +74,17 @@ void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf, Snowflake chan
if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM || !channel->GuildID.has_value() || plain) { if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM || !channel->GuildID.has_value() || plain) {
if (plain) { if (plain) {
replacement = "@" + user->Username + "#" + user->Discriminator; replacement = "@" + user->GetUsername();
} else { } else {
replacement = user->GetEscapedBoldString<true>(); replacement = user->GetUsernameEscapedBoldAt();
} }
} else { } else {
const auto role_id = user->GetHoistedRole(*channel->GuildID, true); const auto role_id = user->GetHoistedRole(*channel->GuildID, true);
const auto role = discord.GetRole(role_id); const auto role = discord.GetRole(role_id);
if (!role.has_value()) if (!role.has_value())
replacement = user->GetEscapedBoldString<true>(); replacement = user->GetUsernameEscapedBoldAt();
else else
replacement = "<span color=\"#" + IntToCSSColor(role->Color) + "\">" + user->GetEscapedBoldString<true>() + "</span>"; replacement = "<span color=\"#" + IntToCSSColor(role->Color) + "\">" + user->GetUsernameEscapedBoldAt() + "</span>";
} }
// regex returns byte positions and theres no straightforward way in the c++ bindings to deal with that :( // regex returns byte positions and theres no straightforward way in the c++ bindings to deal with that :(

View File

@ -88,6 +88,8 @@ bool CheckGuildMessage(const Message &message) {
void Notifications::CheckMessage(const Message &message) { void Notifications::CheckMessage(const Message &message) {
if (!Abaddon::Get().GetSettings().NotificationsEnabled) return; if (!Abaddon::Get().GetSettings().NotificationsEnabled) return;
// ignore if silent message
if (message.Flags.has_value() && ((*message.Flags & MessageFlags::SUPPRESS_NOTIFICATIONS) == MessageFlags::SUPPRESS_NOTIFICATIONS)) return;
// ignore if our status is do not disturb // ignore if our status is do not disturb
if (IsDND()) return; if (IsDND()) return;
auto &discord = Abaddon::Get().GetDiscordClient(); auto &discord = Abaddon::Get().GetDiscordClient();

View File

@ -0,0 +1,351 @@
#ifdef WITH_QRLOGIN
// clang-format off
#include "remoteauthclient.hpp"
#include "http.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/fmt/bin_to_hex.h>
// clang-format on
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;
if (m_timeout_conn) m_timeout_conn.disconnect();
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) {
m_log->trace(str);
auto j = nlohmann::json::parse(str);
const auto opcode = j.at("op").get<std::string>();
if (opcode == "hello") {
HandleGatewayHello(j);
} else if (opcode == "nonce_proof") {
HandleGatewayNonceProof(j);
} else if (opcode == "pending_remote_init") {
HandleGatewayPendingRemoteInit(j);
} else if (opcode == "pending_ticket") {
HandleGatewayPendingTicket(j);
} else if (opcode == "pending_login") {
HandleGatewayPendingLogin(j);
} else if (opcode == "cancel") {
HandleGatewayCancel(j);
}
}
void RemoteAuthClient::HandleGatewayHello(const nlohmann::json &j) {
const auto timeout_ms = j.at("timeout_ms").get<int>();
const auto heartbeat_interval = j.at("heartbeat_interval").get<int>();
m_log->debug("Timeout: {}, Heartbeat: {}", timeout_ms, heartbeat_interval);
m_heartbeat_msec = heartbeat_interval;
m_heartbeat_thread = std::thread(&RemoteAuthClient::HeartbeatThread, this);
m_timeout_conn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &RemoteAuthClient::OnTimeout), timeout_ms);
Init();
m_signal_hello.emit();
}
void RemoteAuthClient::HandleGatewayNonceProof(const nlohmann::json &j) {
m_log->debug("Received encrypted nonce");
const auto encrypted_nonce = Glib::Base64::decode(j.at("encrypted_nonce").get<std::string>());
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());
nlohmann::json reply;
reply["op"] = "nonce_proof";
reply["nonce"] = proof_encoded;
m_ws.Send(reply);
}
void RemoteAuthClient::HandleGatewayPendingRemoteInit(const nlohmann::json &j) {
m_log->debug("Received fingerprint");
m_signal_fingerprint.emit(j.at("fingerprint").get<std::string>());
}
void RemoteAuthClient::HandleGatewayPendingTicket(const nlohmann::json &j) {
const auto encrypted_payload = Glib::Base64::decode(j.at("encrypted_user_payload").get<std::string>());
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()));
const std::vector<Glib::ustring> user_info = Glib::Regex::split_simple(":", std::string(payload.begin(), payload.end()));
Snowflake user_id;
std::string discriminator;
std::string avatar_hash;
std::string username;
if (user_info.size() >= 4) {
user_id = Snowflake(user_info[0]);
discriminator = user_info[1];
avatar_hash = user_info[2];
username = user_info[3];
}
m_signal_pending_ticket.emit(user_id, discriminator, avatar_hash, username);
}
void RemoteAuthClient::HandleGatewayPendingLogin(const nlohmann::json &j) {
Abaddon::Get().GetDiscordClient().RemoteAuthLogin(j.at("ticket").get<std::string>(), sigc::mem_fun(*this, &RemoteAuthClient::OnRemoteAuthLoginResponse));
m_signal_pending_login.emit();
}
void RemoteAuthClient::HandleGatewayCancel(const nlohmann::json &j) {
Stop();
Start();
}
void RemoteAuthClient::OnRemoteAuthLoginResponse(const std::optional<std::string> &encrypted_token, DiscordError err) {
if (!encrypted_token.has_value()) {
m_log->error("Remote auth login failed: {}", static_cast<int>(err));
if (err == DiscordError::CAPTCHA_REQUIRED) {
m_signal_error.emit("Discord is requiring a captcha. You must use a web browser to log in.");
} else {
m_signal_error.emit("An error occurred. Try again.");
}
return;
}
const auto encrypted = Glib::Base64::decode(*encrypted_token);
const auto token = Decrypt(reinterpret_cast<const unsigned char *>(encrypted.data()), encrypted.size());
m_signal_token.emit(std::string(token.begin(), token.end()));
}
void RemoteAuthClient::Init() {
GenerateKey();
const auto key = GetEncodedPublicKey();
if (key.empty()) {
m_log->error("Something went wrong");
// todo disconnect
return;
}
nlohmann::json msg;
msg["op"] = "init";
msg["encoded_public_key"] = key;
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);
if (m_connected) {
m_signal_error.emit("Error. Websocket closed (remote): " + std::to_string(info.code) + " (" + info.reason + ")");
}
} else {
m_log->debug("Websocket closed (local): {} ({})", info.code, info.reason);
if (m_connected) {
m_signal_error.emit("Error. Websocket closed (local): " + std::to_string(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;
nlohmann::json hb;
hb["op"] = "heartbeat";
m_ws.Send(hb);
}
}
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);
}
bool RemoteAuthClient::OnTimeout() {
m_log->trace("Socket timeout");
Stop();
Start();
return false; // disconnect
}
RemoteAuthClient::type_signal_hello RemoteAuthClient::signal_hello() {
return m_signal_hello;
}
RemoteAuthClient::type_signal_fingerprint RemoteAuthClient::signal_fingerprint() {
return m_signal_fingerprint;
}
RemoteAuthClient::type_signal_pending_ticket RemoteAuthClient::signal_pending_ticket() {
return m_signal_pending_ticket;
}
RemoteAuthClient::type_signal_pending_login RemoteAuthClient::signal_pending_login() {
return m_signal_pending_login;
}
RemoteAuthClient::type_signal_token RemoteAuthClient::signal_token() {
return m_signal_token;
}
RemoteAuthClient::type_signal_error RemoteAuthClient::signal_error() {
return m_signal_error;
}
#endif

View File

@ -0,0 +1,95 @@
#pragma once
#ifdef WITH_QRLOGIN
// clang-format off
#include <string>
#include <queue>
#include <spdlog/logger.h>
#include "ssl.hpp"
#include "discord/waiter.hpp"
#include "discord/websocket.hpp"
// clang-format on
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 HandleGatewayPendingLogin(const nlohmann::json &j);
void HandleGatewayCancel(const nlohmann::json &j);
void OnRemoteAuthLoginResponse(const std::optional<std::string> &encrypted_token, DiscordError err);
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();
bool OnTimeout();
sigc::connection m_timeout_conn;
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_hello = sigc::signal<void()>;
using type_signal_fingerprint = sigc::signal<void(std::string)>;
using type_signal_pending_ticket = sigc::signal<void(Snowflake, std::string, std::string, std::string)>;
using type_signal_pending_login = sigc::signal<void()>;
using type_signal_token = sigc::signal<void(std::string)>;
using type_signal_error = sigc::signal<void(std::string)>;
type_signal_hello signal_hello();
type_signal_fingerprint signal_fingerprint();
type_signal_pending_ticket signal_pending_ticket();
type_signal_pending_login signal_pending_login();
type_signal_token signal_token();
type_signal_error signal_error();
private:
type_signal_hello m_signal_hello;
type_signal_fingerprint m_signal_fingerprint;
type_signal_pending_ticket m_signal_pending_ticket;
type_signal_pending_login m_signal_pending_login;
type_signal_token m_signal_token;
type_signal_error m_signal_error;
};
#endif

View File

@ -0,0 +1,132 @@
#ifdef WITH_QRLOGIN
// clang-format off
#include "remoteauthdialog.hpp"
#include <qrcodegen.hpp>
// clang-format on
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_hello().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnHello));
m_ra.signal_fingerprint().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnFingerprint));
m_ra.signal_pending_ticket().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnPendingTicket));
m_ra.signal_pending_login().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnPendingLogin));
m_ra.signal_token().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnToken));
m_ra.signal_error().connect(sigc::mem_fun(*this, &RemoteAuthDialog::OnError));
m_ra.Start();
m_image.set_size_request(256, 256);
m_status.set_text("Connecting...");
m_status.set_hexpand(true);
m_status.set_halign(Gtk::ALIGN_CENTER);
m_layout.add(m_image);
m_layout.add(m_status);
m_layout.add(m_bbox);
get_content_area()->add(m_layout);
show_all_children();
}
std::string RemoteAuthDialog::GetToken() {
return m_token;
}
void RemoteAuthDialog::OnHello() {
m_status.set_text("Handshaking...");
}
void RemoteAuthDialog::OnFingerprint(const std::string &fingerprint) {
m_status.set_text("Waiting for mobile device...");
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;
const auto module_set = "192 0 255";
const auto module_clr = "255 255 255";
std::ostringstream sb;
sb << "P3\n";
sb << size + border * 2 << " " << size + border * 2 << " 255\n";
for (int y = -border; y < size + border; y++) {
for (int x = -border; x < size + border; x++) {
if (qr.getModule(x, y)) {
sb << module_set << "\n";
} else {
sb << module_clr << "\n";
}
}
}
const auto img = sb.str();
auto loader = Gdk::PixbufLoader::create();
loader->write(reinterpret_cast<const guint8 *>(img.data()), img.size());
loader->close();
const auto pb = loader->get_pixbuf()->scale_simple(256, 256, Gdk::INTERP_NEAREST);
m_image.property_pixbuf() = pb;
}
void RemoteAuthDialog::OnPendingTicket(Snowflake user_id, const std::string &discriminator, const std::string &avatar_hash, const std::string &username) {
Glib::ustring name = username;
if (discriminator != "0") {
name += "#" + discriminator;
}
m_status.set_text("Waiting for confirmation... (" + name + ")");
if (!avatar_hash.empty()) {
const auto url = "https://cdn.discordapp.com/avatars/" + std::to_string(user_id) + "/" + avatar_hash + ".png?size=256";
const auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_image.property_pixbuf() = pb->scale_simple(256, 256, Gdk::INTERP_BILINEAR);
};
Abaddon::Get().GetImageManager().LoadFromURL(url, sigc::track_obj(cb, *this));
}
}
void RemoteAuthDialog::OnPendingLogin() {
m_status.set_text("Logging in!");
}
void RemoteAuthDialog::OnToken(const std::string &token) {
m_token = token;
m_ra.Stop();
response(Gtk::RESPONSE_OK);
}
void RemoteAuthDialog::OnError(const std::string &error) {
m_ra.Stop();
Abaddon::Get().ShowConfirm(error, dynamic_cast<Gtk::Window *>(get_toplevel()));
response(Gtk::RESPONSE_CANCEL);
}
#endif

View File

@ -0,0 +1,38 @@
#pragma once
#ifdef WITH_QRLOGIN
// clang-format off
#include <gtkmm/dialog.h>
#include "remoteauthclient.hpp"
// clang-format on
class RemoteAuthDialog : public Gtk::Dialog {
public:
RemoteAuthDialog(Gtk::Window &parent);
std::string GetToken();
protected:
Gtk::Image m_image;
Gtk::Label m_status;
Gtk::Box m_layout;
Gtk::Button m_ok;
Gtk::Button m_cancel;
Gtk::ButtonBox m_bbox;
private:
RemoteAuthClient m_ra;
void OnHello();
void OnFingerprint(const std::string &fingerprint);
void OnPendingTicket(Snowflake user_id, const std::string &discriminator, const std::string &avatar_hash, const std::string &username);
void OnPendingLogin();
void OnToken(const std::string &token);
void OnError(const std::string &error);
std::string m_token;
};
#endif

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

@ -70,6 +70,7 @@ void SettingsManager::ReadSettings() {
SMSTR("style", "unreadcolor", UnreadIndicatorColor); SMSTR("style", "unreadcolor", UnreadIndicatorColor);
SMBOOL("notifications", "enabled", NotificationsEnabled); SMBOOL("notifications", "enabled", NotificationsEnabled);
SMBOOL("notifications", "playsound", NotificationsPlaySound); SMBOOL("notifications", "playsound", NotificationsPlaySound);
SMBOOL("windows", "hideconsole", HideConsole);
#ifdef WITH_KEYCHAIN #ifdef WITH_KEYCHAIN
keychain::Error error {}; keychain::Error error {};
@ -153,6 +154,7 @@ void SettingsManager::Close() {
SMSTR("style", "unreadcolor", UnreadIndicatorColor); SMSTR("style", "unreadcolor", UnreadIndicatorColor);
SMBOOL("notifications", "enabled", NotificationsEnabled); SMBOOL("notifications", "enabled", NotificationsEnabled);
SMBOOL("notifications", "playsound", NotificationsPlaySound); SMBOOL("notifications", "playsound", NotificationsPlaySound);
SMBOOL("windows", "hideconsole", HideConsole);
#ifdef WITH_KEYCHAIN #ifdef WITH_KEYCHAIN
keychain::Error error {}; keychain::Error error {};

View File

@ -52,6 +52,9 @@ public:
bool NotificationsEnabled { true }; bool NotificationsEnabled { true };
#endif #endif
bool NotificationsPlaySound { true }; bool NotificationsPlaySound { true };
// [windows]
bool HideConsole { false };
}; };
SettingsManager(const std::string &filename); SettingsManager(const std::string &filename);

View File

@ -38,7 +38,7 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
Glib::ustring user_markup = "<b>Unknown User</b>"; Glib::ustring user_markup = "<b>Unknown User</b>";
if (entry.UserID.has_value()) { if (entry.UserID.has_value()) {
if (auto user = discord.GetUser(*entry.UserID); user.has_value()) if (auto user = discord.GetUser(*entry.UserID); user.has_value())
user_markup = discord.GetUser(*entry.UserID)->GetEscapedBoldString<false>(); user_markup = discord.GetUser(*entry.UserID)->GetUsernameEscapedBold();
} }
// spaghetti moment // spaghetti moment
@ -177,7 +177,7 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" kicked <b>" + " kicked <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b>"; "</b>";
} break; } break;
case AuditLogActionType::MEMBER_PRUNE: { case AuditLogActionType::MEMBER_PRUNE: {
@ -193,21 +193,21 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" banned <b>" + " banned <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b>"; "</b>";
} break; } break;
case AuditLogActionType::MEMBER_BAN_REMOVE: { case AuditLogActionType::MEMBER_BAN_REMOVE: {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" removed the ban for <b>" + " removed the ban for <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b>"; "</b>";
} break; } break;
case AuditLogActionType::MEMBER_UPDATE: { case AuditLogActionType::MEMBER_UPDATE: {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" updated <b>" + " updated <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b>"; "</b>";
if (entry.Changes.has_value()) if (entry.Changes.has_value())
for (const auto &change : *entry.Changes) { for (const auto &change : *entry.Changes) {
@ -227,7 +227,7 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" updated roles for <b>" + " updated roles for <b>" +
target_user->GetEscapedString() + "</b>"; target_user->GetUsernameEscaped() + "</b>";
if (entry.Changes.has_value()) if (entry.Changes.has_value())
for (const auto &change : *entry.Changes) { for (const auto &change : *entry.Changes) {
if (change.Key == "$remove" && change.NewValue.has_value()) { if (change.Key == "$remove" && change.NewValue.has_value()) {
@ -262,7 +262,7 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" added <b>" + " added <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b> to the server"; "</b> to the server";
} break; } break;
case AuditLogActionType::ROLE_CREATE: { case AuditLogActionType::ROLE_CREATE: {
@ -450,14 +450,14 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" pinned a message by <b>" + " pinned a message by <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b>"; "</b>";
} break; } break;
case AuditLogActionType::MESSAGE_UNPIN: { case AuditLogActionType::MESSAGE_UNPIN: {
const auto target_user = discord.GetUser(entry.TargetID); const auto target_user = discord.GetUser(entry.TargetID);
markup = user_markup + markup = user_markup +
" unpinned a message by <b>" + " unpinned a message by <b>" +
target_user->GetEscapedString() + target_user->GetUsernameEscaped() +
"</b>"; "</b>";
} break; } break;
case AuditLogActionType::STAGE_INSTANCE_CREATE: { case AuditLogActionType::STAGE_INSTANCE_CREATE: {

View File

@ -72,7 +72,7 @@ void GuildSettingsBansPane::OnGuildBanFetch(const BanData &ban) {
auto row = *m_model->append(); auto row = *m_model->append();
row[m_columns.m_col_id] = ban.User.ID; row[m_columns.m_col_id] = ban.User.ID;
if (user.has_value()) if (user.has_value())
row[m_columns.m_col_user] = user->Username + "#" + user->Discriminator; row[m_columns.m_col_user] = user->GetUsername();
else else
row[m_columns.m_col_user] = "<@" + std::to_string(ban.User.ID) + ">"; row[m_columns.m_col_user] = "<@" + std::to_string(ban.User.ID) + ">";
@ -84,7 +84,7 @@ void GuildSettingsBansPane::OnGuildBansFetch(const std::vector<BanData> &bans) {
const auto user = Abaddon::Get().GetDiscordClient().GetUser(ban.User.ID); const auto user = Abaddon::Get().GetDiscordClient().GetUser(ban.User.ID);
auto row = *m_model->append(); auto row = *m_model->append();
row[m_columns.m_col_id] = user->ID; row[m_columns.m_col_id] = user->ID;
row[m_columns.m_col_user] = user->Username + "#" + user->Discriminator; row[m_columns.m_col_user] = user->GetUsername();
row[m_columns.m_col_reason] = ban.Reason; row[m_columns.m_col_reason] = ban.Reason;
} }
} }
@ -148,7 +148,7 @@ void GuildSettingsBansPane::OnBanAdd(Snowflake guild_id, Snowflake user_id) {
auto user = *discord.GetUser(user_id); auto user = *discord.GetUser(user_id);
auto row = *m_model->append(); auto row = *m_model->append();
row[m_columns.m_col_id] = user_id; row[m_columns.m_col_id] = user_id;
row[m_columns.m_col_user] = user.Username + "#" + user.Discriminator; row[m_columns.m_col_user] = user.GetUsername();
row[m_columns.m_col_reason] = ""; row[m_columns.m_col_reason] = "";
} }
} }

View File

@ -119,7 +119,7 @@ void GuildSettingsEmojisPane::AddEmojiRow(const EmojiData &emoji) {
row[m_columns.m_col_pixbuf] = img.GetPlaceholder(32); row[m_columns.m_col_pixbuf] = img.GetPlaceholder(32);
row[m_columns.m_col_name] = emoji.Name; row[m_columns.m_col_name] = emoji.Name;
if (emoji.Creator.has_value()) if (emoji.Creator.has_value())
row[m_columns.m_col_creator] = emoji.Creator->Username + "#" + emoji.Creator->Discriminator; row[m_columns.m_col_creator] = emoji.Creator->GetUsername();
if (emoji.IsAnimated.has_value()) if (emoji.IsAnimated.has_value())
row[m_columns.m_col_animated] = *emoji.IsAnimated ? "Yes" : "No"; row[m_columns.m_col_animated] = *emoji.IsAnimated ? "Yes" : "No";
else else

View File

@ -51,7 +51,7 @@ void GuildSettingsInvitesPane::AppendInvite(const InviteData &invite) {
auto row = *m_model->append(); auto row = *m_model->append();
row[m_columns.m_col_code] = invite.Code; row[m_columns.m_col_code] = invite.Code;
if (invite.Inviter.has_value()) if (invite.Inviter.has_value())
row[m_columns.m_col_inviter] = invite.Inviter->Username + "#" + invite.Inviter->Discriminator; row[m_columns.m_col_inviter] = invite.Inviter->GetUsername();
if (invite.MaxAge.has_value()) { if (invite.MaxAge.has_value()) {
if (*invite.MaxAge == 0) if (*invite.MaxAge == 0)

View File

@ -103,7 +103,7 @@ GuildSettingsMembersListItem::GuildSettingsMembersListItem(const GuildData &guil
else else
m_avatar.SetURL(member.User->GetAvatarURL("png", "32")); m_avatar.SetURL(member.User->GetAvatarURL("png", "32"));
DisplayTerm = member.User->Username + "#" + member.User->Discriminator; DisplayTerm = member.User->GetUsername();
const auto member_update_cb = [this](Snowflake guild_id, Snowflake user_id) { const auto member_update_cb = [this](Snowflake guild_id, Snowflake user_id) {
if (user_id == UserID) if (user_id == UserID)
@ -150,9 +150,9 @@ void GuildSettingsMembersListItem::UpdateColor() {
const auto user = *discord.GetUser(UserID); const auto user = *discord.GetUser(UserID);
if (auto color_id = discord.GetMemberHoistedRole(GuildID, UserID, true); color_id.IsValid()) { if (auto color_id = discord.GetMemberHoistedRole(GuildID, UserID, true); color_id.IsValid()) {
auto role = *discord.GetRole(color_id); auto role = *discord.GetRole(color_id);
m_name.set_markup("<span color='#" + IntToCSSColor(role.Color) + "'>" + user.GetEscapedBoldString<false>() + "</span>"); m_name.set_markup("<span color='#" + IntToCSSColor(role.Color) + "'>" + user.GetUsernameEscapedBold() + "</span>");
} else } else
m_name.set_markup(user.GetEscapedBoldString<false>()); m_name.set_markup(user.GetUsernameEscapedBold());
} }
GuildSettingsMembersPaneInfo::GuildSettingsMembersPaneInfo(Snowflake guild_id) GuildSettingsMembersPaneInfo::GuildSettingsMembersPaneInfo(Snowflake guild_id)

View File

@ -168,6 +168,10 @@ void MainWindow::ToggleMenuVisibility() {
m_menu_bar.set_visible(!m_menu_bar.get_visible()); m_menu_bar.set_visible(!m_menu_bar.get_visible());
} }
void MainWindow::EditMessage(Snowflake message_id) {
m_chat.StartEditing(message_id);
}
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
void MainWindow::GoBack() { void MainWindow::GoBack() {
m_chat.GoBack(); m_chat.GoBack();
@ -206,6 +210,9 @@ void MainWindow::OnDiscordSubmenuPopup() {
m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active); m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active);
m_menu_discord_disconnect.set_sensitive(discord_active); m_menu_discord_disconnect.set_sensitive(discord_active);
m_menu_discord_set_token.set_sensitive(!discord_active); m_menu_discord_set_token.set_sensitive(!discord_active);
#ifdef WITH_QRLOGIN
m_menu_discord_login_qr.set_sensitive(!discord_active);
#endif
m_menu_discord_set_status.set_sensitive(discord_active); m_menu_discord_set_status.set_sensitive(discord_active);
} }
@ -247,12 +254,18 @@ void MainWindow::SetupMenu() {
m_menu_discord_disconnect.set_label("Disconnect"); m_menu_discord_disconnect.set_label("Disconnect");
m_menu_discord_disconnect.set_sensitive(false); m_menu_discord_disconnect.set_sensitive(false);
m_menu_discord_set_token.set_label("Set Token"); m_menu_discord_set_token.set_label("Set Token");
m_menu_discord_login_qr.set_label("Login with QR Code");
#ifndef WITH_QRLOGIN
m_menu_discord_login_qr.set_sensitive(false);
m_menu_discord_login_qr.set_tooltip_text("Not compiled with support");
#endif
m_menu_discord_set_status.set_label("Set Status"); m_menu_discord_set_status.set_label("Set Status");
m_menu_discord_set_status.set_sensitive(false); m_menu_discord_set_status.set_sensitive(false);
m_menu_discord_add_recipient.set_label("Add user to DM"); 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_connect);
m_menu_discord_sub.append(m_menu_discord_disconnect); 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_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_set_status);
m_menu_discord_sub.append(m_menu_discord_add_recipient); m_menu_discord_sub.append(m_menu_discord_add_recipient);
m_menu_discord.set_submenu(m_menu_discord_sub); m_menu_discord.set_submenu(m_menu_discord_sub);
@ -331,6 +344,10 @@ void MainWindow::SetupMenu() {
m_signal_action_set_token.emit(); 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_menu_file_reload_css.signal_activate().connect([this] {
m_signal_action_reload_css.emit(); m_signal_action_reload_css.emit();
}); });
@ -421,6 +438,10 @@ MainWindow::type_signal_action_set_token MainWindow::signal_action_set_token() {
return m_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() { MainWindow::type_signal_action_reload_css MainWindow::signal_action_reload_css() {
return m_signal_action_reload_css; return m_signal_action_reload_css;
} }

View File

@ -29,6 +29,7 @@ public:
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param); void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
void UpdateMenus(); void UpdateMenus();
void ToggleMenuVisibility(); void ToggleMenuVisibility();
void EditMessage(Snowflake message_id);
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
void GoBack(); void GoBack();
@ -74,6 +75,7 @@ private:
Gtk::MenuItem m_menu_discord_connect; Gtk::MenuItem m_menu_discord_connect;
Gtk::MenuItem m_menu_discord_disconnect; Gtk::MenuItem m_menu_discord_disconnect;
Gtk::MenuItem m_menu_discord_set_token; 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_set_status;
Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day
void OnDiscordSubmenuPopup(); void OnDiscordSubmenuPopup();
@ -103,6 +105,7 @@ public:
typedef sigc::signal<void> type_signal_action_connect; typedef sigc::signal<void> type_signal_action_connect;
typedef sigc::signal<void> type_signal_action_disconnect; typedef sigc::signal<void> type_signal_action_disconnect;
typedef sigc::signal<void> type_signal_action_set_token; 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_reload_css;
typedef sigc::signal<void> type_signal_action_set_status; typedef sigc::signal<void> type_signal_action_set_status;
// this should probably be removed // this should probably be removed
@ -113,6 +116,7 @@ public:
type_signal_action_connect signal_action_connect(); type_signal_action_connect signal_action_connect();
type_signal_action_disconnect signal_action_disconnect(); type_signal_action_disconnect signal_action_disconnect();
type_signal_action_set_token signal_action_set_token(); 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_reload_css signal_action_reload_css();
type_signal_action_set_status signal_action_set_status(); type_signal_action_set_status signal_action_set_status();
type_signal_action_add_recipient signal_action_add_recipient(); type_signal_action_add_recipient signal_action_add_recipient();
@ -123,6 +127,7 @@ private:
type_signal_action_connect m_signal_action_connect; type_signal_action_connect m_signal_action_connect;
type_signal_action_disconnect m_signal_action_disconnect; type_signal_action_disconnect m_signal_action_disconnect;
type_signal_action_set_token m_signal_action_set_token; 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_reload_css m_signal_action_reload_css;
type_signal_action_set_status m_signal_action_set_status; type_signal_action_set_status m_signal_action_set_status;
type_signal_action_add_recipient m_signal_action_add_recipient; type_signal_action_add_recipient m_signal_action_add_recipient;

View File

@ -22,7 +22,7 @@ MutualFriendItem::MutualFriendItem(const UserData &user)
img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
} }
m_name.set_markup(user.GetEscapedBoldString<false>()); m_name.set_markup(user.GetUsernameEscapedBold());
m_name.set_valign(Gtk::ALIGN_CENTER); m_name.set_valign(Gtk::ALIGN_CENTER);
add(m_avatar); add(m_avatar);

View File

@ -5,6 +5,7 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
, m_main(Gtk::ORIENTATION_VERTICAL) , m_main(Gtk::ORIENTATION_VERTICAL)
, m_upper(Gtk::ORIENTATION_HORIZONTAL) , m_upper(Gtk::ORIENTATION_HORIZONTAL)
, m_badges(Gtk::ORIENTATION_HORIZONTAL) , m_badges(Gtk::ORIENTATION_HORIZONTAL)
, m_name_box(Gtk::ORIENTATION_VERTICAL)
, m_pane_info(user_id) , m_pane_info(user_id)
, m_pane_guilds(user_id) , m_pane_guilds(user_id)
, m_pane_friends(user_id) { , m_pane_friends(user_id) {
@ -15,14 +16,15 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
set_name("user-profile"); set_name("user-profile");
set_default_size(450, 375); set_default_size(450, 375);
set_title(user.Username + "#" + user.Discriminator); set_title(user.GetUsername());
set_position(Gtk::WIN_POS_CENTER); set_position(Gtk::WIN_POS_CENTER);
get_style_context()->add_class("app-window"); get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup"); get_style_context()->add_class("app-popup");
get_style_context()->add_class("user-profile-window"); get_style_context()->add_class("user-profile-window");
m_main.get_style_context()->add_class("profile-main-container"); m_main.get_style_context()->add_class("profile-main-container");
m_avatar.get_style_context()->add_class("profile-avatar"); m_avatar.get_style_context()->add_class("profile-avatar");
m_username.get_style_context()->add_class("profile-username"); m_displayname.get_style_context()->add_class("profile-username");
m_username.get_style_context()->add_class("profile-username-nondisplay");
m_switcher.get_style_context()->add_class("profile-switcher"); m_switcher.get_style_context()->add_class("profile-switcher");
m_stack.get_style_context()->add_class("profile-stack"); m_stack.get_style_context()->add_class("profile-stack");
m_badges.get_style_context()->add_class("profile-badges"); m_badges.get_style_context()->add_class("profile-badges");
@ -31,8 +33,8 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
m_scroll.set_vexpand(true); m_scroll.set_vexpand(true);
m_scroll.set_propagate_natural_height(true); m_scroll.set_propagate_natural_height(true);
if (user.HasAvatar()) if (user.HasAvatar()) AddPointerCursor(m_avatar_ev);
AddPointerCursor(m_avatar_ev);
m_avatar_ev.signal_button_release_event().connect([user](GdkEventButton *event) -> bool { m_avatar_ev.signal_button_release_event().connect([user](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) { if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
if (user.HasAnimatedAvatar()) if (user.HasAnimatedAvatar())
@ -62,7 +64,8 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
img.LoadFromURL(user.GetAvatarURL("png", "64"), sigc::track_obj(cb, *this)); img.LoadFromURL(user.GetAvatarURL("png", "64"), sigc::track_obj(cb, *this));
} }
m_username.set_markup(user.GetEscapedString()); m_displayname.set_markup(user.GetDisplayNameEscaped());
m_username.set_label(user.GetUsername());
m_switcher.set_stack(m_stack); m_switcher.set_stack(m_stack);
m_switcher.set_halign(Gtk::ALIGN_START); m_switcher.set_halign(Gtk::ALIGN_START);
@ -79,10 +82,13 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
m_upper.set_halign(Gtk::ALIGN_START); m_upper.set_halign(Gtk::ALIGN_START);
m_avatar.set_halign(Gtk::ALIGN_START); m_avatar.set_halign(Gtk::ALIGN_START);
m_displayname.set_halign(Gtk::ALIGN_START);
m_username.set_halign(Gtk::ALIGN_START); m_username.set_halign(Gtk::ALIGN_START);
m_avatar_ev.add(m_avatar); m_avatar_ev.add(m_avatar);
m_upper.add(m_avatar_ev); m_upper.add(m_avatar_ev);
m_upper.add(m_username); m_upper.add(m_name_box);
m_name_box.add(m_displayname);
m_name_box.add(m_username);
m_badges_scroll.add(m_badges); m_badges_scroll.add(m_badges);
m_upper.add(m_badges_scroll); m_upper.add(m_badges_scroll);
m_main.add(m_upper); m_main.add(m_upper);
@ -97,8 +103,13 @@ void ProfileWindow::OnFetchProfile(const UserProfileData &data) {
m_pane_info.SetProfile(data); m_pane_info.SetProfile(data);
m_pane_guilds.SetMutualGuilds(data.MutualGuilds); m_pane_guilds.SetMutualGuilds(data.MutualGuilds);
for (auto child : m_badges.get_children()) if (data.LegacyUsername.has_value()) {
m_username.set_tooltip_text("Originally known as " + *data.LegacyUsername);
}
for (auto child : m_badges.get_children()) {
delete child; delete child;
}
if (!data.User.PublicFlags.has_value()) return; if (!data.User.PublicFlags.has_value()) return;
const auto x = *data.User.PublicFlags; const auto x = *data.User.PublicFlags;

View File

@ -16,9 +16,11 @@ private:
Gtk::Box m_main; Gtk::Box m_main;
Gtk::Box m_upper; Gtk::Box m_upper;
Gtk::Box m_badges; Gtk::Box m_badges;
Gtk::Box m_name_box;
Gtk::ScrolledWindow m_badges_scroll; Gtk::ScrolledWindow m_badges_scroll;
Gtk::EventBox m_avatar_ev; Gtk::EventBox m_avatar_ev;
Gtk::Image m_avatar; Gtk::Image m_avatar;
Gtk::Label m_displayname;
Gtk::Label m_username; Gtk::Label m_username;
Gtk::ScrolledWindow m_scroll; Gtk::ScrolledWindow m_scroll;
Gtk::Stack m_stack; Gtk::Stack m_stack;

View File

@ -110,9 +110,17 @@ VoiceSettingsWindow::VoiceSettingsWindow()
} }
}); });
m_gain.set_increments(1.0, 5.0);
m_gain.set_range(0.0, 6969696969.0);
m_gain.set_value(Abaddon::Get().GetAudio().GetCaptureGain() * 100.0);
m_gain.signal_value_changed().connect([this]() {
m_signal_gain.emit(m_gain.get_value() / 100.0);
});
m_main.add(m_encoding_mode); m_main.add(m_encoding_mode);
m_main.add(m_signal); m_main.add(m_signal);
m_main.add(m_bitrate); m_main.add(m_bitrate);
m_main.add(m_gain);
add(m_main); add(m_main);
show_all_children(); show_all_children();
@ -122,4 +130,8 @@ VoiceSettingsWindow::VoiceSettingsWindow()
}); });
} }
VoiceSettingsWindow::type_signal_gain VoiceSettingsWindow::signal_gain() {
return m_signal_gain;
}
#endif #endif

View File

@ -18,8 +18,14 @@ public:
Gtk::ComboBoxText m_encoding_mode; Gtk::ComboBoxText m_encoding_mode;
Gtk::ComboBoxText m_signal; Gtk::ComboBoxText m_signal;
Gtk::Scale m_bitrate; Gtk::Scale m_bitrate;
Gtk::SpinButton m_gain;
private: private:
using type_signal_gain = sigc::signal<void(double)>;
type_signal_gain m_signal_gain;
public:
type_signal_gain signal_gain();
}; };
#endif #endif

View File

@ -39,7 +39,7 @@ public:
auto &discord = Abaddon::Get().GetDiscordClient(); auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(id); const auto user = discord.GetUser(id);
if (user.has_value()) { if (user.has_value()) {
m_name.set_text(user->Username); m_name.set_text(user->GetUsername());
m_avatar.SetURL(user->GetAvatarURL("png", "32")); m_avatar.SetURL(user->GetAvatarURL("png", "32"));
} else { } else {
m_name.set_text("Unknown user"); m_name.set_text("Unknown user");
@ -139,7 +139,9 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
m_playback_combo.set_hexpand(true); m_playback_combo.set_hexpand(true);
m_playback_combo.set_halign(Gtk::ALIGN_FILL); m_playback_combo.set_halign(Gtk::ALIGN_FILL);
m_playback_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetPlaybackDeviceModel()); m_playback_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetPlaybackDeviceModel());
m_playback_combo.set_active(Abaddon::Get().GetAudio().GetDevices().GetActivePlaybackDevice()); if (const auto iter = Abaddon::Get().GetAudio().GetDevices().GetActivePlaybackDevice()) {
m_playback_combo.set_active(iter);
}
m_playback_combo.pack_start(*playback_renderer); m_playback_combo.pack_start(*playback_renderer);
m_playback_combo.add_attribute(*playback_renderer, "text", 0); m_playback_combo.add_attribute(*playback_renderer, "text", 0);
m_playback_combo.signal_changed().connect([this]() { m_playback_combo.signal_changed().connect([this]() {
@ -151,7 +153,9 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
m_capture_combo.set_hexpand(true); m_capture_combo.set_hexpand(true);
m_capture_combo.set_halign(Gtk::ALIGN_FILL); m_capture_combo.set_halign(Gtk::ALIGN_FILL);
m_capture_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetCaptureDeviceModel()); m_capture_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetCaptureDeviceModel());
m_capture_combo.set_active(Abaddon::Get().GetAudio().GetDevices().GetActiveCaptureDevice()); if (const auto iter = Abaddon::Get().GetAudio().GetDevices().GetActiveCaptureDevice()) {
m_capture_combo.set_active(iter);
}
m_capture_combo.pack_start(*capture_renderer); m_capture_combo.pack_start(*capture_renderer);
m_capture_combo.add_attribute(*capture_renderer, "text", 0); m_capture_combo.add_attribute(*capture_renderer, "text", 0);
m_capture_combo.signal_changed().connect([this]() { m_capture_combo.signal_changed().connect([this]() {
@ -163,6 +167,11 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
m_menu_view_sub.append(m_menu_view_settings); m_menu_view_sub.append(m_menu_view_settings);
m_menu_view_settings.signal_activate().connect([this]() { m_menu_view_settings.signal_activate().connect([this]() {
auto *window = new VoiceSettingsWindow; auto *window = new VoiceSettingsWindow;
const auto cb = [this](double gain) {
m_capture_gain.set_value(gain * 100.0);
m_signal_gain.emit(gain);
};
window->signal_gain().connect(sigc::track_obj(cb, *this));
window->show(); window->show();
}); });

1
subprojects/qrcodegen Submodule

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