60 Commits

Author SHA1 Message Date
ouwou
039cc7458d copy over new dlls for win build 2022-08-14 22:32:33 -04:00
ouwou
d99f16f82d fix ci run on mac 2022-08-14 00:02:19 -04:00
dragontamer8740
243e48e609 remove unnecessary cstdio include (#101) 2022-08-14 03:36:56 +00:00
dragontamer8740
fac9f1ba58 Big-endian 'emojis.bin' decoding fix (#100)
* Big-endian patch: fixes emojis.bin reading on big-endian systems (tested only on 32-bit PowerPC linux)
2022-08-14 02:59:08 +00:00
ouwou
6a5ecb4d95 Merge branch 'attachments' 2022-08-12 18:35:58 -04:00
ouwou
dc28eae95a spoof a bunch of headers like the web client 2022-08-11 23:37:39 -04:00
ouwou
baf96da80c add copy url menu item to attachments (closes #96)
also refactor menu popup to fit over entire message width
2022-08-11 20:53:36 -04:00
ouwou
31ca6d9fd2 update capabilities and client build number 2022-08-10 23:38:12 -04:00
ouwou
04befeb180 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-08-10 23:29:10 -04:00
ouwou
a4c8a2290d remove ability to create dms 2022-08-10 23:29:00 -04:00
ouwou
96ec5bb665 fix removing roles from members (maybe) 2022-08-10 23:20:27 -04:00
ouwou
f60cea2216 control icon pos in css 2022-08-10 00:03:04 -04:00
ouwou
02741f2c1b remove unnecessary verbosity 2022-08-08 02:41:54 -04:00
ouwou
955b9239b9 hide browse icon when not in channel with perms 2022-08-08 00:40:20 -04:00
ouwou
53ac853367 fix some permission checks 2022-08-08 00:29:18 -04:00
ouwou
1c38671356 add SEND_MESSAGES permission check finally 2022-08-07 19:56:01 -04:00
ouwou
91527fbd0d pull chat input permission check out of signal 2022-08-07 19:54:39 -04:00
ouwou
537d4163c2 add some Get()s that i forgot somehow 2022-08-07 19:19:52 -04:00
ouwou
c0e4a3a988 give focus to input after choosing file 2022-08-07 19:18:05 -04:00
ouwou
860049fad5 Update README.md 2022-08-07 20:15:47 +00:00
ouwou
344f269414 add file picker to chat input 2022-08-07 02:16:20 -04:00
ouwou
3487353fc7 css tweaks 2022-08-07 02:14:26 -04:00
ouwou
86fc8f4186 Merge branch 'master' into attachments 2022-08-06 02:27:37 -04:00
ouwou
acb80da387 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-08-06 02:23:15 -04:00
ouwou
d99d8443ee dont override expansion state because of active channel 2022-08-06 02:23:08 -04:00
dragontamer8740
319f9c392c fixed text input box to not resize when typing (#89) 2022-08-05 04:22:59 +00:00
ouwou
a61a630ee6 handle null from std::locale::locale (fixes #88) 2022-08-03 02:27:43 -04:00
ouwou
e8260c164f stop printing every event type 2022-07-31 17:53:55 -04:00
ouwou
4e4986f670 grey out leave button if user owns the guild 2022-07-31 17:23:00 -04:00
ouwou
3610a2508b limit how often progress bar can update 2022-07-25 01:10:31 -04:00
ouwou
8396d07d9e try to align stuff a little better 2022-07-25 00:26:04 -04:00
ouwou
59acd0f82f handle max message payload + show filename label 2022-07-23 18:34:33 -04:00
ouwou
544ae6f915 fix potential deadlock 2022-07-14 04:12:17 -04:00
ouwou
111399cf4a move progress bar so it doesnt conflict with other stuff 2022-07-14 03:13:27 -04:00
ouwou
fba5cee519 try to fix compile again 2022-07-10 02:38:19 +00:00
ouwou
f95d79129e handle premium server upload size limits 2022-07-09 03:05:48 -04:00
ouwou
02ce353c6d check nitro size restriction + fix replying border 2022-07-09 01:57:56 -04:00
ouwou
241d9a2140 fix compile 2022-07-08 02:27:21 -04:00
ouwou
849ebf17f1 try to properly clear chat list
im sure this will break something somehow
2022-07-08 02:27:09 -04:00
ouwou
41776fbd02 add upload progress bar 2022-07-07 03:09:54 -04:00
ouwou
5c7631e713 fix checks for is bot 2022-07-05 03:53:00 -04:00
ouwou
41e2478a6f grab focus when adding attachment 2022-07-04 03:29:30 -04:00
ouwou
a9d35dcccd fix compile 2022-07-03 18:58:48 -04:00
ouwou
e87766f106 dnd'd files show preview images if displayable 2022-06-30 04:20:42 -04:00
ouwou
a038f47a25 add icon to attachments without preview 2022-06-23 01:51:30 -04:00
ouwou
d841a2c862 add change filename 2022-06-23 00:48:00 -04:00
ouwou
4ee7025ab0 add file upload via dnd + rework http 2022-06-17 02:46:55 -04:00
ouwou
d0fa308f6e preserve attachment insertion order 2022-06-16 01:09:54 -04:00
ouwou
4456c8771d refactor send message params into one struct 2022-06-14 02:36:04 -04:00
ouwou
caa551a469 Merge branch 'master' into attachments 2022-06-14 02:23:22 -04:00
ouwou
ccf5afbba9 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-06-14 02:16:03 -04:00
ouwou
2474ffc2ba hoisted_role can be missing from guild member list update 2022-06-14 02:15:08 -04:00
ouwou
5cf4d8e160 dont use tmpnam because its stupid 2022-06-10 01:28:27 -04:00
ouwou
49ff9a249e remove temp attachment files when theyre actually done being uploaded 2022-06-09 01:48:24 -04:00
betty "reenii" bessa
abc448eca0 readme: Simplify submodule install process. (#81)
* readme: Simplify submodule install process.

Not only the linux version did not include any instructions on getting submodules (thus, attempting to follow the guide line-by-line would cause in a unavoiable cmake configure halt before even building), But the usage of `submodule init/update` in it's own can be simplified by using the `--recursive` flag directly in the clone instead.

* Only target "submodules" folder for cloning
2022-06-06 19:07:59 +00:00
ouwou
d7177cac97 remove temporary image file when attachment removed 2022-06-06 03:59:20 -04:00
ouwou
c6182e8923 only save temporary image when theres room in container 2022-06-06 03:55:25 -04:00
ouwou
270730d9b3 start attachments (image paste and upload) 2022-06-05 21:41:57 -04:00
ouwou
4ec5c1dfcc try to fix multiple attachment handling 2022-06-05 01:53:11 -04:00
Adrian Schollmeyer
da27c67e6b Remove zipped resource archive in repo (#74)
* res: Remove zipped archive

This was probably added by accident.

* gitignore: Ignore archive files

Archives don't belong in the repo, so make sure they don't get
accidentally added again.
2022-05-23 16:55:27 +00:00
42 changed files with 1440 additions and 198 deletions

View File

@@ -73,6 +73,18 @@ jobs:
cp -r res/css res/res res/fonts build/artifactdir/bin
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
cp /usr/bin/msys-ffi-8.dll build/artifactdir/bin/libffi-8.dll
mkdir -p build/artifactdir/share/icons/Adwaita
cd build/artifactdir/share/icons/Adwaita
mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions
cd ../../../../../
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/16x16/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/24x24/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/32x32/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/48x48/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/64x64/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/96x96/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/actions/%.svg build/artifactdir/share/icons/Adwaita/scalable/actions || :
- name: Upload build (1)
uses: haya14busa/action-cond@v1
@@ -106,6 +118,7 @@ jobs:
run: |
brew install gtkmm3
brew install nlohmann-json
brew install jpeg
- name: Build
uses: lukka/run-cmake@v3

6
.gitignore vendored
View File

@@ -356,3 +356,9 @@ build/
out/
fonts/fonts.conf
# To make sure no zipped resources are added to the repo
*.7z
*.zip
*.tar.*
*.rar

View File

@@ -33,6 +33,12 @@ if (WIN32)
add_compile_definitions(NOMINMAX)
endif ()
include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if (IS_BIG_ENDIAN)
add_compile_definitions(ABADDON_IS_BIG_ENDIAN)
endif ()
configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
file(GLOB_RECURSE ABADDON_SOURCES

View File

@@ -25,13 +25,13 @@ Current features:
* Thread support<sup>3</sup>
* Animated avatars, server icons, emojis (can be turned off)
1 - Abaddon tries its best to make Discord think it's a legitimate web client. Some of the things done to do this
1 - Abaddon tries its best (though is not perfect) to make Discord think it's a legitimate web client. Some of the things done to do this
include: using a browser user agent, sending the same IDENTIFY message that the official web client does, using API v9
endpoints in all cases, and not using endpoints the web client does not normally use. There are still a few smaller
inconsistencies, however. For example the web client sends lots of telemetry via the `/science` endpoint (uBlock origin
stops this) as well as in the headers of all requests. **In any case,** you should use an official client for joining
servers, sending new DMs, or managing your friends list if you are afraid of being caught in Discord's spam filters
(unlikely).
stops this) as well as in the headers of all requests.<br>
**See [here](#the-spam-filter)** for things you might want to avoid if you are worried about being caught in the spam filter.
2 - Unicode emojis are substituted manually as opposed to rendered by GTK on non-Windows platforms. This can be changed
with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to
@@ -52,6 +52,7 @@ the result of fundamental issues with Discord's thread implementation.
* mingw-w64-x86_64-curl
* mingw-w64-x86_64-zlib
* mingw-w64-x86_64-gtkmm3
* mingw-w64-x86_64-libhandy
2. `git clone --recurse-submodules="subprojects" https://github.com/uowuo/abaddon && cd abaddon`
3. `mkdir build && cd build`
4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..`
@@ -59,18 +60,17 @@ the result of fundamental issues with Discord's thread implementation.
#### Mac:
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `brew install gtkmm3 nlohmann-json`
3. `git submodule update --init subprojects`
4. `mkdir build && cd build`
5. `cmake ..`
6. `make`
1. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
2. `brew install gtkmm3 nlohmann-json libhandy`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
#### Linux:
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`,
and [nlohmann-json](https://github.com/nlohmann/json)
2. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
@@ -94,6 +94,16 @@ On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is
no `abaddon.ini` in the working directory
#### The Spam Filter
Discord likes disabling accounts/forcing them to reset their passwords if they think the user is a spam bot or potentially had their account compromised. While the official client still often gets users caught in the spam filter, third party clients tend to upset the spam filter more often. If you get caught by it, you can usually [appeal](https://support.discord.com/hc/en-us/requests/new?ticket_form_id=360000029731) it and get it restored. Here are some things you might want to do with the official client instead if you are particularly afraid of evoking the spam filter's wrath:
* Joining or leaving servers (usually main cause of getting caught)
* Frequently disconnecting and reconnecting
* Starting new DMs with people
* Managing your friends list
* Managing your user profile while connected to a third party client
#### Dependencies:
* [gtkmm](https://www.gtkmm.org/en/)
@@ -102,6 +112,7 @@ no `abaddon.ini` in the working directory
* [libcurl](https://curl.se/)
* [zlib](https://zlib.net/)
* [SQLite3](https://www.sqlite.org/index.html)
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
### TODO:

View File

@@ -56,3 +56,4 @@
/bin/libwinpthread-1.dll
/bin/libzstd.dll
/bin/zlib1.dll
../usr/bin/msys-2.0.dll

1
ci/used-icons.txt Normal file
View File

@@ -0,0 +1 @@
document-send-symbolic

View File

@@ -101,17 +101,38 @@
.message-input, .message-input textview, .message-input textview text {
background-color: #242424;
color: #adadad;
border-radius: 15px;
border-radius: 3px;
border: 1px solid transparent;
}
.message-input {
border: 1px solid #444444;
margin-right: 15px;
}
.message-input.replying {
border: 1px solid #026FB9;
}
.message-input {
.message-input.bad-input {
border: 1px solid #dd3300;
}
.message-input-browse-icon {
color: #b9bbbe;
margin-left: 5px;
margin-top: 11px;
}
/* i dont think theres a way to circumvent having to do this to adjust around the browse icon */
.message-input:not(.with-browser-icon) {
padding: 0px 0px 0px 5px;
}
.message-input.with-browse-icon {
padding: 0px 0px 0px 30px;
}
.members {
background-color: @background_color;
}
@@ -323,3 +344,19 @@
.channel-tab-switcher tab > button:hover {
background-color: alpha(#ff0000, 0.5);
}
.message-progress {
border: none;
margin-bottom: -8px;
}
.message-progress trough {
border: none;
background-color: transparent;
}
.message-progress progress {
border: none;
background-color: #dd3300;
margin-left: 1px;
}

Binary file not shown.

View File

@@ -11,6 +11,7 @@
#include "dialogs/setstatus.hpp"
#include "dialogs/friendpicker.hpp"
#include "dialogs/verificationgate.hpp"
#include "dialogs/textinput.hpp"
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
@@ -389,6 +390,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
for (const auto child : m_user_menu_roles_submenu->get_children())
delete child;
if (guild.has_value() && user.has_value()) {
const auto roles = user->GetSortedRoles();
m_user_menu_roles->set_visible(!roles.empty());
@@ -411,7 +413,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
if (me == id) {
m_user_menu_ban->set_visible(false);
m_user_menu_kick->set_visible(false);
m_user_menu_open_dm->set_visible(false);
m_user_menu_open_dm->set_sensitive(false);
} else {
const bool has_kick = m_discord.HasGuildPermission(me, guild_id, Permission::KICK_MEMBERS);
const bool has_ban = m_discord.HasGuildPermission(me, guild_id, Permission::BAN_MEMBERS);
@@ -419,7 +421,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
m_user_menu_kick->set_visible(has_kick && can_manage);
m_user_menu_ban->set_visible(has_ban && can_manage);
m_user_menu_open_dm->set_visible(true);
m_user_menu_open_dm->set_sensitive(m_discord.FindDM(id).has_value());
}
m_user_menu_remove_recipient->hide();
@@ -467,7 +469,7 @@ void Abaddon::SetupUserMenu() {
m_user_menu_ban = Gtk::manage(new Gtk::MenuItem("Ban"));
m_user_menu_kick = Gtk::manage(new Gtk::MenuItem("Kick"));
m_user_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Open DM"));
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Go to DM"));
m_user_menu_roles = Gtk::manage(new Gtk::MenuItem("Roles"));
m_user_menu_info = Gtk::manage(new Gtk::MenuItem("View Profile"));
m_user_menu_remove_recipient = Gtk::manage(new Gtk::MenuItem("Remove From Group"));
@@ -543,7 +545,7 @@ void Abaddon::LoadState() {
#ifdef WITH_LIBHANDY
m_main_window->GetChatWindow()->UseTabsState(state.Tabs);
#endif
ActionChannelOpened(state.ActiveChannel);
ActionChannelOpened(state.ActiveChannel, false);
} catch (const std::exception &e) {
printf("failed to load application state: %s\n", e.what());
}
@@ -578,18 +580,9 @@ void Abaddon::on_user_menu_copy_id() {
void Abaddon::on_user_menu_open_dm() {
const auto existing = m_discord.FindDM(m_shown_user_menu_id);
if (existing.has_value())
if (existing.has_value()) {
ActionChannelOpened(*existing);
else
m_discord.CreateDM(m_shown_user_menu_id, [this](DiscordError code, Snowflake channel_id) {
if (code == DiscordError::NONE) {
// give the gateway a little window to send CHANNEL_CREATE
auto cb = [this, channel_id] {
ActionChannelOpened(channel_id);
};
Glib::signal_timeout().connect_once(sigc::track_obj(cb, *this), 200);
}
});
}
}
void Abaddon::on_user_menu_remove_recipient() {
@@ -655,12 +648,17 @@ void Abaddon::ActionJoinGuildDialog() {
}
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
if (!id.IsValid()) {
m_discord.SetReferringChannel(Snowflake::Invalid);
return;
}
if (id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) {
m_discord.SetReferringChannel(Snowflake::Invalid);
m_main_window->UpdateChatActiveChannel(Snowflake::Invalid, false);
m_main_window->UpdateChatWindowContents();
return;
@@ -709,6 +707,7 @@ void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
}
m_main_window->UpdateMenus();
m_discord.SetReferringChannel(id);
}
void Abaddon::ActionChatLoadHistory(Snowflake id) {
@@ -743,17 +742,13 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
});
}
void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message) {
if (msg.substr(0, 7) == "/shrug " || msg == "/shrug")
msg = msg.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
void Abaddon::ActionChatInputSubmit(ChatSubmitParams data) {
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
if (!channel.IsValid()) return;
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, channel, Permission::VIEW_CHANNEL)) return;
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, data.ChannelID, Permission::VIEW_CHANNEL)) return;
if (referenced_message.IsValid())
m_discord.SendChatMessage(msg, channel, referenced_message);
else
m_discord.SendChatMessage(msg, channel);
m_discord.SendChatMessage(data, NOOP_CALLBACK);
}
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
@@ -862,6 +857,15 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) {
window->show();
}
std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) {
TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window);
const auto code = dlg.run();
if (code == Gtk::RESPONSE_OK)
return dlg.GetInput();
else
return {};
}
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
dlg.SetConfirmText(prompt);
@@ -898,11 +902,15 @@ int main(int argc, char **argv) {
char *systemLocale = std::setlocale(LC_ALL, "");
try {
std::locale::global(std::locale(systemLocale));
if (systemLocale != nullptr) {
std::locale::global(std::locale(systemLocale));
}
} catch (...) {
try {
std::locale::global(std::locale::classic());
std::setlocale(LC_ALL, systemLocale);
if (systemLocale != nullptr) {
std::setlocale(LC_ALL, systemLocale);
}
} catch (...) {}
}

View File

@@ -36,7 +36,7 @@ public:
void ActionSetToken();
void ActionJoinGuildDialog();
void ActionChannelOpened(Snowflake id, bool expand_to = true);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChatInputSubmit(ChatSubmitParams data);
void ActionChatLoadHistory(Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
void ActionInsertMention(Snowflake id);
@@ -51,6 +51,7 @@ public:
void ActionViewPins(Snowflake channel_id);
void ActionViewThreads(Snowflake channel_id);
std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
void ActionReloadCSS();

View File

@@ -911,10 +911,15 @@ void ChannelList::OnGuildSubmenuPopup() {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsGuildMuted(id))
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsGuildMuted(id))
m_menu_guild_toggle_mute.set_label("Unmute");
else
m_menu_guild_toggle_mute.set_label("Mute");
const auto guild = discord.GetGuild(id);
const auto self_id = discord.GetUserData().ID;
m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id));
}
void ChannelList::OnCategorySubmenuPopup() {

View File

@@ -1,11 +1,14 @@
#include "chatinput.hpp"
#include "abaddon.hpp"
#include "constants.hpp"
#include <filesystem>
ChatInput::ChatInput() {
ChatInputText::ChatInputText() {
get_style_context()->add_class("message-input");
set_propagate_natural_height(true);
set_min_content_height(20);
set_max_content_height(250);
set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
set_policy(Gtk::POLICY_EXTERNAL, Gtk::POLICY_AUTOMATIC);
// hack
auto cb = [this](GdkEventKey *e) -> bool {
@@ -20,22 +23,26 @@ ChatInput::ChatInput() {
add(m_textview);
}
void ChatInput::InsertText(const Glib::ustring &text) {
void ChatInputText::InsertText(const Glib::ustring &text) {
GetBuffer()->insert_at_cursor(text);
m_textview.grab_focus();
}
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
Glib::RefPtr<Gtk::TextBuffer> ChatInputText::GetBuffer() {
return m_textview.get_buffer();
}
// this isnt connected directly so that the chat window can handle stuff like the completer first
bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
bool ChatInputText::ProcessKeyPress(GdkEventKey *event) {
if (event->keyval == GDK_KEY_Escape) {
m_signal_escape.emit();
return true;
}
if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) {
return CheckHandleClipboardPaste();
}
if (event->keyval == GDK_KEY_Return) {
if (event->state & GDK_SHIFT_MASK)
return false;
@@ -53,10 +60,514 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
return false;
}
void ChatInput::on_grab_focus() {
void ChatInputText::on_grab_focus() {
m_textview.grab_focus();
}
bool ChatInputText::CheckHandleClipboardPaste() {
auto clip = Gtk::Clipboard::get();
if (!clip->wait_is_image_available()) return false;
const auto pb = clip->wait_for_image();
if (pb) {
m_signal_image_paste.emit(pb);
return true;
} else {
return false;
}
}
ChatInputText::type_signal_submit ChatInputText::signal_submit() {
return m_signal_submit;
}
ChatInputText::type_signal_escape ChatInputText::signal_escape() {
return m_signal_escape;
}
ChatInputText::type_signal_image_paste ChatInputText::signal_image_paste() {
return m_signal_image_paste;
}
ChatInputTextContainer::ChatInputTextContainer() {
// triple hack !!!
auto cb = [this](GdkEventKey *e) -> bool {
return event(reinterpret_cast<GdkEvent *>(e));
};
m_input.signal_key_press_event().connect(cb, false);
m_upload_img.property_icon_name() = "document-send-symbolic";
m_upload_img.property_icon_size() = Gtk::ICON_SIZE_LARGE_TOOLBAR;
m_upload_img.get_style_context()->add_class("message-input-browse-icon");
AddPointerCursor(m_upload_ev);
m_upload_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->button == GDK_BUTTON_PRIMARY) {
ShowFileChooser();
// return focus
m_input.grab_focus();
return true;
}
return false;
});
m_upload_ev.add(m_upload_img);
add_overlay(m_upload_ev);
add(m_input);
show_all_children();
// stop the overlay from using (start) padding
signal_get_child_position().connect(sigc::mem_fun(*this, &ChatInputTextContainer::GetChildPosition), false);
}
void ChatInputTextContainer::ShowFileChooser() {
auto dlg = Gtk::FileChooserNative::create("Choose file", Gtk::FILE_CHOOSER_ACTION_OPEN);
dlg->set_select_multiple(true);
dlg->set_modal(true);
dlg->signal_response().connect([this, dlg](int response) {
if (response == Gtk::RESPONSE_ACCEPT) {
for (const auto &file : dlg->get_files()) {
m_signal_add_attachment.emit(file);
}
}
});
auto filter_all = Gtk::FileFilter::create();
filter_all->set_name("All files (*.*)");
filter_all->add_pattern("*.*");
dlg->add_filter(filter_all);
dlg->run();
}
ChatInputText &ChatInputTextContainer::Get() {
return m_input;
}
void ChatInputTextContainer::ShowChooserIcon() {
m_upload_ev.show();
}
void ChatInputTextContainer::HideChooserIcon() {
m_upload_ev.hide();
}
bool ChatInputTextContainer::GetChildPosition(Gtk::Widget *child, Gdk::Rectangle &pos) {
Gtk::Allocation main_alloc;
{
auto *grandchild = m_input.get_child();
int x, y;
if (grandchild->translate_coordinates(m_input, 0, 0, x, y)) {
main_alloc.set_x(x);
main_alloc.set_y(y);
} else {
main_alloc.set_x(0);
main_alloc.set_y(0);
}
main_alloc.set_width(grandchild->get_allocated_width());
main_alloc.set_height(grandchild->get_allocated_height());
}
Gtk::Requisition min, req;
child->get_preferred_size(min, req);
// let css move it around
pos.set_x(0);
pos.set_y(0);
pos.set_width(std::max(min.width, std::min(main_alloc.get_width(), req.width)));
pos.set_height(std::max(min.height, std::min(main_alloc.get_height(), req.height)));
return true;
}
ChatInputTextContainer::type_signal_add_attachment ChatInputTextContainer::signal_add_attachment() {
return m_signal_add_attachment;
}
ChatInputAttachmentContainer::ChatInputAttachmentContainer()
: m_box(Gtk::ORIENTATION_HORIZONTAL) {
get_style_context()->add_class("attachment-container");
m_box.set_halign(Gtk::ALIGN_START);
add(m_box);
m_box.show();
set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER);
set_vexpand(true);
set_size_request(-1, AttachmentItemSize + 10);
}
void ChatInputAttachmentContainer::Clear() {
for (auto *item : m_attachments) {
item->RemoveIfTemp();
delete item;
}
m_attachments.clear();
}
void ChatInputAttachmentContainer::ClearNoPurge() {
for (auto *item : m_attachments) {
delete item;
}
m_attachments.clear();
}
bool ChatInputAttachmentContainer::AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (m_attachments.size() == 10) return false;
static unsigned go_up = 0;
std::string dest_name = "pasted-image-" + std::to_string(go_up++);
const auto path = (std::filesystem::temp_directory_path() / "abaddon-cache" / dest_name).string();
try {
pb->save(path, "png");
} catch (...) {
fprintf(stderr, "pasted image save error\n");
return false;
}
auto *item = Gtk::make_managed<ChatInputAttachmentItem>(Gio::File::create_for_path(path), pb);
item->set_valign(Gtk::ALIGN_FILL);
item->set_vexpand(true);
item->set_margin_bottom(5);
item->show();
m_box.add(*item);
m_attachments.push_back(item);
item->signal_item_removed().connect([this, item] {
item->RemoveIfTemp();
if (auto it = std::find(m_attachments.begin(), m_attachments.end(), item); it != m_attachments.end())
m_attachments.erase(it);
delete item;
if (m_attachments.empty())
m_signal_emptied.emit();
});
return true;
}
bool ChatInputAttachmentContainer::AddFile(const Glib::RefPtr<Gio::File> &file, Glib::RefPtr<Gdk::Pixbuf> pb) {
if (m_attachments.size() == 10) return false;
ChatInputAttachmentItem *item;
if (pb)
item = Gtk::make_managed<ChatInputAttachmentItem>(file, pb, true);
else
item = Gtk::make_managed<ChatInputAttachmentItem>(file);
item->set_valign(Gtk::ALIGN_FILL);
item->set_vexpand(true);
item->set_margin_bottom(5);
item->show();
m_box.add(*item);
m_attachments.push_back(item);
item->signal_item_removed().connect([this, item] {
if (auto it = std::find(m_attachments.begin(), m_attachments.end(), item); it != m_attachments.end())
m_attachments.erase(it);
delete item;
if (m_attachments.empty())
m_signal_emptied.emit();
});
return true;
}
std::vector<ChatSubmitParams::Attachment> ChatInputAttachmentContainer::GetAttachments() const {
std::vector<ChatSubmitParams::Attachment> ret;
for (auto *x : m_attachments) {
if (!x->GetFile()->query_exists())
puts("bad!");
ret.push_back({ x->GetFile(), x->GetType(), x->GetFilename() });
}
return ret;
}
ChatInputAttachmentContainer::type_signal_emptied ChatInputAttachmentContainer::signal_emptied() {
return m_signal_emptied;
}
ChatInputAttachmentItem::ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file)
: m_file(file)
, m_img(Gtk::make_managed<Gtk::Image>())
, m_type(ChatSubmitParams::ExtantFile)
, m_box(Gtk::ORIENTATION_VERTICAL) {
get_style_context()->add_class("attachment-item");
set_size_request(AttachmentItemSize, AttachmentItemSize);
set_halign(Gtk::ALIGN_START);
m_box.set_hexpand(true);
m_box.set_vexpand(true);
m_box.set_halign(Gtk::ALIGN_FILL);
m_box.set_valign(Gtk::ALIGN_FILL);
m_box.add(*m_img);
m_box.add(m_label);
add(m_box);
show_all_children();
m_label.set_valign(Gtk::ALIGN_END);
m_label.set_max_width_chars(0); // will constrain to given size
m_label.set_ellipsize(Pango::ELLIPSIZE_MIDDLE);
m_label.set_margin_start(7);
m_label.set_margin_end(7);
m_img->set_vexpand(true);
m_img->property_icon_name() = "document-send-symbolic";
m_img->property_icon_size() = Gtk::ICON_SIZE_DIALOG; // todo figure out how to not use this weird property??? i dont know how icons work (screw your theme)
SetFilenameFromFile();
SetupMenu();
UpdateTooltip();
}
ChatInputAttachmentItem::ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file, const Glib::RefPtr<Gdk::Pixbuf> &pb, bool is_extant)
: m_file(file)
, m_img(Gtk::make_managed<Gtk::Image>())
, m_type(is_extant ? ChatSubmitParams::ExtantFile : ChatSubmitParams::PastedImage)
, m_filename("unknown.png")
, m_label("unknown.png")
, m_box(Gtk::ORIENTATION_VERTICAL) {
get_style_context()->add_class("attachment-item");
int outw, outh;
GetImageDimensions(pb->get_width(), pb->get_height(), outw, outh, AttachmentItemSize, AttachmentItemSize);
m_img->property_pixbuf() = pb->scale_simple(outw, outh, Gdk::INTERP_BILINEAR);
set_size_request(AttachmentItemSize, AttachmentItemSize);
set_halign(Gtk::ALIGN_START);
m_box.set_hexpand(true);
m_box.set_vexpand(true);
m_box.set_halign(Gtk::ALIGN_FILL);
m_box.set_valign(Gtk::ALIGN_FILL);
m_box.add(*m_img);
m_box.add(m_label);
add(m_box);
show_all_children();
m_label.set_valign(Gtk::ALIGN_END);
m_label.set_max_width_chars(0); // will constrain to given size
m_label.set_ellipsize(Pango::ELLIPSIZE_MIDDLE);
m_label.set_margin_start(7);
m_label.set_margin_end(7);
m_img->set_vexpand(true);
if (is_extant)
SetFilenameFromFile();
SetupMenu();
UpdateTooltip();
}
Glib::RefPtr<Gio::File> ChatInputAttachmentItem::GetFile() const {
return m_file;
}
ChatSubmitParams::AttachmentType ChatInputAttachmentItem::GetType() const {
return m_type;
}
std::string ChatInputAttachmentItem::GetFilename() const {
return m_filename;
}
bool ChatInputAttachmentItem::IsTemp() const noexcept {
return m_type == ChatSubmitParams::PastedImage;
}
void ChatInputAttachmentItem::RemoveIfTemp() {
if (IsTemp())
m_file->remove();
}
void ChatInputAttachmentItem::SetFilenameFromFile() {
auto info = m_file->query_info(G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
m_filename = info->get_attribute_string(G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
m_label.set_text(m_filename);
}
void ChatInputAttachmentItem::SetupMenu() {
m_menu_remove.set_label("Remove");
m_menu_remove.signal_activate().connect([this] {
m_signal_item_removed.emit();
});
m_menu_set_filename.set_label("Change Filename");
m_menu_set_filename.signal_activate().connect([this] {
const auto name = Abaddon::Get().ShowTextPrompt("Enter new filename for attachment", "Enter filename", m_filename);
if (name.has_value()) {
m_filename = *name;
m_label.set_text(m_filename);
UpdateTooltip();
}
});
m_menu.add(m_menu_set_filename);
m_menu.add(m_menu_remove);
m_menu.show_all();
signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->button == GDK_BUTTON_SECONDARY) {
m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
return true;
}
return false;
});
}
void ChatInputAttachmentItem::UpdateTooltip() {
set_tooltip_text(m_filename);
}
ChatInputAttachmentItem::type_signal_item_removed ChatInputAttachmentItem::signal_item_removed() {
return m_signal_item_removed;
}
ChatInput::ChatInput()
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
m_input.signal_add_attachment().connect(sigc::mem_fun(*this, &ChatInput::AddAttachment));
m_input.Get().signal_escape().connect([this] {
m_attachments.Clear();
m_attachments_revealer.set_reveal_child(false);
m_signal_escape.emit();
});
m_input.Get().signal_submit().connect([this](const Glib::ustring &input) -> bool {
ChatSubmitParams data;
data.Message = input;
data.Attachments = m_attachments.GetAttachments();
bool b = m_signal_submit.emit(data);
if (b) {
m_attachments_revealer.set_reveal_child(false);
m_attachments.ClearNoPurge();
}
return b;
});
m_attachments.set_vexpand(false);
m_attachments_revealer.set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP);
m_attachments_revealer.add(m_attachments);
add(m_attachments_revealer);
add(m_input);
show_all_children();
m_input.Get().signal_image_paste().connect([this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (CanAttachFiles() && m_attachments.AddImage(pb))
m_attachments_revealer.set_reveal_child(true);
});
// double hack !
auto cb = [this](GdkEventKey *e) -> bool {
return event(reinterpret_cast<GdkEvent *>(e));
};
m_input.signal_key_press_event().connect(cb, false);
m_attachments.signal_emptied().connect([this] {
m_attachments_revealer.set_reveal_child(false);
});
SetActiveChannel(Snowflake::Invalid);
}
void ChatInput::InsertText(const Glib::ustring &text) {
m_input.Get().InsertText(text);
}
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
return m_input.Get().GetBuffer();
}
bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
return m_input.Get().ProcessKeyPress(event);
}
void ChatInput::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
if (!CanAttachFiles()) return;
std::string content_type;
try {
const auto info = file->query_info(G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
content_type = info->get_attribute_string(G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
} catch (const Gio::Error &err) {
printf("io error: %s\n", err.what().c_str());
return;
} catch (...) {
puts("attachment query exception");
return;
}
static const std::unordered_set<std::string> image_exts {
".png",
".jpg",
};
if (image_exts.find(content_type) != image_exts.end()) {
if (AddFileAsImageAttachment(file)) {
m_attachments_revealer.set_reveal_child(true);
m_input.Get().grab_focus();
}
} else if (m_attachments.AddFile(file)) {
m_attachments_revealer.set_reveal_child(true);
m_input.Get().grab_focus();
}
}
void ChatInput::IndicateTooLarge() {
m_input.Get().get_style_context()->add_class("bad-input");
const auto cb = [this] {
m_input.Get().get_style_context()->remove_class("bad-input");
};
Glib::signal_timeout().connect_seconds_once(sigc::track_obj(cb, *this), 2);
}
void ChatInput::SetActiveChannel(Snowflake id) {
m_active_channel = id;
if (CanAttachFiles()) {
m_input.Get().get_style_context()->add_class("with-browse-icon");
m_input.ShowChooserIcon();
} else {
m_input.Get().get_style_context()->remove_class("with-browse-icon");
m_input.HideChooserIcon();
}
}
void ChatInput::StartReplying() {
m_input.Get().grab_focus();
m_input.Get().get_style_context()->add_class("replying");
}
void ChatInput::StopReplying() {
m_input.Get().get_style_context()->remove_class("replying");
}
bool ChatInput::AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file) {
try {
const auto read_stream = file->read();
if (!read_stream) return false;
const auto pb = Gdk::Pixbuf::create_from_stream(read_stream);
return m_attachments.AddFile(file, pb);
} catch (...) {
return m_attachments.AddFile(file);
}
}
bool ChatInput::CanAttachFiles() {
return Abaddon::Get().GetDiscordClient().HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES | Permission::SEND_MESSAGES);
}
ChatInput::type_signal_submit ChatInput::signal_submit() {
return m_signal_submit;
}

View File

@@ -1,9 +1,72 @@
#pragma once
#include <gtkmm.h>
#include "discord/chatsubmitparams.hpp"
#include "discord/permissions.hpp"
class ChatInput : public Gtk::ScrolledWindow {
class ChatInputAttachmentItem : public Gtk::EventBox {
public:
ChatInput();
ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file);
ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file, const Glib::RefPtr<Gdk::Pixbuf> &pb, bool is_extant = false);
[[nodiscard]] Glib::RefPtr<Gio::File> GetFile() const;
[[nodiscard]] ChatSubmitParams::AttachmentType GetType() const;
[[nodiscard]] std::string GetFilename() const;
[[nodiscard]] bool IsTemp() const noexcept;
void RemoveIfTemp();
private:
void SetFilenameFromFile();
void SetupMenu();
void UpdateTooltip();
Gtk::Menu m_menu;
Gtk::MenuItem m_menu_remove;
Gtk::MenuItem m_menu_set_filename;
Gtk::Box m_box;
Gtk::Label m_label;
Gtk::Image *m_img = nullptr;
Glib::RefPtr<Gio::File> m_file;
ChatSubmitParams::AttachmentType m_type;
std::string m_filename;
private:
using type_signal_item_removed = sigc::signal<void>;
type_signal_item_removed m_signal_item_removed;
public:
type_signal_item_removed signal_item_removed();
};
class ChatInputAttachmentContainer : public Gtk::ScrolledWindow {
public:
ChatInputAttachmentContainer();
void Clear();
void ClearNoPurge();
bool AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb);
bool AddFile(const Glib::RefPtr<Gio::File> &file, Glib::RefPtr<Gdk::Pixbuf> pb = {});
[[nodiscard]] std::vector<ChatSubmitParams::Attachment> GetAttachments() const;
private:
std::vector<ChatInputAttachmentItem *> m_attachments;
Gtk::Box m_box;
private:
using type_signal_emptied = sigc::signal<void>;
type_signal_emptied m_signal_emptied;
public:
type_signal_emptied signal_emptied();
};
class ChatInputText : public Gtk::ScrolledWindow {
public:
ChatInputText();
void InsertText(const Glib::ustring &text);
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
@@ -15,9 +78,78 @@ protected:
private:
Gtk::TextView m_textview;
bool CheckHandleClipboardPaste();
public:
typedef sigc::signal<bool, Glib::ustring> type_signal_submit;
typedef sigc::signal<void> type_signal_escape;
using type_signal_submit = sigc::signal<bool, Glib::ustring>;
using type_signal_escape = sigc::signal<void>;
using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>>;
type_signal_submit signal_submit();
type_signal_escape signal_escape();
type_signal_image_paste signal_image_paste();
private:
type_signal_submit m_signal_submit;
type_signal_escape m_signal_escape;
type_signal_image_paste m_signal_image_paste;
};
// file upload, text
class ChatInputTextContainer : public Gtk::Overlay {
public:
ChatInputTextContainer();
// not proxying everythign lol!!
ChatInputText &Get();
void ShowChooserIcon();
void HideChooserIcon();
private:
void ShowFileChooser();
bool GetChildPosition(Gtk::Widget *child, Gdk::Rectangle &pos);
Gtk::EventBox m_upload_ev;
Gtk::Image m_upload_img;
ChatInputText m_input;
public:
using type_signal_add_attachment = sigc::signal<void, Glib::RefPtr<Gio::File>>;
type_signal_add_attachment signal_add_attachment();
private:
type_signal_add_attachment m_signal_add_attachment;
};
class ChatInput : public Gtk::Box {
public:
ChatInput();
void InsertText(const Glib::ustring &text);
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
bool ProcessKeyPress(GdkEventKey *event);
void AddAttachment(const Glib::RefPtr<Gio::File> &file);
void IndicateTooLarge();
void SetActiveChannel(Snowflake id);
void StartReplying();
void StopReplying();
private:
bool AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file);
bool CanAttachFiles();
Gtk::Revealer m_attachments_revealer;
ChatInputAttachmentContainer m_attachments;
ChatInputTextContainer m_input;
Snowflake m_active_channel;
public:
using type_signal_submit = sigc::signal<bool, ChatSubmitParams>;
using type_signal_escape = sigc::signal<void>;
type_signal_submit signal_submit();
type_signal_escape signal_escape();

