Merge branch 'master' of https://github.com/uowuo/abaddon into voice
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
|
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} -DUSE_LIBHANDY=OFF -DENABLE_VOICE=OFF -DENABLE_NOTIFICATION_SOUNDS=OFF
|
||||||
cmake --build build
|
cmake --build build
|
||||||
if_false: |
|
if_false: |
|
||||||
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
|
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
|
||||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -10,3 +10,6 @@
|
|||||||
[submodule "subprojects/keychain"]
|
[submodule "subprojects/keychain"]
|
||||||
path = subprojects/keychain
|
path = subprojects/keychain
|
||||||
url = https://github.com/hrantzsch/keychain
|
url = https://github.com/hrantzsch/keychain
|
||||||
|
[submodule "subprojects/miniaudio"]
|
||||||
|
path = subprojects/miniaudio
|
||||||
|
url = https://github.com/mackron/miniaudio
|
||||||
|
@@ -10,6 +10,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
|
|||||||
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
|
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
|
||||||
option(ENABLE_VOICE "Enable voice suppport" ON)
|
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)
|
||||||
|
|
||||||
find_package(nlohmann_json REQUIRED)
|
find_package(nlohmann_json REQUIRED)
|
||||||
find_package(CURL)
|
find_package(CURL)
|
||||||
@@ -49,6 +50,9 @@ file(GLOB_RECURSE ABADDON_SOURCES
|
|||||||
"src/*.cpp"
|
"src/*.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
list(FILTER ABADDON_SOURCES EXCLUDE REGEX ".*notifier_gio\\.cpp$")
|
||||||
|
list(FILTER ABADDON_SOURCES EXCLUDE REGEX ".*notifier_null\\.cpp$")
|
||||||
|
|
||||||
add_executable(abaddon ${ABADDON_SOURCES})
|
add_executable(abaddon ${ABADDON_SOURCES})
|
||||||
target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src)
|
target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src)
|
||||||
target_include_directories(abaddon PUBLIC ${PROJECT_BINARY_DIR})
|
target_include_directories(abaddon PUBLIC ${PROJECT_BINARY_DIR})
|
||||||
@@ -65,6 +69,12 @@ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
|
|||||||
target_link_libraries(abaddon stdc++fs)
|
target_link_libraries(abaddon stdc++fs)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
if (NOT WIN32)
|
||||||
|
target_sources(abaddon PRIVATE src/notifications/notifier_gio.cpp)
|
||||||
|
else ()
|
||||||
|
target_sources(abaddon PRIVATE src/notifications/notifier_null.cpp)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (IXWebSocket_LIBRARIES)
|
if (IXWebSocket_LIBRARIES)
|
||||||
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
|
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
|
||||||
find_library(MBEDTLS_X509_LIBRARY mbedx509)
|
find_library(MBEDTLS_X509_LIBRARY mbedx509)
|
||||||
@@ -119,12 +129,14 @@ if (USE_KEYCHAIN)
|
|||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
set(USE_MINIAUDIO FALSE)
|
||||||
|
|
||||||
if (ENABLE_VOICE)
|
if (ENABLE_VOICE)
|
||||||
target_compile_definitions(abaddon PRIVATE WITH_VOICE)
|
target_compile_definitions(abaddon PRIVATE WITH_VOICE)
|
||||||
|
|
||||||
find_package(PkgConfig)
|
find_package(PkgConfig)
|
||||||
|
|
||||||
target_include_directories(abaddon PUBLIC subprojects/miniaudio)
|
set(USE_MINIAUDIO TRUE)
|
||||||
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus)
|
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus)
|
||||||
target_link_libraries(abaddon PkgConfig::Opus)
|
target_link_libraries(abaddon PkgConfig::Opus)
|
||||||
|
|
||||||
@@ -133,3 +145,12 @@ if (ENABLE_VOICE)
|
|||||||
|
|
||||||
target_link_libraries(abaddon ${CMAKE_DL_LIBS})
|
target_link_libraries(abaddon ${CMAKE_DL_LIBS})
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
if (${ENABLE_NOTIFICATION_SOUNDS})
|
||||||
|
set(USE_MINIAUDIO TRUE)
|
||||||
|
target_compile_definitions(abaddon PRIVATE ENABLE_NOTIFICATION_SOUNDS)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (USE_MINIAUDIO)
|
||||||
|
target_include_directories(abaddon PUBLIC subprojects/miniaudio)
|
||||||
|
endif ()
|
||||||
|
14
README.md
14
README.md
@@ -14,6 +14,7 @@ Current features:
|
|||||||
* Identifies to Discord as the web client unlike other clients so less likely to be falsely flagged as spam<sup>1</sup>
|
* Identifies to Discord as the web client unlike other clients so less likely to be falsely flagged as spam<sup>1</sup>
|
||||||
* Set status
|
* Set status
|
||||||
* Unread and mention indicators
|
* Unread and mention indicators
|
||||||
|
* Notifications (non-Windows)
|
||||||
* Start new DMs and group DMs
|
* Start new DMs and group DMs
|
||||||
* View user profiles (notes, mutual servers, mutual friends)
|
* View user profiles (notes, mutual servers, mutual friends)
|
||||||
* Kick, ban, and unban members
|
* Kick, ban, and unban members
|
||||||
@@ -134,15 +135,17 @@ spam filter's wrath:
|
|||||||
|
|
||||||
* [gtkmm](https://www.gtkmm.org/en/)
|
* [gtkmm](https://www.gtkmm.org/en/)
|
||||||
* [JSON for Modern C++](https://github.com/nlohmann/json)
|
* [JSON for Modern C++](https://github.com/nlohmann/json)
|
||||||
* [IXWebSocket](https://github.com/machinezone/IXWebSocket)
|
* [IXWebSocket](https://github.com/machinezone/IXWebSocket) (provided as submodule)
|
||||||
* [libcurl](https://curl.se/)
|
* [libcurl](https://curl.se/)
|
||||||
* [zlib](https://zlib.net/)
|
* [zlib](https://zlib.net/)
|
||||||
* [SQLite3](https://www.sqlite.org/index.html)
|
* [SQLite3](https://www.sqlite.org/index.html)
|
||||||
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
|
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
|
||||||
|
* [keychain](https://github.com/hrantzsch/keychain) (optional, provided as submodule)
|
||||||
|
* [miniaudio](https://miniaud.io/) (optional, provided as submodule)
|
||||||
|
|
||||||
### TODO:
|
### TODO:
|
||||||
|
|
||||||
* Voice support
|
* Voice support (in progress)
|
||||||
* User activities
|
* User activities
|
||||||
* More server management stuff
|
* More server management stuff
|
||||||
* A bunch of other stuff
|
* A bunch of other stuff
|
||||||
@@ -301,6 +304,13 @@ For example, memory_db would be set by adding `memory_db = true` under the line
|
|||||||
| `mentionbadgetextcolor` | string | color to use for number displayed on mention badges |
|
| `mentionbadgetextcolor` | string | color to use for number displayed on mention badges |
|
||||||
| `unreadcolor` | string | color to use for the unread indicator |
|
| `unreadcolor` | string | color to use for the unread indicator |
|
||||||
|
|
||||||
|
#### notifications
|
||||||
|
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|-------------|---------|--------------------------|-------------------------------------------------------------------------------|
|
||||||
|
| `enabled` | boolean | true (if not on Windows) | Enable desktop notifications |
|
||||||
|
| `playsound` | boolean | true | Enable notification sounds. Requires ENABLE_NOTIFICATION_SOUNDS=TRUE in CMake |
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| variable | Description |
|
| variable | Description |
|
||||||
|
@@ -20,6 +20,7 @@
|
|||||||
#include "windows/threadswindow.hpp"
|
#include "windows/threadswindow.hpp"
|
||||||
#include "windows/voicewindow.hpp"
|
#include "windows/voicewindow.hpp"
|
||||||
#include "startup.hpp"
|
#include "startup.hpp"
|
||||||
|
#include "notifications/notifications.hpp"
|
||||||
|
|
||||||
#ifdef WITH_LIBHANDY
|
#ifdef WITH_LIBHANDY
|
||||||
#include <handy.h>
|
#include <handy.h>
|
||||||
@@ -303,6 +304,14 @@ int Abaddon::StartGTK() {
|
|||||||
|
|
||||||
m_main_window->UpdateMenus();
|
m_main_window->UpdateMenus();
|
||||||
|
|
||||||
|
auto action_go_to_channel = Gio::SimpleAction::create("go-to-channel", Glib::VariantType("s"));
|
||||||
|
action_go_to_channel->signal_activate().connect([this](const Glib::VariantBase ¶m) {
|
||||||
|
const auto id_str = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(param);
|
||||||
|
const Snowflake id = id_str.get();
|
||||||
|
ActionChannelOpened(id, false);
|
||||||
|
});
|
||||||
|
m_gtk_app->add_action(action_go_to_channel);
|
||||||
|
|
||||||
m_gtk_app->hold();
|
m_gtk_app->hold();
|
||||||
m_main_window->show();
|
m_main_window->show();
|
||||||
|
|
||||||
@@ -360,6 +369,7 @@ void Abaddon::DiscordOnReady() {
|
|||||||
|
|
||||||
void Abaddon::DiscordOnMessageCreate(const Message &message) {
|
void Abaddon::DiscordOnMessageCreate(const Message &message) {
|
||||||
m_main_window->UpdateChatNewMessage(message);
|
m_main_window->UpdateChatNewMessage(message);
|
||||||
|
m_notifications.CheckMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Abaddon::DiscordOnMessageDelete(Snowflake id, Snowflake channel_id) {
|
void Abaddon::DiscordOnMessageDelete(Snowflake id, Snowflake channel_id) {
|
||||||
@@ -774,6 +784,18 @@ std::string Abaddon::GetStateCachePath(const std::string &path) {
|
|||||||
return GetStateCachePath() + path;
|
return GetStateCachePath() + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Glib::RefPtr<Gtk::Application> Abaddon::GetApp() {
|
||||||
|
return m_gtk_app;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Abaddon::IsMainWindowActive() {
|
||||||
|
return m_main_window->has_toplevel_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Snowflake Abaddon::GetActiveChannelID() const noexcept {
|
||||||
|
return m_main_window->GetChatActiveChannel();
|
||||||
|
}
|
||||||
|
|
||||||
void Abaddon::ActionConnect() {
|
void Abaddon::ActionConnect() {
|
||||||
if (!m_discord.IsStarted())
|
if (!m_discord.IsStarted())
|
||||||
StartDiscord();
|
StartDiscord();
|
||||||
@@ -803,6 +825,8 @@ void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
|
|||||||
}
|
}
|
||||||
if (id == m_main_window->GetChatActiveChannel()) return;
|
if (id == m_main_window->GetChatActiveChannel()) return;
|
||||||
|
|
||||||
|
m_notifications.WithdrawChannel(id);
|
||||||
|
|
||||||
m_main_window->GetChatWindow()->SetTopic("");
|
m_main_window->GetChatWindow()->SetTopic("");
|
||||||
|
|
||||||
const auto channel = m_discord.GetChannel(id);
|
const auto channel = m_discord.GetChannel(id);
|
||||||
|
@@ -3,11 +3,15 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <gtkmm/application.h>
|
||||||
|
#include <gtkmm/cssprovider.h>
|
||||||
|
#include <gtkmm/statusicon.h>
|
||||||
#include "discord/discord.hpp"
|
#include "discord/discord.hpp"
|
||||||
#include "windows/mainwindow.hpp"
|
#include "windows/mainwindow.hpp"
|
||||||
#include "settings.hpp"
|
#include "settings.hpp"
|
||||||
#include "imgmanager.hpp"
|
#include "imgmanager.hpp"
|
||||||
#include "emojis.hpp"
|
#include "emojis.hpp"
|
||||||
|
#include "notifications/notifications.hpp"
|
||||||
|
|
||||||
#define APP_TITLE "Abaddon"
|
#define APP_TITLE "Abaddon"
|
||||||
|
|
||||||
@@ -110,6 +114,10 @@ public:
|
|||||||
static std::string GetResPath(const std::string &path);
|
static std::string GetResPath(const std::string &path);
|
||||||
static std::string GetStateCachePath(const std::string &path);
|
static std::string GetStateCachePath(const std::string &path);
|
||||||
|
|
||||||
|
[[nodiscard]] Glib::RefPtr<Gtk::Application> GetApp();
|
||||||
|
[[nodiscard]] bool IsMainWindowActive();
|
||||||
|
[[nodiscard]] Snowflake GetActiveChannelID() const noexcept;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void RunFirstTimeDiscordStartup();
|
void RunFirstTimeDiscordStartup();
|
||||||
|
|
||||||
@@ -172,4 +180,6 @@ private:
|
|||||||
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
|
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
|
||||||
Glib::RefPtr<Gtk::StatusIcon> m_tray;
|
Glib::RefPtr<Gtk::StatusIcon> m_tray;
|
||||||
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
|
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
|
||||||
|
|
||||||
|
Notifications m_notifications;
|
||||||
};
|
};
|
||||||
|
@@ -1,14 +1,9 @@
|
|||||||
#include "chatmessage.hpp"
|
#include "chatmessage.hpp"
|
||||||
|
#include "constants.hpp"
|
||||||
#include "lazyimage.hpp"
|
#include "lazyimage.hpp"
|
||||||
|
#include "misc/chatutil.hpp"
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
constexpr static int EmojiSize = 24; // settings eventually
|
|
||||||
constexpr static int AvatarSize = 32;
|
|
||||||
constexpr static int EmbedImageWidth = 400;
|
|
||||||
constexpr static int EmbedImageHeight = 300;
|
|
||||||
constexpr static int ThumbnailSize = 100;
|
|
||||||
constexpr static int StickerComponentSize = 160;
|
|
||||||
|
|
||||||
ChatMessageItemContainer::ChatMessageItemContainer()
|
ChatMessageItemContainer::ChatMessageItemContainer()
|
||||||
: m_main(Gtk::ORIENTATION_VERTICAL) {
|
: m_main(Gtk::ORIENTATION_VERTICAL) {
|
||||||
add(m_main);
|
add(m_main);
|
||||||
@@ -190,11 +185,11 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
|||||||
case MessageType::DEFAULT:
|
case MessageType::DEFAULT:
|
||||||
case MessageType::INLINE_REPLY:
|
case MessageType::INLINE_REPLY:
|
||||||
b->insert(s, data->Content);
|
b->insert(s, data->Content);
|
||||||
HandleRoleMentions(b);
|
ChatUtil::HandleRoleMentions(b);
|
||||||
HandleUserMentions(b);
|
ChatUtil::HandleUserMentions(b, ChannelID, false);
|
||||||
HandleLinks(*tv);
|
HandleLinks(*tv);
|
||||||
HandleChannelMentions(tv);
|
HandleChannelMentions(tv);
|
||||||
HandleEmojis(*tv);
|
ChatUtil::HandleEmojis(*tv);
|
||||||
break;
|
break;
|
||||||
case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION:
|
case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION:
|
||||||
b->insert_markup(s, "<span color='#999999'><i>[boosted server]</i></span>");
|
b->insert_markup(s, "<span color='#999999'><i>[boosted server]</i></span>");
|
||||||
@@ -216,10 +211,10 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b->insert(s, data->Content);
|
b->insert(s, data->Content);
|
||||||
HandleUserMentions(b);
|
ChatUtil::HandleUserMentions(b, ChannelID, false);
|
||||||
HandleLinks(*tv);
|
HandleLinks(*tv);
|
||||||
HandleChannelMentions(tv);
|
HandleChannelMentions(tv);
|
||||||
HandleEmojis(*tv);
|
ChatUtil::HandleEmojis(*tv);
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case MessageType::RECIPIENT_ADD: {
|
case MessageType::RECIPIENT_ADD: {
|
||||||
@@ -711,8 +706,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
|
|||||||
Gtk::TextBuffer::iterator start, end;
|
Gtk::TextBuffer::iterator start, end;
|
||||||
buf->get_bounds(start, end);
|
buf->get_bounds(start, end);
|
||||||
buf->set_text(referenced.Content);
|
buf->set_text(referenced.Content);
|
||||||
CleanupEmojis(buf);
|
ChatUtil::CleanupEmojis(buf);
|
||||||
HandleUserMentions(buf);
|
ChatUtil::HandleUserMentions(buf, ChannelID, false);
|
||||||
HandleChannelMentions(buf);
|
HandleChannelMentions(buf);
|
||||||
text = Glib::Markup::escape_text(buf->get_text());
|
text = Glib::Markup::escape_text(buf->get_text());
|
||||||
}
|
}
|
||||||
@@ -734,13 +729,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
Glib::ustring ChatMessageItemContainer::GetText(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
|
||||||
Gtk::TextBuffer::iterator a, b;
|
|
||||||
buf->get_bounds(a, b);
|
|
||||||
auto slice = buf->get_slice(a, b, true);
|
|
||||||
return slice;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) {
|
bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) {
|
||||||
if (!data.Thumbnail.has_value()) return false;
|
if (!data.Thumbnail.has_value()) return false;
|
||||||
if (data.Author.has_value()) return false;
|
if (data.Author.has_value()) return false;
|
||||||
@@ -752,199 +740,10 @@ bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) {
|
|||||||
return data.Thumbnail->ProxyURL.has_value() && data.Thumbnail->URL.has_value() && data.Thumbnail->Width.has_value() && data.Thumbnail->Height.has_value();
|
return data.Thumbnail->ProxyURL.has_value() && data.Thumbnail->URL.has_value() && data.Thumbnail->Width.has_value() && data.Thumbnail->Height.has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatMessageItemContainer::HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
|
||||||
constexpr static const auto mentions_regex = R"(<@&(\d+)>)";
|
|
||||||
|
|
||||||
static auto rgx = Glib::Regex::create(mentions_regex);
|
|
||||||
|
|
||||||
Glib::ustring text = GetText(buf);
|
|
||||||
const auto &discord = Abaddon::Get().GetDiscordClient();
|
|
||||||
|
|
||||||
int startpos = 0;
|
|
||||||
Glib::MatchInfo match;
|
|
||||||
while (rgx->match(text, startpos, match)) {
|
|
||||||
int mstart, mend;
|
|
||||||
if (!match.fetch_pos(0, mstart, mend)) break;
|
|
||||||
const Glib::ustring role_id = match.fetch(1);
|
|
||||||
const auto role = discord.GetRole(role_id);
|
|
||||||
if (!role.has_value()) {
|
|
||||||
startpos = mend;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Glib::ustring replacement;
|
|
||||||
if (role->HasColor()) {
|
|
||||||
replacement = "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">@" + role->GetEscapedName() + "</span></b>";
|
|
||||||
} else {
|
|
||||||
replacement = "<b>@" + role->GetEscapedName() + "</b>";
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
|
||||||
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
|
||||||
const auto start_it = buf->get_iter_at_offset(chars_start);
|
|
||||||
const auto end_it = buf->get_iter_at_offset(chars_end);
|
|
||||||
|
|
||||||
auto it = buf->erase(start_it, end_it);
|
|
||||||
buf->insert_markup(it, replacement);
|
|
||||||
|
|
||||||
text = GetText(buf);
|
|
||||||
startpos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatMessageItemContainer::HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) const {
|
|
||||||
constexpr static const auto mentions_regex = R"(<@!?(\d+)>)";
|
|
||||||
|
|
||||||
static auto rgx = Glib::Regex::create(mentions_regex);
|
|
||||||
|
|
||||||
Glib::ustring text = GetText(buf);
|
|
||||||
const auto &discord = Abaddon::Get().GetDiscordClient();
|
|
||||||
|
|
||||||
int startpos = 0;
|
|
||||||
Glib::MatchInfo match;
|
|
||||||
while (rgx->match(text, startpos, match)) {
|
|
||||||
int mstart, mend;
|
|
||||||
if (!match.fetch_pos(0, mstart, mend)) break;
|
|
||||||
const Glib::ustring user_id = match.fetch(1);
|
|
||||||
const auto user = discord.GetUser(user_id);
|
|
||||||
const auto channel = discord.GetChannel(ChannelID);
|
|
||||||
if (!user.has_value() || !channel.has_value()) {
|
|
||||||
startpos = mend;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Glib::ustring replacement;
|
|
||||||
|
|
||||||
if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM)
|
|
||||||
replacement = user->GetEscapedBoldString<true>();
|
|
||||||
else {
|
|
||||||
const auto role_id = user->GetHoistedRole(*channel->GuildID, true);
|
|
||||||
const auto role = discord.GetRole(role_id);
|
|
||||||
if (!role.has_value())
|
|
||||||
replacement = user->GetEscapedBoldString<true>();
|
|
||||||
else
|
|
||||||
replacement = "<span color=\"#" + IntToCSSColor(role->Color) + "\">" + user->GetEscapedBoldString<true>() + "</span>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// regex returns byte positions and theres no straightforward way in the c++ bindings to deal with that :(
|
|
||||||
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
|
||||||
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
|
||||||
const auto start_it = buf->get_iter_at_offset(chars_start);
|
|
||||||
const auto end_it = buf->get_iter_at_offset(chars_end);
|
|
||||||
|
|
||||||
auto it = buf->erase(start_it, end_it);
|
|
||||||
buf->insert_markup(it, replacement);
|
|
||||||
|
|
||||||
text = GetText(buf);
|
|
||||||
startpos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatMessageItemContainer::HandleStockEmojis(Gtk::TextView &tv) {
|
|
||||||
Abaddon::Get().GetEmojis().ReplaceEmojis(tv.get_buffer(), EmojiSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) {
|
|
||||||
static auto rgx = Glib::Regex::create(R"(<a?:([\w\d_]+):(\d+)>)");
|
|
||||||
|
|
||||||
auto &img = Abaddon::Get().GetImageManager();
|
|
||||||
|
|
||||||
auto buf = tv.get_buffer();
|
|
||||||
auto text = GetText(buf);
|
|
||||||
|
|
||||||
Glib::MatchInfo match;
|
|
||||||
int startpos = 0;
|
|
||||||
while (rgx->match(text, startpos, match)) {
|
|
||||||
int mstart, mend;
|
|
||||||
if (!match.fetch_pos(0, mstart, mend)) break;
|
|
||||||
const bool is_animated = match.fetch(0)[1] == 'a';
|
|
||||||
|
|
||||||
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
|
||||||
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
|
||||||
auto start_it = buf->get_iter_at_offset(chars_start);
|
|
||||||
auto end_it = buf->get_iter_at_offset(chars_end);
|
|
||||||
|
|
||||||
startpos = mend;
|
|
||||||
if (is_animated && Abaddon::Get().GetSettings().ShowAnimations) {
|
|
||||||
const auto mark_start = buf->create_mark(start_it, false);
|
|
||||||
end_it.backward_char();
|
|
||||||
const auto mark_end = buf->create_mark(end_it, false);
|
|
||||||
const auto cb = [&tv, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::PixbufAnimation> &pixbuf) {
|
|
||||||
auto start_it = mark_start->get_iter();
|
|
||||||
auto end_it = mark_end->get_iter();
|
|
||||||
end_it.forward_char();
|
|
||||||
buf->delete_mark(mark_start);
|
|
||||||
buf->delete_mark(mark_end);
|
|
||||||
auto it = buf->erase(start_it, end_it);
|
|
||||||
const auto anchor = buf->create_child_anchor(it);
|
|
||||||
auto img = Gtk::manage(new Gtk::Image(pixbuf));
|
|
||||||
img->show();
|
|
||||||
tv.add_child_at_anchor(*img, anchor);
|
|
||||||
};
|
|
||||||
img.LoadAnimationFromURL(EmojiData::URLFromID(match.fetch(2), "gif"), EmojiSize, EmojiSize, sigc::track_obj(cb, tv));
|
|
||||||
} else {
|
|
||||||
// can't erase before pixbuf is ready or else marks that are in the same pos get mixed up
|
|
||||||
const auto mark_start = buf->create_mark(start_it, false);
|
|
||||||
end_it.backward_char();
|
|
||||||
const auto mark_end = buf->create_mark(end_it, false);
|
|
||||||
const auto cb = [buf, mark_start, mark_end](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
|
|
||||||
auto start_it = mark_start->get_iter();
|
|
||||||
auto end_it = mark_end->get_iter();
|
|
||||||
end_it.forward_char();
|
|
||||||
buf->delete_mark(mark_start);
|
|
||||||
buf->delete_mark(mark_end);
|
|
||||||
auto it = buf->erase(start_it, end_it);
|
|
||||||
int width, height;
|
|
||||||
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), width, height, EmojiSize, EmojiSize);
|
|
||||||
buf->insert_pixbuf(it, pixbuf->scale_simple(width, height, Gdk::INTERP_BILINEAR));
|
|
||||||
};
|
|
||||||
img.LoadFromURL(EmojiData::URLFromID(match.fetch(2)), sigc::track_obj(cb, tv));
|
|
||||||
}
|
|
||||||
|
|
||||||
text = GetText(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatMessageItemContainer::HandleEmojis(Gtk::TextView &tv) {
|
|
||||||
if (Abaddon::Get().GetSettings().ShowStockEmojis) HandleStockEmojis(tv);
|
|
||||||
if (Abaddon::Get().GetSettings().ShowCustomEmojis) HandleCustomEmojis(tv);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatMessageItemContainer::CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
|
||||||
static auto rgx = Glib::Regex::create(R"(<a?:([\w\d_]+):(\d+)>)");
|
|
||||||
|
|
||||||
auto text = GetText(buf);
|
|
||||||
|
|
||||||
Glib::MatchInfo match;
|
|
||||||
int startpos = 0;
|
|
||||||
while (rgx->match(text, startpos, match)) {
|
|
||||||
int mstart, mend;
|
|
||||||
if (!match.fetch_pos(0, mstart, mend)) break;
|
|
||||||
|
|
||||||
const auto new_term = ":" + match.fetch(1) + ":";
|
|
||||||
|
|
||||||
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
|
||||||
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
|
||||||
auto start_it = buf->get_iter_at_offset(chars_start);
|
|
||||||
auto end_it = buf->get_iter_at_offset(chars_end);
|
|
||||||
|
|
||||||
startpos = mend;
|
|
||||||
const auto it = buf->erase(start_it, end_it);
|
|
||||||
const int alen = static_cast<int>(text.size());
|
|
||||||
text = GetText(buf);
|
|
||||||
const int blen = static_cast<int>(text.size());
|
|
||||||
startpos -= (alen - blen);
|
|
||||||
|
|
||||||
buf->insert(it, new_term);
|
|
||||||
|
|
||||||
text = GetText(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||||
static auto rgx = Glib::Regex::create(R"(<#(\d+)>)");
|
static auto rgx = Glib::Regex::create(R"(<#(\d+)>)");
|
||||||
|
|
||||||
Glib::ustring text = GetText(buf);
|
Glib::ustring text = ChatUtil::GetText(buf);
|
||||||
|
|
||||||
const auto &discord = Abaddon::Get().GetDiscordClient();
|
const auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
|
|
||||||
@@ -975,7 +774,7 @@ void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::Tex
|
|||||||
it = buf->insert_with_tag(it, "#" + *chan->Name, tag);
|
it = buf->insert_with_tag(it, "#" + *chan->Name, tag);
|
||||||
|
|
||||||
// rescan the whole thing so i dont have to deal with fixing match positions
|
// rescan the whole thing so i dont have to deal with fixing match positions
|
||||||
text = GetText(buf);
|
text = ChatUtil::GetText(buf);
|
||||||
startpos = 0;
|
startpos = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1036,7 +835,7 @@ void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) {
|
|||||||
const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)");
|
const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)");
|
||||||
|
|
||||||
auto buf = tv.get_buffer();
|
auto buf = tv.get_buffer();
|
||||||
Glib::ustring text = GetText(buf);
|
Glib::ustring text = ChatUtil::GetText(buf);
|
||||||
|
|
||||||
int startpos = 0;
|
int startpos = 0;
|
||||||
Glib::MatchInfo match;
|
Glib::MatchInfo match;
|
||||||
|
@@ -29,17 +29,8 @@ protected:
|
|||||||
Gtk::Widget *CreateReactionsComponent(const Message &data);
|
Gtk::Widget *CreateReactionsComponent(const Message &data);
|
||||||
Gtk::Widget *CreateReplyComponent(const Message &data);
|
Gtk::Widget *CreateReplyComponent(const Message &data);
|
||||||
|
|
||||||
static Glib::ustring GetText(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
|
||||||
|
|
||||||
static bool IsEmbedImageOnly(const EmbedData &data);
|
static bool IsEmbedImageOnly(const EmbedData &data);
|
||||||
|
|
||||||
static void HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
|
||||||
void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) const;
|
|
||||||
static void HandleStockEmojis(Gtk::TextView &tv);
|
|
||||||
static void HandleCustomEmojis(Gtk::TextView &tv);
|
|
||||||
static void HandleEmojis(Gtk::TextView &tv);
|
|
||||||
static void CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
|
||||||
|
|
||||||
void HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
void HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||||
void HandleChannelMentions(Gtk::TextView *tv);
|
void HandleChannelMentions(Gtk::TextView *tv);
|
||||||
bool OnClickChannel(GdkEventButton *ev);
|
bool OnClickChannel(GdkEventButton *ev);
|
||||||
|
@@ -10,3 +10,9 @@ constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024;
|
|||||||
constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;
|
constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;
|
||||||
constexpr static int BoostLevel3AttachmentSizeLimit = 100 * 1024 * 1024;
|
constexpr static int BoostLevel3AttachmentSizeLimit = 100 * 1024 * 1024;
|
||||||
constexpr static int MaxMessagePayloadSize = 199 * 1024 * 1024;
|
constexpr static int MaxMessagePayloadSize = 199 * 1024 * 1024;
|
||||||
|
constexpr static int EmojiSize = 24; // settings eventually
|
||||||
|
constexpr static int AvatarSize = 32;
|
||||||
|
constexpr static int EmbedImageWidth = 400;
|
||||||
|
constexpr static int EmbedImageHeight = 300;
|
||||||
|
constexpr static int ThumbnailSize = 100;
|
||||||
|
constexpr static int StickerComponentSize = 160;
|
||||||
|
@@ -94,6 +94,18 @@ const UserData &DiscordClient::GetUserData() const {
|
|||||||
return m_user_data;
|
return m_user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UserGuildSettingsData &DiscordClient::GetUserGuildSettings() const {
|
||||||
|
return m_user_guild_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<UserGuildSettingsEntry> DiscordClient::GetSettingsForGuild(Snowflake id) const {
|
||||||
|
if (const auto it = m_user_guild_settings.Entries.find(id); it != m_user_guild_settings.Entries.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Snowflake> DiscordClient::GetUserSortedGuilds() const {
|
std::vector<Snowflake> DiscordClient::GetUserSortedGuilds() const {
|
||||||
// sort order is unfolder'd guilds sorted by id descending, then guilds in folders in array order
|
// sort order is unfolder'd guilds sorted by id descending, then guilds in folders in array order
|
||||||
// todo: make sure folder'd guilds are sorted properly
|
// todo: make sure folder'd guilds are sorted properly
|
||||||
@@ -1701,6 +1713,8 @@ void DiscordClient::HandleGatewayReady(const GatewayMessage &msg) {
|
|||||||
m_session_id = data.SessionID;
|
m_session_id = data.SessionID;
|
||||||
m_user_data = data.SelfUser;
|
m_user_data = data.SelfUser;
|
||||||
m_user_settings = data.Settings;
|
m_user_settings = data.Settings;
|
||||||
|
m_user_guild_settings = data.GuildSettings;
|
||||||
|
// TODO handle update
|
||||||
|
|
||||||
HandleReadyReadState(data);
|
HandleReadyReadState(data);
|
||||||
HandleReadyGuildSettings(data);
|
HandleReadyGuildSettings(data);
|
||||||
@@ -2118,6 +2132,9 @@ void DiscordClient::HandleGatewayMessageAck(const GatewayMessage &msg) {
|
|||||||
|
|
||||||
void DiscordClient::HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg) {
|
void DiscordClient::HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg) {
|
||||||
UserGuildSettingsUpdateData data = msg.Data;
|
UserGuildSettingsUpdateData data = msg.Data;
|
||||||
|
|
||||||
|
m_user_guild_settings.Entries[data.Settings.GuildID] = data.Settings;
|
||||||
|
|
||||||
const bool for_dms = !data.Settings.GuildID.IsValid();
|
const bool for_dms = !data.Settings.GuildID.IsValid();
|
||||||
|
|
||||||
const auto channels = for_dms ? GetPrivateChannels() : GetChannelsInGuild(data.Settings.GuildID);
|
const auto channels = for_dms ? GetPrivateChannels() : GetChannelsInGuild(data.Settings.GuildID);
|
||||||
@@ -2678,7 +2695,7 @@ void DiscordClient::HandleReadyGuildSettings(const ReadyEventData &data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto now = Snowflake::FromNow();
|
const auto now = Snowflake::FromNow();
|
||||||
for (const auto &entry : data.GuildSettings.Entries) {
|
for (const auto &[guild_id, entry] : data.GuildSettings.Entries) {
|
||||||
// even if muted is true a guild/channel can be unmuted if the current time passes mute_config.end_time
|
// even if muted is true a guild/channel can be unmuted if the current time passes mute_config.end_time
|
||||||
if (entry.Muted) {
|
if (entry.Muted) {
|
||||||
if (entry.MuteConfig.EndTime.has_value()) {
|
if (entry.MuteConfig.EndTime.has_value()) {
|
||||||
|
@@ -33,6 +33,8 @@ public:
|
|||||||
|
|
||||||
std::unordered_set<Snowflake> GetGuilds() const;
|
std::unordered_set<Snowflake> GetGuilds() const;
|
||||||
const UserData &GetUserData() const;
|
const UserData &GetUserData() const;
|
||||||
|
const UserGuildSettingsData &GetUserGuildSettings() const;
|
||||||
|
std::optional<UserGuildSettingsEntry> GetSettingsForGuild(Snowflake id) const;
|
||||||
std::vector<Snowflake> GetUserSortedGuilds() const;
|
std::vector<Snowflake> GetUserSortedGuilds() const;
|
||||||
std::vector<Message> GetMessagesForChannel(Snowflake id, size_t limit = 50) const;
|
std::vector<Message> GetMessagesForChannel(Snowflake id, size_t limit = 50) const;
|
||||||
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit = 50) const;
|
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit = 50) const;
|
||||||
@@ -328,6 +330,7 @@ private:
|
|||||||
|
|
||||||
UserData m_user_data;
|
UserData m_user_data;
|
||||||
UserSettings m_user_settings;
|
UserSettings m_user_settings;
|
||||||
|
UserGuildSettingsData m_user_guild_settings;
|
||||||
|
|
||||||
Store m_store;
|
Store m_store;
|
||||||
HTTPClient m_http;
|
HTTPClient m_http;
|
||||||
|
@@ -8,6 +8,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
|
enum class DefaultNotificationLevel {
|
||||||
|
ALL_MESSAGES = 0,
|
||||||
|
ONLY_MENTIONS = 1,
|
||||||
|
};
|
||||||
|
|
||||||
enum class GuildApplicationStatus {
|
enum class GuildApplicationStatus {
|
||||||
STARTED,
|
STARTED,
|
||||||
PENDING,
|
PENDING,
|
||||||
@@ -36,9 +41,6 @@ struct GuildApplicationData {
|
|||||||
|
|
||||||
// a bot is apparently only supposed to receive the `id` and `unavailable` as false
|
// a bot is apparently only supposed to receive the `id` and `unavailable` as false
|
||||||
// but user tokens seem to get the full objects (minus users)
|
// but user tokens seem to get the full objects (minus users)
|
||||||
|
|
||||||
// everythings optional cuz of muh partial guild object
|
|
||||||
// anything not marked optional in https://discord.com/developers/docs/resources/guild#guild-object is guaranteed to be set when returned from Store::GetGuild
|
|
||||||
struct GuildData {
|
struct GuildData {
|
||||||
Snowflake ID;
|
Snowflake ID;
|
||||||
std::string Name;
|
std::string Name;
|
||||||
@@ -55,7 +57,7 @@ struct GuildData {
|
|||||||
std::optional<bool> IsEmbedEnabled; // deprecated
|
std::optional<bool> IsEmbedEnabled; // deprecated
|
||||||
std::optional<Snowflake> EmbedChannelID; // null, deprecated
|
std::optional<Snowflake> EmbedChannelID; // null, deprecated
|
||||||
std::optional<int> VerificationLevel;
|
std::optional<int> VerificationLevel;
|
||||||
std::optional<int> DefaultMessageNotifications;
|
std::optional<DefaultNotificationLevel> DefaultMessageNotifications;
|
||||||
std::optional<int> ExplicitContentFilter;
|
std::optional<int> ExplicitContentFilter;
|
||||||
std::optional<std::vector<RoleData>> Roles;
|
std::optional<std::vector<RoleData>> Roles;
|
||||||
std::optional<std::vector<EmojiData>> Emojis; // only access id
|
std::optional<std::vector<EmojiData>> Emojis; // only access id
|
||||||
|
@@ -197,7 +197,7 @@ void from_json(const nlohmann::json &j, Message &m) {
|
|||||||
JS_D("tts", m.IsTTS);
|
JS_D("tts", m.IsTTS);
|
||||||
JS_D("mention_everyone", m.DoesMentionEveryone);
|
JS_D("mention_everyone", m.DoesMentionEveryone);
|
||||||
JS_D("mentions", m.Mentions);
|
JS_D("mentions", m.Mentions);
|
||||||
// JS_D("mention_roles", m.MentionRoles);
|
JS_D("mention_roles", m.MentionRoles);
|
||||||
// JS_O("mention_channels", m.MentionChannels);
|
// JS_O("mention_channels", m.MentionChannels);
|
||||||
JS_D("attachments", m.Attachments);
|
JS_D("attachments", m.Attachments);
|
||||||
JS_D("embeds", m.Embeds);
|
JS_D("embeds", m.Embeds);
|
||||||
@@ -235,6 +235,7 @@ void Message::from_json_edited(const nlohmann::json &j) {
|
|||||||
JS_O("tts", IsTTS);
|
JS_O("tts", IsTTS);
|
||||||
JS_O("mention_everyone", DoesMentionEveryone);
|
JS_O("mention_everyone", DoesMentionEveryone);
|
||||||
JS_O("mentions", Mentions);
|
JS_O("mentions", Mentions);
|
||||||
|
JS_O("mention_roles", MentionRoles);
|
||||||
JS_O("embeds", Embeds);
|
JS_O("embeds", Embeds);
|
||||||
JS_O("nonce", Nonce);
|
JS_O("nonce", Nonce);
|
||||||
JS_O("pinned", IsPinned);
|
JS_O("pinned", IsPinned);
|
||||||
@@ -264,9 +265,17 @@ bool Message::IsEdited() const {
|
|||||||
return m_edited;
|
return m_edited;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Message::DoesMention(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) {
|
||||||
return user.ID == id;
|
return user.ID == id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Message::DoesMention(Snowflake id) const noexcept {
|
||||||
|
if (DoesMentionEveryoneOrUser(id)) return true;
|
||||||
|
if (!GuildID.has_value()) return false; // nothing left to check
|
||||||
|
const auto member = Abaddon::Get().GetDiscordClient().GetMember(id, *GuildID);
|
||||||
|
if (!member.has_value()) return false;
|
||||||
|
return std::find_first_of(MentionRoles.begin(), MentionRoles.end(), member->Roles.begin(), member->Roles.end()) != MentionRoles.end();
|
||||||
|
}
|
||||||
|
@@ -183,7 +183,7 @@ struct Message {
|
|||||||
bool IsTTS;
|
bool IsTTS;
|
||||||
bool DoesMentionEveryone;
|
bool DoesMentionEveryone;
|
||||||
std::vector<UserData> Mentions; // full user accessible
|
std::vector<UserData> Mentions; // full user accessible
|
||||||
// std::vector<RoleData> MentionRoles;
|
std::vector<Snowflake> MentionRoles;
|
||||||
// std::optional<std::vector<ChannelMentionData>> MentionChannels;
|
// std::optional<std::vector<ChannelMentionData>> MentionChannels;
|
||||||
std::vector<AttachmentData> Attachments;
|
std::vector<AttachmentData> Attachments;
|
||||||
std::vector<EmbedData> Embeds;
|
std::vector<EmbedData> Embeds;
|
||||||
@@ -212,6 +212,7 @@ struct Message {
|
|||||||
[[nodiscard]] bool IsDeleted() const;
|
[[nodiscard]] bool IsDeleted() const;
|
||||||
[[nodiscard]] bool IsEdited() const;
|
[[nodiscard]] bool IsEdited() const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool DoesMentionEveryoneOrUser(Snowflake id) const noexcept;
|
||||||
[[nodiscard]] bool DoesMention(Snowflake id) const noexcept;
|
[[nodiscard]] bool DoesMention(Snowflake id) const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@@ -199,10 +199,26 @@ void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m) {
|
|||||||
j["version"] = m.Version;
|
j["version"] = m.Version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<UserGuildSettingsChannelOverride> UserGuildSettingsEntry::GetOverride(Snowflake channel_id) const {
|
||||||
|
for (const auto &override : ChannelOverrides) {
|
||||||
|
if (override.ChannelID == channel_id) return override;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
void from_json(const nlohmann::json &j, UserGuildSettingsData &m) {
|
void from_json(const nlohmann::json &j, UserGuildSettingsData &m) {
|
||||||
JS_D("version", m.Version);
|
JS_D("version", m.Version);
|
||||||
JS_D("partial", m.IsPartial);
|
JS_D("partial", m.IsPartial);
|
||||||
JS_D("entries", m.Entries);
|
|
||||||
|
{
|
||||||
|
std::vector<UserGuildSettingsEntry> entries;
|
||||||
|
JS_D("entries", entries);
|
||||||
|
|
||||||
|
for (const auto &entry : entries) {
|
||||||
|
m.Entries[entry.GuildID] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void from_json(const nlohmann::json &j, ReadyEventData &m) {
|
void from_json(const nlohmann::json &j, ReadyEventData &m) {
|
||||||
|
@@ -274,10 +274,17 @@ struct ReadStateData {
|
|||||||
friend void from_json(const nlohmann::json &j, ReadStateData &m);
|
friend void from_json(const nlohmann::json &j, ReadStateData &m);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class NotificationLevel {
|
||||||
|
ALL_MESSAGES = 0,
|
||||||
|
ONLY_MENTIONS = 1,
|
||||||
|
NO_MESSAGES = 2,
|
||||||
|
USE_UPPER = 3, // actually called "NULL"
|
||||||
|
};
|
||||||
|
|
||||||
struct UserGuildSettingsChannelOverride {
|
struct UserGuildSettingsChannelOverride {
|
||||||
bool Muted;
|
bool Muted;
|
||||||
MuteConfigData MuteConfig;
|
MuteConfigData MuteConfig;
|
||||||
int MessageNotifications;
|
NotificationLevel MessageNotifications;
|
||||||
bool Collapsed;
|
bool Collapsed;
|
||||||
Snowflake ChannelID;
|
Snowflake ChannelID;
|
||||||
|
|
||||||
@@ -292,19 +299,21 @@ struct UserGuildSettingsEntry {
|
|||||||
bool Muted;
|
bool Muted;
|
||||||
MuteConfigData MuteConfig;
|
MuteConfigData MuteConfig;
|
||||||
bool MobilePush;
|
bool MobilePush;
|
||||||
int MessageNotifications;
|
NotificationLevel MessageNotifications;
|
||||||
bool HideMutedChannels;
|
bool HideMutedChannels;
|
||||||
Snowflake GuildID;
|
Snowflake GuildID;
|
||||||
std::vector<UserGuildSettingsChannelOverride> ChannelOverrides;
|
std::vector<UserGuildSettingsChannelOverride> ChannelOverrides;
|
||||||
|
|
||||||
friend void from_json(const nlohmann::json &j, UserGuildSettingsEntry &m);
|
friend void from_json(const nlohmann::json &j, UserGuildSettingsEntry &m);
|
||||||
friend void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m);
|
friend void to_json(nlohmann::json &j, const UserGuildSettingsEntry &m);
|
||||||
|
|
||||||
|
std::optional<UserGuildSettingsChannelOverride> GetOverride(Snowflake channel_id) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UserGuildSettingsData {
|
struct UserGuildSettingsData {
|
||||||
int Version;
|
int Version;
|
||||||
bool IsPartial;
|
bool IsPartial;
|
||||||
std::vector<UserGuildSettingsEntry> Entries;
|
std::map<Snowflake, UserGuildSettingsEntry> Entries;
|
||||||
|
|
||||||
friend void from_json(const nlohmann::json &j, UserGuildSettingsData &m);
|
friend void from_json(const nlohmann::json &j, UserGuildSettingsData &m);
|
||||||
};
|
};
|
||||||
|
@@ -346,6 +346,15 @@ void Store::SetMessage(Snowflake id, const Message &message) {
|
|||||||
s->Reset();
|
s->Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const auto &r : message.MentionRoles) {
|
||||||
|
auto &s = m_stmt_set_role_mention;
|
||||||
|
s->Bind(1, id);
|
||||||
|
s->Bind(2, r);
|
||||||
|
if (!s->Insert())
|
||||||
|
fprintf(stderr, "message role mention insert failed for %" PRIu64 "/%" PRIu64 ": %s\n", static_cast<uint64_t>(id), static_cast<uint64_t>(r), m_db.ErrStr());
|
||||||
|
s->Reset();
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto &a : message.Attachments) {
|
for (const auto &a : message.Attachments) {
|
||||||
auto &s = m_stmt_set_attachment;
|
auto &s = m_stmt_set_attachment;
|
||||||
s->Bind(1, id);
|
s->Bind(1, id);
|
||||||
@@ -779,6 +788,7 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
|
|||||||
s->Get(1, r.Name);
|
s->Get(1, r.Name);
|
||||||
s->Get(2, r.Icon);
|
s->Get(2, r.Icon);
|
||||||
s->Get(5, r.OwnerID);
|
s->Get(5, r.OwnerID);
|
||||||
|
s->Get(11, r.DefaultMessageNotifications);
|
||||||
s->Get(20, r.IsUnavailable);
|
s->Get(20, r.IsUnavailable);
|
||||||
s->Get(27, r.PremiumTier);
|
s->Get(27, r.PremiumTier);
|
||||||
|
|
||||||
@@ -986,6 +996,17 @@ Message Store::GetMessageBound(std::unique_ptr<Statement> &s) const {
|
|||||||
s->Reset();
|
s->Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto &s = m_stmt_get_role_mentions;
|
||||||
|
s->Bind(1, r.ID);
|
||||||
|
while (s->FetchOne()) {
|
||||||
|
Snowflake id;
|
||||||
|
s->Get(0, id);
|
||||||
|
r.MentionRoles.push_back(id);
|
||||||
|
}
|
||||||
|
s->Reset();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto &s = m_stmt_get_reactions;
|
auto &s = m_stmt_get_reactions;
|
||||||
s->Bind(1, r.ID);
|
s->Bind(1, r.ID);
|
||||||
@@ -1436,6 +1457,14 @@ bool Store::CreateTables() {
|
|||||||
)
|
)
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
const char *create_mention_roles = R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS mention_roles (
|
||||||
|
message INTEGER NOT NULL,
|
||||||
|
role INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(message, role)
|
||||||
|
)
|
||||||
|
)";
|
||||||
|
|
||||||
const char *create_attachments = R"(
|
const char *create_attachments = R"(
|
||||||
CREATE TABLE IF NOT EXISTS attachments (
|
CREATE TABLE IF NOT EXISTS attachments (
|
||||||
message INTEGER NOT NULL,
|
message INTEGER NOT NULL,
|
||||||
@@ -1555,6 +1584,11 @@ bool Store::CreateTables() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_db.Execute(create_mention_roles) != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "failed to create role mentions table: %s\n", m_db.ErrStr());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_db.Execute(create_attachments) != SQLITE_OK) {
|
if (m_db.Execute(create_attachments) != SQLITE_OK) {
|
||||||
fprintf(stderr, "failed to create attachments table: %s\n", m_db.ErrStr());
|
fprintf(stderr, "failed to create attachments table: %s\n", m_db.ErrStr());
|
||||||
return false;
|
return false;
|
||||||
@@ -2118,6 +2152,24 @@ bool Store::CreateStatements() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_stmt_set_role_mention = std::make_unique<Statement>(m_db, R"(
|
||||||
|
REPLACE INTO mention_roles VALUES (
|
||||||
|
?, ?
|
||||||
|
)
|
||||||
|
)");
|
||||||
|
if (!m_stmt_set_role_mention->OK()) {
|
||||||
|
fprintf(stderr, "failed to prepare set role mention statement: %s\n", m_db.ErrStr());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stmt_get_role_mentions = std::make_unique<Statement>(m_db, R"(
|
||||||
|
SELECT role FROM mention_roles WHERE message = ?
|
||||||
|
)");
|
||||||
|
if (!m_stmt_get_role_mentions->OK()) {
|
||||||
|
fprintf(stderr, "failed to prepare get role mentions statement: %s\n", m_db.ErrStr());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
m_stmt_set_attachment = std::make_unique<Statement>(m_db, R"(
|
m_stmt_set_attachment = std::make_unique<Statement>(m_db, R"(
|
||||||
REPLACE INTO attachments VALUES (
|
REPLACE INTO attachments VALUES (
|
||||||
?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?
|
||||||
|
@@ -299,6 +299,8 @@ private:
|
|||||||
STMT(get_emoji_roles);
|
STMT(get_emoji_roles);
|
||||||
STMT(set_mention);
|
STMT(set_mention);
|
||||||
STMT(get_mentions);
|
STMT(get_mentions);
|
||||||
|
STMT(set_role_mention);
|
||||||
|
STMT(get_role_mentions);
|
||||||
STMT(set_attachment);
|
STMT(set_attachment);
|
||||||
STMT(get_attachments);
|
STMT(get_attachments);
|
||||||
STMT(set_recipient);
|
STMT(set_recipient);
|
||||||
|
@@ -101,3 +101,7 @@ Glib::RefPtr<Gdk::Pixbuf> ImageManager::GetPlaceholder(int size) {
|
|||||||
return Glib::RefPtr<Gdk::Pixbuf>(nullptr);
|
return Glib::RefPtr<Gdk::Pixbuf>(nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cache &ImageManager::GetCache() {
|
||||||
|
return m_cache;
|
||||||
|
}
|
||||||
|
@@ -18,6 +18,7 @@ public:
|
|||||||
void LoadAnimationFromURL(const std::string &url, int w, int h, const callback_anim_type &cb);
|
void LoadAnimationFromURL(const std::string &url, int w, int h, const callback_anim_type &cb);
|
||||||
void Prefetch(const std::string &url);
|
void Prefetch(const std::string &url);
|
||||||
Glib::RefPtr<Gdk::Pixbuf> GetPlaceholder(int size);
|
Glib::RefPtr<Gdk::Pixbuf> GetPlaceholder(int size);
|
||||||
|
Cache &GetCache();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static Glib::RefPtr<Gdk::Pixbuf> ReadFileToPixbuf(std::string path);
|
static Glib::RefPtr<Gdk::Pixbuf> ReadFileToPixbuf(std::string path);
|
||||||
|
204
src/misc/chatutil.cpp
Normal file
204
src/misc/chatutil.cpp
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#include "chatutil.hpp"
|
||||||
|
#include "constants.hpp"
|
||||||
|
|
||||||
|
namespace ChatUtil {
|
||||||
|
Glib::ustring GetText(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||||
|
Gtk::TextBuffer::iterator a, b;
|
||||||
|
buf->get_bounds(a, b);
|
||||||
|
auto slice = buf->get_slice(a, b, true);
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||||
|
constexpr static const auto mentions_regex = R"(<@&(\d+)>)";
|
||||||
|
|
||||||
|
static auto rgx = Glib::Regex::create(mentions_regex);
|
||||||
|
|
||||||
|
Glib::ustring text = GetText(buf);
|
||||||
|
const auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
|
|
||||||
|
int startpos = 0;
|
||||||
|
Glib::MatchInfo match;
|
||||||
|
while (rgx->match(text, startpos, match)) {
|
||||||
|
int mstart, mend;
|
||||||
|
if (!match.fetch_pos(0, mstart, mend)) break;
|
||||||
|
const Glib::ustring role_id = match.fetch(1);
|
||||||
|
const auto role = discord.GetRole(role_id);
|
||||||
|
if (!role.has_value()) {
|
||||||
|
startpos = mend;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::ustring replacement;
|
||||||
|
if (role->HasColor()) {
|
||||||
|
replacement = "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">@" + role->GetEscapedName() + "</span></b>";
|
||||||
|
} else {
|
||||||
|
replacement = "<b>@" + role->GetEscapedName() + "</b>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
||||||
|
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
||||||
|
const auto start_it = buf->get_iter_at_offset(chars_start);
|
||||||
|
const auto end_it = buf->get_iter_at_offset(chars_end);
|
||||||
|
|
||||||
|
auto it = buf->erase(start_it, end_it);
|
||||||
|
buf->insert_markup(it, replacement);
|
||||||
|
|
||||||
|
text = GetText(buf);
|
||||||
|
startpos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf, Snowflake channel_id, bool plain) {
|
||||||
|
constexpr static const auto mentions_regex = R"(<@!?(\d+)>)";
|
||||||
|
|
||||||
|
static auto rgx = Glib::Regex::create(mentions_regex);
|
||||||
|
|
||||||
|
Glib::ustring text = GetText(buf);
|
||||||
|
const auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
|
|
||||||
|
int startpos = 0;
|
||||||
|
Glib::MatchInfo match;
|
||||||
|
while (rgx->match(text, startpos, match)) {
|
||||||
|
int mstart, mend;
|
||||||
|
if (!match.fetch_pos(0, mstart, mend)) break;
|
||||||
|
const Glib::ustring user_id = match.fetch(1);
|
||||||
|
const auto user = discord.GetUser(user_id);
|
||||||
|
const auto channel = discord.GetChannel(channel_id);
|
||||||
|
if (!user.has_value() || !channel.has_value()) {
|
||||||
|
startpos = mend;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::ustring replacement;
|
||||||
|
|
||||||
|
if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM || !channel->GuildID.has_value() || plain) {
|
||||||
|
if (plain) {
|
||||||
|
replacement = "@" + user->Username + "#" + user->Discriminator;
|
||||||
|
} else {
|
||||||
|
replacement = user->GetEscapedBoldString<true>();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const auto role_id = user->GetHoistedRole(*channel->GuildID, true);
|
||||||
|
const auto role = discord.GetRole(role_id);
|
||||||
|
if (!role.has_value())
|
||||||
|
replacement = user->GetEscapedBoldString<true>();
|
||||||
|
else
|
||||||
|
replacement = "<span color=\"#" + IntToCSSColor(role->Color) + "\">" + user->GetEscapedBoldString<true>() + "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// regex returns byte positions and theres no straightforward way in the c++ bindings to deal with that :(
|
||||||
|
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
||||||
|
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
||||||
|
const auto start_it = buf->get_iter_at_offset(chars_start);
|
||||||
|
const auto end_it = buf->get_iter_at_offset(chars_end);
|
||||||
|
|
||||||
|
auto it = buf->erase(start_it, end_it);
|
||||||
|
buf->insert_markup(it, replacement);
|
||||||
|
|
||||||
|
text = GetText(buf);
|
||||||
|
startpos = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleStockEmojis(Gtk::TextView &tv) {
|
||||||
|
Abaddon::Get().GetEmojis().ReplaceEmojis(tv.get_buffer(), EmojiSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleCustomEmojis(Gtk::TextView &tv) {
|
||||||
|
static auto rgx = Glib::Regex::create(R"(<a?:([\w\d_]+):(\d+)>)");
|
||||||
|
|
||||||
|
auto &img = Abaddon::Get().GetImageManager();
|
||||||
|
|
||||||
|
auto buf = tv.get_buffer();
|
||||||
|
auto text = GetText(buf);
|
||||||
|
|
||||||
|
Glib::MatchInfo match;
|
||||||
|
int startpos = 0;
|
||||||
|
while (rgx->match(text, startpos, match)) {
|
||||||
|
int mstart, mend;
|
||||||
|
if (!match.fetch_pos(0, mstart, mend)) break;
|
||||||
|
const bool is_animated = match.fetch(0)[1] == 'a';
|
||||||
|
|
||||||
|
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
||||||
|
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
||||||
|
auto start_it = buf->get_iter_at_offset(chars_start);
|
||||||
|
auto end_it = buf->get_iter_at_offset(chars_end);
|
||||||
|
|
||||||
|
startpos = mend;
|
||||||
|
if (is_animated && Abaddon::Get().GetSettings().ShowAnimations) {
|
||||||
|
const auto mark_start = buf->create_mark(start_it, false);
|
||||||
|
end_it.backward_char();
|
||||||
|
const auto mark_end = buf->create_mark(end_it, false);
|
||||||
|
const auto cb = [&tv, buf, mark_start, mark_end](const Glib::RefPtr<Gdk::PixbufAnimation> &pixbuf) {
|
||||||
|
auto start_it = mark_start->get_iter();
|
||||||
|
auto end_it = mark_end->get_iter();
|
||||||
|
end_it.forward_char();
|
||||||
|
buf->delete_mark(mark_start);
|
||||||
|
buf->delete_mark(mark_end);
|
||||||
|
auto it = buf->erase(start_it, end_it);
|
||||||
|
const auto anchor = buf->create_child_anchor(it);
|
||||||
|
auto img = Gtk::manage(new Gtk::Image(pixbuf));
|
||||||
|
img->show();
|
||||||
|
tv.add_child_at_anchor(*img, anchor);
|
||||||
|
};
|
||||||
|
img.LoadAnimationFromURL(EmojiData::URLFromID(match.fetch(2), "gif"), EmojiSize, EmojiSize, sigc::track_obj(cb, tv));
|
||||||
|
} else {
|
||||||
|
// can't erase before pixbuf is ready or else marks that are in the same pos get mixed up
|
||||||
|
const auto mark_start = buf->create_mark(start_it, false);
|
||||||
|
end_it.backward_char();
|
||||||
|
const auto mark_end = buf->create_mark(end_it, false);
|
||||||
|
const auto cb = [buf, mark_start, mark_end](const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) {
|
||||||
|
auto start_it = mark_start->get_iter();
|
||||||
|
auto end_it = mark_end->get_iter();
|
||||||
|
end_it.forward_char();
|
||||||
|
buf->delete_mark(mark_start);
|
||||||
|
buf->delete_mark(mark_end);
|
||||||
|
auto it = buf->erase(start_it, end_it);
|
||||||
|
int width, height;
|
||||||
|
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), width, height, EmojiSize, EmojiSize);
|
||||||
|
buf->insert_pixbuf(it, pixbuf->scale_simple(width, height, Gdk::INTERP_BILINEAR));
|
||||||
|
};
|
||||||
|
img.LoadFromURL(EmojiData::URLFromID(match.fetch(2)), sigc::track_obj(cb, tv));
|
||||||
|
}
|
||||||
|
|
||||||
|
text = GetText(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleEmojis(Gtk::TextView &tv) {
|
||||||
|
if (Abaddon::Get().GetSettings().ShowStockEmojis) HandleStockEmojis(tv);
|
||||||
|
if (Abaddon::Get().GetSettings().ShowCustomEmojis) HandleCustomEmojis(tv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf) {
|
||||||
|
static auto rgx = Glib::Regex::create(R"(<a?:([\w\d_]+):(\d+)>)");
|
||||||
|
|
||||||
|
auto text = GetText(buf);
|
||||||
|
|
||||||
|
Glib::MatchInfo match;
|
||||||
|
int startpos = 0;
|
||||||
|
while (rgx->match(text, startpos, match)) {
|
||||||
|
int mstart, mend;
|
||||||
|
if (!match.fetch_pos(0, mstart, mend)) break;
|
||||||
|
|
||||||
|
const auto new_term = ":" + match.fetch(1) + ":";
|
||||||
|
|
||||||
|
const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart);
|
||||||
|
const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend);
|
||||||
|
auto start_it = buf->get_iter_at_offset(chars_start);
|
||||||
|
auto end_it = buf->get_iter_at_offset(chars_end);
|
||||||
|
|
||||||
|
startpos = mend;
|
||||||
|
const auto it = buf->erase(start_it, end_it);
|
||||||
|
const int alen = static_cast<int>(text.size());
|
||||||
|
text = GetText(buf);
|
||||||
|
const int blen = static_cast<int>(text.size());
|
||||||
|
startpos -= (alen - blen);
|
||||||
|
|
||||||
|
buf->insert(it, new_term);
|
||||||
|
|
||||||
|
text = GetText(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace ChatUtil
|
19
src/misc/chatutil.hpp
Normal file
19
src/misc/chatutil.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <glibmm/refptr.h>
|
||||||
|
#include <glibmm/ustring.h>
|
||||||
|
#include "discord/snowflake.hpp"
|
||||||
|
|
||||||
|
namespace Gtk {
|
||||||
|
class TextBuffer;
|
||||||
|
class TextView;
|
||||||
|
} // namespace Gtk
|
||||||
|
|
||||||
|
namespace ChatUtil {
|
||||||
|
Glib::ustring GetText(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||||
|
void HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||||
|
void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf, Snowflake channel_id, bool plain);
|
||||||
|
void HandleStockEmojis(Gtk::TextView &tv);
|
||||||
|
void HandleCustomEmojis(Gtk::TextView &tv);
|
||||||
|
void HandleEmojis(Gtk::TextView &tv);
|
||||||
|
void CleanupEmojis(const Glib::RefPtr<Gtk::TextBuffer> &buf);
|
||||||
|
} // namespace ChatUtil
|
169
src/notifications/notifications.cpp
Normal file
169
src/notifications/notifications.cpp
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
#include "notifications.hpp"
|
||||||
|
#include "misc/chatutil.hpp"
|
||||||
|
#include "discord/message.hpp"
|
||||||
|
|
||||||
|
Notifications::Notifications() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckGuildMessage(const Message &message) {
|
||||||
|
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
|
if (!message.GuildID.has_value()) return false;
|
||||||
|
|
||||||
|
const auto guild = discord.GetGuild(*message.GuildID);
|
||||||
|
if (!guild.has_value()) return false;
|
||||||
|
|
||||||
|
const auto guild_settings = discord.GetSettingsForGuild(*message.GuildID);
|
||||||
|
|
||||||
|
// if theres no guild settings then just use default message notifications level
|
||||||
|
// (there will be no channel settings)
|
||||||
|
if (!guild_settings.has_value()) {
|
||||||
|
if (guild->DefaultMessageNotifications.has_value()) {
|
||||||
|
switch (*guild->DefaultMessageNotifications) {
|
||||||
|
case DefaultNotificationLevel::ALL_MESSAGES:
|
||||||
|
return true;
|
||||||
|
case DefaultNotificationLevel::ONLY_MENTIONS:
|
||||||
|
return message.DoesMention(discord.GetUserData().ID);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (discord.IsGuildMuted(*message.GuildID)) {
|
||||||
|
// if there are guild settings and the guild is muted then dont notify
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the channel category is muted then dont notify
|
||||||
|
const auto channel = discord.GetChannel(message.ChannelID);
|
||||||
|
std::optional<UserGuildSettingsChannelOverride> category_settings;
|
||||||
|
if (channel.has_value() && channel->ParentID.has_value()) {
|
||||||
|
category_settings = guild_settings->GetOverride(*channel->ParentID);
|
||||||
|
if (discord.IsChannelMuted(*channel->ParentID)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto channel_settings = guild_settings->GetOverride(message.ChannelID);
|
||||||
|
|
||||||
|
// 🥴
|
||||||
|
NotificationLevel level;
|
||||||
|
if (channel_settings.has_value()) {
|
||||||
|
if (channel_settings->MessageNotifications == NotificationLevel::USE_UPPER) {
|
||||||
|
if (category_settings.has_value()) {
|
||||||
|
if (category_settings->MessageNotifications == NotificationLevel::USE_UPPER) {
|
||||||
|
level = guild_settings->MessageNotifications;
|
||||||
|
} else {
|
||||||
|
level = category_settings->MessageNotifications;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
level = guild_settings->MessageNotifications;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
level = channel_settings->MessageNotifications;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (category_settings.has_value()) {
|
||||||
|
if (category_settings->MessageNotifications == NotificationLevel::USE_UPPER) {
|
||||||
|
level = guild_settings->MessageNotifications;
|
||||||
|
} else {
|
||||||
|
level = category_settings->MessageNotifications;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
level = guild_settings->MessageNotifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there are channel settings, so use them
|
||||||
|
switch (level) {
|
||||||
|
case NotificationLevel::ALL_MESSAGES:
|
||||||
|
return true;
|
||||||
|
case NotificationLevel::ONLY_MENTIONS:
|
||||||
|
return message.DoesMention(discord.GetUserData().ID);
|
||||||
|
case NotificationLevel::NO_MESSAGES:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifications::CheckMessage(const Message &message) {
|
||||||
|
if (!Abaddon::Get().GetSettings().NotificationsEnabled) return;
|
||||||
|
// ignore if our status is do not disturb
|
||||||
|
if (IsDND()) return;
|
||||||
|
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
|
// ignore if the channel is muted
|
||||||
|
if (discord.IsChannelMuted(message.ChannelID)) return;
|
||||||
|
// ignore if focused and message's channel is active
|
||||||
|
if (Abaddon::Get().IsMainWindowActive() && Abaddon::Get().GetActiveChannelID() == message.ChannelID) return;
|
||||||
|
// ignore if its our own message
|
||||||
|
if (message.Author.ID == Abaddon::Get().GetDiscordClient().GetUserData().ID) return;
|
||||||
|
// notify messages in DMs
|
||||||
|
const auto channel = discord.GetChannel(message.ChannelID);
|
||||||
|
if (channel->IsDM()) {
|
||||||
|
m_chan_notifications[message.ChannelID].push_back(message.ID);
|
||||||
|
NotifyMessageDM(message);
|
||||||
|
} else if (CheckGuildMessage(message)) {
|
||||||
|
m_chan_notifications[message.ChannelID].push_back(message.ID);
|
||||||
|
NotifyMessageGuild(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifications::WithdrawChannel(Snowflake channel_id) {
|
||||||
|
if (auto it = m_chan_notifications.find(channel_id); it != m_chan_notifications.end()) {
|
||||||
|
for (const auto notification_id : it->second) {
|
||||||
|
m_notifier.Withdraw(std::to_string(notification_id));
|
||||||
|
}
|
||||||
|
it->second.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::ustring Sanitize(const Message &message) {
|
||||||
|
auto buf = Gtk::TextBuffer::create();
|
||||||
|
Gtk::TextBuffer::iterator begin, end;
|
||||||
|
buf->get_bounds(begin, end);
|
||||||
|
buf->insert(end, message.Content);
|
||||||
|
ChatUtil::CleanupEmojis(buf);
|
||||||
|
ChatUtil::HandleUserMentions(buf, message.ChannelID, true);
|
||||||
|
return Glib::Markup::escape_text(buf->get_text());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifications::NotifyMessageDM(const Message &message) {
|
||||||
|
Glib::ustring default_action = "app.go-to-channel";
|
||||||
|
default_action += "::";
|
||||||
|
default_action += std::to_string(message.ChannelID);
|
||||||
|
const auto title = message.Author.Username;
|
||||||
|
const auto body = Sanitize(message);
|
||||||
|
|
||||||
|
Abaddon::Get().GetImageManager().GetCache().GetFileFromURL(message.Author.GetAvatarURL("png", "64"), [=](const std::string &path) {
|
||||||
|
m_notifier.Notify(std::to_string(message.ID), title, body, default_action, path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifications::NotifyMessageGuild(const Message &message) {
|
||||||
|
Glib::ustring default_action = "app.go-to-channel";
|
||||||
|
default_action += "::";
|
||||||
|
default_action += std::to_string(message.ChannelID);
|
||||||
|
Glib::ustring title = message.Author.Username;
|
||||||
|
if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(message.ChannelID); channel.has_value() && channel->Name.has_value()) {
|
||||||
|
if (channel->ParentID.has_value()) {
|
||||||
|
const auto category = Abaddon::Get().GetDiscordClient().GetChannel(*channel->ParentID);
|
||||||
|
if (category.has_value() && category->Name.has_value()) {
|
||||||
|
title += " (#" + *channel->Name + ", " + *category->Name + ")";
|
||||||
|
} else {
|
||||||
|
title += " (#" + *channel->Name + ")";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title += " (#" + *channel->Name + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto body = Sanitize(message);
|
||||||
|
Abaddon::Get().GetImageManager().GetCache().GetFileFromURL(message.Author.GetAvatarURL("png", "64"), [=](const std::string &path) {
|
||||||
|
m_notifier.Notify(std::to_string(message.ID), title, body, default_action, path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Notifications::IsDND() const {
|
||||||
|
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
|
const auto status = discord.GetUserStatus(discord.GetUserData().ID);
|
||||||
|
return status == PresenceStatus::DND;
|
||||||
|
}
|
23
src/notifications/notifications.hpp
Normal file
23
src/notifications/notifications.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "notifier.hpp"
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Message;
|
||||||
|
|
||||||
|
class Notifications {
|
||||||
|
public:
|
||||||
|
Notifications();
|
||||||
|
|
||||||
|
void CheckMessage(const Message &message);
|
||||||
|
void WithdrawChannel(Snowflake channel_id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void NotifyMessageDM(const Message &message);
|
||||||
|
void NotifyMessageGuild(const Message &message);
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsDND() const;
|
||||||
|
|
||||||
|
Notifier m_notifier;
|
||||||
|
std::map<Snowflake, std::vector<Snowflake>> m_chan_notifications;
|
||||||
|
};
|
21
src/notifications/notifier.hpp
Normal file
21
src/notifications/notifier.hpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <glibmm/ustring.h>
|
||||||
|
#include <gdkmm/pixbuf.h>
|
||||||
|
|
||||||
|
#ifdef ENABLE_NOTIFICATION_SOUNDS
|
||||||
|
#include <miniaudio.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class Notifier {
|
||||||
|
public:
|
||||||
|
Notifier();
|
||||||
|
~Notifier();
|
||||||
|
|
||||||
|
void Notify(const Glib::ustring &id, const Glib::ustring &title, const Glib::ustring &text, const Glib::ustring &default_action, const std::string &icon_path);
|
||||||
|
void Withdraw(const Glib::ustring &id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef ENABLE_NOTIFICATION_SOUNDS
|
||||||
|
ma_engine m_engine;
|
||||||
|
#endif
|
||||||
|
};
|
46
src/notifications/notifier_gio.cpp
Normal file
46
src/notifications/notifier_gio.cpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#include "notifier.hpp"
|
||||||
|
#include <giomm/notification.h>
|
||||||
|
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include <miniaudio.h>
|
||||||
|
|
||||||
|
Notifier::Notifier() {
|
||||||
|
#ifdef ENABLE_NOTIFICATION_SOUNDS
|
||||||
|
if (ma_engine_init(nullptr, &m_engine) != MA_SUCCESS) {
|
||||||
|
printf("failed to initialize miniaudio engine\n");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifier::~Notifier() {
|
||||||
|
#ifdef ENABLE_NOTIFICATION_SOUNDS
|
||||||
|
ma_engine_uninit(&m_engine);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifier::Notify(const Glib::ustring &id, const Glib::ustring &title, const Glib::ustring &text, const Glib::ustring &default_action, const std::string &icon_path) {
|
||||||
|
auto n = Gio::Notification::create(title);
|
||||||
|
n->set_body(text);
|
||||||
|
n->set_default_action(default_action);
|
||||||
|
|
||||||
|
// i dont think giomm provides an interface for this
|
||||||
|
|
||||||
|
auto *file = g_file_new_for_path(icon_path.c_str());
|
||||||
|
auto *icon = g_file_icon_new(file);
|
||||||
|
g_notification_set_icon(n->gobj(), icon);
|
||||||
|
|
||||||
|
Abaddon::Get().GetApp()->send_notification(id, n);
|
||||||
|
|
||||||
|
g_object_unref(icon);
|
||||||
|
g_object_unref(file);
|
||||||
|
|
||||||
|
#ifdef ENABLE_NOTIFICATION_SOUNDS
|
||||||
|
if (Abaddon::Get().GetSettings().NotificationsPlaySound) {
|
||||||
|
ma_engine_play_sound(&m_engine, Abaddon::Get().GetResPath("/sound/message.mp3").c_str(), nullptr);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifier::Withdraw(const Glib::ustring &id) {
|
||||||
|
Abaddon::Get().GetApp()->withdraw_notification(id);
|
||||||
|
}
|
9
src/notifications/notifier_null.cpp
Normal file
9
src/notifications/notifier_null.cpp
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#include "notifier.hpp"
|
||||||
|
|
||||||
|
Notifier::Notifier() {}
|
||||||
|
|
||||||
|
Notifier::~Notifier() {}
|
||||||
|
|
||||||
|
void Notifier::Notify(const Glib::ustring &id, const Glib::ustring &title, const Glib::ustring &text, const Glib::ustring &default_action, const std::string &icon_path) {}
|
||||||
|
|
||||||
|
void Notifier::Withdraw(const Glib::ustring &id) {}
|
@@ -68,6 +68,8 @@ void SettingsManager::ReadSettings() {
|
|||||||
SMSTR("style", "mentionbadgecolor", MentionBadgeColor);
|
SMSTR("style", "mentionbadgecolor", MentionBadgeColor);
|
||||||
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
||||||
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
||||||
|
SMBOOL("notifications", "enabled", NotificationsEnabled);
|
||||||
|
SMBOOL("notifications", "playsound", NotificationsPlaySound);
|
||||||
|
|
||||||
#ifdef WITH_KEYCHAIN
|
#ifdef WITH_KEYCHAIN
|
||||||
keychain::Error error {};
|
keychain::Error error {};
|
||||||
@@ -149,6 +151,8 @@ void SettingsManager::Close() {
|
|||||||
SMSTR("style", "mentionbadgecolor", MentionBadgeColor);
|
SMSTR("style", "mentionbadgecolor", MentionBadgeColor);
|
||||||
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
||||||
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
||||||
|
SMBOOL("notifications", "enabled", NotificationsEnabled);
|
||||||
|
SMBOOL("notifications", "playsound", NotificationsPlaySound);
|
||||||
|
|
||||||
#ifdef WITH_KEYCHAIN
|
#ifdef WITH_KEYCHAIN
|
||||||
keychain::Error error {};
|
keychain::Error error {};
|
||||||
|
@@ -44,6 +44,14 @@ public:
|
|||||||
std::string MentionBadgeColor { "#b82525" };
|
std::string MentionBadgeColor { "#b82525" };
|
||||||
std::string MentionBadgeTextColor { "#fbfbfb" };
|
std::string MentionBadgeTextColor { "#fbfbfb" };
|
||||||
std::string UnreadIndicatorColor { "#ffffff" };
|
std::string UnreadIndicatorColor { "#ffffff" };
|
||||||
|
|
||||||
|
// [notifications]
|
||||||
|
#ifdef _WIN32
|
||||||
|
bool NotificationsEnabled { false };
|
||||||
|
#else
|
||||||
|
bool NotificationsEnabled { true };
|
||||||
|
#endif
|
||||||
|
bool NotificationsPlaySound { true };
|
||||||
};
|
};
|
||||||
|
|
||||||
SettingsManager(const std::string &filename);
|
SettingsManager(const std::string &filename);
|
||||||
|
Reference in New Issue
Block a user