Merge branch 'master' into keychain

This commit is contained in:
ouwou 2022-11-29 15:53:02 -05:00
commit 573a619191
36 changed files with 727 additions and 444 deletions

View File

@ -73,6 +73,7 @@ jobs:
cp -r res/css res/res res/fonts build/artifactdir/bin 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 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 || : 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 mkdir -p build/artifactdir/share/icons/Adwaita
cd 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 mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions
@ -117,6 +118,7 @@ jobs:
run: | run: |
brew install gtkmm3 brew install gtkmm3
brew install nlohmann-json brew install nlohmann-json
brew install jpeg
- name: Build - name: Build
uses: lukka/run-cmake@v3 uses: lukka/run-cmake@v3

View File

@ -34,6 +34,12 @@ if (WIN32)
add_compile_definitions(NOMINMAX) add_compile_definitions(NOMINMAX)
endif () 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) configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
file(GLOB_RECURSE ABADDON_SOURCES file(GLOB_RECURSE ABADDON_SOURCES

231
README.md
View File

@ -25,13 +25,15 @@ Current features:
* Thread support<sup>3</sup> * Thread support<sup>3</sup>
* Animated avatars, server icons, emojis (can be turned off) * 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 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 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 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 stops this) as well as in the headers of all requests.<br>
servers, sending new DMs, or managing your friends list if you are afraid of being caught in Discord's spam filters
(unlikely). **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 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 with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to
@ -52,6 +54,7 @@ the result of fundamental issues with Discord's thread implementation.
* mingw-w64-x86_64-curl * mingw-w64-x86_64-curl
* mingw-w64-x86_64-zlib * mingw-w64-x86_64-zlib
* mingw-w64-x86_64-gtkmm3 * mingw-w64-x86_64-gtkmm3
* mingw-w64-x86_64-libhandy
2. `git clone --recurse-submodules="subprojects" https://github.com/uowuo/abaddon && cd abaddon` 2. `git clone --recurse-submodules="subprojects" https://github.com/uowuo/abaddon && cd abaddon`
3. `mkdir build && cd build` 3. `mkdir build && cd build`
4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..` 4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..`
@ -60,15 +63,18 @@ the result of fundamental issues with Discord's thread implementation.
#### Mac: #### Mac:
1. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon` 1. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
2. `brew install gtkmm3 nlohmann-json` 2. `brew install gtkmm3 nlohmann-json libhandy`
3. `mkdir build && cd build` 3. `mkdir build && cd build`
4. `cmake ..` 4. `cmake ..`
5. `make` 5. `make`
#### Linux: #### Linux:
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`, 1. Install dependencies
and [nlohmann-json](https://github.com/nlohmann/json) * On Ubuntu 20.04 (Focal) and newer:
```Shell
$ sudo apt install g++ cmake libgtkmm-3.0-dev libcurl4-gnutls-dev libsqlite3-dev libssl-dev nlohmann-json3-dev
```
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon` 2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
3. `mkdir build && cd build` 3. `mkdir build && cd build`
4. `cmake ..` 4. `cmake ..`
@ -86,13 +92,28 @@ Latest release version: https://github.com/uowuo/abaddon/releases/latest
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now), - Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now),
requires gtkmm3. built on Ubuntu 18.04 + gcc9 requires gtkmm3. built on Ubuntu 18.04 + gcc9
⚠️ If you use Windows, make sure to start from the `bin` directory > **Warning**: If you use Windows, make sure to start from the `bin` directory
On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/usr/share/abaddon` On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/usr/share/abaddon`
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is `abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is
no `abaddon.ini` in the working directory 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: #### Dependencies:
* [gtkmm](https://www.gtkmm.org/en/) * [gtkmm](https://www.gtkmm.org/en/)
@ -101,6 +122,7 @@ no `abaddon.ini` in the working directory
* [libcurl](https://curl.se/) * [libcurl](https://curl.se/)
* [zlib](https://zlib.net/) * [zlib](https://zlib.net/)
* [SQLite3](https://www.sqlite.org/index.html) * [SQLite3](https://www.sqlite.org/index.html)
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
### TODO: ### TODO:
@ -113,90 +135,90 @@ no `abaddon.ini` in the working directory
#### CSS selectors #### CSS selectors
.app-window - Applied to all windows. This means the main window and all popups **`.app-window`** - Applied to all windows. This means the main window and all popups
.app-popup - Additional class for `.app-window`s when the window is not the main window **`.app-popup`** - Additional class for `.app-window`s when the window is not the main window
.channel-list - Container of the channel list **`.channel-list`** - Container of the channel list
.messages - Container of user messages **`.messages`** - Container of user messages
.message-container - The container which holds a user's messages **`.message-container`** - The container which holds a user's messages
.message-container-author - The author label for a message container **`.message-container-author`** - The author label for a message container
.message-container-timestamp - The timestamp label for a message container **`.message-container-timestamp`** - The timestamp label for a message container
.message-container-avatar - Avatar for a user in a message **`.message-container-avatar`** - Avatar for a user in a message
.message-container-extra - Label containing BOT/Webhook **`.message-container-extra`** - Label containing BOT/Webhook
.message-text - The text of a user message **`.message-text`** - The text of a user message
.pending - Extra class of .message-text for messages pending to be sent **`.pending`** - Extra class of .message-text for messages pending to be sent
.failed - Extra class of .message-text for messages that failed to be sent **`.failed`** - Extra class of .message-text for messages that failed to be sent
.message-attachment-box - Contains attachment info **`.message-attachment-box`** - Contains attachment info
.message-reply - Container for the replied-to message in a reply (these elements will also have .message-text set) **`.message-reply`** - Container for the replied-to message in a reply (these elements will also have .message-text set)
.message-input - Applied to the chat input container **`.message-input`** - Applied to the chat input container
.replying - Extra class for chat input container when a reply is currently being created **`.replying`** - Extra class for chat input container when a reply is currently being created
.reaction-box - Contains a reaction image and the count **`.reaction-box`** - Contains a reaction image and the count
.reacted - Additional class for reaction-box when the user has reacted with a particular reaction **`.reacted`** - Additional class for reaction-box when the user has reacted with a particular reaction
.reaction-count - Contains the count for reaction **`.reaction-count`** - Contains the count for reaction
.completer - Container for the message completer **`.completer`** - Container for the message completer
.completer-entry - Container for a single entry in the completer **`.completer-entry`** - Container for a single entry in the completer
.completer-entry-label - Contains the label for an entry in the completer **`.completer-entry-label`** - Contains the label for an entry in the completer
.completer-entry-image - Contains the image for an entry in the completer **`.completer-entry-image`** - Contains the image for an entry in the completer
.embed - Container for a message embed **`.embed`** - Container for a message embed
.embed-author - The author of an embed **`.embed-author`** - The author of an embed
.embed-title - The title of an embed **`.embed-title`** - The title of an embed
.embed-description - The description of an embed **`.embed-description`** - The description of an embed
.embed-field-title - The title of an embed field **`.embed-field-title`** - The title of an embed field
.embed-field-value - The value of an embed field **`.embed-field-value`** - The value of an embed field
.embed-footer - The footer of an embed **`.embed-footer`** - The footer of an embed
.members - Container of the member list **`.members`** - Container of the member list
.members-row - All rows within the members container **`.members-row`** - All rows within the members container
.members-row-label - All labels in the members container **`.members-row-label`** - All labels in the members container
.members-row-member - Rows containing a member **`.members-row-member`** - Rows containing a member
.members-row-role - Rows containing a role **`.members-row-role`** - Rows containing a role
.members-row-avatar - Contains the avatar for a row in the member list **`.members-row-avatar`** - Contains the avatar for a row in the member list
.status-indicator - The status indicator **`.status-indicator`** - The status indicator
.online - Applied to status indicators when the associated user is online **`.online`** - Applied to status indicators when the associated user is online
.idle - Applied to status indicators when the associated user is away **`.idle`** - Applied to status indicators when the associated user is away
.dnd - Applied to status indicators when the associated user is on do not disturb **`.dnd`** - Applied to status indicators when the associated user is on do not disturb
.offline - Applied to status indicators when the associated user is offline **`.offline`** - Applied to status indicators when the associated user is offline
.typing-indicator - The typing indicator (also used for replies) **`.typing-indicator`** - The typing indicator (also used for replies)
Used in reorderable list implementation: Used in reorderable list implementation:
.drag-icon .drag-hover-top .drag-hover-bottom **`.drag-icon`** **`.drag-hover-top`** **`.drag-hover-bottom`**
Used in guild settings popup: Used in guild settings popup:
.guild-settings-window **`.guild-settings-window`**
.guild-members-pane-list - Container for list of members in the members pane **`.guild-members-pane-list`** - Container for list of members in the members pane
.guild-members-pane-info - Container for member info **`.guild-members-pane-info`** - Container for member info
.guild-roles-pane-list - Container for list of roles in the roles pane **`.guild-roles-pane-list`** - Container for list of roles in the roles pane
Used in profile popup: Used in profile popup:
.mutual-friend-item - Applied to every item in the mutual friends list **`.mutual-friend-item`** - Applied to every item in the mutual friends list
.mutual-friend-item-name - Name in mutual friend item **`.mutual-friend-item-name`** - Name in mutual friend item
.mutual-friend-item-avatar - Avatar in mutual friend item **`.mutual-friend-item-avatar`** - Avatar in mutual friend item
.mutual-guild-item - Applied to every item in the mutual guilds list **`.mutual-guild-item`** - Applied to every item in the mutual guilds list
.mutual-guild-item-name - Name in mutual guild item **`.mutual-guild-item-name`** - Name in mutual guild item
.mutual-guild-item-icon - Icon in mutual guild item **`.mutual-guild-item-icon`** - Icon in mutual guild item
.mutual-guild-item-nick - User nickname in mutual guild item **`.mutual-guild-item-nick`** - User nickname in mutual guild item
.profile-connection - Applied to every item in the user connections list **`.profile-connection`** - Applied to every item in the user connections list
.profile-connection-label - Label in profile connection item **`.profile-connection-label`** - Label in profile connection item
.profile-connection-check - Checkmark in verified profile connection items **`.profile-connection-check`** - Checkmark in verified profile connection items
.profile-connections - Container for profile connections **`.profile-connections`** - Container for profile connections
.profile-notes - Container for notes in profile window **`.profile-notes`** - Container for notes in profile window
.profile-notes-label - Label that says "NOTE" **`.profile-notes-label`** - Label that says "NOTE"
.profile-notes-text - Actual note text **`.profile-notes-text`** - Actual note text
.profile-info-pane - Applied to container for info section of profile popup **`.profile-info-pane`** - Applied to container for info section of profile popup
.profile-info-created - Label for creation date of profile **`.profile-info-created`** - Label for creation date of profile
.user-profile-window **`.user-profile-window`**
.profile-main-container - Inner container for profile **`.profile-main-container`** - Inner container for profile
.profile-avatar **`.profile-avatar`**
.profile-username **`.profile-username`**
.profile-switcher - Buttons used to switch viewed section of profile **`.profile-switcher`** - Buttons used to switch viewed section of profile
.profile-stack - Container for profile info that can be switched between **`.profile-stack`** - Container for profile info that can be switched between
.profile-badges - Container for badges **`.profile-badges`** - Container for badges
.profile-badge **`.profile-badge`**
### Settings ### Settings
@ -212,43 +234,46 @@ For example, memory_db would be set by adding `memory_db = true` under the line
#### discord #### discord
* gateway (string) - override url for Discord gateway. must be json format and use zlib stream compression * **`gateway`** (string) - override url for Discord gateway. must be json format and use zlib stream compression
* api_base (string) - override base url for Discord API * **`api_base`** (string) - override base url for Discord API
* memory_db (true or false, default false) - if true, Discord data will be kept in memory as opposed to on disk * **`memory_db`** (true or false, `default: false`) - if true, Discord data will be kept in memory as opposed to on disk
* token (string) - Discord token used to login, this can be set from the menu * **`token`** (string) - Discord token used to login, this can be set from the menu
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be * **`prefetch`** (true or false, `default: false`) - if true, new messages will cause the avatar and image attachments to be
automatically downloaded automatically downloaded
#### http #### http
* user_agent (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images) * **`user_agent`** (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images)
* concurrent (int, default 20) - how many images can be concurrently retrieved * **`concurrent`** (int, `default: 20`) - how many images can be concurrently retrieved
#### gui #### gui
* member_list_discriminator (true or false, default true) - show user discriminators in the member list * **`member_list_discriminator`** (true or false, `default: true`) - show user discriminators in the member list
* stock_emojis (true or false, default true) - allow abaddon to substitute unicode emojis with images from emojis.bin, * **`stock_emojis`** (true or false, `default: true`) - allow abaddon to substitute unicode emojis with images from emojis.bin,
must be false to allow GTK to render emojis itself must be false to allow GTK to render emojis itself
* custom_emojis (true or false, default true) - download and use custom Discord emojis * **`custom_emojis`** (true or false, `default: true`) - download and use custom Discord emojis
* css (string) - path to the main CSS file * **`css`** (string) - path to the main CSS file
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars). * **`animations`** (true or false, `default: true`) - use animated images where available (e.g. server icons, emojis, avatars).
false means static images will be used false means static images will be used
* animated_guild_hover_only (true or false, default true) - only animate guild icons when the guild is being hovered * **`animated_guild_hover_only`** (true or false, `default: true`) - only animate guild icons when the guild is being hovered
over over
* owner_crown (true or false, default true) - show a crown next to the owner * **`owner_crown`** (true or false, `default: true`) - show a crown next to the owner
* unreads (true or false, default true) - show unread indicators and mention badges * **`unreads`** (true or false, `default: true`) - show unread indicators and mention badges
* **`save_state`** (true or false, `default: true`) - save the state of the gui (active channels, tabs, expanded channels)
* **`alt_menu`** (true or false, `default: false`) - keep the menu hidden unless revealed with alt key
* **`hide_to_tray`** (true or false, `default: false`) - hide abaddon to the system tray on window close
#### style #### style
* linkcolor (string) - color to use for links in messages * **`linkcolor`** (string) - color to use for links in messages
* expandercolor (string) - color to use for the expander in the channel list * **`expandercolor`** (string) - color to use for the expander in the channel list
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list * **`nsfwchannelcolor`** (string) - color to use for NSFW channels in the channel list
* channelcolor (string) - color to use for SFW channels in the channel list * **`channelcolor`** (string) - color to use for SFW channels in the channel list
* mentionbadgecolor (string) - background color for mention badges * **`mentionbadgecolor`** (string) - background color for mention badges
* mentionbadgetextcolor (string) - color to use for number displayed on mention badges * **`mentionbadgetextcolor`** (string) - color to use for number displayed on mention badges
* unreadcolor (string) - color to use for the unread indicator * **`unreadcolor`** (string) - color to use for the unread indicator
### Environment variables ### Environment variables
* ABADDON_NO_FC (Windows only) - don't use custom font config * **`ABADDON_NO_FC`** (Windows only) - don't use custom font config
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute * **`ABADDON_CONFIG`** - change path of configuration file to use. relative to cwd or can be absolute

View File

@ -14,7 +14,7 @@
/bin/libdeflate.dll /bin/libdeflate.dll
/bin/libepoxy-0.dll /bin/libepoxy-0.dll
/bin/libexpat-1.dll /bin/libexpat-1.dll
/bin/libffi-7.dll /bin/libffi-8.dll
/bin/libfontconfig-1.dll /bin/libfontconfig-1.dll
/bin/libfreetype-6.dll /bin/libfreetype-6.dll
/bin/libfribidi-0.dll /bin/libfribidi-0.dll
@ -42,7 +42,7 @@
/bin/libpangoft2-1.0-0.dll /bin/libpangoft2-1.0-0.dll
/bin/libpangomm-1.4-1.dll /bin/libpangomm-1.4-1.dll
/bin/libpangowin32-1.0-0.dll /bin/libpangowin32-1.0-0.dll
/bin/libpcre-1.dll /bin/libpcre2-8-0.dll
/bin/libpixman-1-0.dll /bin/libpixman-1-0.dll
/bin/libpng16-16.dll /bin/libpng16-16.dll
/bin/libpsl-5.dll /bin/libpsl-5.dll
@ -56,3 +56,4 @@
/bin/libwinpthread-1.dll /bin/libwinpthread-1.dll
/bin/libzstd.dll /bin/libzstd.dll
/bin/zlib1.dll /bin/zlib1.dll
/../usr/bin/msys-2.0.dll

View File

@ -120,6 +120,8 @@
.message-input-browse-icon { .message-input-browse-icon {
color: #b9bbbe; 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 */ /* i dont think theres a way to circumvent having to do this to adjust around the browse icon */

View File

@ -6,7 +6,6 @@
#include "discord/discord.hpp" #include "discord/discord.hpp"
#include "dialogs/token.hpp" #include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp" #include "dialogs/editmessage.hpp"
#include "dialogs/joinguild.hpp"
#include "dialogs/confirm.hpp" #include "dialogs/confirm.hpp"
#include "dialogs/setstatus.hpp" #include "dialogs/setstatus.hpp"
#include "dialogs/friendpicker.hpp" #include "dialogs/friendpicker.hpp"
@ -17,6 +16,7 @@
#include "windows/profilewindow.hpp" #include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp" #include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp" #include "windows/threadswindow.hpp"
#include "startup.hpp"
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
#include <handy.h> #include <handy.h>
@ -68,14 +68,13 @@ Abaddon &Abaddon::Get() {
return instance; return instance;
} }
#ifdef WITH_LIBHANDY #ifdef _WIN32
#ifdef _WIN32
constexpr static guint BUTTON_BACK = 4; constexpr static guint BUTTON_BACK = 4;
constexpr static guint BUTTON_FORWARD = 5; constexpr static guint BUTTON_FORWARD = 5;
#else #else
constexpr static guint BUTTON_BACK = 8; constexpr static guint BUTTON_BACK = 8;
constexpr static guint BUTTON_FORWARD = 9; constexpr static guint BUTTON_FORWARD = 9;
#endif #endif
static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) { static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
if (event->type != GDK_BUTTON_PRESS) return false; if (event->type != GDK_BUTTON_PRESS) return false;
@ -85,6 +84,7 @@ static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
auto *window = gtk_widget_get_toplevel(widget); auto *window = gtk_widget_get_toplevel(widget);
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false; // is this the right way??? if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false; // is this the right way???
#ifdef WITH_LIBHANDY
switch (event->button.button) { switch (event->button.button) {
case BUTTON_BACK: case BUTTON_BACK:
main_window->GoBack(); main_window->GoBack();
@ -93,6 +93,7 @@ static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
main_window->GoForward(); main_window->GoForward();
break; break;
} }
#endif
return false; return false;
} }
@ -108,6 +109,15 @@ static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
const bool ctrl = (event->key.state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK; const bool ctrl = (event->key.state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK;
const bool shft = (event->key.state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK; const bool shft = (event->key.state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK;
constexpr static guint EXCLUDE_STATES = GDK_CONTROL_MASK | GDK_SHIFT_MASK;
if (!(event->key.state & EXCLUDE_STATES) && event->key.keyval == GDK_KEY_Alt_L) {
if (Abaddon::Get().GetSettings().AltMenu) {
main_window->ToggleMenuVisibility();
}
}
#ifdef WITH_LIBHANDY
if (ctrl) { if (ctrl) {
switch (event->key.keyval) { switch (event->key.keyval) {
case GDK_KEY_Tab: case GDK_KEY_Tab:
@ -134,6 +144,7 @@ static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
return true; return true;
} }
} }
#endif
return false; return false;
} }
@ -143,7 +154,6 @@ static void MainEventHandler(GdkEvent *event, void *main_window) {
if (HandleKeyEvents(event, static_cast<MainWindow *>(main_window))) return; if (HandleKeyEvents(event, static_cast<MainWindow *>(main_window))) return;
gtk_main_do_event(event); gtk_main_do_event(event);
} }
#endif
int Abaddon::StartGTK() { int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon"); m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
@ -228,7 +238,6 @@ int Abaddon::StartGTK() {
m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect)); m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect));
m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken)); m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken));
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS)); m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
m_main_window->signal_action_join_guild().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinGuildDialog));
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus)); m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient)); m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins)); m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
@ -247,12 +256,29 @@ int Abaddon::StartGTK() {
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove)); m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));
ActionReloadCSS(); ActionReloadCSS();
if (m_settings.GetSettings().HideToTray) {
m_tray = Gtk::StatusIcon::create("discord");
m_tray->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_tray_click));
m_tray->signal_popup_menu().connect(sigc::mem_fun(*this, &Abaddon::on_tray_popup_menu));
}
m_tray_menu = Gtk::make_managed<Gtk::Menu>();
m_tray_exit = Gtk::make_managed<Gtk::MenuItem>("Quit", false);
m_tray_exit->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_tray_menu_click));
m_tray_menu->append(*m_tray_exit);
m_tray_menu->show_all();
m_main_window->signal_hide().connect(sigc::mem_fun(*this, &Abaddon::on_window_hide));
m_gtk_app->signal_shutdown().connect(sigc::mem_fun(*this, &Abaddon::OnShutdown), false); m_gtk_app->signal_shutdown().connect(sigc::mem_fun(*this, &Abaddon::OnShutdown), false);
m_main_window->UpdateMenus(); m_main_window->UpdateMenus();
m_gtk_app->hold();
m_main_window->show(); m_main_window->show();
RunFirstTimeDiscordStartup();
return m_gtk_app->run(*m_main_window); return m_gtk_app->run(*m_main_window);
} }
@ -390,6 +416,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
for (const auto child : m_user_menu_roles_submenu->get_children()) for (const auto child : m_user_menu_roles_submenu->get_children())
delete child; delete child;
if (guild.has_value() && user.has_value()) { if (guild.has_value() && user.has_value()) {
const auto roles = user->GetSortedRoles(); const auto roles = user->GetSortedRoles();
m_user_menu_roles->set_visible(!roles.empty()); m_user_menu_roles->set_visible(!roles.empty());
@ -412,7 +439,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
if (me == id) { if (me == id) {
m_user_menu_ban->set_visible(false); m_user_menu_ban->set_visible(false);
m_user_menu_kick->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 { } else {
const bool has_kick = m_discord.HasGuildPermission(me, guild_id, Permission::KICK_MEMBERS); 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); const bool has_ban = m_discord.HasGuildPermission(me, guild_id, Permission::BAN_MEMBERS);
@ -420,7 +447,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
m_user_menu_kick->set_visible(has_kick && can_manage); m_user_menu_kick->set_visible(has_kick && can_manage);
m_user_menu_ban->set_visible(has_ban && 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(); m_user_menu_remove_recipient->hide();
@ -434,6 +461,48 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
m_user_menu->popup_at_pointer(event); m_user_menu->popup_at_pointer(event);
} }
void Abaddon::RunFirstTimeDiscordStartup() {
DiscordStartupDialog dlg(*m_main_window);
dlg.set_position(Gtk::WIN_POS_CENTER);
std::optional<std::string> cookie;
std::optional<uint32_t> build_number;
dlg.signal_response().connect([&](int response) {
if (response == Gtk::RESPONSE_OK) {
cookie = dlg.GetCookie();
build_number = dlg.GetBuildNumber();
}
});
dlg.run();
Glib::signal_idle().connect_once([this, cookie, build_number]() {
if (cookie.has_value()) {
m_discord.SetCookie(*cookie);
} else {
ConfirmDialog confirm(*m_main_window);
confirm.SetConfirmText("Cookies could not be fetched. This may increase your chances of being flagged by Discord's anti-spam");
confirm.SetAcceptOnly(true);
confirm.run();
}
if (build_number.has_value()) {
m_discord.SetBuildNumber(*build_number);
} else {
ConfirmDialog confirm(*m_main_window);
confirm.SetConfirmText("Build number could not be fetched. This may increase your chances of being flagged by Discord's anti-spam");
confirm.SetAcceptOnly(true);
confirm.run();
}
// autoconnect
if (cookie.has_value() && build_number.has_value() && GetSettings().Autoconnect && !GetDiscordToken().empty()) {
ActionConnect();
}
});
}
void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) { void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
VerificationGateDialog dlg(*m_main_window, guild_id); VerificationGateDialog dlg(*m_main_window, guild_id);
if (dlg.run() == Gtk::RESPONSE_OK) { if (dlg.run() == Gtk::RESPONSE_OK) {
@ -468,7 +537,7 @@ void Abaddon::SetupUserMenu() {
m_user_menu_ban = Gtk::manage(new Gtk::MenuItem("Ban")); m_user_menu_ban = Gtk::manage(new Gtk::MenuItem("Ban"));
m_user_menu_kick = Gtk::manage(new Gtk::MenuItem("Kick")); 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_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_roles = Gtk::manage(new Gtk::MenuItem("Roles"));
m_user_menu_info = Gtk::manage(new Gtk::MenuItem("View Profile")); m_user_menu_info = Gtk::manage(new Gtk::MenuItem("View Profile"));
m_user_menu_remove_recipient = Gtk::manage(new Gtk::MenuItem("Remove From Group")); m_user_menu_remove_recipient = Gtk::manage(new Gtk::MenuItem("Remove From Group"));
@ -579,18 +648,9 @@ void Abaddon::on_user_menu_copy_id() {
void Abaddon::on_user_menu_open_dm() { void Abaddon::on_user_menu_open_dm() {
const auto existing = m_discord.FindDM(m_shown_user_menu_id); const auto existing = m_discord.FindDM(m_shown_user_menu_id);
if (existing.has_value()) if (existing.has_value()) {
ActionChannelOpened(*existing); 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() { void Abaddon::on_user_menu_remove_recipient() {
@ -646,22 +706,18 @@ void Abaddon::ActionSetToken() {
m_main_window->UpdateMenus(); m_main_window->UpdateMenus();
} }
void Abaddon::ActionJoinGuildDialog() {
JoinGuildDialog dlg(*m_main_window);
auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) {
auto code = dlg.GetCode();
m_discord.JoinGuild(code);
}
}
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) { 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(""); m_main_window->GetChatWindow()->SetTopic("");
const auto channel = m_discord.GetChannel(id); const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) { if (!channel.has_value()) {
m_discord.SetReferringChannel(Snowflake::Invalid);
m_main_window->UpdateChatActiveChannel(Snowflake::Invalid, false); m_main_window->UpdateChatActiveChannel(Snowflake::Invalid, false);
m_main_window->UpdateChatWindowContents(); m_main_window->UpdateChatWindowContents();
return; return;
@ -710,6 +766,7 @@ void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
} }
m_main_window->UpdateMenus(); m_main_window->UpdateMenus();
m_discord.SetReferringChannel(id);
} }
void Abaddon::ActionChatLoadHistory(Snowflake id) { void Abaddon::ActionChatLoadHistory(Snowflake id) {
@ -898,6 +955,21 @@ EmojiResource &Abaddon::GetEmojis() {
return m_emojis; return m_emojis;
} }
void Abaddon::on_tray_click() {
m_main_window->set_visible(!m_main_window->is_visible());
}
void Abaddon::on_tray_menu_click() {
m_gtk_app->quit();
}
void Abaddon::on_tray_popup_menu(int button, int activate_time) {
m_tray->popup_menu_at_position(*m_tray_menu, button, activate_time);
}
void Abaddon::on_window_hide() {
if (!m_settings.GetSettings().HideToTray) {
m_gtk_app->quit();
}
}
int main(int argc, char **argv) { int main(int argc, char **argv) {
if (std::getenv("ABADDON_NO_FC") == nullptr) if (std::getenv("ABADDON_NO_FC") == nullptr)
Platform::SetupFonts(); Platform::SetupFonts();

View File

@ -1,3 +1,4 @@
#pragma once
#include <gtkmm.h> #include <gtkmm.h>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@ -93,6 +94,8 @@ public:
static std::string GetStateCachePath(const std::string &path); static std::string GetStateCachePath(const std::string &path);
protected: protected:
void RunFirstTimeDiscordStartup();
void ShowGuildVerificationGateDialog(Snowflake guild_id); void ShowGuildVerificationGateDialog(Snowflake guild_id);
void CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs); void CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs);
@ -114,6 +117,8 @@ protected:
Gtk::MenuItem *m_user_menu_roles; Gtk::MenuItem *m_user_menu_roles;
Gtk::MenuItem *m_user_menu_remove_recipient; Gtk::MenuItem *m_user_menu_remove_recipient;
Gtk::Menu *m_user_menu_roles_submenu; Gtk::Menu *m_user_menu_roles_submenu;
Gtk::Menu *m_tray_menu;
Gtk::MenuItem *m_tray_exit;
void on_user_menu_insert_mention(); void on_user_menu_insert_mention();
void on_user_menu_ban(); void on_user_menu_ban();
@ -121,6 +126,10 @@ protected:
void on_user_menu_copy_id(); void on_user_menu_copy_id();
void on_user_menu_open_dm(); void on_user_menu_open_dm();
void on_user_menu_remove_recipient(); void on_user_menu_remove_recipient();
void on_tray_click();
void on_tray_popup_menu(int button, int activate_time);
void on_tray_menu_click();
void on_window_hide();
private: private:
SettingsManager m_settings; SettingsManager m_settings;
@ -139,5 +148,6 @@ private:
Glib::RefPtr<Gtk::Application> m_gtk_app; Glib::RefPtr<Gtk::Application> m_gtk_app;
Glib::RefPtr<Gtk::CssProvider> m_css_provider; Glib::RefPtr<Gtk::CssProvider> m_css_provider;
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you Glib::RefPtr<Gtk::StatusIcon> m_tray;
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
}; };

View File

@ -176,11 +176,10 @@ bool ChatInputTextContainer::GetChildPosition(Gtk::Widget *child, Gdk::Rectangle
Gtk::Requisition min, req; Gtk::Requisition min, req;
child->get_preferred_size(min, req); child->get_preferred_size(min, req);
// yummy hardcoded values // let css move it around
pos.set_x(5); 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_width(std::max(min.width, std::min(main_alloc.get_width(), req.width)));
pos.set_y(12);
pos.set_height(std::max(min.height, std::min(main_alloc.get_height(), req.height))); pos.set_height(std::max(min.height, std::min(main_alloc.get_height(), req.height)));
return true; return true;

View File

@ -32,7 +32,6 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
if (!data.Content.empty() || data.Type != MessageType::DEFAULT) { if (!data.Content.empty() || data.Type != MessageType::DEFAULT) {
container->m_text_component = container->CreateTextComponent(data); container->m_text_component = container->CreateTextComponent(data);
container->AttachEventHandlers(*container->m_text_component);
container->m_main.add(*container->m_text_component); container->m_main.add(*container->m_text_component);
} }
@ -101,7 +100,6 @@ void ChatMessageItemContainer::UpdateContent() {
if (!data->Embeds.empty()) { if (!data->Embeds.empty()) {
m_embed_component = CreateEmbedsComponent(data->Embeds); m_embed_component = CreateEmbedsComponent(data->Embeds);
AttachEventHandlers(*m_embed_component);
m_main.add(*m_embed_component); m_main.add(*m_embed_component);
m_embed_component->show_all(); m_embed_component->show_all();
} }
@ -152,12 +150,12 @@ void ChatMessageItemContainer::UpdateAttributes() {
void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, const std::string &url) { void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, const std::string &url) {
// clang-format off // clang-format off
widget->signal_button_press_event().connect([url](GdkEventButton *event) -> bool { widget->signal_button_release_event().connect([url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url); LaunchBrowser(url);
return false; return true;
} }
return true; return false;
}, false); }, false);
// clang-format on // clang-format on
} }
@ -174,6 +172,8 @@ Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data
tv->set_halign(Gtk::ALIGN_FILL); tv->set_halign(Gtk::ALIGN_FILL);
tv->set_hexpand(true); tv->set_hexpand(true);
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnTextViewButtonPress), false);
UpdateTextComponent(tv); UpdateTextComponent(tv);
return tv; return tv;
@ -281,8 +281,6 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
tag->property_weight() = Pango::WEIGHT_BOLD; tag->property_weight() = Pango::WEIGHT_BOLD;
m_channel_tagmap[tag] = *data->MessageReference->ChannelID; m_channel_tagmap[tag] = *data->MessageReference->ChannelID;
b->insert_with_tag(iter, data->Content, tag); b->insert_with_tag(iter, data->Content, tag);
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
} else { } else {
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>"); b->insert_markup(s, "<i><span color='#999999'>" + author->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)) { if (IsEmbedImageOnly(embed)) {
auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height); auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height);
widget->show(); widget->show();
AttachEventHandlers(*widget);
box->add(*widget); box->add(*widget);
} else { } else {
auto *widget = CreateEmbedComponent(embed); auto *widget = CreateEmbedComponent(embed);
widget->show(); widget->show();
AttachEventHandlers(*widget);
box->add(*widget); box->add(*widget);
} }
} }
@ -361,8 +357,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &emb
if (embed.URL.has_value()) { if (embed.URL.has_value()) {
AddPointerCursor(*title_ev); AddPointerCursor(*title_ev);
auto url = *embed.URL; auto url = *embed.URL;
title_ev->signal_button_press_event().connect([url = std::move(url)](GdkEventButton *event) -> bool { title_ev->signal_button_release_event().connect([url = std::move(url)](GdkEventButton *event) -> bool {
if (event->button == GDK_BUTTON_PRIMARY) { if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url); LaunchBrowser(url);
return true; return true;
} }
@ -493,12 +489,22 @@ Gtk::Widget *ChatMessageItemContainer::CreateImageComponent(const std::string &p
Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox); Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox);
Gtk::Image *widget = Gtk::manage(new LazyImage(proxy_url, w, h, false)); Gtk::Image *widget = Gtk::manage(new LazyImage(proxy_url, w, h, false));
ev->add(*widget); ev->add(*widget);
ev->set_halign(Gtk::ALIGN_START);
widget->set_halign(Gtk::ALIGN_START); widget->set_halign(Gtk::ALIGN_START);
widget->set_size_request(w, h); widget->set_size_request(w, h);
AttachEventHandlers(*ev);
AddClickHandler(ev, url); 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; return ev;
} }
@ -510,9 +516,18 @@ Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const Attachmen
ev->get_style_context()->add_class("message-attachment-box"); ev->get_style_context()->add_class("message-attachment-box");
ev->add(*btn); ev->add(*btn);
AttachEventHandlers(*ev);
AddClickHandler(ev, data.URL); 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; return ev;
} }
@ -534,7 +549,6 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector
box->show(); box->show();
AttachEventHandlers(*box);
return box; return box;
} }
@ -641,13 +655,19 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
const auto role = discord.GetRole(role_id); const auto role = discord.GetRole(role_id);
if (role.has_value()) { if (role.has_value()) {
const auto author = discord.GetUser(author_id); const auto author = discord.GetUser(author_id);
return "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">" + author->GetEscapedString() + "</span></b>"; if (author.has_value()) {
return "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">" + author->GetEscapedString() + "</span></b>";
}
} }
} }
} }
const auto author = discord.GetUser(author_id); const auto author = discord.GetUser(author_id);
return author->GetEscapedBoldString<false>(); if (author.has_value()) {
return author->GetEscapedBoldString<false>();
}
return "<b>Unknown User</b>";
}; };
// if the message wasnt fetched from store it might have an un-fetched reference // if the message wasnt fetched from store it might have an un-fetched reference
@ -659,15 +679,15 @@ Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data)
} }
if (data.Interaction.has_value()) { if (data.Interaction.has_value()) {
const auto user = *discord.GetUser(data.Interaction->User.ID);
if (data.GuildID.has_value()) { if (data.GuildID.has_value()) {
lbl->set_markup(get_author_markup(user.ID, *data.GuildID) + lbl->set_markup(get_author_markup(data.Interaction->User.ID, *data.GuildID) +
" used <span color='#697ec4'>/" + " used <span color='#697ec4'>/" +
Glib::Markup::escape_text(data.Interaction->Name) + Glib::Markup::escape_text(data.Interaction->Name) +
"</span>"); "</span>");
} else if (const auto user = discord.GetUser(data.Interaction->User.ID); user.has_value()) {
lbl->set_markup(user->GetEscapedBoldString<false>());
} else { } else {
lbl->set_markup(user.GetEscapedBoldString<false>()); lbl->set_markup("<b>Unknown User</b>");
} }
} else if (referenced_message.has_value()) { } else if (referenced_message.has_value()) {
if (referenced_message.value() == nullptr) { if (referenced_message.value() == nullptr) {
@ -956,7 +976,6 @@ void ChatMessageItemContainer::HandleChannelMentions(const Glib::RefPtr<Gtk::Tex
} }
void ChatMessageItemContainer::HandleChannelMentions(Gtk::TextView *tv) { void ChatMessageItemContainer::HandleChannelMentions(Gtk::TextView *tv) {
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
HandleChannelMentions(tv->get_buffer()); HandleChannelMentions(tv->get_buffer());
} }
@ -990,6 +1009,20 @@ bool ChatMessageItemContainer::OnClickChannel(GdkEventButton *ev) {
return false; 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() { void ChatMessageItemContainer::on_link_menu_copy() {
Gtk::Clipboard::get()->set_text(m_selected_link); Gtk::Clipboard::get()->set_text(m_selected_link);
} }
@ -997,8 +1030,6 @@ void ChatMessageItemContainer::on_link_menu_copy() {
void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) { void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) {
const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)"); const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)");
tv.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnLinkClick), false);
auto buf = tv.get_buffer(); auto buf = tv.get_buffer();
Glib::ustring text = GetText(buf); Glib::ustring text = GetText(buf);
@ -1070,18 +1101,6 @@ ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemCont
return m_signal_action_reaction_remove; 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) ChatMessageHeader::ChatMessageHeader(const Message &data)
: m_main_box(Gtk::ORIENTATION_HORIZONTAL) : m_main_box(Gtk::ORIENTATION_HORIZONTAL)
, m_content_box(Gtk::ORIENTATION_VERTICAL) , m_content_box(Gtk::ORIENTATION_VERTICAL)