View File

@@ -34,6 +34,9 @@ void ChatList::Clear() {
delete *it;
it++;
}
m_id_to_widget.clear();
m_num_messages = 0;
m_num_rows = 0;
}
void ChatList::SetActiveChannel(Snowflake id) {
@@ -352,10 +355,6 @@ ChatList::type_signal_action_message_edit ChatList::signal_action_message_edit()
return m_signal_action_message_edit;
}
ChatList::type_signal_action_chat_submit ChatList::signal_action_chat_submit() {
return m_signal_action_chat_submit;
}
ChatList::type_signal_action_chat_load_history ChatList::signal_action_chat_load_history() {
return m_signal_action_chat_load_history;
}

View File

@@ -63,7 +63,6 @@ private:
public:
// these are all forwarded by the parent
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
using type_signal_action_channel_click = sigc::signal<void, Snowflake>;
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
@@ -73,7 +72,6 @@ public:
using type_signal_action_reply_to = sigc::signal<void, Snowflake>;
type_signal_action_message_edit signal_action_message_edit();
type_signal_action_chat_submit signal_action_chat_submit();
type_signal_action_chat_load_history signal_action_chat_load_history();
type_signal_action_channel_click signal_action_channel_click();
type_signal_action_insert_mention signal_action_insert_mention();
@@ -84,7 +82,6 @@ public:
private:
type_signal_action_message_edit m_signal_action_message_edit;
type_signal_action_chat_submit m_signal_action_chat_submit;
type_signal_action_chat_load_history m_signal_action_chat_load_history;
type_signal_action_channel_click m_signal_action_channel_click;
type_signal_action_insert_mention m_signal_action_insert_mention;

