forked from OpenGamers/abaddon
Compare commits
36 Commits
v0.1.7
...
member-lis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
190118bb58 | ||
|
|
6926b12a50 | ||
|
|
fb010b5ac5 | ||
|
|
acb03642c2 | ||
|
|
ba2aea86f9 | ||
|
|
c704703b14 | ||
|
|
33a329f16a | ||
|
|
f0df06a795 | ||
|
|
9ae41b7335 | ||
|
|
b9fee0f6c9 | ||
|
|
92273829bb | ||
|
|
86f6f81d9b | ||
|
|
573a619191 | ||
|
|
c5807a3463 | ||
|
|
b0370ee489 | ||
|
|
2a9f49a148 | ||
|
|
64245bf745 | ||
|
|
772598996c | ||
|
|
ccb82c1676 | ||
|
|
cd900cdfee | ||
|
|
1767575728 | ||
|
|
0a34c04b44 | ||
|
|
7e85168576 | ||
|
|
3027e00905 | ||
|
|
00fe6642a9 | ||
|
|
2ecbacc924 | ||
|
|
84eb56d6b1 | ||
|
|
f3e5dcbe65 | ||
|
|
a78fdd386f | ||
|
|
348c1cb965 | ||
|
|
32fc7def7c | ||
|
|
14602a7384 | ||
|
|
c683ef9ad9 | ||
|
|
77dd9fabfa | ||
|
|
ee67037a3f | ||
|
|
b46cf53be5 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -160,7 +160,7 @@ jobs:
|
|||||||
cd deps
|
cd deps
|
||||||
git clone https://github.com/nlohmann/json
|
git clone https://github.com/nlohmann/json
|
||||||
cd json
|
cd json
|
||||||
git checkout db78ac1d7716f56fc9f1b030b715f872f93964e4
|
git checkout bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d
|
||||||
mkdir build
|
mkdir build
|
||||||
cd build
|
cd build
|
||||||
cmake ..
|
cmake ..
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -4,3 +4,6 @@
|
|||||||
[submodule "subprojects/ixwebsocket"]
|
[submodule "subprojects/ixwebsocket"]
|
||||||
path = subprojects/ixwebsocket
|
path = subprojects/ixwebsocket
|
||||||
url = https://github.com/machinezone/ixwebsocket
|
url = https://github.com/machinezone/ixwebsocket
|
||||||
|
[submodule "subprojects/keychain"]
|
||||||
|
path = subprojects/keychain
|
||||||
|
url = https://github.com/hrantzsch/keychain
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
|
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
|
||||||
|
|
||||||
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
|
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
|
||||||
|
option(USE_KEYCHAIN "Store the token in the keychain (default)" ON)
|
||||||
|
|
||||||
find_package(nlohmann_json REQUIRED)
|
find_package(nlohmann_json REQUIRED)
|
||||||
find_package(CURL)
|
find_package(CURL)
|
||||||
@@ -106,3 +107,13 @@ if (USE_LIBHANDY)
|
|||||||
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
|
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
|
||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
if (USE_KEYCHAIN)
|
||||||
|
find_package(keychain QUIET)
|
||||||
|
if (NOT keychain_FOUND)
|
||||||
|
message("keychain was not found and will be included as a submodule")
|
||||||
|
add_subdirectory(subprojects/keychain)
|
||||||
|
target_link_libraries(abaddon keychain)
|
||||||
|
target_compile_definitions(abaddon PRIVATE WITH_KEYCHAIN)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|||||||
262
README.md
262
README.md
@@ -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 (though is not perfect) 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.<br>
|
stops this) as well as in the headers of all requests.<br>
|
||||||
|
|
||||||
**See [here](#the-spam-filter)** for things you might want to avoid if you are worried about being caught in the spam filter.
|
**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
|
||||||
@@ -68,8 +70,15 @@ the result of fundamental issues with Discord's thread implementation.
|
|||||||
|
|
||||||
#### 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
|
||||||
|
```
|
||||||
|
* On Arch Linux
|
||||||
|
```Shell
|
||||||
|
$ sudo pacman -S gcc cmake gtkmm3 libcurl-gnutls lib32-sqlite lib32-openssl nlohmann-json libhandy
|
||||||
|
```
|
||||||
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 ..`
|
||||||
@@ -87,7 +96,7 @@ 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`
|
||||||
|
|
||||||
@@ -96,7 +105,12 @@ no `abaddon.ini` in the working directory
|
|||||||
|
|
||||||
#### The Spam Filter
|
#### 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:
|
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)
|
* Joining or leaving servers (usually main cause of getting caught)
|
||||||
* Frequently disconnecting and reconnecting
|
* Frequently disconnecting and reconnecting
|
||||||
@@ -125,142 +139,158 @@ Discord likes disabling accounts/forcing them to reset their passwords if they t
|
|||||||
|
|
||||||
#### CSS selectors
|
#### CSS selectors
|
||||||
|
|
||||||
.app-window - Applied to all windows. This means the main window and all popups
|
| Selector | Description |
|
||||||
.app-popup - Additional class for `.app-window`s when the window is not the main window
|
|--------------------------------|---------------------------------------------------------------------------------------------------|
|
||||||
|
| `.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 |
|
||||||
|
| `.channel-list` | Container of the channel list |
|
||||||
|
| `.messages` | Container of user messages |
|
||||||
|
| `.message-container` | The container which holds a user's messages |
|
||||||
|
| `.message-container-author` | The author 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-extra` | Label containing BOT/Webhook |
|
||||||
|
| `.message-text` | The text of a user message |
|
||||||
|
| `.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 |
|
||||||
|
| `.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-input` | Applied to the chat input container |
|
||||||
|
| `.replying` | Extra class for chat input container when a reply is currently being created |
|
||||||
|
| `.reaction-box` | Contains a reaction image and the count |
|
||||||
|
| `.reacted` | Additional class for reaction-box when the user has reacted with a particular reaction |
|
||||||
|
| `.reaction-count` | Contains the count for reaction |
|
||||||
|
| `.completer` | Container for the message 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-image` | Contains the image for an entry in the completer |
|
||||||
|
| `.embed` | Container for a message embed |
|
||||||
|
| `.embed-author` | The author of an embed |
|
||||||
|
| `.embed-title` | The title of an embed |
|
||||||
|
| `.embed-description` | The description of an embed |
|
||||||
|
| `.embed-field-title` | The title of an embed field |
|
||||||
|
| `.embed-field-value` | The value of an embed field |
|
||||||
|
| `.embed-footer` | The footer of an embed |
|
||||||
|
| `.members` | Container of the member list |
|
||||||
|
| `.members-row` | All rows within the members container |
|
||||||
|
| `.members-row-label` | All labels in the members container |
|
||||||
|
| `.members-row-member` | Rows containing a member |
|
||||||
|
| `.members-row-role` | Rows containing a role |
|
||||||
|
| `.members-row-avatar` | Contains the avatar for a row in the member list |
|
||||||
|
| `.status-indicator` | The status indicator |
|
||||||
|
| `.online` | Applied to status indicators when the associated user is online |
|
||||||
|
| `.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 |
|
||||||
|
| `.offline` | Applied to status indicators when the associated user is offline |
|
||||||
|
| `.typing-indicator` | The typing indicator (also used for replies) |
|
||||||
|
|
||||||
.channel-list - Container of the channel list
|
Used in reorderable list implementation:
|
||||||
|
| Selector |
|
||||||
|
|----------------------|
|
||||||
|
| `.drag-icon` |
|
||||||
|
| `.drag-hover-top` |
|
||||||
|
| `.drag-hover-bottom` |
|
||||||
|
|
||||||
.messages - Container of user messages
|
Used in guild settings popup:
|
||||||
.message-container - The container which holds a user's messages
|
|
||||||
.message-container-author - The author 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-extra - Label containing BOT/Webhook
|
|
||||||
.message-text - The text of a user message
|
|
||||||
.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
|
|
||||||
.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-input - Applied to the chat input container
|
|
||||||
.replying - Extra class for chat input container when a reply is currently being created
|
|
||||||
.reaction-box - Contains a reaction image and the count
|
|
||||||
.reacted - Additional class for reaction-box when the user has reacted with a particular reaction
|
|
||||||
.reaction-count - Contains the count for reaction
|
|
||||||
|
|
||||||
.completer - Container for the message completer
|
| Selector | Description |
|
||||||
.completer-entry - Container for a single entry in the completer
|
|----------------------------|---------------------------------------------------|
|
||||||
.completer-entry-label - Contains the label for an entry in the completer
|
| `.guild-settings-window` | Container for list of members in the members pane |
|
||||||
.completer-entry-image - Contains the image for an entry in the completer
|
| `.guild-members-pane-list` | |
|
||||||
|
| `.guild-members-pane-info` | Container for member info |
|
||||||
|
| `.guild-roles-pane-list` | Container for list of roles in the roles pane |
|
||||||
|
|
||||||
.embed - Container for a message embed
|
Used in profile popup:
|
||||||
.embed-author - The author of an embed
|
|
||||||
.embed-title - The title of an embed
|
|
||||||
.embed-description - The description of an embed
|
|
||||||
.embed-field-title - The title of an embed field
|
|
||||||
.embed-field-value - The value of an embed field
|
|
||||||
.embed-footer - The footer of an embed
|
|
||||||
|
|
||||||
.members - Container of the member list
|
| Selector | Description |
|
||||||
.members-row - All rows within the members container
|
|------------------------------|---------------------------------------------------------|
|
||||||
.members-row-label - All labels in the members container
|
| `.mutual-friend-item` | Applied to every item in the mutual friends list |
|
||||||
.members-row-member - Rows containing a member
|
| `.mutual-friend-item-name` | Name in mutual friend item |
|
||||||
.members-row-role - Rows containing a role
|
| `.mutual-friend-item-avatar` | Avatar in mutual friend item |
|
||||||
.members-row-avatar - Contains the avatar for a row in the member list
|
| `.mutual-guild-item` | Applied to every item in the mutual guilds list |
|
||||||
|
| `.mutual-guild-item-name` | Name in mutual guild item |
|
||||||
.status-indicator - The status indicator
|
| `.mutual-guild-item-icon` | Icon in mutual guild item |
|
||||||
.online - Applied to status indicators when the associated user is online
|
| `.mutual-guild-item-nick` | User nickname in mutual guild item |
|
||||||
.idle - Applied to status indicators when the associated user is away
|
| `.profile-connection` | Applied to every item in the user connections list |
|
||||||
.dnd - Applied to status indicators when the associated user is on do not disturb
|
| `.profile-connection-label` | Label in profile connection item |
|
||||||
.offline - Applied to status indicators when the associated user is offline
|
| `.profile-connection-check` | Checkmark in verified profile connection items |
|
||||||
|
| `.profile-connections` | Container for profile connections |
|
||||||
.typing-indicator - The typing indicator (also used for replies)
|
| `.profile-notes` | Container for notes in profile window |
|
||||||
|
| `.profile-notes-label` | Label that says "NOTE" |
|
||||||
Used in reorderable list implementation:
|
| `.profile-notes-text` | Actual note text |
|
||||||
.drag-icon .drag-hover-top .drag-hover-bottom
|
| `.profile-info-pane` | Applied to container for info section of profile popup |
|
||||||
|
| `.profile-info-created` | Label for creation date of profile |
|
||||||
Used in guild settings popup:
|
| `.user-profile-window` | |
|
||||||
.guild-settings-window
|
| `.profile-main-container` | Inner container for profile |
|
||||||
.guild-members-pane-list - Container for list of members in the members pane
|
| `.profile-avatar` | |
|
||||||
.guild-members-pane-info - Container for member info
|
| `.profile-username` | |
|
||||||
.guild-roles-pane-list - Container for list of roles in the roles pane
|
| `.profile-switcher` | Buttons used to switch viewed section of profile |
|
||||||
|
| `.profile-stack` | Container for profile info that can be switched between |
|
||||||
Used in profile popup:
|
| `.profile-badges` | Container for badges |
|
||||||
.mutual-friend-item - Applied to every item in the mutual friends list
|
| `.profile-badge` | |
|
||||||
.mutual-friend-item-name - Name 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-name - Name in mutual guild item
|
|
||||||
.mutual-guild-item-icon - Icon 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-label - Label in profile connection item
|
|
||||||
.profile-connection-check - Checkmark in verified profile connection items
|
|
||||||
.profile-connections - Container for profile connections
|
|
||||||
.profile-notes - Container for notes in profile window
|
|
||||||
.profile-notes-label - Label that says "NOTE"
|
|
||||||
.profile-notes-text - Actual note text
|
|
||||||
.profile-info-pane - Applied to container for info section of profile popup
|
|
||||||
.profile-info-created - Label for creation date of profile
|
|
||||||
.user-profile-window
|
|
||||||
.profile-main-container - Inner container for profile
|
|
||||||
.profile-avatar
|
|
||||||
.profile-username
|
|
||||||
.profile-switcher - Buttons used to switch viewed section of profile
|
|
||||||
.profile-stack - Container for profile info that can be switched between
|
|
||||||
.profile-badges - Container for badges
|
|
||||||
.profile-badge
|
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
Settings are configured (for now) by editing abaddon.ini
|
Settings are configured (for now) by editing `abaddon.ini`.
|
||||||
The format is similar to the standard Windows ini format **except**:
|
The format is similar to the standard Windows ini format **except**:
|
||||||
|
|
||||||
* `#` is used to begin comments as opposed to `;`
|
* `#` is used to begin comments as opposed to `;`
|
||||||
* Section and key names are case-sensitive
|
* Section and key names are case-sensitive
|
||||||
|
|
||||||
You should edit these while the client is closed even though there's an option to reload while running
|
> **Warning**: You should edit these while the client is closed, even though there's an option to reload while running.
|
||||||
This listing is organized by section.
|
|
||||||
|
This listing is organized by section.
|
||||||
For example, memory_db would be set by adding `memory_db = true` under the line `[discord]`
|
For example, memory_db would be set by adding `memory_db = true` under the line `[discord]`
|
||||||
|
|
||||||
#### discord
|
#### discord
|
||||||
|
|
||||||
* gateway (string) - override url for Discord gateway. must be json format and use zlib stream compression
|
| Setting | Type | Default | Description |
|
||||||
* 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
|
| `gateway` | string | | override url for Discord gateway. must be json format and use zlib stream compression |
|
||||||
* token (string) - Discord token used to login, this can be set from the menu
|
| `api_base` | string | | override base url for Discord API |
|
||||||
* prefetch (true or false, default false) - if true, new messages will cause the avatar and image attachments to be
|
| `memory_db` | boolean | false | if true, Discord data will be kept in memory as opposed to on disk |
|
||||||
automatically downloaded
|
| `token` | string | | Discord token used to login, this can be set from the menu |
|
||||||
|
| `prefetch` | boolean | false | if true, new messages will cause the avatar and image attachments to be automatically downloaded |
|
||||||
|
| `autoconnect` | boolean | false | autoconnect to discord |
|
||||||
|
|
||||||
#### http
|
#### http
|
||||||
|
|
||||||
* user_agent (string) - sets the user-agent to use in HTTP requests to the Discord API (not including media/images)
|
| Setting | Type | Default | Description |
|
||||||
* concurrent (int, default 20) - how many images can be concurrently retrieved
|
|--------------|--------|---------|---------------------------------------------------------------------------------------------|
|
||||||
|
| `user_agent` | string | | sets the user-agent to use in HTTP requests to the Discord API (not including media/images) |
|
||||||
|
| `concurrent` | int | 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
|
| Setting | Type | Default | Description |
|
||||||
* 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
|
| `member_list_discriminator` | boolean | true | show user discriminators in the member list |
|
||||||
* custom_emojis (true or false, default true) - download and use custom Discord emojis
|
| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself |
|
||||||
* css (string) - path to the main CSS file
|
| `custom_emojis` | boolean | true | download and use custom Discord emojis |
|
||||||
* animations (true or false, default true) - use animated images where available (e.g. server icons, emojis, avatars).
|
| `css` | string | | path to the main CSS file |
|
||||||
false means static images will be used
|
| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). 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` | boolean | true | only animate guild icons when the guild is being hovered over |
|
||||||
over
|
| `owner_crown` | boolean | true | show a crown next to the owner |
|
||||||
* owner_crown (true or false, default true) - show a crown next to the owner
|
| `unreads` | boolean | true | show unread indicators and mention badges |
|
||||||
* unreads (true or false, default true) - show unread indicators and mention badges
|
| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) |
|
||||||
|
| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key |
|
||||||
|
| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close |
|
||||||
|
|
||||||
#### style
|
#### style
|
||||||
|
|
||||||
* linkcolor (string) - color to use for links in messages
|
| Setting | Type | Description |
|
||||||
* expandercolor (string) - color to use for the expander in the channel list
|
|-------------------------|--------|-----------------------------------------------------|
|
||||||
* nsfwchannelcolor (string) - color to use for NSFW channels in the channel list
|
| `linkcolor` | string | color to use for links in messages |
|
||||||
* channelcolor (string) - color to use for SFW channels in the channel list
|
| `expandercolor` | string | color to use for the expander in the channel list |
|
||||||
* mentionbadgecolor (string) - background color for mention badges
|
| `nsfwchannelcolor` | string | color to use for NSFW channels in the channel list |
|
||||||
* mentionbadgetextcolor (string) - color to use for number displayed on mention badges
|
| `channelcolor` | string | color to use for SFW channels in the channel list |
|
||||||
* unreadcolor (string) - color to use for the unread indicator
|
| `mentionbadgecolor` | string | background color for mention badges |
|
||||||
|
| `mentionbadgetextcolor` | string | color to use for number displayed on mention badges |
|
||||||
|
| `unreadcolor` | string | color to use for the unread indicator |
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
* ABADDON_NO_FC (Windows only) - don't use custom font config
|
| variable | Description |
|
||||||
* ABADDON_CONFIG - change path of configuration file to use. relative to cwd or can be absolute
|
|------------------|------------------------------------------------------------------------------|
|
||||||
|
| `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 |
|
||||||
|
|||||||
@@ -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,4 +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
|
/../usr/bin/msys-2.0.dll
|
||||||
|
|||||||
106
src/abaddon.cpp
106
src/abaddon.cpp
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,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) {
|
||||||
@@ -638,15 +706,6 @@ 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()) {
|
if (!id.IsValid()) {
|
||||||
m_discord.SetReferringChannel(Snowflake::Invalid);
|
m_discord.SetReferringChannel(Snowflake::Invalid);
|
||||||
@@ -896,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();
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ 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 true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -357,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;
|
||||||
}
|
}
|
||||||
@@ -655,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
|
||||||
@@ -673,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) {
|
||||||
|
|||||||
@@ -1,85 +1,32 @@
|
|||||||
#include "memberlist.hpp"
|
#include "memberlist.hpp"
|
||||||
#include "abaddon.hpp"
|
#include "abaddon.hpp"
|
||||||
#include "util.hpp"
|
|
||||||
#include "lazyimage.hpp"
|
|
||||||
#include "statusindicator.hpp"
|
|
||||||
|
|
||||||
constexpr static const int MaxMemberListRows = 200;
|
MemberList::MemberList()
|
||||||
|
: m_model(Gtk::TreeStore::create(m_columns)) {
|
||||||
|
add(m_view);
|
||||||
|
show_all_children();
|
||||||
|
|
||||||
MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data) {
|
m_view.set_activate_on_single_click(true);
|
||||||
ID = data.ID;
|
m_view.set_hexpand(true);
|
||||||
m_ev = Gtk::manage(new Gtk::EventBox);
|
m_view.set_vexpand(true);
|
||||||
m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
|
|
||||||
m_label = Gtk::manage(new Gtk::Label);
|
|
||||||
m_avatar = Gtk::manage(new LazyImage(16, 16));
|
|
||||||
m_status_indicator = Gtk::manage(new StatusIndicator(ID));
|
|
||||||
|
|
||||||
if (Abaddon::Get().GetSettings().ShowOwnerCrown && guild.has_value() && guild->OwnerID == data.ID) {
|
m_view.set_show_expanders(false);
|
||||||
try {
|
m_view.set_enable_search(false);
|
||||||
const static auto crown_path = Abaddon::GetResPath("/crown.png");
|
m_view.set_headers_visible(false);
|
||||||
auto pixbuf = Gdk::Pixbuf::create_from_file(crown_path, 12, 12);
|
m_view.set_model(m_model);
|
||||||
m_crown = Gtk::manage(new Gtk::Image(pixbuf));
|
|
||||||
m_crown->set_valign(Gtk::ALIGN_CENTER);
|
|
||||||
m_crown->set_margin_end(8);
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_status_indicator->set_margin_start(3);
|
m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_DESCENDING);
|
||||||
|
|
||||||
if (guild.has_value())
|
auto *column = Gtk::make_managed<Gtk::TreeView::Column>("display");
|
||||||
m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png"));
|
auto *renderer = Gtk::make_managed<CellRendererMemberList>();
|
||||||
else
|
column->pack_start(*renderer);
|
||||||
m_avatar->SetURL(data.GetAvatarURL("png"));
|
column->add_attribute(renderer->property_type(), m_columns.m_type);
|
||||||
|
column->add_attribute(renderer->property_id(), m_columns.m_id);
|
||||||
|
column->add_attribute(renderer->property_markup(), m_columns.m_markup);
|
||||||
|
column->add_attribute(renderer->property_icon(), m_columns.m_icon);
|
||||||
|
m_view.append_column(*column);
|
||||||
|
|
||||||
get_style_context()->add_class("members-row");
|
m_view.expand_all();
|
||||||
get_style_context()->add_class("members-row-member");
|
|
||||||
m_label->get_style_context()->add_class("members-row-label");
|
|
||||||
m_avatar->get_style_context()->add_class("members-row-avatar");
|
|
||||||
|
|
||||||
m_label->set_single_line_mode(true);
|
|
||||||
m_label->set_ellipsize(Pango::ELLIPSIZE_END);
|
|
||||||
|
|
||||||
std::string display = data.Username;
|
|
||||||
if (Abaddon::Get().GetSettings().ShowMemberListDiscriminators)
|
|
||||||
display += "#" + data.Discriminator;
|
|
||||||
if (guild.has_value()) {
|
|
||||||
if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) {
|
|
||||||
auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color;
|
|
||||||
m_label->set_use_markup(true);
|
|
||||||
m_label->set_markup("<span color='#" + IntToCSSColor(color) + "'>" + Glib::Markup::escape_text(display) + "</span>");
|
|
||||||
} else {
|
|
||||||
m_label->set_text(display);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_label->set_text(display);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_label->set_halign(Gtk::ALIGN_START);
|
|
||||||
m_box->add(*m_avatar);
|
|
||||||
m_box->add(*m_status_indicator);
|
|
||||||
m_box->add(*m_label);
|
|
||||||
if (m_crown != nullptr)
|
|
||||||
m_box->add(*m_crown);
|
|
||||||
m_ev->add(*m_box);
|
|
||||||
add(*m_ev);
|
|
||||||
show_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
MemberList::MemberList() {
|
|
||||||
m_main = Gtk::manage(new Gtk::ScrolledWindow);
|
|
||||||
m_listbox = Gtk::manage(new Gtk::ListBox);
|
|
||||||
|
|
||||||
m_listbox->get_style_context()->add_class("members");
|
|
||||||
|
|
||||||
m_listbox->set_selection_mode(Gtk::SELECTION_NONE);
|
|
||||||
|
|
||||||
m_main->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
|
|
||||||
m_main->add(*m_listbox);
|
|
||||||
m_main->show_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk::Widget *MemberList::GetRoot() const {
|
|
||||||
return m_main;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MemberList::Clear() {
|
void MemberList::Clear() {
|
||||||
@@ -97,131 +44,110 @@ void MemberList::SetActiveChannel(Snowflake id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MemberList::UpdateMemberList() {
|
void MemberList::UpdateMemberList() {
|
||||||
m_id_to_row.clear();
|
m_model->clear();
|
||||||
|
|
||||||
auto children = m_listbox->get_children();
|
|
||||||
auto it = children.begin();
|
|
||||||
while (it != children.end()) {
|
|
||||||
delete *it;
|
|
||||||
it++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Abaddon::Get().GetDiscordClient().IsStarted()) return;
|
|
||||||
if (!m_chan_id.IsValid()) return;
|
|
||||||
|
|
||||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||||
const auto chan = discord.GetChannel(m_chan_id);
|
if (!discord.IsStarted()) return;
|
||||||
if (!chan.has_value()) return;
|
|
||||||
if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM) {
|
const auto channel = discord.GetChannel(m_chan_id);
|
||||||
int num_rows = 0;
|
if (!channel.has_value()) return;
|
||||||
for (const auto &user : chan->GetDMRecipients()) {
|
|
||||||
if (num_rows++ > MaxMemberListRows) break;
|
// dm
|
||||||
auto *row = Gtk::manage(new MemberListUserRow(std::nullopt, user));
|
if (channel->IsDM()) {
|
||||||
m_id_to_row[user.ID] = row;
|
// todo eliminate for dm
|
||||||
AttachUserMenuHandler(row, user.ID);
|
auto everyone_row = *m_model->append();
|
||||||
m_listbox->add(*row);
|
everyone_row[m_columns.m_type] = MemberListRenderType::Role;
|
||||||
|
everyone_row[m_columns.m_id] = Snowflake::Invalid;
|
||||||
|
everyone_row[m_columns.m_markup] = "<b>Users</b>";
|
||||||
|
|
||||||
|
for (const auto &user : channel->GetDMRecipients()) {
|
||||||
|
auto row = *m_model->append(everyone_row.children());
|
||||||
|
row[m_columns.m_type] = MemberListRenderType::Member;
|
||||||
|
row[m_columns.m_id] = user.ID;
|
||||||
|
row[m_columns.m_markup] = Glib::Markup::escape_text(user.Username + "#" + user.Discriminator);
|
||||||
|
row[m_columns.m_icon] = Abaddon::Get().GetImageManager().GetPlaceholder(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::set<Snowflake> ids;
|
std::vector<UserData> users;
|
||||||
if (chan->IsThread()) {
|
if (channel->IsThread()) {
|
||||||
const auto x = discord.GetUsersInThread(m_chan_id);
|
// auto x = discord.GetUsersInThread(m_chan_id);
|
||||||
ids = { x.begin(), x.end() };
|
// ids = { x.begin(), x.end() };
|
||||||
} else
|
} else {
|
||||||
ids = discord.GetUsersInGuild(m_guild_id);
|
users = discord.GetUserDataInGuildBulk(m_guild_id);
|
||||||
|
}
|
||||||
|
|
||||||
// process all the shit first so its in proper order
|
|
||||||
std::map<int, RoleData> pos_to_role;
|
std::map<int, RoleData> pos_to_role;
|
||||||
std::map<int, std::vector<UserData>> pos_to_users;
|
std::map<int, std::vector<UserData>> pos_to_users;
|
||||||
std::unordered_map<Snowflake, int> user_to_color;
|
// std::unordered_map<Snowflake, int> user_to_color;
|
||||||
std::vector<Snowflake> roleless_users;
|
std::vector<Snowflake> roleless_users;
|
||||||
|
|
||||||
for (const auto &id : ids) {
|
for (const auto &user : users) {
|
||||||
auto user = discord.GetUser(id);
|
if (user.IsDeleted())
|
||||||
if (!user.has_value() || user->IsDeleted())
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
auto pos_role_id = discord.GetMemberHoistedRole(m_guild_id, id); // role for positioning
|
auto pos_role_id = discord.GetMemberHoistedRole(m_guild_id, user.ID); // role for positioning
|
||||||
auto col_role_id = discord.GetMemberHoistedRole(m_guild_id, id, true); // role for color
|
// auto col_role_id = discord.GetMemberHoistedRole(m_guild_id, id, true); // role for color
|
||||||
auto pos_role = discord.GetRole(pos_role_id);
|
auto pos_role = discord.GetRole(pos_role_id);
|
||||||
auto col_role = discord.GetRole(col_role_id);
|
// auto col_role = discord.GetRole(col_role_id);
|
||||||
|
|
||||||
if (!pos_role.has_value()) {
|
if (!pos_role.has_value()) {
|
||||||
roleless_users.push_back(id);
|
roleless_users.push_back(user.ID);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos_to_role[pos_role->Position] = *pos_role;
|
pos_to_role[pos_role->Position] = *pos_role;
|
||||||
pos_to_users[pos_role->Position].push_back(std::move(*user));
|
pos_to_users[pos_role->Position].push_back(user);
|
||||||
if (col_role.has_value())
|
// if (col_role.has_value())
|
||||||
user_to_color[id] = col_role->Color;
|
// user_to_color[id] = col_role->Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
int num_rows = 0;
|
|
||||||
const auto guild = discord.GetGuild(m_guild_id);
|
const auto guild = discord.GetGuild(m_guild_id);
|
||||||
if (!guild.has_value()) return;
|
if (!guild.has_value()) return;
|
||||||
auto add_user = [this, &num_rows, guild](const UserData &data) -> bool {
|
auto add_user = [this](const UserData &user, const Gtk::TreeNodeChildren &node) -> bool {
|
||||||
if (num_rows++ > MaxMemberListRows) return false;
|
auto row = *m_model->append(node);
|
||||||
auto *row = Gtk::manage(new MemberListUserRow(*guild, data));
|
row[m_columns.m_type] = MemberListRenderType::Member;
|
||||||
m_id_to_row[data.ID] = row;
|
row[m_columns.m_id] = user.ID;
|
||||||
AttachUserMenuHandler(row, data.ID);
|
row[m_columns.m_markup] = user.GetEscapedName() + "#" + user.Discriminator;
|
||||||
m_listbox->add(*row);
|
row[m_columns.m_sort] = static_cast<int>(user.ID);
|
||||||
|
row[m_columns.m_icon] = Abaddon::Get().GetImageManager().GetPlaceholder(16);
|
||||||
|
// come on
|
||||||
|
Gtk::TreeRowReference ref(m_model, m_model->get_path(row));
|
||||||
|
Abaddon::Get().GetImageManager().LoadFromURL(user.GetAvatarURL(), [this, ref = std::move(ref)](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
|
||||||
|
if (ref.is_valid()) {
|
||||||
|
auto row = *m_model->get_iter(ref.get_path());
|
||||||
|
row[m_columns.m_icon] = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
|
||||||
|
}
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto add_role = [this](const std::string &name) {
|
auto add_role = [this](const RoleData &role) -> Gtk::TreeRow {
|
||||||
auto *role_row = Gtk::manage(new Gtk::ListBoxRow);
|
auto row = *m_model->append();
|
||||||
auto *role_lbl = Gtk::manage(new Gtk::Label);
|
row[m_columns.m_type] = MemberListRenderType::Role;
|
||||||
|
row[m_columns.m_id] = role.ID;
|
||||||
role_row->get_style_context()->add_class("members-row");
|
row[m_columns.m_markup] = "<b>" + role.GetEscapedName() + "</b>";
|
||||||
role_row->get_style_context()->add_class("members-row-role");
|
row[m_columns.m_sort] = role.Position;
|
||||||
role_lbl->get_style_context()->add_class("members-row-label");
|
return row;
|
||||||
|
|
||||||
role_lbl->set_single_line_mode(true);
|
|
||||||
role_lbl->set_ellipsize(Pango::ELLIPSIZE_END);
|
|
||||||
role_lbl->set_use_markup(true);
|
|
||||||
role_lbl->set_markup("<b>" + Glib::Markup::escape_text(name) + "</b>");
|
|
||||||
role_lbl->set_halign(Gtk::ALIGN_START);
|
|
||||||
role_row->add(*role_lbl);
|
|
||||||
role_row->show_all();
|
|
||||||
m_listbox->add(*role_row);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto it = pos_to_role.crbegin(); it != pos_to_role.crend(); it++) {
|
for (auto &[pos, role] : pos_to_role) {
|
||||||
auto pos = it->first;
|
auto role_children = add_role(role).children();
|
||||||
const auto &role = it->second;
|
if (auto it = pos_to_users.find(pos); it != pos_to_users.end()) {
|
||||||
|
for (const auto &user : it->second) {
|
||||||
add_role(role.Name);
|
if (!add_user(user, role_children)) break;
|
||||||
|
}
|
||||||
if (pos_to_users.find(pos) == pos_to_users.end()) continue;
|
|
||||||
|
|
||||||
auto &users = pos_to_users.at(pos);
|
|
||||||
AlphabeticalSort(users.begin(), users.end(), [](const auto &e) { return e.Username; });
|
|
||||||
|
|
||||||
for (const auto &data : users)
|
|
||||||
if (!add_user(data)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM)
|
|
||||||
add_role("Users");
|
|
||||||
else
|
|
||||||
add_role("@everyone");
|
|
||||||
for (const auto &id : roleless_users) {
|
|
||||||
const auto user = discord.GetUser(id);
|
|
||||||
if (user.has_value())
|
|
||||||
if (!add_user(*user)) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
|
|
||||||
row->signal_button_press_event().connect([this, id](GdkEventButton *e) -> bool {
|
|
||||||
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
|
|
||||||
Abaddon::Get().ShowUserMenu(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
m_view.expand_all();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
MemberList::ModelColumns::ModelColumns() {
|
||||||
|
add(m_type);
|
||||||
|
add(m_id);
|
||||||
|
add(m_markup);
|
||||||
|
add(m_icon);
|
||||||
|
add(m_sort);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,37 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <gtkmm.h>
|
#include <variant>
|
||||||
#include <mutex>
|
#include <gtkmm/scrolledwindow.h>
|
||||||
#include <unordered_map>
|
#include <gtkmm/treemodel.h>
|
||||||
#include <optional>
|
#include <gtkmm/treestore.h>
|
||||||
#include "discord/discord.hpp"
|
#include <gtkmm/treeview.h>
|
||||||
|
#include "discord/snowflake.hpp"
|
||||||
|
#include "memberlistcellrenderer.hpp"
|
||||||
|
|
||||||
class LazyImage;
|
class MemberList : public Gtk::ScrolledWindow {
|
||||||
class StatusIndicator;
|
|
||||||
class MemberListUserRow : public Gtk::ListBoxRow {
|
|
||||||
public:
|
|
||||||
MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data);
|
|
||||||
|
|
||||||
Snowflake ID;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Gtk::EventBox *m_ev;
|
|
||||||
Gtk::Box *m_box;
|
|
||||||
LazyImage *m_avatar;
|
|
||||||
StatusIndicator *m_status_indicator;
|
|
||||||
Gtk::Label *m_label;
|
|
||||||
Gtk::Image *m_crown = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
class MemberList {
|
|
||||||
public:
|
public:
|
||||||
MemberList();
|
MemberList();
|
||||||
Gtk::Widget *GetRoot() const;
|
|
||||||
|
|
||||||
void UpdateMemberList();
|
void UpdateMemberList();
|
||||||
void Clear();
|
void Clear();
|
||||||
void SetActiveChannel(Snowflake id);
|
void SetActiveChannel(Snowflake id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id);
|
class ModelColumns : public Gtk::TreeModel::ColumnRecord {
|
||||||
|
public:
|
||||||
|
ModelColumns();
|
||||||
|
|
||||||
Gtk::ScrolledWindow *m_main;
|
Gtk::TreeModelColumn<MemberListRenderType> m_type;
|
||||||
Gtk::ListBox *m_listbox;
|
Gtk::TreeModelColumn<uint64_t> m_id;
|
||||||
|
Gtk::TreeModelColumn<Glib::ustring> m_markup;
|
||||||
|
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_icon;
|
||||||
|
|
||||||
|
Gtk::TreeModelColumn<int> m_sort;
|
||||||
|
};
|
||||||
|
|
||||||
|
ModelColumns m_columns;
|
||||||
|
Glib::RefPtr<Gtk::TreeStore> m_model;
|
||||||
|
Gtk::TreeView m_view;
|
||||||
|
|
||||||
Snowflake m_guild_id;
|
Snowflake m_guild_id;
|
||||||
Snowflake m_chan_id;
|
Snowflake m_chan_id;
|
||||||
|
|
||||||
std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_id_to_row;
|
|
||||||
};
|
};
|
||||||
|
|||||||
177
src/components/memberlistcellrenderer.cpp
Normal file
177
src/components/memberlistcellrenderer.cpp
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#include "memberlistcellrenderer.hpp"
|
||||||
|
#include <gdkmm/general.h>
|
||||||
|
|
||||||
|
CellRendererMemberList::CellRendererMemberList()
|
||||||
|
: Glib::ObjectBase(typeid(CellRendererMemberList))
|
||||||
|
, m_property_type(*this, "render-type")
|
||||||
|
, m_property_id(*this, "id")
|
||||||
|
, m_property_markup(*this, "markup")
|
||||||
|
, m_property_icon(*this, "pixbuf") {
|
||||||
|
m_renderer_text.property_ellipsize() = Pango::ELLIPSIZE_END;
|
||||||
|
m_property_markup.get_proxy().signal_changed().connect([this]() {
|
||||||
|
m_renderer_text.property_markup() = m_property_markup;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::PropertyProxy<MemberListRenderType> CellRendererMemberList::property_type() {
|
||||||
|
return m_property_type.get_proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::PropertyProxy<uint64_t> CellRendererMemberList::property_id() {
|
||||||
|
return m_property_id.get_proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::PropertyProxy<Glib::ustring> CellRendererMemberList::property_markup() {
|
||||||
|
return m_property_markup.get_proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererMemberList::property_icon() {
|
||||||
|
return m_property_icon.get_proxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||||
|
switch (m_property_type.get_value()) {
|
||||||
|
case MemberListRenderType::Member:
|
||||||
|
return get_preferred_width_vfunc_member(widget, minimum_width, natural_width);
|
||||||
|
case MemberListRenderType::Role:
|
||||||
|
return get_preferred_width_vfunc_role(widget, minimum_width, natural_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||||
|
switch (m_property_type.get_value()) {
|
||||||
|
case MemberListRenderType::Member:
|
||||||
|
return get_preferred_width_for_height_vfunc_member(widget, height, minimum_width, natural_width);
|
||||||
|
case MemberListRenderType::Role:
|
||||||
|
return get_preferred_width_for_height_vfunc_role(widget, height, minimum_width, natural_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||||
|
switch (m_property_type.get_value()) {
|
||||||
|
case MemberListRenderType::Member:
|
||||||
|
return get_preferred_height_vfunc_member(widget, minimum_height, natural_height);
|
||||||
|
case MemberListRenderType::Role:
|
||||||
|
return get_preferred_height_vfunc_role(widget, minimum_height, natural_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||||
|
switch (m_property_type.get_value()) {
|
||||||
|
case MemberListRenderType::Member:
|
||||||
|
return get_preferred_height_for_width_vfunc_member(widget, width, minimum_height, natural_height);
|
||||||
|
case MemberListRenderType::Role:
|
||||||
|
return get_preferred_height_for_width_vfunc_role(widget, width, minimum_height, natural_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||||
|
switch (m_property_type.get_value()) {
|
||||||
|
case MemberListRenderType::Member:
|
||||||
|
return render_vfunc_member(cr, widget, background_area, cell_area, flags);
|
||||||
|
case MemberListRenderType::Role:
|
||||||
|
return render_vfunc_role(cr, widget, background_area, cell_area, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_width_vfunc_role(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||||
|
int text_min, text_nat;
|
||||||
|
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
|
||||||
|
int xpad, ypad;
|
||||||
|
get_padding(xpad, ypad);
|
||||||
|
minimum_width = text_min + xpad * 2;
|
||||||
|
natural_width = text_nat + xpad * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_width_for_height_vfunc_role(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||||
|
get_preferred_width_vfunc_role(widget, minimum_width, natural_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_height_vfunc_role(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||||
|
int text_min, text_nat;
|
||||||
|
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
|
||||||
|
int xpad, ypad;
|
||||||
|
get_padding(xpad, ypad);
|
||||||
|
minimum_height = text_min + ypad * 2;
|
||||||
|
natural_height = text_nat + ypad * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_height_for_width_vfunc_role(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||||
|
get_preferred_height_vfunc_role(widget, minimum_height, natural_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::render_vfunc_role(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||||
|
Gtk::Requisition text_min, text_nat, min, nat;
|
||||||
|
m_renderer_text.get_preferred_size(widget, text_min, text_nat);
|
||||||
|
get_preferred_size(widget, min, nat);
|
||||||
|
|
||||||
|
int x = background_area.get_x() + 5;
|
||||||
|
int y = background_area.get_y() + background_area.get_height() / 2.0 - text_nat.height / 2.0;
|
||||||
|
int w = text_nat.width;
|
||||||
|
int h = text_nat.height;
|
||||||
|
|
||||||
|
Gdk::Rectangle text_cell_area(x, y, w, h);
|
||||||
|
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_width_vfunc_member(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||||
|
int text_min, text_nat;
|
||||||
|
m_renderer_text.get_preferred_width(widget, text_min, text_nat);
|
||||||
|
int xpad, ypad;
|
||||||
|
get_padding(xpad, ypad);
|
||||||
|
minimum_width = text_min + xpad * 2;
|
||||||
|
natural_width = text_nat + xpad * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_width_for_height_vfunc_member(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||||
|
get_preferred_width_vfunc_role(widget, minimum_width, natural_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_height_vfunc_member(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||||
|
int text_min, text_nat;
|
||||||
|
m_renderer_text.get_preferred_height(widget, text_min, text_nat);
|
||||||
|
int xpad, ypad;
|
||||||
|
get_padding(xpad, ypad);
|
||||||
|
minimum_height = text_min + ypad * 2;
|
||||||
|
natural_height = text_nat + ypad * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::get_preferred_height_for_width_vfunc_member(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||||
|
get_preferred_height_vfunc_role(widget, minimum_height, natural_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CellRendererMemberList::render_vfunc_member(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||||
|
Gtk::Requisition text_min, text_nat, min, nat;
|
||||||
|
m_renderer_text.get_preferred_size(widget, text_min, text_nat);
|
||||||
|
get_preferred_size(widget, min, nat);
|
||||||
|
|
||||||
|
int pixbuf_w = 0, pixbuf_h = 0;
|
||||||
|
if (auto pixbuf = m_property_icon.get_value()) {
|
||||||
|
pixbuf_w = pixbuf->get_width();
|
||||||
|
pixbuf_h = pixbuf->get_height();
|
||||||
|
}
|
||||||
|
|
||||||
|
const double icon_w = pixbuf_w;
|
||||||
|
const double icon_h = pixbuf_h;
|
||||||
|
const double icon_x = background_area.get_x() + 3.0;
|
||||||
|
const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
|
||||||
|
|
||||||
|
const double x = icon_x + icon_w + 5;
|
||||||
|
const double y = background_area.get_y() + background_area.get_height() / 2.0 - text_nat.height / 2.0;
|
||||||
|
const double w = text_nat.width;
|
||||||
|
const double h = text_nat.height;
|
||||||
|
|
||||||
|
if (auto pixbuf = m_property_icon.get_value()) {
|
||||||
|
Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
|
||||||
|
cr->rectangle(icon_x, icon_y, icon_w, icon_h);
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
Gdk::Rectangle text_cell_area(
|
||||||
|
static_cast<int>(x),
|
||||||
|
static_cast<int>(y),
|
||||||
|
static_cast<int>(w),
|
||||||
|
static_cast<int>(h));
|
||||||
|
|
||||||
|
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||||
|
}
|
||||||
60
src/components/memberlistcellrenderer.hpp
Normal file
60
src/components/memberlistcellrenderer.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <gdkmm/pixbuf.h>
|
||||||
|
#include <glibmm/property.h>
|
||||||
|
#include <gtkmm/cellrenderertext.h>
|
||||||
|
|
||||||
|
enum class MemberListRenderType {
|
||||||
|
Role,
|
||||||
|
Member,
|
||||||
|
};
|
||||||
|
|
||||||
|
class CellRendererMemberList : public Gtk::CellRenderer {
|
||||||
|
public:
|
||||||
|
CellRendererMemberList();
|
||||||
|
~CellRendererMemberList() = default;
|
||||||
|
|
||||||
|
Glib::PropertyProxy<MemberListRenderType> property_type();
|
||||||
|
Glib::PropertyProxy<uint64_t> property_id();
|
||||||
|
Glib::PropertyProxy<Glib::ustring> property_markup();
|
||||||
|
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override;
|
||||||
|
void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override;
|
||||||
|
void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override;
|
||||||
|
void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override;
|
||||||
|
void render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||||
|
Gtk::Widget &widget,
|
||||||
|
const Gdk::Rectangle &background_area,
|
||||||
|
const Gdk::Rectangle &cell_area,
|
||||||
|
Gtk::CellRendererState flags) override;
|
||||||
|
|
||||||
|
// role
|
||||||
|
void get_preferred_width_vfunc_role(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||||
|
void get_preferred_width_for_height_vfunc_role(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||||
|
void get_preferred_height_vfunc_role(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||||
|
void get_preferred_height_for_width_vfunc_role(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||||
|
void render_vfunc_role(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||||
|
Gtk::Widget &widget,
|
||||||
|
const Gdk::Rectangle &background_area,
|
||||||
|
const Gdk::Rectangle &cell_area,
|
||||||
|
Gtk::CellRendererState flags);
|
||||||
|
|
||||||
|
// member
|
||||||
|
void get_preferred_width_vfunc_member(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||||
|
void get_preferred_width_for_height_vfunc_member(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||||
|
void get_preferred_height_vfunc_member(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||||
|
void get_preferred_height_for_width_vfunc_member(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||||
|
void render_vfunc_member(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||||
|
Gtk::Widget &widget,
|
||||||
|
const Gdk::Rectangle &background_area,
|
||||||
|
const Gdk::Rectangle &cell_area,
|
||||||
|
Gtk::CellRendererState flags);
|
||||||
|
|
||||||
|
Gtk::CellRendererText m_renderer_text;
|
||||||
|
|
||||||
|
Glib::Property<MemberListRenderType> m_property_type;
|
||||||
|
Glib::Property<uint64_t> m_property_id;
|
||||||
|
Glib::Property<Glib::ustring> m_property_markup;
|
||||||
|
Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_icon;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -262,6 +262,12 @@ std::set<Snowflake> DiscordClient::GetUsersInGuild(Snowflake id) const {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<UserData> DiscordClient::GetUserDataInGuildBulk(Snowflake id) {
|
||||||
|
const auto ids = GetUsersInGuild(id);
|
||||||
|
std::vector<Snowflake> test;
|
||||||
|
return m_store.GetUserDataBulk(ids.begin(), ids.end());
|
||||||
|
}
|
||||||
|
|
||||||
std::set<Snowflake> DiscordClient::GetChannelsInGuild(Snowflake id) const {
|
std::set<Snowflake> DiscordClient::GetChannelsInGuild(Snowflake id) const {
|
||||||
auto it = m_guild_to_channels.find(id);
|
auto it = m_guild_to_channels.find(id);
|
||||||
if (it != m_guild_to_channels.end())
|
if (it != m_guild_to_channels.end())
|
||||||
@@ -573,10 +579,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) {});
|
||||||
}
|
}
|
||||||
@@ -1188,6 +1190,14 @@ void DiscordClient::SetReferringChannel(Snowflake id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -2314,7 +2324,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 = 141021;
|
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;
|
||||||
@@ -2433,9 +2443,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();
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public:
|
|||||||
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
|
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
|
||||||
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
|
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
|
||||||
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
|
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
|
||||||
|
std::vector<UserData> GetUserDataInGuildBulk(Snowflake id);
|
||||||
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
|
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
|
||||||
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
|
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
|
||||||
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;
|
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;
|
||||||
@@ -113,7 +114,6 @@ 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
|
||||||
@@ -206,6 +206,9 @@ public:
|
|||||||
|
|
||||||
void SetReferringChannel(Snowflake id);
|
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);
|
||||||
|
|
||||||
@@ -303,6 +306,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;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -23,6 +22,10 @@ void HTTPClient::SetPersistentHeader(std::string name, std::string value) {
|
|||||||
m_headers.insert_or_assign(std::move(name), std::move(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] {
|
||||||
@@ -31,10 +34,6 @@ void HTTPClient::MakeDELETE(const std::string &path, const std::function<void(ht
|
|||||||
req.set_header("Authorization", m_authorization);
|
req.set_header("Authorization", m_authorization);
|
||||||
req.set_header("Origin", "https://discord.com");
|
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();
|
||||||
|
|
||||||
@@ -52,10 +51,6 @@ void HTTPClient::MakePATCH(const std::string &path, const std::string &payload,
|
|||||||
req.set_header("Origin", "https://discord.com");
|
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();
|
||||||
|
|
||||||
@@ -73,10 +68,6 @@ void HTTPClient::MakePOST(const std::string &path, const std::string &payload, c
|
|||||||
req.set_header("Origin", "https://discord.com");
|
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();
|
||||||
|
|
||||||
@@ -95,10 +86,6 @@ void HTTPClient::MakePUT(const std::string &path, const std::string &payload, co
|
|||||||
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();
|
||||||
|
|
||||||
@@ -113,10 +100,6 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
|
|||||||
AddHeaders(req);
|
AddHeaders(req);
|
||||||
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
|
|
||||||
|
|
||||||
auto res = req.execute();
|
auto res = req.execute();
|
||||||
|
|
||||||
@@ -128,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +142,8 @@ void HTTPClient::AddHeaders(http::request &r) {
|
|||||||
for (const auto &[name, val] : m_headers) {
|
for (const auto &[name, val] : m_headers) {
|
||||||
r.set_header(name, val);
|
r.set_header(name, val);
|
||||||
}
|
}
|
||||||
curl_easy_setopt(r.get_curl(), CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br");
|
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) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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 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);
|
||||||
@@ -44,4 +45,5 @@ private:
|
|||||||
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::unordered_map<std::string, std::string> m_headers;
|
||||||
|
std::string m_cookie;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1033,6 +1033,22 @@ RoleData Store::GetRoleBound(std::unique_ptr<Statement> &s) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserData Store::GetUserBound(Statement *s) const {
|
||||||
|
UserData r;
|
||||||
|
|
||||||
|
s->Get(0, r.ID);
|
||||||
|
s->Get(1, r.Username);
|
||||||
|
s->Get(2, r.Discriminator);
|
||||||
|
s->Get(3, r.Avatar);
|
||||||
|
s->Get(4, r.IsBot);
|
||||||
|
s->Get(5, r.IsSystem);
|
||||||
|
s->Get(6, r.IsMFAEnabled);
|
||||||
|
s->Get(7, r.PremiumType);
|
||||||
|
s->Get(8, r.PublicFlags);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<UserData> Store::GetUser(Snowflake id) const {
|
std::optional<UserData> Store::GetUser(Snowflake id) const {
|
||||||
auto &s = m_stmt_get_user;
|
auto &s = m_stmt_get_user;
|
||||||
s->Bind(1, id);
|
s->Bind(1, id);
|
||||||
@@ -1043,17 +1059,7 @@ std::optional<UserData> Store::GetUser(Snowflake id) const {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
UserData r;
|
auto r = GetUserBound(s.get());
|
||||||
|
|
||||||
r.ID = id;
|
|
||||||
s->Get(1, r.Username);
|
|
||||||
s->Get(2, r.Discriminator);
|
|
||||||
s->Get(3, r.Avatar);
|
|
||||||
s->Get(4, r.IsBot);
|
|
||||||
s->Get(5, r.IsSystem);
|
|
||||||
s->Get(6, r.IsMFAEnabled);
|
|
||||||
s->Get(7, r.PremiumType);
|
|
||||||
s->Get(8, r.PublicFlags);
|
|
||||||
|
|
||||||
s->Reset();
|
s->Reset();
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,36 @@ public:
|
|||||||
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
|
std::optional<BanData> GetBan(Snowflake guild_id, Snowflake user_id) const;
|
||||||
std::vector<BanData> GetBans(Snowflake guild_id) const;
|
std::vector<BanData> GetBans(Snowflake guild_id) const;
|
||||||
|
|
||||||
|
template<typename Iter>
|
||||||
|
std::vector<UserData> GetUserDataBulk(Iter start, Iter end) {
|
||||||
|
std::string query = "SELECT * FROM users WHERE id IN (";
|
||||||
|
for (Iter it = start; it != end; it++) {
|
||||||
|
query += "?,";
|
||||||
|
}
|
||||||
|
query.pop_back();
|
||||||
|
query += ")";
|
||||||
|
|
||||||
|
Statement stmt(m_db, query.c_str());
|
||||||
|
if (!stmt.OK()) {
|
||||||
|
printf("failed to prepare GetUserDataBulk: %s\n", m_db.ErrStr());
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (Iter it = start; it != end; it++) {
|
||||||
|
i++;
|
||||||
|
if (stmt.Bind(i, *it) != SQLITE_OK) {
|
||||||
|
printf("failed to bind GetUserDataBulk: %s\n", m_db.ErrStr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<UserData> r;
|
||||||
|
while (stmt.FetchOne()) {
|
||||||
|
r.push_back(GetUserBound(&stmt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Message> GetLastMessages(Snowflake id, size_t num) const;
|
std::vector<Message> GetLastMessages(Snowflake id, size_t num) const;
|
||||||
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit) const;
|
std::vector<Message> GetMessagesBefore(Snowflake channel_id, Snowflake message_id, size_t limit) const;
|
||||||
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
|
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
|
||||||
@@ -240,6 +270,7 @@ private:
|
|||||||
|
|
||||||
Message GetMessageBound(std::unique_ptr<Statement> &stmt) const;
|
Message GetMessageBound(std::unique_ptr<Statement> &stmt) const;
|
||||||
static RoleData GetRoleBound(std::unique_ptr<Statement> &stmt);
|
static RoleData GetRoleBound(std::unique_ptr<Statement> &stmt);
|
||||||
|
UserData GetUserBound(Statement *stmt) const;
|
||||||
|
|
||||||
void SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction);
|
void SetMessageInteractionPair(Snowflake message_id, const MessageInteractionData &interaction);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
10
src/http.cpp
10
src/http.cpp
@@ -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)) {
|
||||||
@@ -147,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)
|
||||||
@@ -164,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
#include "settings.hpp"
|
#include "settings.hpp"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <glibmm/miscutils.h>
|
||||||
|
|
||||||
|
#ifdef WITH_KEYCHAIN
|
||||||
|
#include <keychain/keychain.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const std::string KeychainPackage = "com.github.uowuo.abaddon";
|
||||||
|
const std::string KeychainService = "abaddon-client-token";
|
||||||
|
const std::string KeychainUser = "";
|
||||||
|
|
||||||
SettingsManager::SettingsManager(const std::string &filename)
|
SettingsManager::SettingsManager(const std::string &filename)
|
||||||
: m_filename(filename) {
|
: m_filename(filename) {
|
||||||
@@ -36,9 +45,9 @@ void SettingsManager::ReadSettings() {
|
|||||||
|
|
||||||
SMSTR("discord", "api_base", APIBaseURL);
|
SMSTR("discord", "api_base", APIBaseURL);
|
||||||
SMSTR("discord", "gateway", GatewayURL);
|
SMSTR("discord", "gateway", GatewayURL);
|
||||||
SMSTR("discord", "token", DiscordToken);
|
|
||||||
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);
|
||||||
@@ -48,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);
|
||||||
@@ -58,6 +69,32 @@ void SettingsManager::ReadSettings() {
|
|||||||
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
||||||
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
||||||
|
|
||||||
|
#ifdef WITH_KEYCHAIN
|
||||||
|
keychain::Error error {};
|
||||||
|
|
||||||
|
// convert to keychain if present in normal settings
|
||||||
|
SMSTR("discord", "token", DiscordToken);
|
||||||
|
|
||||||
|
if (!m_settings.DiscordToken.empty()) {
|
||||||
|
keychain::Error set_error {};
|
||||||
|
|
||||||
|
keychain::setPassword(KeychainPackage, KeychainService, KeychainUser, m_settings.DiscordToken, set_error);
|
||||||
|
if (set_error) {
|
||||||
|
printf("keychain error setting token: %s\n", set_error.message.c_str());
|
||||||
|
} else {
|
||||||
|
m_file.remove_key("discord", "token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_settings.DiscordToken = keychain::getPassword(KeychainPackage, KeychainService, KeychainUser, error);
|
||||||
|
if (error && error.type != keychain::ErrorType::NotFound) {
|
||||||
|
printf("keychain error reading token: %s (%d)\n", error.message.c_str(), error.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
SMSTR("discord", "token", DiscordToken);
|
||||||
|
#endif
|
||||||
|
|
||||||
#undef SMBOOL
|
#undef SMBOOL
|
||||||
#undef SMSTR
|
#undef SMSTR
|
||||||
#undef SMINT
|
#undef SMINT
|
||||||
@@ -89,9 +126,9 @@ void SettingsManager::Close() {
|
|||||||
|
|
||||||
SMSTR("discord", "api_base", APIBaseURL);
|
SMSTR("discord", "api_base", APIBaseURL);
|
||||||
SMSTR("discord", "gateway", GatewayURL);
|
SMSTR("discord", "gateway", GatewayURL);
|
||||||
SMSTR("discord", "token", DiscordToken);
|
|
||||||
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);
|
||||||
@@ -101,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);
|
||||||
@@ -111,6 +150,17 @@ void SettingsManager::Close() {
|
|||||||
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor);
|
||||||
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
SMSTR("style", "unreadcolor", UnreadIndicatorColor);
|
||||||
|
|
||||||
|
#ifdef WITH_KEYCHAIN
|
||||||
|
keychain::Error error {};
|
||||||
|
|
||||||
|
keychain::setPassword(KeychainPackage, KeychainService, KeychainUser, m_settings.DiscordToken, error);
|
||||||
|
if (error) {
|
||||||
|
printf("keychain error setting token: %s\n", error.message.c_str());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
SMSTR("discord", "token", DiscordToken);
|
||||||
|
#endif
|
||||||
|
|
||||||
#undef SMSTR
|
#undef SMSTR
|
||||||
#undef SMBOOL
|
#undef SMBOOL
|
||||||
#undef SMINT
|
#undef SMINT
|
||||||
|
|||||||
@@ -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
126
src/startup.cpp
Normal 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
25
src/startup.hpp
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "mainwindow.hpp"
|
#include "mainwindow.hpp"
|
||||||
#include "abaddon.hpp"
|
#include "abaddon.hpp"
|
||||||
|
|
||||||
|
#include "components/memberlist.hpp" // TMP!!!!
|
||||||
|
|
||||||
MainWindow::MainWindow()
|
MainWindow::MainWindow()
|
||||||
: m_main_box(Gtk::ORIENTATION_VERTICAL)
|
: m_main_box(Gtk::ORIENTATION_VERTICAL)
|
||||||
, m_content_box(Gtk::ORIENTATION_HORIZONTAL)
|
, m_content_box(Gtk::ORIENTATION_HORIZONTAL)
|
||||||
@@ -20,7 +22,6 @@ MainWindow::MainWindow()
|
|||||||
m_main_box.add(m_content_box);
|
m_main_box.add(m_content_box);
|
||||||
m_main_box.show();
|
m_main_box.show();
|
||||||
|
|
||||||
auto *member_list = m_members.GetRoot();
|
|
||||||
auto *chat = m_chat.GetRoot();
|
auto *chat = m_chat.GetRoot();
|
||||||
|
|
||||||
chat->set_vexpand(true);
|
chat->set_vexpand(true);
|
||||||
@@ -37,8 +38,8 @@ MainWindow::MainWindow()
|
|||||||
m_channel_list.set_size_request(-1, -1);
|
m_channel_list.set_size_request(-1, -1);
|
||||||
m_channel_list.show();
|
m_channel_list.show();
|
||||||
|
|
||||||
member_list->set_vexpand(true);
|
m_members.set_vexpand(true);
|
||||||
member_list->show();
|
m_members.show();
|
||||||
|
|
||||||
m_friends.set_vexpand(true);
|
m_friends.set_vexpand(true);
|
||||||
m_friends.set_hexpand(true);
|
m_friends.set_hexpand(true);
|
||||||
@@ -63,11 +64,11 @@ MainWindow::MainWindow()
|
|||||||
m_channel_list.UsePanedHack(m_chan_content_paned);
|
m_channel_list.UsePanedHack(m_chan_content_paned);
|
||||||
|
|
||||||
m_content_members_paned.pack1(m_content_stack);
|
m_content_members_paned.pack1(m_content_stack);
|
||||||
m_content_members_paned.pack2(*member_list);
|
m_content_members_paned.pack2(m_members);
|
||||||
m_content_members_paned.child_property_shrink(m_content_stack) = true;
|
m_content_members_paned.child_property_shrink(m_content_stack) = true;
|
||||||
m_content_members_paned.child_property_resize(m_content_stack) = true;
|
m_content_members_paned.child_property_resize(m_content_stack) = true;
|
||||||
m_content_members_paned.child_property_shrink(*member_list) = true;
|
m_content_members_paned.child_property_shrink(m_members) = true;
|
||||||
m_content_members_paned.child_property_resize(*member_list) = true;
|
m_content_members_paned.child_property_resize(m_members) = true;
|
||||||
int w, h;
|
int w, h;
|
||||||
get_default_size(w, h); // :s
|
get_default_size(w, h); // :s
|
||||||
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
|
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
|
||||||
@@ -158,6 +159,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 +200,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 +242,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 +266,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 +282,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 +292,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 +324,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 +363,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.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 +414,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 +428,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;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "components/channels.hpp"
|
#include "components/channels.hpp"
|
||||||
#include "components/chatwindow.hpp"
|
#include "components/chatwindow.hpp"
|
||||||
#include "components/memberlist.hpp"
|
|
||||||
#include "components/friendslist.hpp"
|
#include "components/friendslist.hpp"
|
||||||
|
#include "components/memberlist.hpp"
|
||||||
#include <gtkmm.h>
|
#include <gtkmm.h>
|
||||||
|
|
||||||
class MainWindow : public Gtk::Window {
|
class MainWindow : public Gtk::Window {
|
||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring ¶m);
|
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring ¶m);
|
||||||
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring ¶m);
|
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring ¶m);
|
||||||
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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
subprojects/keychain
Submodule
1
subprojects/keychain
Submodule
Submodule subprojects/keychain added at 44b517d096
Reference in New Issue
Block a user