View File

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

View File

@ -34,3 +34,7 @@ ConfirmDialog::ConfirmDialog(Gtk::Window &parent)
void ConfirmDialog::SetConfirmText(const Glib::ustring &text) { void ConfirmDialog::SetConfirmText(const Glib::ustring &text) {
m_label.set_text(text); m_label.set_text(text);
} }
void ConfirmDialog::SetAcceptOnly(bool accept_only) {
m_cancel.set_visible(!accept_only);
}

View File

@ -5,6 +5,7 @@ class ConfirmDialog : public Gtk::Dialog {
public: public:
ConfirmDialog(Gtk::Window &parent); ConfirmDialog(Gtk::Window &parent);
void SetConfirmText(const Glib::ustring &text); void SetConfirmText(const Glib::ustring &text);
void SetAcceptOnly(bool accept_only);
protected: protected:
Gtk::Label m_label; Gtk::Label m_label;

View File

@ -1,97 +0,0 @@
#include "joinguild.hpp"
#include "abaddon.hpp"
#include <nlohmann/json.hpp>
#include <regex>
JoinGuildDialog::JoinGuildDialog(Gtk::Window &parent)
: Gtk::Dialog("Join Server", parent, true)
, m_layout(Gtk::ORIENTATION_VERTICAL)
, m_ok("OK")
, m_cancel("Cancel")
, m_info("Enter code") {
set_default_size(300, 50);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
Glib::signal_idle().connect(sigc::mem_fun(*this, &JoinGuildDialog::on_idle_slot));
m_entry.signal_changed().connect(sigc::mem_fun(*this, &JoinGuildDialog::on_entry_changed));
m_ok.set_sensitive(false);
m_ok.signal_clicked().connect([&]() {
response(Gtk::RESPONSE_OK);
});
m_cancel.signal_clicked().connect([&]() {
response(Gtk::RESPONSE_CANCEL);
});
m_entry.set_hexpand(true);
m_layout.add(m_entry);
m_lower.set_hexpand(true);
m_lower.pack_start(m_info);
m_info.set_halign(Gtk::ALIGN_START);
m_lower.pack_start(m_ok, Gtk::PACK_SHRINK);
m_lower.pack_start(m_cancel, Gtk::PACK_SHRINK);
m_ok.set_halign(Gtk::ALIGN_END);
m_cancel.set_halign(Gtk::ALIGN_END);
m_layout.add(m_lower);
get_content_area()->add(m_layout);
show_all_children();
}
void JoinGuildDialog::on_entry_changed() {
std::string s = m_entry.get_text();
std::regex invite_regex(R"((https?:\/\/)?discord\.(gg(\/invite)?\/|com\/invite\/)([A-Za-z0-9\-]+))", std::regex_constants::ECMAScript);
std::smatch match;
bool full_url = std::regex_search(s, match, invite_regex);
if (full_url || IsCode(s)) {
m_code = full_url ? match[4].str() : s;
m_needs_request = true;
m_ok.set_sensitive(false);
} else {
m_ok.set_sensitive(false);
}
}
void JoinGuildDialog::CheckCode() {
auto cb = [this](const std::optional<InviteData> &invite) {
if (invite.has_value()) {
m_ok.set_sensitive(true);
if (invite->Guild.has_value()) {
if (invite->MemberCount.has_value())
m_info.set_text(invite->Guild->Name + " (" + std::to_string(*invite->MemberCount) + " members)");
else
m_info.set_text(invite->Guild->Name);
} else {
m_info.set_text("Group DM (" + std::to_string(*invite->MemberCount) + " members)");
}
} else {
m_ok.set_sensitive(false);
m_info.set_text("Invalid invite");
}
};
Abaddon::Get().GetDiscordClient().FetchInvite(m_code, sigc::track_obj(cb, *this));
}
bool JoinGuildDialog::IsCode(std::string str) {
return str.length() >= 2 && std::all_of(str.begin(), str.end(), [](char c) -> bool { return std::isalnum(c) || c == '-'; });
}
std::string JoinGuildDialog::GetCode() {
return m_code;
}
static const constexpr int RateLimitMS = 1500;
bool JoinGuildDialog::on_idle_slot() {
const auto now = std::chrono::steady_clock::now();
if (m_needs_request && ((now - m_last_req_time) > std::chrono::milliseconds(RateLimitMS))) {
m_needs_request = false;
m_last_req_time = now;
CheckCode();
}
return true;
}

View File

@ -1,31 +0,0 @@
#pragma once
#include <gtkmm.h>
#include <string>
#include <chrono>
class JoinGuildDialog : public Gtk::Dialog {
public:
JoinGuildDialog(Gtk::Window &parent);
std::string GetCode();
protected:
void on_entry_changed();
static bool IsCode(std::string str);
Gtk::Box m_layout;
Gtk::Button m_ok;
Gtk::Button m_cancel;
Gtk::Box m_lower;
Gtk::Label m_info;
Gtk::Entry m_entry;
void CheckCode();
// needs a rate limit cuz if u hit it u get ip banned from /invites for a long time :(
bool m_needs_request = false;
std::chrono::time_point<std::chrono::steady_clock> m_last_req_time;
bool on_idle_slot();
private:
std::string m_code;
};

View File

@ -30,6 +30,7 @@ void DiscordClient::Start() {
if (m_client_started) return; if (m_client_started) return;
m_http.SetBase(GetAPIURL()); m_http.SetBase(GetAPIURL());
SetHeaders();
std::memset(&m_zstream, 0, sizeof(m_zstream)); std::memset(&m_zstream, 0, sizeof(m_zstream));
inflateInit2(&m_zstream, MAX_WBITS + 32); inflateInit2(&m_zstream, MAX_WBITS + 32);
@ -572,10 +573,6 @@ void DiscordClient::SendThreadLazyLoad(Snowflake id) {
m_websocket.Send(msg); m_websocket.Send(msg);
} }
void DiscordClient::JoinGuild(const std::string &code) {
m_http.MakePOST("/invites/" + code, "{}", [](auto) {});
}
void DiscordClient::LeaveGuild(Snowflake id) { void DiscordClient::LeaveGuild(Snowflake id) {
m_http.MakeDELETE("/users/@me/guilds/" + std::to_string(id), [](auto) {}); m_http.MakeDELETE("/users/@me/guilds/" + std::to_string(id), [](auto) {});
} }
@ -610,19 +607,6 @@ void DiscordClient::UpdateStatus(PresenceStatus status, bool is_afk, const Activ
m_signal_presence_update.emit(GetUserData(), status); 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) { void DiscordClient::CloseDM(Snowflake channel_id) {
m_http.MakeDELETE("/channels/" + std::to_string(channel_id), [](const http::response &response) { m_http.MakeDELETE("/channels/" + std::to_string(channel_id), [](const http::response &response) {
CheckCode(response); CheckCode(response);
@ -1181,6 +1165,33 @@ 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::SetBuildNumber(uint32_t build_number) {
m_build_number = build_number;
}
void DiscordClient::SetCookie(std::string_view cookie) {
m_http.SetCookie(cookie);
}
void DiscordClient::UpdateToken(const std::string &token) { void DiscordClient::UpdateToken(const std::string &token) {
if (!IsStarted()) { if (!IsStarted()) {
m_token = token; m_token = token;
@ -2294,7 +2305,7 @@ void DiscordClient::HeartbeatThread() {
void DiscordClient::SendIdentify() { void DiscordClient::SendIdentify() {
IdentifyMessage msg; IdentifyMessage msg;
msg.Token = m_token; 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.OS = "Windows";
msg.Properties.Browser = "Chrome"; msg.Properties.Browser = "Chrome";
msg.Properties.Device = ""; msg.Properties.Device = "";
@ -2307,7 +2318,7 @@ void DiscordClient::SendIdentify() {
msg.Properties.ReferrerCurrent = ""; msg.Properties.ReferrerCurrent = "";
msg.Properties.ReferringDomainCurrent = ""; msg.Properties.ReferringDomainCurrent = "";
msg.Properties.ReleaseChannel = "stable"; msg.Properties.ReleaseChannel = "stable";
msg.Properties.ClientBuildNumber = 105691; msg.Properties.ClientBuildNumber = m_build_number;
msg.Properties.ClientEventSource = ""; msg.Properties.ClientEventSource = "";
msg.Presence.Status = "online"; msg.Presence.Status = "online";
msg.Presence.Since = 0; msg.Presence.Since = 0;
@ -2316,6 +2327,7 @@ void DiscordClient::SendIdentify() {
msg.ClientState.HighestLastMessageID = "0"; msg.ClientState.HighestLastMessageID = "0";
msg.ClientState.ReadStateVersion = 0; msg.ClientState.ReadStateVersion = 0;
msg.ClientState.UserGuildSettingsVersion = -1; msg.ClientState.UserGuildSettingsVersion = -1;
SetSuperPropertiesFromIdentity(msg);
const bool b = m_websocket.GetPrintMessages(); const bool b = m_websocket.GetPrintMessages();
m_websocket.SetPrintMessages(false); m_websocket.SetPrintMessages(false);
m_websocket.Send(msg); m_websocket.Send(msg);
@ -2330,6 +2342,36 @@ void DiscordClient::SendResume() {
m_websocket.Send(msg); 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() { void DiscordClient::HandleSocketOpen() {
} }
@ -2395,9 +2437,11 @@ void DiscordClient::StoreMessageData(Message &msg) {
if (msg.Member.has_value()) if (msg.Member.has_value())
m_store.SetGuildMember(*msg.GuildID, msg.Author.ID, *msg.Member); m_store.SetGuildMember(*msg.GuildID, msg.Author.ID, *msg.Member);
if (msg.Interaction.has_value() && msg.Interaction->Member.has_value()) { if (msg.Interaction.has_value()) {
m_store.SetUser(msg.Interaction->User.ID, msg.Interaction->User); m_store.SetUser(msg.Interaction->User.ID, msg.Interaction->User);
m_store.SetGuildMember(*msg.GuildID, msg.Interaction->User.ID, *msg.Interaction->Member); if (msg.Interaction->Member.has_value()) {
m_store.SetGuildMember(*msg.GuildID, msg.Interaction->User.ID, *msg.Interaction->Member);
}
} }
m_store.EndTransaction(); m_store.EndTransaction();

View File

@ -113,13 +113,11 @@ public:
void EditMessage(Snowflake channel_id, Snowflake id, std::string content); void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id); void SendLazyLoad(Snowflake id);
void SendThreadLazyLoad(Snowflake id); void SendThreadLazyLoad(Snowflake id);
void JoinGuild(const std::string &code);
void LeaveGuild(Snowflake id); void LeaveGuild(Snowflake id);
void KickUser(Snowflake user_id, Snowflake guild_id); void KickUser(Snowflake user_id, Snowflake guild_id);
void BanUser(Snowflake user_id, Snowflake guild_id); // todo: reason, delete messages 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);
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj); 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); void CloseDM(Snowflake channel_id);
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
void AddReaction(Snowflake id, Glib::ustring param); void AddReaction(Snowflake id, Glib::ustring param);
@ -205,6 +203,11 @@ public:
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback); void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback); void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
void SetReferringChannel(Snowflake id);
void SetBuildNumber(uint32_t build_number);
void SetCookie(std::string_view cookie);
void UpdateToken(const std::string &token); void UpdateToken(const std::string &token);
void SetUserAgent(const std::string &agent); void SetUserAgent(const std::string &agent);
@ -286,6 +289,9 @@ private:
void SendIdentify(); void SendIdentify();
void SendResume(); void SendResume();
void SetHeaders();
void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity);
void HandleSocketOpen(); void HandleSocketOpen();
void HandleSocketClose(uint16_t code); void HandleSocketClose(uint16_t code);
@ -299,6 +305,8 @@ private:
std::string m_token; std::string m_token;
uint32_t m_build_number = 142000;
void AddUserToGuild(Snowflake user_id, Snowflake guild_id); void AddUserToGuild(Snowflake user_id, Snowflake guild_id);
std::map<Snowflake, std::set<Snowflake>> m_guild_to_users; std::map<Snowflake, std::set<Snowflake>> m_guild_to_users;
std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels; std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels;

View File

@ -2,7 +2,6 @@
#include <utility> #include <utility>
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient() { HTTPClient::HTTPClient() {
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks)); m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
} }
@ -19,16 +18,22 @@ void HTTPClient::SetAuth(std::string auth) {
m_authorization = std::move(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::SetCookie(std::string_view cookie) {
m_cookie = cookie;
}
void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb) { void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb) {
printf("DELETE %s\n", path.c_str()); printf("DELETE %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] { m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_DELETE, m_api_base + path); http::request req(http::REQUEST_DELETE, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization); req.set_header("Authorization", m_authorization);
req.set_header("Origin", "https://discord.com");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon"); 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
auto res = req.execute(); auto res = req.execute();
@ -40,14 +45,12 @@ void HTTPClient::MakePATCH(const std::string &path, const std::string &payload,
printf("PATCH %s\n", path.c_str()); printf("PATCH %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] { m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PATCH, m_api_base + path); http::request req(http::REQUEST_PATCH, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization); req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json"); 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_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_body(payload); req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute(); auto res = req.execute();
@ -59,14 +62,12 @@ void HTTPClient::MakePOST(const std::string &path, const std::string &payload, c
printf("POST %s\n", path.c_str()); printf("POST %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] { m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_POST, m_api_base + path); http::request req(http::REQUEST_POST, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization); req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json"); 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_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_body(payload); req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute(); auto res = req.execute();
@ -78,15 +79,13 @@ void HTTPClient::MakePUT(const std::string &path, const std::string &payload, co
printf("PUT %s\n", path.c_str()); printf("PUT %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] { m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PUT, m_api_base + path); http::request req(http::REQUEST_PUT, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization); req.set_header("Authorization", m_authorization);
req.set_header("Origin", "https://discord.com");
if (!payload.empty()) if (!payload.empty())
req.set_header("Content-Type", "application/json"); req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon"); req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
req.set_body(payload); req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute(); auto res = req.execute();
@ -98,13 +97,9 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
printf("GET %s\n", path.c_str()); printf("GET %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] { m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_GET, m_api_base + path); http::request req(http::REQUEST_GET, m_api_base + path);
AddHeaders(req);
req.set_header("Authorization", m_authorization); req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon"); 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
auto res = req.execute(); auto res = req.execute();
@ -116,10 +111,6 @@ http::request HTTPClient::CreateRequest(http::EMethod method, std::string path)
http::request req(method, m_api_base + path); http::request req(method, m_api_base + path);
req.set_header("Authorization", m_authorization); req.set_header("Authorization", m_authorization);
req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon"); 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; return req;
} }
@ -147,6 +138,14 @@ void HTTPClient::RunCallbacks() {
m_mutex.unlock(); 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_COOKIE, m_cookie.c_str());
curl_easy_setopt(r.get_curl(), CURLOPT_ACCEPT_ENCODING, "");
}
void HTTPClient::OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb) { void HTTPClient::OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb) {
CleanupFutures(); CleanupFutures();
try { try {

View File

@ -17,6 +17,9 @@ public:
void SetUserAgent(std::string agent); void SetUserAgent(std::string agent);
void SetAuth(std::string auth); void SetAuth(std::string auth);
void SetPersistentHeader(std::string name, std::string value);
void SetCookie(std::string_view cookie);
void MakeDELETE(const std::string &path, const std::function<void(http::response_type r)> &cb); 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 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 MakePATCH(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
@ -27,6 +30,8 @@ public:
void Execute(http::request &&req, const std::function<void(http::response_type r)> &cb); void Execute(http::request &&req, const std::function<void(http::response_type r)> &cb);
private: private:
void AddHeaders(http::request &r);
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb); void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
void CleanupFutures(); void CleanupFutures();
@ -39,4 +44,6 @@ private:
std::string m_api_base; std::string m_api_base;
std::string m_authorization; std::string m_authorization;
std::string m_agent; std::string m_agent;
std::unordered_map<std::string, std::string> m_headers;
std::string m_cookie;
}; };

View File

@ -262,6 +262,7 @@ void to_json(nlohmann::json &j, const ClientStateProperties &m) {
j["highest_last_message_id"] = m.HighestLastMessageID; j["highest_last_message_id"] = m.HighestLastMessageID;
j["read_state_version"] = m.ReadStateVersion; j["read_state_version"] = m.ReadStateVersion;
j["user_guild_settings_version"] = m.UserGuildSettingsVersion; j["user_guild_settings_version"] = m.UserGuildSettingsVersion;
j["user_settings_version"] = m.UserSettingsVersion;
} }
void to_json(nlohmann::json &j, const IdentifyMessage &m) { void to_json(nlohmann::json &j, const IdentifyMessage &m) {

View File

@ -382,6 +382,7 @@ struct ClientStateProperties {
std::string HighestLastMessageID = "0"; std::string HighestLastMessageID = "0";
int ReadStateVersion = 0; int ReadStateVersion = 0;
int UserGuildSettingsVersion = -1; int UserGuildSettingsVersion = -1;
int UserSettingsVersion = -1;
friend void to_json(nlohmann::json &j, const ClientStateProperties &m); 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(); 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; auto &s = m_stmt_set_member_roles;
@ -1882,6 +1890,20 @@ bool Store::CreateStatements() {
return false; 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"( m_stmt_set_guild_emoji = std::make_unique<Statement>(m_db, R"(
REPLACE INTO guild_emojis VALUES ( REPLACE INTO guild_emojis VALUES (
?, ? ?, ?

View File

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

View File

@ -8,33 +8,5 @@ void from_json(const nlohmann::json &j, UserSettingsGuildFoldersEntry &m) {
} }
void from_json(const nlohmann::json &j, UserSettings &m) { void from_json(const nlohmann::json &j, UserSettings &m) {
JS_D("timezone_offset", m.TimezoneOffset);
JS_D("theme", m.Theme);
JS_D("stream_notifications_enabled", m.AreStreamNotificationsEnabled);
JS_D("status", m.Status);
JS_D("show_current_game", m.ShouldShowCurrentGame);
// JS_D("restricted_guilds", m.RestrictedGuilds);
JS_D("render_reactions", m.ShouldRenderReactions);
JS_D("render_embeds", m.ShouldRenderEmbeds);
JS_D("native_phone_integration_enabled", m.IsNativePhoneIntegrationEnabled);
JS_D("message_display_compact", m.ShouldMessageDisplayCompact);
JS_D("locale", m.Locale);
JS_D("inline_embed_media", m.ShouldInlineEmbedMedia);
JS_D("inline_attachment_media", m.ShouldInlineAttachmentMedia);
JS_D("guild_positions", m.GuildPositions);
JS_D("guild_folders", m.GuildFolders); JS_D("guild_folders", m.GuildFolders);
JS_D("gif_auto_play", m.ShouldGIFAutoplay);
// JS_D("friend_source_flags", m.FriendSourceFlags);
JS_D("explicit_content_filter", m.ExplicitContentFilter);
JS_D("enable_tts_command", m.IsTTSCommandEnabled);
JS_D("disable_games_tab", m.ShouldDisableGamesTab);
JS_D("developer_mode", m.DeveloperMode);
JS_D("detect_platform_accounts", m.ShouldDetectPlatformAccounts);
JS_D("default_guilds_restricted", m.AreDefaultGuildsRestricted);
// JS_N("custom_status", m.CustomStatus);
JS_D("convert_emoticons", m.ShouldConvertEmoticons);
JS_D("contact_sync_enabled", m.IsContactSyncEnabled);
JS_D("animate_emoji", m.ShouldAnimateEmojis);
JS_D("allow_accessibility_detection", m.IsAccessibilityDetectionAllowed);
JS_D("afk_timeout", m.AFKTimeout);
} }

View File

@ -13,35 +13,36 @@ struct UserSettingsGuildFoldersEntry {
}; };
struct UserSettings { struct UserSettings {
int TimezoneOffset; // std::vector<UserSettingsGuildFoldersEntry> GuildFolders;
std::string Theme; // /*
bool AreStreamNotificationsEnabled; // int TimezoneOffset;
std::string Status; // std::string Theme;
bool ShouldShowCurrentGame; // bool AreStreamNotificationsEnabled;
// std::vector<Unknown> RestrictedGuilds; // std::string Status;
bool ShouldRenderReactions; // bool ShouldShowCurrentGame;
bool ShouldRenderEmbeds; // // std::vector<Unknown> RestrictedGuilds;
bool IsNativePhoneIntegrationEnabled; // bool ShouldRenderReactions;
bool ShouldMessageDisplayCompact; // bool ShouldRenderEmbeds;
std::string Locale; // bool IsNativePhoneIntegrationEnabled;
bool ShouldInlineEmbedMedia; // bool ShouldMessageDisplayCompact;
bool ShouldInlineAttachmentMedia; // std::string Locale;
std::vector<Snowflake> GuildPositions; // deprecated? bool ShouldInlineEmbedMedia;
std::vector<UserSettingsGuildFoldersEntry> GuildFolders; // bool ShouldInlineAttachmentMedia;
bool ShouldGIFAutoplay; // std::vector<Snowflake> GuildPositions; // deprecated?
// Unknown FriendSourceFlags; // bool ShouldGIFAutoplay;
int ExplicitContentFilter; // // Unknown FriendSourceFlags;
bool IsTTSCommandEnabled; // int ExplicitContentFilter;
bool ShouldDisableGamesTab; // bool IsTTSCommandEnabled;
bool DeveloperMode; // bool ShouldDisableGamesTab;
bool ShouldDetectPlatformAccounts; // bool DeveloperMode;
bool AreDefaultGuildsRestricted; // bool ShouldDetectPlatformAccounts;
bool AreDefaultGuildsRestricted;
// Unknown CustomStatus; // null // Unknown CustomStatus; // null
bool ShouldConvertEmoticons; // bool ShouldConvertEmoticons;
bool IsContactSyncEnabled; // bool IsContactSyncEnabled;
bool ShouldAnimateEmojis; // bool ShouldAnimateEmojis;
bool IsAccessibilityDetectionAllowed; // bool IsAccessibilityDetectionAllowed;
int AFKTimeout; int AFKTimeout;*/
friend void from_json(const nlohmann::json &j, UserSettings &m); friend void from_json(const nlohmann::json &j, UserSettings &m);
}; };

View File

@ -2,6 +2,22 @@
#include <sstream> #include <sstream>
#include <utility> #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) EmojiResource::EmojiResource(std::string filepath)
: m_filepath(std::move(filepath)) {} : m_filepath(std::move(filepath)) {}
@ -11,18 +27,22 @@ bool EmojiResource::Load() {
int index_offset; int index_offset;
std::fread(&index_offset, 4, 1, m_fp); std::fread(&index_offset, 4, 1, m_fp);
index_offset = emojis_int32_correct_endian(index_offset);
std::fseek(m_fp, index_offset, SEEK_SET); std::fseek(m_fp, index_offset, SEEK_SET);
int emojis_count; int emojis_count;
std::fread(&emojis_count, 4, 1, m_fp); std::fread(&emojis_count, 4, 1, m_fp);
emojis_count = emojis_int32_correct_endian(emojis_count);
for (int i = 0; i < emojis_count; i++) { for (int i = 0; i < emojis_count; i++) {
std::vector<std::string> shortcodes; std::vector<std::string> shortcodes;
int shortcodes_count; int shortcodes_count;
std::fread(&shortcodes_count, 4, 1, m_fp); std::fread(&shortcodes_count, 4, 1, m_fp);
shortcodes_count = emojis_int32_correct_endian(shortcodes_count);
for (int j = 0; j < shortcodes_count; j++) { for (int j = 0; j < shortcodes_count; j++) {
int shortcode_length; int shortcode_length;
std::fread(&shortcode_length, 4, 1, m_fp); std::fread(&shortcode_length, 4, 1, m_fp);
shortcode_length = emojis_int32_correct_endian(shortcode_length);
std::string shortcode(shortcode_length, '\0'); std::string shortcode(shortcode_length, '\0');
std::fread(shortcode.data(), shortcode_length, 1, m_fp); std::fread(shortcode.data(), shortcode_length, 1, m_fp);
shortcodes.push_back(std::move(shortcode)); shortcodes.push_back(std::move(shortcode));
@ -30,13 +50,16 @@ bool EmojiResource::Load() {
int surrogates_count; int surrogates_count;
std::fread(&surrogates_count, 4, 1, m_fp); std::fread(&surrogates_count, 4, 1, m_fp);
surrogates_count = emojis_int32_correct_endian(surrogates_count);
std::string surrogates(surrogates_count, '\0'); std::string surrogates(surrogates_count, '\0');
std::fread(surrogates.data(), surrogates_count, 1, m_fp); std::fread(surrogates.data(), surrogates_count, 1, m_fp);
m_patterns.emplace_back(surrogates); m_patterns.emplace_back(surrogates);
int data_size, data_offset; int data_size, data_offset;
std::fread(&data_size, 4, 1, m_fp); std::fread(&data_size, 4, 1, m_fp);
data_size = emojis_int32_correct_endian(data_size);
std::fread(&data_offset, 4, 1, m_fp); std::fread(&data_offset, 4, 1, m_fp);
data_offset = emojis_int32_correct_endian(data_offset);
m_index[surrogates] = { data_offset, data_size }; m_index[surrogates] = { data_offset, data_size };
for (const auto &shortcode : shortcodes) for (const auto &shortcode : shortcodes)

View File

@ -2,6 +2,8 @@
#include <utility> #include <utility>
// #define USE_LOCAL_PROXY
namespace http { namespace http {
request::request(EMethod method, std::string url) request::request(EMethod method, std::string url)
: m_url(std::move(url)) { : m_url(std::move(url)) {
@ -34,7 +36,6 @@ request::request(request &&other) noexcept
, m_url(std::exchange(other.m_url, "")) , m_url(std::exchange(other.m_url, ""))
, m_method(std::exchange(other.m_method, nullptr)) , m_method(std::exchange(other.m_method, nullptr))
, m_header_list(std::exchange(other.m_header_list, 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_form(std::exchange(other.m_form, nullptr))
, m_read_streams(std::move(other.m_read_streams)) , m_read_streams(std::move(other.m_read_streams))
, m_progress_callback(std::move(other.m_progress_callback)) { , m_progress_callback(std::move(other.m_progress_callback)) {
@ -98,6 +99,10 @@ void request::set_user_agent(const std::string &data) {
curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str()); curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str());
} }
CURL *request::get_curl() {
return m_curl;
}
void request::make_form() { void request::make_form() {
m_form = curl_mime_init(m_curl); m_form = curl_mime_init(m_curl);
} }
@ -143,14 +148,16 @@ response request::execute() {
detail::check_init(); detail::check_init();
std::string str; std::string str;
#ifdef USE_LOCAL_PROXY
set_proxy("http://127.0.0.1:8888");
set_verify_ssl(false);
#endif
curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, m_method); curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, m_method);
curl_easy_setopt(m_curl, CURLOPT_URL, m_url.c_str()); curl_easy_setopt(m_curl, CURLOPT_URL, m_url.c_str());
curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, detail::curl_write_data_callback); curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, detail::curl_write_data_callback);
curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &str); curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &str);
curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_error_buf);
m_error_buf[0] = '\0';
if (m_header_list != nullptr) if (m_header_list != nullptr)
curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list); curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list);
if (m_form != nullptr) if (m_form != nullptr)
@ -160,7 +167,6 @@ response request::execute() {
if (result != CURLE_OK) { if (result != CURLE_OK) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform); auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
response.error_string = curl_easy_strerror(result); response.error_string = curl_easy_strerror(result);
response.error_string += " " + std::string(m_error_buf.data());
return response; return response;
} }

View File

@ -122,6 +122,8 @@ struct request {
response execute(); response execute();
CURL *get_curl();
private: private:
void prepare(); void prepare();
@ -129,7 +131,6 @@ private:
std::string m_url; std::string m_url;
const char *m_method; const char *m_method;
curl_slist *m_header_list = nullptr; curl_slist *m_header_list = nullptr;
std::array<char, CURL_ERROR_SIZE> m_error_buf = { 0 };
curl_mime *m_form = nullptr; curl_mime *m_form = nullptr;
std::function<void(curl_off_t, curl_off_t)> m_progress_callback; std::function<void(curl_off_t, curl_off_t)> m_progress_callback;

View File

@ -47,6 +47,7 @@ void SettingsManager::ReadSettings() {
SMSTR("discord", "gateway", GatewayURL); SMSTR("discord", "gateway", GatewayURL);
SMBOOL("discord", "memory_db", UseMemoryDB); SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch); SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect);
SMSTR("gui", "css", MainCSS); SMSTR("gui", "css", MainCSS);
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly); SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
SMBOOL("gui", "animations", ShowAnimations); SMBOOL("gui", "animations", ShowAnimations);
@ -56,6 +57,8 @@ void SettingsManager::ReadSettings() {
SMBOOL("gui", "save_state", SaveState); SMBOOL("gui", "save_state", SaveState);
SMBOOL("gui", "stock_emojis", ShowStockEmojis); SMBOOL("gui", "stock_emojis", ShowStockEmojis);
SMBOOL("gui", "unreads", Unreads); SMBOOL("gui", "unreads", Unreads);
SMBOOL("gui", "alt_menu", AltMenu);
SMBOOL("gui", "hide_to_tray", HideToTray);
SMINT("http", "concurrent", CacheHTTPConcurrency); SMINT("http", "concurrent", CacheHTTPConcurrency);
SMSTR("http", "user_agent", UserAgent); SMSTR("http", "user_agent", UserAgent);
SMSTR("style", "expandercolor", ChannelsExpanderColor); SMSTR("style", "expandercolor", ChannelsExpanderColor);
@ -125,6 +128,7 @@ void SettingsManager::Close() {
SMSTR("discord", "gateway", GatewayURL); SMSTR("discord", "gateway", GatewayURL);
SMBOOL("discord", "memory_db", UseMemoryDB); SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch); SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect);
SMSTR("gui", "css", MainCSS); SMSTR("gui", "css", MainCSS);
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly); SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
SMBOOL("gui", "animations", ShowAnimations); SMBOOL("gui", "animations", ShowAnimations);
@ -134,6 +138,8 @@ void SettingsManager::Close() {
SMBOOL("gui", "save_state", SaveState); SMBOOL("gui", "save_state", SaveState);
SMBOOL("gui", "stock_emojis", ShowStockEmojis); SMBOOL("gui", "stock_emojis", ShowStockEmojis);
SMBOOL("gui", "unreads", Unreads); SMBOOL("gui", "unreads", Unreads);
SMBOOL("gui", "alt_menu", AltMenu);
SMBOOL("gui", "hide_to_tray", HideToTray);
SMINT("http", "concurrent", CacheHTTPConcurrency); SMINT("http", "concurrent", CacheHTTPConcurrency);
SMSTR("http", "user_agent", UserAgent); SMSTR("http", "user_agent", UserAgent);
SMSTR("style", "expandercolor", ChannelsExpanderColor); SMSTR("style", "expandercolor", ChannelsExpanderColor);

View File

@ -12,6 +12,7 @@ public:
std::string DiscordToken; std::string DiscordToken;
bool UseMemoryDB { false }; bool UseMemoryDB { false };
bool Prefetch { false }; bool Prefetch { false };
bool Autoconnect { false };
// [gui] // [gui]
std::string MainCSS { "main.css" }; std::string MainCSS { "main.css" };
@ -27,6 +28,8 @@ public:
bool ShowStockEmojis { true }; bool ShowStockEmojis { true };
#endif #endif
bool Unreads { true }; bool Unreads { true };
bool AltMenu { false };
bool HideToTray { false };
// [http] // [http]
int CacheHTTPConcurrency { 20 }; int CacheHTTPConcurrency { 20 };

126
src/startup.cpp Normal file
View File

@ -0,0 +1,126 @@
#include "startup.hpp"
#include "abaddon.hpp"
#include <future>
#include <memory>
DiscordStartupDialog::DiscordStartupDialog(Gtk::Window &window)
: Gtk::MessageDialog(window, "", false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_NONE, true) {
m_dispatcher.connect(sigc::mem_fun(*this, &DiscordStartupDialog::DispatchCallback));
property_text() = "Getting connection info...";
RunAsync();
}
std::optional<std::string> DiscordStartupDialog::GetCookie() const {
return m_cookie;
}
std::optional<uint32_t> DiscordStartupDialog::GetBuildNumber() const {
return m_build_number;
}
// good enough
std::optional<std::pair<std::string, std::string>> ParseCookie(const Glib::ustring &str) {
auto regex = Glib::Regex::create("\\t");
const std::vector<Glib::ustring> split = regex->split(str);
if (split.size() < 7) return {};
return { { split[5], split[6] } };
}
std::optional<Glib::ustring> GetJavascriptFileFromAppPage(const Glib::ustring &contents) {
auto regex = Glib::Regex::create(R"(app-mount.*(/assets/[\w\d]*.js).*/assets/[\w\d]*.js)");
Glib::MatchInfo match;
if (regex->match(contents, match)) {
return match.fetch(1);
}
return {};
}
std::optional<uint32_t> GetBuildNumberFromJSURL(const Glib::ustring &url, const std::string &cookie) {
http::request req(http::REQUEST_GET, "https://discord.com" + url);
req.set_header("Accept-Language", "en-US,en;q=0.9");
req.set_header("Sec-Fetch-Dest", "document");
req.set_header("Sec-Fetch-Mode", "navigate");
req.set_header("Sec-Fetch-Site", "none");
req.set_header("Sec-Fetch-User", "?1");
req.set_user_agent(Abaddon::Get().GetSettings().UserAgent);
curl_easy_setopt(req.get_curl(), CURLOPT_COOKIE, cookie.c_str());
auto res = req.execute();
if (res.error) return {};
auto regex = Glib::Regex::create(R"("buildNumber",null!==\(t="(\d+)\"\))");
Glib::MatchInfo match;
if (regex->match(res.text, match)) {
const auto str_value = match.fetch(1);
try {
return std::stoul(str_value);
} catch (...) { return {}; }
}
return {};
}
std::pair<std::optional<std::string>, std::string> GetCookieTask() {
http::request req(http::REQUEST_GET, "https://discord.com/app");
req.set_header("Accept-Language", "en-US,en;q=0.9");
req.set_header("Sec-Fetch-Dest", "document");
req.set_header("Sec-Fetch-Mode", "navigate");
req.set_header("Sec-Fetch-Site", "none");
req.set_header("Sec-Fetch-User", "?1");
req.set_user_agent(Abaddon::Get().GetSettings().UserAgent);
curl_easy_setopt(req.get_curl(), CURLOPT_COOKIEFILE, "");
auto res = req.execute();
if (res.error) return {};
curl_slist *slist;
if (curl_easy_getinfo(req.get_curl(), CURLINFO_COOKIELIST, &slist) != CURLE_OK) {
return {};
}
std::string dcfduid;
std::string sdcfduid;
for (auto *cur = slist; cur != nullptr; cur = cur->next) {
const auto cookie = ParseCookie(cur->data);
if (cookie.has_value()) {
if (cookie->first == "__dcfduid") {
dcfduid = cookie->second;
} else if (cookie->first == "__sdcfduid") {
sdcfduid = cookie->second;
}
}
}
curl_slist_free_all(slist);
if (!dcfduid.empty() && !sdcfduid.empty()) {
return { "__dcfduid=" + dcfduid + "; __sdcfduid=" + sdcfduid, res.text };
}
return {};
}
void DiscordStartupDialog::RunAsync() {
auto futptr = std::make_shared<std::future<void>>();
*futptr = std::async(std::launch::async, [this, futptr] {
auto [opt_cookie, app_page] = GetCookieTask();
m_cookie = opt_cookie;
if (opt_cookie.has_value()) {
auto js_url = GetJavascriptFileFromAppPage(app_page);
if (js_url.has_value()) {
m_build_number = GetBuildNumberFromJSURL(*js_url, *opt_cookie);
}
}
m_dispatcher.emit();
});
}
void DiscordStartupDialog::DispatchCallback() {
response(Gtk::RESPONSE_OK);
}

25
src/startup.hpp Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include <glibmm/dispatcher.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/window.h>
#include <optional>
// fetch cookies, build number async
class DiscordStartupDialog : public Gtk::MessageDialog {
public:
DiscordStartupDialog(Gtk::Window &window);
[[nodiscard]] std::optional<std::string> GetCookie() const;
[[nodiscard]] std::optional<uint32_t> GetBuildNumber() const;
private:
void RunAsync();
void DispatchCallback();
Glib::Dispatcher m_dispatcher;
std::optional<std::string> m_cookie;
std::optional<uint32_t> m_build_number;
};

View File

@ -56,10 +56,9 @@ GuildSettingsInfoPane::GuildSettingsInfoPane(Snowflake id)
guild_icon_url = guild.GetIconURL("gif", "512"); guild_icon_url = guild.GetIconURL("gif", "512");
else else
guild_icon_url = guild.GetIconURL("png", "512"); guild_icon_url = guild.GetIconURL("png", "512");
m_guild_icon_ev.signal_button_press_event().connect([guild_icon_url](GdkEventButton *event) -> bool { m_guild_icon_ev.signal_button_release_event().connect([guild_icon_url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS) if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY)
if (event->button == GDK_BUTTON_PRIMARY) LaunchBrowser(guild_icon_url);
LaunchBrowser(guild_icon_url);
return false; return false;
}); });

View File

@ -158,6 +158,10 @@ void MainWindow::UpdateMenus() {
OnViewSubmenuPopup(); OnViewSubmenuPopup();
} }
void MainWindow::ToggleMenuVisibility() {
m_menu_bar.set_visible(!m_menu_bar.get_visible());
}
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
void MainWindow::GoBack() { void MainWindow::GoBack() {
m_chat.GoBack(); m_chat.GoBack();
@ -195,7 +199,6 @@ void MainWindow::OnDiscordSubmenuPopup() {
std::string token = Abaddon::Get().GetDiscordToken(); std::string token = Abaddon::Get().GetDiscordToken();
m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active); m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active);
m_menu_discord_disconnect.set_sensitive(discord_active); m_menu_discord_disconnect.set_sensitive(discord_active);
m_menu_discord_join_guild.set_sensitive(discord_active);
m_menu_discord_set_token.set_sensitive(!discord_active); m_menu_discord_set_token.set_sensitive(!discord_active);
m_menu_discord_set_status.set_sensitive(discord_active); m_menu_discord_set_status.set_sensitive(discord_active);
} }
@ -238,15 +241,12 @@ void MainWindow::SetupMenu() {
m_menu_discord_disconnect.set_label("Disconnect"); m_menu_discord_disconnect.set_label("Disconnect");
m_menu_discord_disconnect.set_sensitive(false); m_menu_discord_disconnect.set_sensitive(false);
m_menu_discord_set_token.set_label("Set Token"); m_menu_discord_set_token.set_label("Set Token");
m_menu_discord_join_guild.set_label("Accept Invite");
m_menu_discord_join_guild.set_sensitive(false);
m_menu_discord_set_status.set_label("Set Status"); m_menu_discord_set_status.set_label("Set Status");
m_menu_discord_set_status.set_sensitive(false); m_menu_discord_set_status.set_sensitive(false);
m_menu_discord_add_recipient.set_label("Add user to DM"); m_menu_discord_add_recipient.set_label("Add user to DM");
m_menu_discord_sub.append(m_menu_discord_connect); m_menu_discord_sub.append(m_menu_discord_connect);
m_menu_discord_sub.append(m_menu_discord_disconnect); m_menu_discord_sub.append(m_menu_discord_disconnect);
m_menu_discord_sub.append(m_menu_discord_set_token); m_menu_discord_sub.append(m_menu_discord_set_token);
m_menu_discord_sub.append(m_menu_discord_join_guild);
m_menu_discord_sub.append(m_menu_discord_set_status); m_menu_discord_sub.append(m_menu_discord_set_status);
m_menu_discord_sub.append(m_menu_discord_add_recipient); m_menu_discord_sub.append(m_menu_discord_add_recipient);
m_menu_discord.set_submenu(m_menu_discord_sub); m_menu_discord.set_submenu(m_menu_discord_sub);
@ -265,6 +265,12 @@ void MainWindow::SetupMenu() {
m_menu_view_threads.set_label("Threads"); m_menu_view_threads.set_label("Threads");
m_menu_view_mark_guild_as_read.set_label("Mark Server as Read"); m_menu_view_mark_guild_as_read.set_label("Mark Server as Read");
m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE); m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_channels.set_label("Channels");
m_menu_view_channels.add_accelerator("activate", m_accels, GDK_KEY_L, Gdk::CONTROL_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_channels.set_active(true);
m_menu_view_members.set_label("Members");
m_menu_view_members.add_accelerator("activate", m_accels, GDK_KEY_M, Gdk::CONTROL_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_members.set_active(true);
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
m_menu_view_go_back.set_label("Go Back"); m_menu_view_go_back.set_label("Go Back");
m_menu_view_go_forward.set_label("Go Forward"); m_menu_view_go_forward.set_label("Go Forward");
@ -275,6 +281,8 @@ void MainWindow::SetupMenu() {
m_menu_view_sub.append(m_menu_view_pins); m_menu_view_sub.append(m_menu_view_pins);
m_menu_view_sub.append(m_menu_view_threads); m_menu_view_sub.append(m_menu_view_threads);
m_menu_view_sub.append(m_menu_view_mark_guild_as_read); m_menu_view_sub.append(m_menu_view_mark_guild_as_read);
m_menu_view_sub.append(m_menu_view_channels);
m_menu_view_sub.append(m_menu_view_members);
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
m_menu_view_sub.append(m_menu_view_go_back); m_menu_view_sub.append(m_menu_view_go_back);
m_menu_view_sub.append(m_menu_view_go_forward); m_menu_view_sub.append(m_menu_view_go_forward);
@ -283,7 +291,25 @@ void MainWindow::SetupMenu() {
m_menu_bar.append(m_menu_file); m_menu_bar.append(m_menu_file);
m_menu_bar.append(m_menu_discord); m_menu_bar.append(m_menu_discord);
m_menu_bar.append(m_menu_view); m_menu_bar.append(m_menu_view);
m_menu_bar.show_all();
if (Abaddon::Get().GetSettings().AltMenu) {
auto set_hide_cb = [this](Gtk::Menu &menu) {
for (auto *child : menu.get_children()) {
auto *item = dynamic_cast<Gtk::MenuItem *>(child);
if (item != nullptr) {
item->signal_activate().connect([this]() {
m_menu_bar.hide();
});
}
}
};
set_hide_cb(m_menu_discord_sub);
set_hide_cb(m_menu_file_sub);
set_hide_cb(m_menu_view_sub);
m_menu_bar.show_all_children();
} else {
m_menu_bar.show_all();
}
m_menu_discord_connect.signal_activate().connect([this] { m_menu_discord_connect.signal_activate().connect([this] {
m_signal_action_connect.emit(); m_signal_action_connect.emit();
@ -297,10 +323,6 @@ void MainWindow::SetupMenu() {
m_signal_action_set_token.emit(); m_signal_action_set_token.emit();
}); });
m_menu_discord_join_guild.signal_activate().connect([this] {
m_signal_action_join_guild.emit();
});
m_menu_file_reload_css.signal_activate().connect([this] { m_menu_file_reload_css.signal_activate().connect([this] {
m_signal_action_reload_css.emit(); m_signal_action_reload_css.emit();
}); });
@ -340,6 +362,14 @@ void MainWindow::SetupMenu() {
} }
}); });
m_menu_view_channels.signal_activate().connect([this]() {
m_channel_list.set_visible(m_menu_view_channels.get_active());
});
m_menu_view_members.signal_activate().connect([this]() {
m_members.GetRoot()->set_visible(m_menu_view_members.get_active());
});
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
m_menu_view_go_back.signal_activate().connect([this] { m_menu_view_go_back.signal_activate().connect([this] {
GoBack(); GoBack();
@ -383,10 +413,6 @@ MainWindow::type_signal_action_reload_css MainWindow::signal_action_reload_css()
return m_signal_action_reload_css; return m_signal_action_reload_css;
} }
MainWindow::type_signal_action_join_guild MainWindow::signal_action_join_guild() {
return m_signal_action_join_guild;
}
MainWindow::type_signal_action_set_status MainWindow::signal_action_set_status() { MainWindow::type_signal_action_set_status MainWindow::signal_action_set_status() {
return m_signal_action_set_status; return m_signal_action_set_status;
} }
@ -401,4 +427,4 @@ MainWindow::type_signal_action_view_pins MainWindow::signal_action_view_pins() {
MainWindow::type_signal_action_view_threads MainWindow::signal_action_view_threads() { MainWindow::type_signal_action_view_threads MainWindow::signal_action_view_threads() {
return m_signal_action_view_threads; return m_signal_action_view_threads;
} }

View File

@ -24,6 +24,7 @@ public:
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param); void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param);
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param); void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
void UpdateMenus(); void UpdateMenus();
void ToggleMenuVisibility();
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
void GoBack(); void GoBack();
@ -63,7 +64,6 @@ private:
Gtk::MenuItem m_menu_discord_connect; Gtk::MenuItem m_menu_discord_connect;
Gtk::MenuItem m_menu_discord_disconnect; Gtk::MenuItem m_menu_discord_disconnect;
Gtk::MenuItem m_menu_discord_set_token; Gtk::MenuItem m_menu_discord_set_token;
Gtk::MenuItem m_menu_discord_join_guild;
Gtk::MenuItem m_menu_discord_set_status; Gtk::MenuItem m_menu_discord_set_status;
Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day
void OnDiscordSubmenuPopup(); void OnDiscordSubmenuPopup();
@ -79,10 +79,13 @@ private:
Gtk::MenuItem m_menu_view_pins; Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads; Gtk::MenuItem m_menu_view_threads;
Gtk::MenuItem m_menu_view_mark_guild_as_read; Gtk::MenuItem m_menu_view_mark_guild_as_read;
Gtk::CheckMenuItem m_menu_view_channels;
Gtk::CheckMenuItem m_menu_view_members;
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_view_go_back; Gtk::MenuItem m_menu_view_go_back;
Gtk::MenuItem m_menu_view_go_forward; Gtk::MenuItem m_menu_view_go_forward;
#endif #endif
void OnViewSubmenuPopup(); void OnViewSubmenuPopup();
public: public:
@ -90,7 +93,6 @@ public:
typedef sigc::signal<void> type_signal_action_disconnect; typedef sigc::signal<void> type_signal_action_disconnect;
typedef sigc::signal<void> type_signal_action_set_token; typedef sigc::signal<void> type_signal_action_set_token;
typedef sigc::signal<void> type_signal_action_reload_css; typedef sigc::signal<void> type_signal_action_reload_css;
typedef sigc::signal<void> type_signal_action_join_guild;
typedef sigc::signal<void> type_signal_action_set_status; typedef sigc::signal<void> type_signal_action_set_status;
// this should probably be removed // this should probably be removed
typedef sigc::signal<void, Snowflake> type_signal_action_add_recipient; // channel id typedef sigc::signal<void, Snowflake> type_signal_action_add_recipient; // channel id
@ -101,7 +103,6 @@ public:
type_signal_action_disconnect signal_action_disconnect(); type_signal_action_disconnect signal_action_disconnect();
type_signal_action_set_token signal_action_set_token(); type_signal_action_set_token signal_action_set_token();
type_signal_action_reload_css signal_action_reload_css(); type_signal_action_reload_css signal_action_reload_css();
type_signal_action_join_guild signal_action_join_guild();
type_signal_action_set_status signal_action_set_status(); type_signal_action_set_status signal_action_set_status();
type_signal_action_add_recipient signal_action_add_recipient(); type_signal_action_add_recipient signal_action_add_recipient();
type_signal_action_view_pins signal_action_view_pins(); type_signal_action_view_pins signal_action_view_pins();
@ -112,7 +113,6 @@ private:
type_signal_action_disconnect m_signal_action_disconnect; type_signal_action_disconnect m_signal_action_disconnect;
type_signal_action_set_token m_signal_action_set_token; type_signal_action_set_token m_signal_action_set_token;
type_signal_action_reload_css m_signal_action_reload_css; type_signal_action_reload_css m_signal_action_reload_css;
type_signal_action_join_guild m_signal_action_join_guild;
type_signal_action_set_status m_signal_action_set_status; type_signal_action_set_status m_signal_action_set_status;
type_signal_action_add_recipient m_signal_action_add_recipient; type_signal_action_add_recipient m_signal_action_add_recipient;
type_signal_action_view_pins m_signal_action_view_pins; type_signal_action_view_pins m_signal_action_view_pins;

View File

@ -41,13 +41,13 @@ ConnectionItem::ConnectionItem(const ConnectionData &conn)
m_box.add(m_name); m_box.add(m_name);
if (!url.empty()) { if (!url.empty()) {
auto cb = [url](GdkEventButton *event) -> bool { auto cb = [url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url); LaunchBrowser(url);
return true; return true;
} }
return false; return false;
}; };
signal_button_press_event().connect(sigc::track_obj(cb, *this)); signal_button_release_event().connect(sigc::track_obj(cb, *this));
AddPointerCursor(*this); AddPointerCursor(*this);
} }
m_overlay.add(m_box); m_overlay.add(m_box);

View File

@ -34,8 +34,8 @@ ProfileWindow::ProfileWindow(Snowflake user_id)
if (user.HasAvatar()) if (user.HasAvatar())
AddPointerCursor(m_avatar_ev); AddPointerCursor(m_avatar_ev);
m_avatar_ev.signal_button_press_event().connect([user](GdkEventButton *event) -> bool { m_avatar_ev.signal_button_release_event().connect([user](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { if (event->type == GDK_BUTTON_RELEASE && event->button == GDK_BUTTON_PRIMARY) {
if (user.HasAnimatedAvatar()) if (user.HasAnimatedAvatar())
LaunchBrowser(user.GetAvatarURL("gif", "512")); LaunchBrowser(user.GetAvatarURL("gif", "512"));
else else