View File

@@ -32,7 +32,6 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
if (!data.Content.empty() || data.Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(data);
container->AttachEventHandlers(*container->m_text_component);
container->m_main.add(*container->m_text_component);
}
@@ -101,7 +100,6 @@ void ChatMessageItemContainer::UpdateContent() {
if (!data->Embeds.empty()) {
m_embed_component = CreateEmbedsComponent(data->Embeds);
AttachEventHandlers(*m_embed_component);
m_main.add(*m_embed_component);
m_embed_component->show_all();
}
@@ -155,9 +153,9 @@ void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, const std::s
widget->signal_button_press_event().connect([url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url);
return false;
return true;
}
return true;
return false;
}, false);
// clang-format on
}
@@ -174,6 +172,8 @@ Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data
tv->set_halign(Gtk::ALIGN_FILL);
tv->set_hexpand(true);
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnTextViewButtonPress), false);
UpdateTextComponent(tv);
return tv;
@@ -281,8 +281,6 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
tag->property_weight() = Pango::WEIGHT_BOLD;
m_channel_tagmap[tag] = *data->MessageReference->ChannelID;
b->insert_with_tag(iter, data->Content, tag);
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
} else {
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>");
}
@@ -297,12 +295,10 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedsComponent(const std::vector<E
if (IsEmbedImageOnly(embed)) {
auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
widget->show();
AttachEventHandlers(*widget);
box->add(*widget);
} else {
auto *widget = CreateEmbedComponent(embed);
widget->show();
AttachEventHandlers(*widget);
box->add(*widget);
}
}
@@ -493,12 +489,22 @@ Gtk::Widget *ChatMessageItemContainer::CreateImageComponent(const std::string &p
Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox);
Gtk::Image *widget = Gtk::manage(new LazyImage(proxy_url, w, h, false));
ev->add(*widget);
ev->set_halign(Gtk::ALIGN_START);
widget->set_halign(Gtk::ALIGN_START);
widget->set_size_request(w, h);
AttachEventHandlers(*ev);
AddClickHandler(ev, url);
const auto on_button_press_event = [this, url](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
m_selected_link = url;
m_link_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(e));
return true;
}
return false;
};
ev->signal_button_press_event().connect(on_button_press_event, false);
return ev;
}
@@ -510,9 +516,18 @@ Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const Attachmen
ev->get_style_context()->add_class("message-attachment-box");
ev->add(*btn);
AttachEventHandlers(*ev);
AddClickHandler(ev, data.URL);
const auto on_button_press_event = [this, url = data.URL](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
m_selected_link = url;
m_link_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(e));
return true;
}
return false;
};
ev->signal_button_press_event().connect(on_button_press_event, false);
return ev;
}
@@ -534,7 +549,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector
box->show();
AttachEventHandlers(*box);
return box;
}
@@ -956,7 +970,6 @@ void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::Tex
}
void ChatMessageItemContainer::HandleChannelMentions(Gtk::TextView *tv) {
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
HandleChannelMentions(tv->get_buffer());
}
@@ -990,6 +1003,20 @@ bool ChatMessageItemContainer::OnClickChannel(GdkEventButton *ev) {
return false;
}
bool ChatMessageItemContainer::OnTextViewButtonPress(GdkEventButton *ev) {
// run all button press handlers and propagate if none return true
if (OnLinkClick(ev)) return true;
if (OnClickChannel(ev)) return true;
if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) {
// send the event upward skipping TextView's handler because we dont want it
gtk_propagate_event(GTK_WIDGET(m_main.gobj()), reinterpret_cast<GdkEvent *>(ev));
return true;
}
return false;
}
void ChatMessageItemContainer::on_link_menu_copy() {
Gtk::Clipboard::get()->set_text(m_selected_link);
}
@@ -997,8 +1024,6 @@ void ChatMessageItemContainer::on_link_menu_copy() {
void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) {
const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)");
tv.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnLinkClick), false);
auto buf = tv.get_buffer();
Glib::ustring text = GetText(buf);
@@ -1070,18 +1095,6 @@ ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemCont
return m_signal_action_reaction_remove;
}
void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
const auto on_button_press_event = [this](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
event(reinterpret_cast<GdkEvent *>(e)); // illegal ooooooh
return true;
}
return false;
};
widget.signal_button_press_event().connect(on_button_press_event, false);
}
ChatMessageHeader::ChatMessageHeader(const Message &data)
: m_main_box(Gtk::ORIENTATION_HORIZONTAL)
, m_content_box(Gtk::ORIENTATION_VERTICAL)
@@ -1122,7 +1135,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
m_meta_ev.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press));
if (author->IsBot || data.WebhookID.has_value()) {
if (author->IsABot() || data.WebhookID.has_value()) {
m_extra = Gtk::manage(new Gtk::Label);
m_extra->get_style_context()->add_class("message-container-extra");
m_extra->set_single_line_mode(true);
@@ -1130,7 +1143,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
m_extra->set_can_focus(false);
m_extra->set_use_markup(true);
}
if (author->IsBot)
if (author->IsABot())
m_extra->set_markup("<b>BOT</b>");
else if (data.WebhookID.has_value())
m_extra->set_markup("<b>Webhook</b>");

View File

@@ -2,7 +2,7 @@
#include <gtkmm.h>
#include "discord/discord.hpp"
class ChatMessageItemContainer : public Gtk::Box {
class ChatMessageItemContainer : public Gtk::EventBox {
public:
Snowflake ID;
Snowflake ChannelID;
@@ -44,6 +44,7 @@ protected:
void HandleChannelMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf);
void HandleChannelMentions(Gtk::TextView *tv);
bool OnClickChannel(GdkEventButton *ev);
bool OnTextViewButtonPress(GdkEventButton *ev);
// reused for images and links
Gtk::Menu m_link_menu;
@@ -57,8 +58,6 @@ protected:
std::map<Glib::RefPtr<Gtk::TextTag>, std::string> m_link_tagmap;
std::map<Glib::RefPtr<Gtk::TextTag>, Snowflake> m_channel_tagmap;
void AttachEventHandlers(Gtk::Widget &widget);
Gtk::EventBox *_ev;
Gtk::Box m_main;
Gtk::Label *m_attrib_label = nullptr;

View File

@@ -4,12 +4,14 @@
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
#include "chatlist.hpp"
#include "constants.hpp"
#ifdef WITH_LIBHANDY
#include "channeltabswitcherhandy.hpp"
#endif
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_chat = Gtk::manage(new ChatList);
@@ -45,6 +47,8 @@ ChatWindow::ChatWindow() {
m_topic_text.set_halign(Gtk::ALIGN_START);
m_topic_text.show();
m_input->set_valign(Gtk::ALIGN_END);
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() {
if (m_is_replying)
@@ -54,11 +58,11 @@ ChatWindow::ChatWindow() {
m_input->show();
m_completer.SetBuffer(m_input->GetBuffer());
m_completer.SetGetChannelID([this]() -> auto {
m_completer.SetGetChannelID([this]() {
return m_active_channel;
});
m_completer.SetGetRecentAuthors([this]() -> auto {
m_completer.SetGetRecentAuthors([this]() {
return m_chat->GetRecentAuthors();
});
@@ -70,9 +74,6 @@ ChatWindow::ChatWindow() {
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
m_signal_action_chat_load_history.emit(id);
});
m_chat->signal_action_chat_submit().connect([this](const std::string &str, Snowflake channel_id, Snowflake referenced_id) {
m_signal_action_chat_submit.emit(str, channel_id, referenced_id);
});
m_chat->signal_action_insert_mention().connect([this](Snowflake id) {
// lowkey gross
m_signal_action_insert_mention.emit(id);
@@ -107,6 +108,10 @@ ChatWindow::ChatWindow() {
m_main->add(m_completer);
m_main->add(*m_input);
m_main->add(*m_meta);
m_main->add(m_progress);
m_progress.show();
m_main->show();
}
@@ -125,6 +130,7 @@ void ChatWindow::SetMessages(const std::vector<Message> &msgs) {
void ChatWindow::SetActiveChannel(Snowflake id) {
m_active_channel = id;
m_chat->SetActiveChannel(id);
m_input->SetActiveChannel(id);
m_input_indicator->SetActiveChannel(id);
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
@@ -168,6 +174,10 @@ void ChatWindow::SetTopic(const std::string &text) {
m_topic.set_visible(text.length() > 0);
}
void ChatWindow::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
m_input->AddAttachment(file);
}
#ifdef WITH_LIBHANDY
void ChatWindow::OpenNewTab(Snowflake id) {
// open if its the first tab (in which case it really isnt a tab but whatever)
@@ -210,15 +220,64 @@ Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
bool ChatWindow::OnInputSubmit(ChatSubmitParams data) {
auto &discord = Abaddon::Get().GetDiscordClient();
if (!discord.HasSelfChannelPermission(m_active_channel, Permission::SEND_MESSAGES)) return false;
if (!data.Attachments.empty() && !discord.HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES)) return false;
int nitro_restriction = BaseAttachmentSizeLimit;
const auto nitro = discord.GetUserData().PremiumType;
if (!nitro.has_value() || nitro == EPremiumType::None) {
nitro_restriction = BaseAttachmentSizeLimit;
} else if (nitro == EPremiumType::NitroClassic) {
nitro_restriction = NitroClassicAttachmentSizeLimit;
} else if (nitro == EPremiumType::Nitro) {
nitro_restriction = NitroAttachmentSizeLimit;
}
int guild_restriction = BaseAttachmentSizeLimit;
if (const auto channel = discord.GetChannel(m_active_channel); channel.has_value() && channel->GuildID.has_value()) {
if (const auto guild = discord.GetGuild(*channel->GuildID); guild.has_value()) {
if (!guild->PremiumTier.has_value() || guild->PremiumTier == GuildPremiumTier::NONE || guild->PremiumTier == GuildPremiumTier::TIER_1) {
guild_restriction = BaseAttachmentSizeLimit;
} else if (guild->PremiumTier == GuildPremiumTier::TIER_2) {
guild_restriction = BoostLevel2AttachmentSizeLimit;
} else if (guild->PremiumTier == GuildPremiumTier::TIER_3) {
guild_restriction = BoostLevel3AttachmentSizeLimit;
}
}
}
int restriction = std::max(nitro_restriction, guild_restriction);
goffset total_size = 0;
for (const auto &attachment : data.Attachments) {
const auto info = attachment.File->query_info();
if (info) {
const auto size = info->get_size();
if (size > restriction) {
m_input->IndicateTooLarge();
return false;
}
total_size += size;
if (total_size > MaxMessagePayloadSize) {
m_input->IndicateTooLarge();
return false;
}
}
}
if (!m_rate_limit_indicator->CanSpeak())
return false;
if (text.empty())
if (data.Message.empty() && data.Attachments.empty())
return false;
data.ChannelID = m_active_channel;
data.InReplyToID = m_replying_to;
if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // 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();
@@ -241,8 +300,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
const auto author = discord.GetUser(message.Author.ID);
m_replying_to = message_id;
m_is_replying = true;
m_input->grab_focus();
m_input->get_style_context()->add_class("replying");
m_input->StartReplying();
if (author.has_value())
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>());
else
@@ -252,7 +310,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
void ChatWindow::StopReplying() {
m_is_replying = false;
m_replying_to = Snowflake::Invalid;
m_input->get_style_context()->remove_class("replying");
m_input->StopReplying();
m_input_indicator->ClearCustom();
}

View File

@@ -3,8 +3,10 @@
#include <string>
#include <set>
#include "discord/discord.hpp"
#include "discord/chatsubmitparams.hpp"
#include "completer.hpp"
#include "state.hpp"
#include "progressbar.hpp"
#ifdef WITH_LIBHANDY
class ChannelTabSwitcherHandy;
@@ -34,6 +36,7 @@ public:
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
void AddAttachment(const Glib::RefPtr<Gio::File> &file);
#ifdef WITH_LIBHANDY
void OpenNewTab(Snowflake id);
@@ -55,7 +58,7 @@ protected:
Snowflake m_active_channel;
bool OnInputSubmit(const Glib::ustring &text);
bool OnInputSubmit(ChatSubmitParams data);
bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos);
@@ -77,6 +80,7 @@ protected:
ChatInputIndicator *m_input_indicator;
RateLimitIndicator *m_rate_limit_indicator;
Gtk::Box *m_meta;
MessageUploadProgressBar m_progress;
#ifdef WITH_LIBHANDY
ChannelTabSwitcherHandy *m_tab_switcher;
@@ -84,7 +88,7 @@ protected:
public:
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
using type_signal_action_chat_submit = sigc::signal<void, ChatSubmitParams>;
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
using type_signal_action_channel_click = sigc::signal<void, Snowflake, bool>;
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;

View File

@@ -0,0 +1,24 @@
#include "progressbar.hpp"
#include "abaddon.hpp"
MessageUploadProgressBar::MessageUploadProgressBar() {
get_style_context()->add_class("message-progress");
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_message_progress().connect([this](const std::string &nonce, float percent) {
if (nonce == m_last_nonce) {
set_fraction(percent);
}
});
discord.signal_message_send_fail().connect([this](const std::string &nonce, float) {
if (nonce == m_last_nonce)
set_fraction(0.0);
});
discord.signal_message_create().connect([this](const Message &msg) {
if (msg.IsPending) {
m_last_nonce = *msg.Nonce;
} else if (msg.Nonce.has_value() && (*msg.Nonce == m_last_nonce)) {
m_last_nonce = "";
set_fraction(0.0);
}
});
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include <gtkmm/progressbar.h>
#include <string>
class MessageUploadProgressBar : public Gtk::ProgressBar {
public:
MessageUploadProgressBar();
private:
std::string m_last_nonce;
};

View File

@@ -1,4 +1,12 @@
#pragma once
#include <cstdint>
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 AttachmentItemSize = 120;
constexpr static int BaseAttachmentSizeLimit = 8 * 1024 * 1024;
constexpr static int NitroClassicAttachmentSizeLimit = 50 * 1024 * 1024;
constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024;
constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;
constexpr static int BoostLevel3AttachmentSizeLimit = 100 * 1024 * 1024;
constexpr static int MaxMessagePayloadSize = 199 * 1024 * 1024;

26
src/dialogs/textinput.cpp Normal file
View File

@@ -0,0 +1,26 @@
#include "textinput.hpp"
TextInputDialog::TextInputDialog(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window &parent)
: Gtk::Dialog(title, parent, true)
, m_label(prompt) {
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
auto ok = add_button("OK", Gtk::RESPONSE_OK);
auto cancel = add_button("Cancel", Gtk::RESPONSE_CANCEL);
get_content_area()->add(m_label);
get_content_area()->add(m_entry);
m_entry.set_text(placeholder);
m_entry.set_activates_default(true);
ok->set_can_default(true);
ok->grab_default();
show_all_children();
}
Glib::ustring TextInputDialog::GetInput() const {
return m_entry.get_text();
}

14
src/dialogs/textinput.hpp Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <gtkmm/dialog.h>
#include <gtkmm/entry.h>
class TextInputDialog : public Gtk::Dialog {
public:
TextInputDialog(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window &parent);
Glib::ustring GetInput() const;
private:
Gtk::Label m_label;
Gtk::Entry m_entry;
};

View File

@@ -0,0 +1,24 @@
#pragma once
#include <vector>
#include <string>
#include <glibmm/ustring.h>
#include <giomm/file.h>
#include "discord/snowflake.hpp"
struct ChatSubmitParams {
enum AttachmentType {
PastedImage,
ExtantFile,
};
struct Attachment {
Glib::RefPtr<Gio::File> File;
AttachmentType Type;
std::string Filename;
};
Snowflake ChannelID;
Snowflake InReplyToID;
Glib::ustring Message;
std::vector<Attachment> Attachments;
};

View File

@@ -30,6 +30,7 @@ void DiscordClient::Start() {
if (m_client_started) return;
m_http.SetBase(GetAPIURL());
SetHeaders();
std::memset(&m_zstream, 0, sizeof(m_zstream));
inflateInit2(&m_zstream, MAX_WBITS + 32);
@@ -318,6 +319,10 @@ bool DiscordClient::HasGuildPermission(Snowflake user_id, Snowflake guild_id, Pe
return (base & perm) == perm;
}
bool DiscordClient::HasSelfChannelPermission(Snowflake channel_id, Permission perm) const {
return HasChannelPermission(m_user_data.ID, channel_id, perm);
}
bool DiscordClient::HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const {
const auto channel = m_store.GetChannel(channel_id);
if (!channel.has_value() || !channel->GuildID.has_value()) return false;
@@ -409,7 +414,7 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
return actor_highest->Position > target_highest->Position;
}
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response) {
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError)> &callback) {
if (!CheckCode(response)) {
if (response.status_code == http::TooManyRequests) {
try { // not sure if this body is guaranteed
@@ -421,54 +426,108 @@ void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::re
} else {
m_signal_message_send_fail.emit(nonce, 0);
}
// todo actually callback with correct error code (not necessary rn)
callback(DiscordError::GENERIC);
} else {
callback(DiscordError::NONE);
}
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
// @([^@#]{1,32})#(\\d{4})
void DiscordClient::SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
const auto nonce = std::to_string(Snowflake::FromNow());
CreateMessageObject obj;
obj.Content = content;
obj.Content = params.Message;
obj.Nonce = nonce;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
// dummy data so the content can be shown while waiting for MESSAGE_CREATE
if (params.InReplyToID.IsValid())
obj.MessageReference.emplace().MessageID = params.InReplyToID;
m_http.MakePOST("/channels/" + std::to_string(params.ChannelID) + "/messages",
nlohmann::json(obj).dump(),
[this, nonce, callback](const http::response_type &r) {
ChatMessageCallback(nonce, r, callback);
});
// dummy preview data
Message tmp;
tmp.Content = content;
tmp.Content = params.Message;
tmp.ID = nonce;
tmp.ChannelID = channel;
tmp.ChannelID = params.ChannelID;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
tmp.Nonce = obj.Nonce;
tmp.Nonce = nonce;
tmp.IsPending = true;
m_store.SetMessage(tmp.ID, tmp);
m_signal_message_sent.emit(tmp);
m_signal_message_create.emit(tmp);
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
void DiscordClient::SendChatMessageAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
const auto nonce = std::to_string(Snowflake::FromNow());
CreateMessageObject obj;
obj.Content = content;
obj.Content = params.Message;
obj.Nonce = nonce;
obj.MessageReference.emplace().MessageID = referenced_message;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
if (params.InReplyToID.IsValid())
obj.MessageReference.emplace().MessageID = params.InReplyToID;
auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(params.ChannelID) + "/messages");
m_progress_cb_timer.start();
req.set_progress_callback([this, nonce](curl_off_t ultotal, curl_off_t ulnow) {
if (m_progress_cb_timer.elapsed() < 0.0417) return; // try to prevent it from blocking ui
m_progress_cb_timer.start();
m_generic_mutex.lock();
m_generic_queue.push([this, nonce, ultotal, ulnow] {
m_signal_message_progress.emit(
nonce,
static_cast<float>(ulnow) / static_cast<float>(ultotal));
});
m_generic_mutex.unlock();
m_generic_dispatch.emit();
});
req.make_form();
req.add_field("payload_json", nlohmann::json(obj).dump().c_str(), CURL_ZERO_TERMINATED);
for (size_t i = 0; i < params.Attachments.size(); i++) {
const auto field_name = "files[" + std::to_string(i) + "]";
req.add_file(field_name, params.Attachments.at(i).File, params.Attachments.at(i).Filename);
}
m_http.Execute(std::move(req), [this, params, nonce, callback](const http::response_type &res) {
for (const auto &attachment : params.Attachments) {
if (attachment.Type == ChatSubmitParams::AttachmentType::PastedImage) {
attachment.File->remove();
}
}
ChatMessageCallback(nonce, res, callback);
});
// dummy preview data
Message tmp;
tmp.Content = content;
tmp.Content = params.Message;
tmp.ID = nonce;
tmp.ChannelID = channel;
tmp.ChannelID = params.ChannelID;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
tmp.Nonce = obj.Nonce;
tmp.Nonce = nonce;
tmp.IsPending = true;
m_store.SetMessage(tmp.ID, tmp);
m_signal_message_sent.emit(tmp);
m_signal_message_create.emit(tmp);
}
void DiscordClient::SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
if (params.Attachments.empty())
SendChatMessageNoAttachments(params, callback);
else
SendChatMessageAttachments(params, callback);
}
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
@@ -552,19 +611,6 @@ void DiscordClient::UpdateStatus(PresenceStatus status, bool is_afk, const Activ
m_signal_presence_update.emit(GetUserData(), status);
}
void DiscordClient::CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback) {
CreateDMObject obj;
obj.Recipients.push_back(user_id);
m_http.MakePOST("/users/@me/channels", nlohmann::json(obj).dump(), [callback](const http::response &response) {
if (!CheckCode(response)) {
callback(DiscordError::NONE, Snowflake::Invalid);
return;
}
auto channel = nlohmann::json::parse(response.text).get<ChannelData>();
callback(GetCodeFromResponse(response), channel.ID);
});
}
void DiscordClient::CloseDM(Snowflake channel_id) {
m_http.MakeDELETE("/channels/" + std::to_string(channel_id), [](const http::response &response) {
CheckCode(response);
@@ -1123,6 +1169,25 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI
});
}
void DiscordClient::SetReferringChannel(Snowflake id) {
if (!id.IsValid()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
} else {
const auto channel = GetChannel(id);
if (channel.has_value()) {
if (channel->IsDM()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me/" + std::to_string(id));
} else if (channel->GuildID.has_value()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/" + std::to_string(*channel->GuildID) + "/" + std::to_string(id));
} else {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
}
} else {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
}
}
}
void DiscordClient::UpdateToken(const std::string &token) {
if (!IsStarted()) {
m_token = token;
@@ -2236,7 +2301,7 @@ void DiscordClient::HeartbeatThread() {
void DiscordClient::SendIdentify() {
IdentifyMessage msg;
msg.Token = m_token;
msg.Capabilities = 125; // no idea what this is
msg.Capabilities = 509; // no idea what this is
msg.Properties.OS = "Windows";
msg.Properties.Browser = "Chrome";
msg.Properties.Device = "";
@@ -2249,7 +2314,7 @@ void DiscordClient::SendIdentify() {
msg.Properties.ReferrerCurrent = "";
msg.Properties.ReferringDomainCurrent = "";
msg.Properties.ReleaseChannel = "stable";
msg.Properties.ClientBuildNumber = 105691;
msg.Properties.ClientBuildNumber = 141021;
msg.Properties.ClientEventSource = "";
msg.Presence.Status = "online";
msg.Presence.Since = 0;
@@ -2258,6 +2323,7 @@ void DiscordClient::SendIdentify() {
msg.ClientState.HighestLastMessageID = "0";
msg.ClientState.ReadStateVersion = 0;
msg.ClientState.UserGuildSettingsVersion = -1;
SetSuperPropertiesFromIdentity(msg);
const bool b = m_websocket.GetPrintMessages();
m_websocket.SetPrintMessages(false);
m_websocket.Send(msg);
@@ -2272,6 +2338,36 @@ void DiscordClient::SendResume() {
m_websocket.Send(msg);
}
void DiscordClient::SetHeaders() {
m_http.SetPersistentHeader("Sec-Fetch-Dest", "empty");
m_http.SetPersistentHeader("Sec-Fetch-Mode", "cors");
m_http.SetPersistentHeader("Sec-Fetch-Site", "same-origin");
m_http.SetPersistentHeader("X-Debug-Options", "bugReporterEnabled");
m_http.SetPersistentHeader("Accept-Language", "en-US,en;q=0.9");
SetReferringChannel(Snowflake::Invalid);
}
void DiscordClient::SetSuperPropertiesFromIdentity(const IdentifyMessage &identity) {
nlohmann::ordered_json j;
j["os"] = identity.Properties.OS;
j["browser"] = identity.Properties.Browser;
j["device"] = identity.Properties.Device;
j["system_locale"] = identity.Properties.SystemLocale;
j["browser_user_agent"] = identity.Properties.BrowserUserAgent;
j["browser_version"] = identity.Properties.BrowserVersion;
j["os_version"] = identity.Properties.OSVersion;
j["referrer"] = identity.Properties.Referrer;
j["referring_domain"] = identity.Properties.ReferringDomain;
j["referrer_current"] = identity.Properties.ReferrerCurrent;
j["referring_domain_current"] = identity.Properties.ReferringDomainCurrent;
j["release_channel"] = identity.Properties.ReleaseChannel;
j["client_build_number"] = identity.Properties.ClientBuildNumber;
j["client_event_source"] = nullptr; // probably will never be non-null ("") anyways
m_http.SetPersistentHeader("X-Super-Properties", Glib::Base64::encode(j.dump()));
m_http.SetPersistentHeader("X-Discord-Locale", identity.Properties.SystemLocale);
}
void DiscordClient::HandleSocketOpen() {
}
@@ -2539,6 +2635,10 @@ DiscordClient::type_signal_connected DiscordClient::signal_connected() {
return m_signal_connected;
}
DiscordClient::type_signal_message_progress DiscordClient::signal_message_progress() {
return m_signal_message_progress;
}
DiscordClient::type_signal_role_update DiscordClient::signal_role_update() {
return m_signal_role_update;
}

View File

@@ -3,6 +3,7 @@
#include "httpclient.hpp"
#include "objects.hpp"
#include "store.hpp"
#include "chatsubmitparams.hpp"
#include <sigc++/sigc++.h>
#include <nlohmann/json.hpp>
#include <thread>
@@ -96,16 +97,18 @@ public:
bool IsThreadJoined(Snowflake thread_id) const;
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
bool HasSelfChannelPermission(Snowflake channel_id, Permission perm) const;
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
bool HasChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const;
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
void ChatMessageCallback(const std::string &nonce, const http::response_type &response);
void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessageAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessage(const std::string &content, Snowflake channel);
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
void SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void DeleteMessage(Snowflake channel_id, Snowflake id);
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id);
@@ -116,7 +119,6 @@ public:
void BanUser(Snowflake user_id, Snowflake guild_id); // todo: reason, delete messages
void UpdateStatus(PresenceStatus status, bool is_afk);
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj);
void CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback);
void CloseDM(Snowflake channel_id);
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
void AddReaction(Snowflake id, Glib::ustring param);
@@ -202,6 +204,8 @@ public:
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 SetReferringChannel(Snowflake id);
void UpdateToken(const std::string &token);
void SetUserAgent(const std::string &agent);
@@ -283,6 +287,9 @@ private:
void SendIdentify();
void SendResume();
void SetHeaders();
void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity);
void HandleSocketOpen();
void HandleSocketClose(uint16_t code);
@@ -342,6 +349,8 @@ private:
Glib::Dispatcher m_generic_dispatch;
std::queue<std::function<void()>> m_generic_queue;
Glib::Timer m_progress_cb_timer;
std::set<Snowflake> m_channels_pinned_requested;
std::set<Snowflake> m_channels_lazy_loaded;
@@ -401,6 +410,7 @@ public:
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
typedef sigc::signal<void, std::string, float> type_signal_message_progress;
type_signal_gateway_ready signal_gateway_ready();
type_signal_message_create signal_message_create();
@@ -454,6 +464,7 @@ public:
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
type_signal_message_progress signal_message_progress();
protected:
type_signal_gateway_ready m_signal_gateway_ready;
@@ -508,4 +519,5 @@ protected:
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;
type_signal_message_progress m_signal_message_progress;
};

View File

@@ -16,6 +16,13 @@ enum class GuildApplicationStatus {
UNKNOWN,
};
enum class GuildPremiumTier {
NONE = 0,
TIER_1 = 1,
TIER_2 = 2,
TIER_3 = 3,
};
struct GuildApplicationData {
Snowflake UserID;
Snowflake GuildID;
@@ -73,7 +80,7 @@ struct GuildData {
std::optional<std::string> VanityURL; // null
std::optional<std::string> Description; // null
std::optional<std::string> BannerHash; // null
std::optional<int> PremiumTier;
std::optional<GuildPremiumTier> PremiumTier;
std::optional<int> PremiumSubscriptionCount;
std::optional<std::string> PreferredLocale;
std::optional<Snowflake> PublicUpdatesChannelID; // null

View File

@@ -19,11 +19,17 @@ void HTTPClient::SetAuth(std::string auth) {
m_authorization = std::move(auth);
}
void HTTPClient::SetPersistentHeader(std::string name, std::string value) {
m_headers.insert_or_assign(std::move(name), std::move(value));
}
void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb) {
printf("DELETE %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_DELETE, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization);
req.set_header("Origin", "https://discord.com");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
@@ -40,8 +46,10 @@ void HTTPClient::MakePATCH(const std::string &path, const std::string &payload,
printf("PATCH %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PATCH, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_header("Origin", "https://discord.com");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
@@ -59,8 +67,10 @@ void HTTPClient::MakePOST(const std::string &path, const std::string &payload, c
printf("POST %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_POST, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_header("Origin", "https://discord.com");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
@@ -78,7 +88,9 @@ void HTTPClient::MakePUT(const std::string &path, const std::string &payload, co
printf("PUT %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PUT, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization);
req.set_header("Origin", "https://discord.com");
if (!payload.empty())
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
@@ -98,8 +110,8 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
printf("GET %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_GET, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
@@ -112,6 +124,25 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
}));
}
http::request HTTPClient::CreateRequest(http::EMethod method, std::string path) {
http::request req(method, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
return req;
}
void HTTPClient::Execute(http::request &&req, const std::function<void(http::response_type r)> &cb) {
printf("%s %s\n", req.get_method(), req.get_url().c_str());
m_futures.push_back(std::async(std::launch::async, [this, cb, req = std::move(req)]() mutable {
auto res = req.execute();
OnResponse(res, cb);
}));
}
void HTTPClient::CleanupFutures() {
for (auto it = m_futures.begin(); it != m_futures.end();) {
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
@@ -128,6 +159,13 @@ void HTTPClient::RunCallbacks() {
m_mutex.unlock();
}
void HTTPClient::AddHeaders(http::request &r) {
for (const auto &[name, val] : m_headers) {
r.set_header(name, val);
}
curl_easy_setopt(r.get_curl(), CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br");
}
void HTTPClient::OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb) {
CleanupFutures();
try {

View File

@@ -17,13 +17,20 @@ public:
void SetUserAgent(std::string agent);
void SetAuth(std::string auth);
void SetPersistentHeader(std::string name, std::string value);
void MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb);
void MakeGET(const std::string &path, const std::function<void(http::response_type r)> &cb);
void MakePATCH(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
void MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
void MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
[[nodiscard]] http::request CreateRequest(http::EMethod method, std::string path);
void Execute(http::request &&req, const std::function<void(http::response_type r)> &cb);
private:
void AddHeaders(http::request &r);
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
void CleanupFutures();
@@ -36,4 +43,5 @@ private:
std::string m_api_base;
std::string m_authorization;
std::string m_agent;
std::unordered_map<std::string, std::string> m_headers;
};

View File

@@ -41,7 +41,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage::MemberItem
JS_D("mute", m.IsMuted);
JS_D("joined_at", m.JoinedAt);
JS_D("deaf", m.IsDefeaned);
JS_N("hoisted_role", m.HoistedRole);
JS_ON("hoisted_role", m.HoistedRole);
JS_ON("premium_since", m.PremiumSince);
JS_ON("nick", m.Nickname);
JS_ON("presence", m.Presence);
@@ -262,6 +262,7 @@ void to_json(nlohmann::json &j, const ClientStateProperties &m) {
j["highest_last_message_id"] = m.HighestLastMessageID;
j["read_state_version"] = m.ReadStateVersion;
j["user_guild_settings_version"] = m.UserGuildSettingsVersion;
j["user_settings_version"] = m.UserSettingsVersion;
}
void to_json(nlohmann::json &j, const IdentifyMessage &m) {

View File

@@ -382,6 +382,7 @@ struct ClientStateProperties {
std::string HighestLastMessageID = "0";
int ReadStateVersion = 0;
int UserGuildSettingsVersion = -1;
int UserSettingsVersion = -1;
friend void to_json(nlohmann::json &j, const ClientStateProperties &m);
};

View File

@@ -253,6 +253,14 @@ void Store::SetGuildMember(Snowflake guild_id, Snowflake user_id, const GuildMem
s->Reset();
{
auto &s = m_stmt_clr_member_roles;
s->Bind(1, user_id);
s->Bind(2, guild_id);
s->Step();
s->Reset();
}
{
auto &s = m_stmt_set_member_roles;
@@ -738,6 +746,7 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
s->Get(2, r.Icon);
s->Get(5, r.OwnerID);
s->Get(20, r.IsUnavailable);
s->Get(27, r.PremiumTier);
s->Reset();
@@ -906,21 +915,28 @@ Message Store::GetMessageBound(std::unique_ptr<Statement> &s) const {
}
if (!s->IsNull(25)) {
auto &a = r.Attachments.emplace_back();
s->Get(25, a.ID);
s->Get(26, a.Filename);
s->Get(27, a.Bytes);
s->Get(28, a.URL);
s->Get(29, a.ProxyURL);
s->Get(30, a.Height);
s->Get(31, a.Width);
auto &q = r.MessageReference.emplace();
s->Get(25, q.MessageID);
s->Get(26, q.ChannelID);
s->Get(27, q.GuildID);
}
if (!s->IsNull(32)) {
auto &q = r.MessageReference.emplace();
s->Get(32, q.MessageID);
s->Get(33, q.ChannelID);
s->Get(34, q.GuildID);
int num_attachments;
s->Get(28, num_attachments);
if (num_attachments > 0) {
auto &s = m_stmt_get_attachments;
s->Bind(1, r.ID);
while (s->FetchOne()) {
auto &q = r.Attachments.emplace_back();
s->Get(1, q.ID);
s->Get(2, q.Filename);
s->Get(3, q.Bytes);
s->Get(4, q.URL);
s->Get(5, q.ProxyURL);
s->Get(6, q.Height);
s->Get(7, q.Width);
}
s->Reset();
}
{
@@ -1629,55 +1645,43 @@ bool Store::CreateStatements() {
message_interactions.name,
message_interactions.type,
message_interactions.user_id,
attachments.id,
attachments.filename,
attachments.size,
attachments.url,
attachments.proxy,
attachments.height,
attachments.width,
message_references.message,
message_references.channel,
message_references.guild
message_references.guild,
COUNT(attachments.id)
FROM messages
LEFT OUTER JOIN
message_interactions
ON messages.id = message_interactions.message_id
LEFT OUTER JOIN
attachments
ON messages.id = attachments.message
LEFT OUTER JOIN
message_references
ON messages.id = message_references.id
WHERE messages.id = ?1
LEFT JOIN
attachments
ON messages.id = attachments.message
WHERE messages.id = ?1 GROUP BY messages.id
UNION ALL
SELECT messages.*,
message_interactions.interaction_id,
message_interactions.name,
message_interactions.type,
message_interactions.user_id,
attachments.id,
attachments.filename,
attachments.size,
attachments.url,
attachments.proxy,
attachments.height,
attachments.width,
message_references.message,
message_references.channel,
message_references.guild
message_references.guild,
COUNT(attachments.id)
FROM messages
LEFT OUTER JOIN
message_interactions
ON messages.id = message_interactions.message_id
LEFT OUTER JOIN
attachments
ON messages.id = attachments.message
LEFT OUTER JOIN
message_references
ON messages.id = message_references.id
LEFT JOIN
attachments
ON messages.id = attachments.message
WHERE messages.id = (SELECT message FROM message_references WHERE id = ?1)
ORDER BY messages.id DESC
GROUP BY messages.id ORDER BY messages.id DESC
)");
if (!m_stmt_get_msg->OK()) {
fprintf(stderr, "failed to prepare get message statement: %s\n", m_db.ErrStr());
@@ -1701,27 +1705,21 @@ bool Store::CreateStatements() {
message_interactions.name,
message_interactions.type,
message_interactions.user_id,
attachments.id,
attachments.filename,
attachments.size,
attachments.url,
attachments.proxy,
attachments.height,
attachments.width,
message_references.message,
message_references.channel,
message_references.guild
message_references.guild,
COUNT(attachments.id)
FROM messages
LEFT OUTER JOIN
message_interactions
ON messages.id = message_interactions.message_id
LEFT OUTER JOIN
attachments
ON messages.id = attachments.message
LEFT OUTER JOIN
message_references
ON messages.id = message_references.id
WHERE channel_id = ? AND pending = 0 ORDER BY id DESC LIMIT ?
LEFT JOIN
attachments
ON messages.id = attachments.message
WHERE channel_id = ? AND pending = 0 GROUP BY messages.id ORDER BY id DESC LIMIT ?
) ORDER BY id ASC
)");
if (!m_stmt_get_last_msgs->OK()) {
@@ -1892,6 +1890,20 @@ bool Store::CreateStatements() {
return false;
}
m_stmt_clr_member_roles = std::make_unique<Statement>(m_db, R"(
DELETE FROM member_roles
WHERE user = ? AND
EXISTS (
SELECT 1 FROM roles
WHERE member_roles.role = roles.id
AND roles.guild = ?
)
)");
if (!m_stmt_clr_member_roles->OK()) {
fprintf(stderr, "failed to prepare clear member roles statement: %s\n", m_db.ErrStr());
return false;
}
m_stmt_set_guild_emoji = std::make_unique<Statement>(m_db, R"(
REPLACE INTO guild_emojis VALUES (
?, ?

View File

@@ -281,6 +281,7 @@ private:
STMT(set_interaction);
STMT(set_member_roles);
STMT(get_member_roles);
STMT(clr_member_roles);
STMT(set_guild_emoji);
STMT(get_guild_emojis);
STMT(clr_guild_emoji);

View File

@@ -1,6 +1,10 @@
#include "user.hpp"
#include "abaddon.hpp"
bool UserData::IsABot() const noexcept {
return IsBot.has_value() && *IsBot;
}
bool UserData::IsDeleted() const {
return Discriminator == "0000";
}

View File

@@ -60,6 +60,7 @@ struct UserData {
friend void to_json(nlohmann::json &j, const UserData &m);
void update_from_json(const nlohmann::json &j);
[[nodiscard]] bool IsABot() const noexcept;
[[nodiscard]] bool IsDeleted() const;
[[nodiscard]] bool HasAvatar() const;
[[nodiscard]] bool HasAnimatedAvatar() const noexcept;

View File

@@ -2,6 +2,22 @@
#include <sstream>
#include <utility>
#ifdef ABADDON_IS_BIG_ENDIAN
/* Allows processing emojis.bin correctly on big-endian systems. */
int emojis_int32_correct_endian(int little_endian_in) {
/* this does the same thing as __bswap_32() but can be done without
non-standard headers. */
return ((little_endian_in >> 24) & 0xff) | // move byte 3 to byte 0
((little_endian_in << 8) & 0xff0000) | // move byte 1 to byte 2
((little_endian_in >> 8) & 0xff00) | // move byte 2 to byte 1
((little_endian_in << 24) & 0xff000000); // byte 0 to byte 3
}
#else
int emojis_int32_correct_endian(int little_endian_in) {
return little_endian_in;
}
#endif
EmojiResource::EmojiResource(std::string filepath)
: m_filepath(std::move(filepath)) {}
@@ -11,18 +27,22 @@ bool EmojiResource::Load() {
int index_offset;
std::fread(&index_offset, 4, 1, m_fp);
index_offset = emojis_int32_correct_endian(index_offset);
std::fseek(m_fp, index_offset, SEEK_SET);
int emojis_count;
std::fread(&emojis_count, 4, 1, m_fp);
emojis_count = emojis_int32_correct_endian(emojis_count);
for (int i = 0; i < emojis_count; i++) {
std::vector<std::string> shortcodes;
int shortcodes_count;
std::fread(&shortcodes_count, 4, 1, m_fp);
shortcodes_count = emojis_int32_correct_endian(shortcodes_count);
for (int j = 0; j < shortcodes_count; j++) {
int shortcode_length;
std::fread(&shortcode_length, 4, 1, m_fp);
shortcode_length = emojis_int32_correct_endian(shortcode_length);
std::string shortcode(shortcode_length, '\0');
std::fread(shortcode.data(), shortcode_length, 1, m_fp);
shortcodes.push_back(std::move(shortcode));
@@ -30,13 +50,16 @@ bool EmojiResource::Load() {
int surrogates_count;
std::fread(&surrogates_count, 4, 1, m_fp);
surrogates_count = emojis_int32_correct_endian(surrogates_count);
std::string surrogates(surrogates_count, '\0');
std::fread(surrogates.data(), surrogates_count, 1, m_fp);
m_patterns.emplace_back(surrogates);
int data_size, data_offset;
std::fread(&data_size, 4, 1, m_fp);
data_size = emojis_int32_correct_endian(data_size);
std::fread(&data_offset, 4, 1, m_fp);
data_offset = emojis_int32_correct_endian(data_offset);
m_index[surrogates] = { data_offset, data_size };
for (const auto &shortcode : shortcodes)

View File

@@ -29,12 +29,53 @@ request::request(EMethod method, std::string url)
prepare();
}
request::request(request &&other) noexcept
: m_curl(std::exchange(other.m_curl, nullptr))
, m_url(std::exchange(other.m_url, ""))
, m_method(std::exchange(other.m_method, nullptr))
, m_header_list(std::exchange(other.m_header_list, nullptr))
, m_error_buf(other.m_error_buf)
, m_form(std::exchange(other.m_form, nullptr))
, m_read_streams(std::move(other.m_read_streams))
, m_progress_callback(std::move(other.m_progress_callback)) {
if (m_progress_callback) {
curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
}
}
request::~request() {
if (m_curl != nullptr)
curl_easy_cleanup(m_curl);
if (m_header_list != nullptr)
curl_slist_free_all(m_header_list);
if (m_form != nullptr)
curl_mime_free(m_form);
}
const std::string &request::get_url() const {
return m_url;
}
const char *request::get_method() const {
return m_method;
}
size_t http_req_xferinfofunc(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
if (ultotal > 0) {
auto *req = reinterpret_cast<request *>(clientp);
req->m_progress_callback(ultotal, ulnow);
}
return 0;
}
void request::set_progress_callback(std::function<void(curl_off_t, curl_off_t)> func) {
m_progress_callback = std::move(func);
curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, http_req_xferinfofunc);
curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
}
void request::set_verify_ssl(bool verify) {
@@ -57,6 +98,46 @@ void request::set_user_agent(const std::string &data) {
curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str());
}
CURL *request::get_curl() {
return m_curl;
}
void request::make_form() {
m_form = curl_mime_init(m_curl);
}
static size_t http_readfunc(char *buffer, size_t size, size_t nitems, void *arg) {
auto stream = Glib::wrap(G_FILE_INPUT_STREAM(arg), true);
int r = stream->read(buffer, size * nitems);
if (r == -1) {
// https://github.com/curl/curl/blob/ad9bc5976d6661cd5b03ebc379313bf657701c14/lib/mime.c#L724
return size_t(-1);
}
return r;
}
// file must exist until request completes
void request::add_file(std::string_view name, const Glib::RefPtr<Gio::File> &file, std::string_view filename) {
if (!file->query_exists()) return;
auto *field = curl_mime_addpart(m_form);
curl_mime_name(field, name.data());
auto info = file->query_info();
auto stream = file->read();
curl_mime_data_cb(field, info->get_size(), http_readfunc, nullptr, nullptr, stream->gobj());
curl_mime_filename(field, filename.data());
// hold ref
m_read_streams.insert(stream);
}
// copied
void request::add_field(std::string_view name, const char *data, size_t size) {
auto *field = curl_mime_addpart(m_form);
curl_mime_name(field, name.data());
curl_mime_data(field, data, size);
}
response request::execute() {
if (m_curl == nullptr) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit);
@@ -76,12 +157,14 @@ response request::execute() {
m_error_buf[0] = '\0';
if (m_header_list != nullptr)
curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list);
if (m_form != nullptr)
curl_easy_setopt(m_curl, CURLOPT_MIMEPOST, m_form);
CURLcode result = curl_easy_perform(m_curl);
if (result != CURLE_OK) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
response.error_string = curl_easy_strerror(result);
response.error_string += " " + std::string(m_error_buf);
response.error_string += " " + std::string(m_error_buf.data());
return response;
}

View File

@@ -1,8 +1,10 @@
#pragma once
#include <array>
#include <functional>
#include <set>
#include <string>
#include <curl/curl.h>
// i regret not using snake case for everything oh well
#include <giomm/file.h>
namespace http {
enum EStatusCode : int {
@@ -98,16 +100,30 @@ struct response {
struct request {
request(EMethod method, std::string url);
request(request &&other) noexcept;
~request();
request(const request &) = delete;
request &operator=(const request &) = delete;
request &operator=(request &&) noexcept = delete;
const std::string &get_url() const;
const char *get_method() const;
void set_progress_callback(std::function<void(curl_off_t, curl_off_t)> func);
void set_verify_ssl(bool verify);
void set_proxy(const std::string &proxy);
void set_header(const std::string &name, const std::string &value);
void set_body(const std::string &data);
void set_user_agent(const std::string &data);
void make_form();
void add_file(std::string_view name, const Glib::RefPtr<Gio::File> &file, std::string_view filename);
void add_field(std::string_view name, const char *data, size_t size);
response execute();
CURL *get_curl();
private:
void prepare();
@@ -115,7 +131,13 @@ private:
std::string m_url;
const char *m_method;
curl_slist *m_header_list = nullptr;
char m_error_buf[CURL_ERROR_SIZE] = { 0 };
std::array<char, CURL_ERROR_SIZE> m_error_buf = { 0 };
curl_mime *m_form = nullptr;
std::function<void(curl_off_t, curl_off_t)> m_progress_callback;
std::set<Glib::RefPtr<Gio::FileInputStream>> m_read_streams;
friend size_t http_req_xferinfofunc(void *, curl_off_t, curl_off_t, curl_off_t, curl_off_t);
};
using response_type = response;

View File

@@ -204,7 +204,7 @@ void GuildSettingsMembersPaneInfo::SetUser(Snowflake user_id) {
auto member = *discord.GetMember(user_id, GuildID);
member.User = discord.GetUser(user_id);
m_bot.set_visible(member.User->IsBot.has_value() && *member.User->IsBot);
m_bot.set_visible(member.User->IsABot());
m_id.set_text("User ID: " + std::to_string(user_id));
m_created.set_text("Account created: " + user_id.GetLocalTimestamp());

View File

@@ -76,6 +76,7 @@ MainWindow::MainWindow()
add(m_main_box);
SetupMenu();
SetupDND();
}
void MainWindow::UpdateComponents() {
@@ -350,6 +351,22 @@ void MainWindow::SetupMenu() {
#endif
}
void MainWindow::SetupDND() {
std::vector<Gtk::TargetEntry> targets;
targets.emplace_back("text/uri-list", Gtk::TargetFlags(0), 0);
drag_dest_set(targets, Gtk::DEST_DEFAULT_DROP | Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_HIGHLIGHT, Gdk::DragAction::ACTION_COPY);
signal_drag_data_received().connect([this](const Glib::RefPtr<Gdk::DragContext> &ctx, int x, int y, const Gtk::SelectionData &selection, guint info, guint time) {
HandleDroppedURIs(selection);
});
}
void MainWindow::HandleDroppedURIs(const Gtk::SelectionData &selection) {
for (const auto &uri : selection.get_uris()) {
// not using Glib::get_filename_for_uri or whatever because the conversion is BAD (on windows at least)
m_chat.AddAttachment(Gio::File::create_for_uri(uri));
}
}
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {
return m_signal_action_connect;
}

View File

@@ -39,6 +39,9 @@ public:
private:
void SetupMenu();
void SetupDND();
void HandleDroppedURIs(const Gtk::SelectionData &selection);
Gtk::Box m_main_box;
Gtk::Box m_content_box;