20 Commits

Author SHA1 Message Date
ouwou
190118bb58 try optimizing user fetch 2022-12-18 19:59:48 -05:00
ouwou
6926b12a50 fix build 2022-12-18 17:57:43 -05:00
ouwou
fb010b5ac5 Merge branch 'master' into member-list 2022-12-18 17:54:19 -05:00
ouwou
acb03642c2 Merge pull request #124 from uowuo/keychain
store token in keychain
2022-12-18 20:52:19 +00:00
ouwou
ba2aea86f9 Merge branch 'master' into keychain 2022-12-16 19:28:39 -05:00
abdalrzag eisa
c704703b14 doc: Display CSS selectors and Settings as tables (#126) 2022-12-03 23:18:14 +00:00
Altoids1
33a329f16a Fixes a few minor typos in the README (#127) 2022-12-02 20:59:40 +00:00
abdalrzag eisa
f0df06a795 doc: list deps for building on Arch Linux (#125) 2022-12-02 08:57:29 +00:00
ouwou
9ae41b7335 Merge branch 'master' into keychain 2022-12-01 20:00:44 -05:00
ouwou
b9fee0f6c9 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-12-01 02:25:35 -05:00
ouwou
92273829bb update ci run to latest nlohmann/json release 2022-12-01 01:33:10 -05:00
abdalrzag eisa
86f6f81d9b add missing setting (#123) 2022-12-01 00:56:22 +00:00
ouwou
573a619191 Merge branch 'master' into keychain 2022-11-29 15:53:02 -05:00
abdalrzag eisa
c5807a3463 Make README.md more readable (#120)
* More noticeable warnings
* Make CSS selectors stand out more from their description
* Make Settings options stand out more from their description, and make the default value easy to see
2022-11-15 07:47:16 +00:00
ouwou
b0370ee489 Merge branch 'master' into member-list 2022-11-03 16:50:30 -04:00
ouwou
2a9f49a148 add menu item + shortcuts to hide channel and member lists (closes #118) 2022-11-03 00:45:45 -04:00
ouwou
00fe6642a9 basic functionality 2022-09-11 02:46:30 -04:00
ouwou
77dd9fabfa change service clear user 2022-08-09 02:06:24 -04:00
ouwou
ee67037a3f store token in keychain 2022-08-08 23:25:34 -04:00
ouwou
b46cf53be5 add hrantzsch/keychain and link 2022-08-08 22:50:27 -04:00
47 changed files with 695 additions and 3682 deletions

View File

@@ -37,7 +37,6 @@ jobs:
mingw-w64-x86_64-curl mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3 mingw-w64-x86_64-gtkmm3
mingw-w64-x86_64-spdlog
if_false: >- if_false: >-
git git
make make
@@ -50,9 +49,6 @@ jobs:
mingw-w64-x86_64-zlib mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3 mingw-w64-x86_64-gtkmm3
mingw-w64-x86_64-libhandy mingw-w64-x86_64-libhandy
mingw-w64-x86_64-opus
mingw-w64-x86_64-libsodium
mingw-w64-x86_64-spdlog
- name: Setup MSYS2 (2) - name: Setup MSYS2 (2)
uses: msys2/setup-msys2@v2 uses: msys2/setup-msys2@v2
@@ -61,21 +57,11 @@ jobs:
update: true update: true
install: ${{ steps.setupmsys.outputs.value }} install: ${{ steps.setupmsys.outputs.value }}
- name: Build (1) - name: Build
uses: haya14busa/action-cond@v1 run: |
id: buildcmd
with:
cond: ${{ matrix.mindeps == true }}
if_true: |
cmake -GNinja -Bbuild -DUSE_LIBHANDY=OFF -DENABLE_VOICE=OFF -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
cmake --build build
if_false: |
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
cmake --build build cmake --build build
- name: Build (2)
run: ${{ steps.buildcmd.outputs.value }}
- name: Setup Artifact - name: Setup Artifact
run: | run: |
mkdir -p build/artifactdir/bin build/artifactdir/ssl/certs build/artifactdir/lib build/artifactdir/share/glib-2.0/schemas mkdir -p build/artifactdir/bin build/artifactdir/ssl/certs build/artifactdir/lib build/artifactdir/share/glib-2.0/schemas
@@ -133,10 +119,6 @@ jobs:
brew install gtkmm3 brew install gtkmm3
brew install nlohmann-json brew install nlohmann-json
brew install jpeg brew install jpeg
brew install opus
brew install libsodium
brew install spdlog
brew install libhandy
- name: Build - name: Build
uses: lukka/run-cmake@v3 uses: lukka/run-cmake@v3
@@ -159,7 +141,7 @@ jobs:
linux: linux:
name: linux-${{ matrix.buildtype }} name: linux-${{ matrix.buildtype }}
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
buildtype: [Debug, RelWithDebInfo, MinSizeRel] buildtype: [Debug, RelWithDebInfo, MinSizeRel]
@@ -186,10 +168,6 @@ jobs:
sudo make install sudo make install
sudo apt-get install libgtkmm-3.0-dev sudo apt-get install libgtkmm-3.0-dev
sudo apt-get install libcurl4-gnutls-dev sudo apt-get install libcurl4-gnutls-dev
sudo apt-get install libopus-dev
sudo apt-get install libsodium-dev
sudo apt-get install libspdlog-dev
sudo apt-get install libhandy-1-dev
- name: Build - name: Build
uses: lukka/run-cmake@v3 uses: lukka/run-cmake@v3

8
.gitmodules vendored
View File

@@ -3,7 +3,7 @@
url = https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer url = https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer
[submodule "subprojects/ixwebsocket"] [submodule "subprojects/ixwebsocket"]
path = subprojects/ixwebsocket path = subprojects/ixwebsocket
url = https://github.com/ouwou/ixwebsocket url = https://github.com/machinezone/ixwebsocket
[submodule "subprojects/miniaudio"] [submodule "subprojects/keychain"]
path = subprojects/miniaudio path = subprojects/keychain
url = https://github.com/mackron/miniaudio url = https://github.com/hrantzsch/keychain

View File

@@ -8,7 +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(ENABLE_VOICE "Enable voice suppport" 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)
@@ -90,9 +90,6 @@ if (Fontconfig_FOUND)
target_link_libraries(abaddon Fontconfig::Fontconfig) target_link_libraries(abaddon Fontconfig::Fontconfig)
endif () endif ()
find_package(spdlog REQUIRED)
target_link_libraries(abaddon spdlog::spdlog)
target_link_libraries(abaddon ${SQLite3_LIBRARIES}) target_link_libraries(abaddon ${SQLite3_LIBRARIES})
target_link_libraries(abaddon ${GTKMM_LIBRARIES}) target_link_libraries(abaddon ${GTKMM_LIBRARIES})
target_link_libraries(abaddon ${CURL_LIBRARIES}) target_link_libraries(abaddon ${CURL_LIBRARIES})
@@ -111,17 +108,12 @@ if (USE_LIBHANDY)
endif () endif ()
endif () endif ()
if (ENABLE_VOICE) if (USE_KEYCHAIN)
target_compile_definitions(abaddon PRIVATE WITH_VOICE) find_package(keychain QUIET)
if (NOT keychain_FOUND)
find_package(PkgConfig) message("keychain was not found and will be included as a submodule")
add_subdirectory(subprojects/keychain)
target_include_directories(abaddon PUBLIC subprojects/miniaudio) target_link_libraries(abaddon keychain)
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus) target_compile_definitions(abaddon PRIVATE WITH_KEYCHAIN)
target_link_libraries(abaddon PkgConfig::Opus) endif ()
pkg_check_modules(libsodium REQUIRED IMPORTED_TARGET libsodium)
target_link_libraries(abaddon PkgConfig::libsodium)
target_link_libraries(abaddon ${CMAKE_DL_LIBS})
endif () endif ()

241
README.md
View File

@@ -75,6 +75,10 @@ the result of fundamental issues with Discord's thread implementation.
```Shell ```Shell
$ sudo apt install g++ cmake libgtkmm-3.0-dev libcurl4-gnutls-dev libsqlite3-dev libssl-dev nlohmann-json3-dev $ 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 ..`
@@ -92,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`
@@ -135,145 +139,158 @@ spam filter's wrath:
#### 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 |
.channel-list - Container of the channel list | `.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 | `.messages` | Container of user messages |
.message-container - The container which holds a user's messages | `.message-container` | The container which holds a user's messages |
.message-container-author - The author label for a message container | `.message-container-author` | The author label for a message container |
.message-container-timestamp - The timestamp label for a message container | `.message-container-timestamp` | The timestamp label for a message container |
.message-container-avatar - Avatar for a user in a message | `.message-container-avatar` | Avatar for a user in a message |
.message-container-extra - Label containing BOT/Webhook | `.message-container-extra` | Label containing BOT/Webhook |
.message-text - The text of a user message | `.message-text` | The text of a user message |
.pending - Extra class of .message-text for messages pending to be sent | `.pending` | Extra class of .message-text for messages pending to be sent |
.failed - Extra class of .message-text for messages that failed to be sent | `.failed` | Extra class of .message-text for messages that failed to be sent |
.message-attachment-box - Contains attachment info | `.message-attachment-box` | Contains attachment info |
.message-reply - Container for the replied-to message in a reply (these elements will also have .message-text set) | `.message-reply` | Container for the replied-to message in a reply (these elements will also have .message-text set) |
.message-input - Applied to the chat input container | `.message-input` | Applied to the chat input container |
.replying - Extra class for chat input container when a reply is currently being created | `.replying` | Extra class for chat input container when a reply is currently being created |
.reaction-box - Contains a reaction image and the count | `.reaction-box` | Contains a reaction image and the count |
.reacted - Additional class for reaction-box when the user has reacted with a particular reaction | `.reacted` | Additional class for reaction-box when the user has reacted with a particular reaction |
.reaction-count - Contains the count for reaction | `.reaction-count` | Contains the count for reaction |
| `.completer` | Container for the message completer |
.completer - Container for the message completer | `.completer-entry` | Container for a single entry in the completer |
.completer-entry - Container for a single entry in the completer | `.completer-entry-label` | Contains the label for an entry in the completer |
.completer-entry-label - Contains the label for an entry in the completer | `.completer-entry-image` | Contains the image for an entry in the completer |
.completer-entry-image - Contains the image for an entry in the completer | `.embed` | Container for a message embed |
| `.embed-author` | The author of an embed |
.embed - Container for a message embed | `.embed-title` | The title of an embed |
.embed-author - The author of an embed | `.embed-description` | The description of an embed |
.embed-title - The title of an embed | `.embed-field-title` | The title of an embed field |
.embed-description - The description of an embed | `.embed-field-value` | The value of an embed field |
.embed-field-title - The title of an embed field | `.embed-footer` | The footer of an embed |
.embed-field-value - The value of an embed field | `.members` | Container of the member list |
.embed-footer - The footer of an embed | `.members-row` | All rows within the members container |
| `.members-row-label` | All labels in the members container |
.members - Container of the member list | `.members-row-member` | Rows containing a member |
.members-row - All rows within the members container | `.members-row-role` | Rows containing a role |
.members-row-label - All labels in the members container | `.members-row-avatar` | Contains the avatar for a row in the member list |
.members-row-member - Rows containing a member | `.status-indicator` | The status indicator |
.members-row-role - Rows containing a role | `.online` | Applied to status indicators when the associated user is online |
.members-row-avatar - Contains the avatar for a row in the member list | `.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 |
.status-indicator - The status indicator | `.offline` | Applied to status indicators when the associated user is offline |
.online - Applied to status indicators when the associated user is online | `.typing-indicator` | The typing indicator (also used for replies) |
.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)
Used in reorderable list implementation: Used in reorderable list implementation:
.drag-icon .drag-hover-top .drag-hover-bottom | Selector |
|----------------------|
| `.drag-icon` |
| `.drag-hover-top` |
| `.drag-hover-bottom` |
Used in guild settings popup: Used in guild settings popup:
.guild-settings-window
.guild-members-pane-list - Container for list of members in the members pane | Selector | Description |
.guild-members-pane-info - Container for member info |----------------------------|---------------------------------------------------|
.guild-roles-pane-list - Container for list of roles in the roles pane | `.guild-settings-window` | Container for list of members in the members pane |
| `.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 |
Used in profile popup: Used in profile popup:
.mutual-friend-item - Applied to every item in the mutual friends list
.mutual-friend-item-name - Name in mutual friend item | Selector | Description |
.mutual-friend-item-avatar - Avatar in mutual friend item |------------------------------|---------------------------------------------------------|
.mutual-guild-item - Applied to every item in the mutual guilds list | `.mutual-friend-item` | Applied to every item in the mutual friends list |
.mutual-guild-item-name - Name in mutual guild item | `.mutual-friend-item-name` | Name in mutual friend item |
.mutual-guild-item-icon - Icon in mutual guild item | `.mutual-friend-item-avatar` | Avatar in mutual friend item |
.mutual-guild-item-nick - User nickname in mutual guild item | `.mutual-guild-item` | Applied to every item in the mutual guilds list |
.profile-connection - Applied to every item in the user connections list | `.mutual-guild-item-name` | Name in mutual guild item |
.profile-connection-label - Label in profile connection item | `.mutual-guild-item-icon` | Icon in mutual guild item |
.profile-connection-check - Checkmark in verified profile connection items | `.mutual-guild-item-nick` | User nickname in mutual guild item |
.profile-connections - Container for profile connections | `.profile-connection` | Applied to every item in the user connections list |
.profile-notes - Container for notes in profile window | `.profile-connection-label` | Label in profile connection item |
.profile-notes-label - Label that says "NOTE" | `.profile-connection-check` | Checkmark in verified profile connection items |
.profile-notes-text - Actual note text | `.profile-connections` | Container for profile connections |
.profile-info-pane - Applied to container for info section of profile popup | `.profile-notes` | Container for notes in profile window |
.profile-info-created - Label for creation date of profile | `.profile-notes-label` | Label that says "NOTE" |
.user-profile-window | `.profile-notes-text` | Actual note text |
.profile-main-container - Inner container for profile | `.profile-info-pane` | Applied to container for info section of profile popup |
.profile-avatar | `.profile-info-created` | Label for creation date of profile |
.profile-username | `.user-profile-window` | |
.profile-switcher - Buttons used to switch viewed section of profile | `.profile-main-container` | Inner container for profile |
.profile-stack - Container for profile info that can be switched between | `.profile-avatar` | |
.profile-badges - Container for badges | `.profile-username` | |
.profile-badge | `.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) |
* save_state (true or false, default 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 |
* alt_menu (true or false, default false) - keep the menu hidden unless revealed with alt key | `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close |
* hide_to_tray (true or false, default false) - hide abaddon to the system tray on window close
#### style #### style
* linkcolor (string) - color to use for links in messages | 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 |

View File

@@ -47,7 +47,6 @@
/bin/libpng16-16.dll /bin/libpng16-16.dll
/bin/libpsl-5.dll /bin/libpsl-5.dll
/bin/libsigc-2.0-0.dll /bin/libsigc-2.0-0.dll
/bin/libspdlog.dll
/bin/libsqlite3-0.dll /bin/libsqlite3-0.dll
/bin/libssh2-1.dll /bin/libssh2-1.dll
/bin/libssl-1_1-x64.dll /bin/libssl-1_1-x64.dll

View File

@@ -44,7 +44,7 @@ has to be separate to allow main.css to override certain things
background: @secondary_color; background: @secondary_color;
} }
.app-window list, .app-popup list { .app-popup list {
background: @secondary_color; background: @secondary_color;
} }
@@ -87,11 +87,3 @@ has to be separate to allow main.css to override certain things
.app-window colorswatch { .app-window colorswatch {
box-shadow: 0 1px rgba(0, 0, 0, 0); box-shadow: 0 1px rgba(0, 0, 0, 0);
} }
.app-window scale {
padding-top: 0px;
padding-bottom: 0px;
margin-top: 0px;
margin-bottom: 0px;
color: @text_color;
}

View File

@@ -360,21 +360,3 @@
background-color: #dd3300; background-color: #dd3300;
margin-left: 1px; margin-left: 1px;
} }
.voice-info {
background-color: #0B0B0B;
padding: 5px;
border: 1px solid #202020;
}
.voice-info-disconnect-image {
color: #DDDDDD;
}
.voice-info-status {
font-weight: bold;
}
.voice-info-location {
}

View File

@@ -1,12 +1,8 @@
#include <gtkmm.h> #include <gtkmm.h>
#include <memory> #include <memory>
#include <spdlog/spdlog.h>
#include <spdlog/cfg/env.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <string> #include <string>
#include <algorithm> #include <algorithm>
#include "platform.hpp" #include "platform.hpp"
#include "audio/manager.hpp"
#include "discord/discord.hpp" #include "discord/discord.hpp"
#include "dialogs/token.hpp" #include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp" #include "dialogs/editmessage.hpp"
@@ -20,7 +16,6 @@
#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 "windows/voicewindow.hpp"
#include "startup.hpp" #include "startup.hpp"
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
@@ -41,8 +36,7 @@ Abaddon::Abaddon()
std::string ua = GetSettings().UserAgent; std::string ua = GetSettings().UserAgent;
m_discord.SetUserAgent(ua); m_discord.SetUserAgent(ua);
// todo rename funcs m_discord.signal_gateway_ready().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady));
m_discord.signal_gateway_ready_supplemental().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady));
m_discord.signal_message_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageCreate)); m_discord.signal_message_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageCreate));
m_discord.signal_message_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete)); m_discord.signal_message_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete));
m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate)); m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate));
@@ -54,16 +48,6 @@ Abaddon::Abaddon()
m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate)); m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate));
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent)); m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect)); m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
#ifdef WITH_VOICE
m_discord.signal_voice_connected().connect(sigc::mem_fun(*this, &Abaddon::OnVoiceConnected));
m_discord.signal_voice_disconnected().connect(sigc::mem_fun(*this, &Abaddon::OnVoiceDisconnected));
m_discord.signal_voice_speaking().connect([this](const VoiceSpeakingData &m) {
printf("%llu has ssrc %u\n", (uint64_t)m.UserID, m.SSRC);
m_audio->AddSSRC(m.SSRC);
});
#endif
m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) { m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) {
if (!accessible) if (!accessible)
m_channels_requested.erase(id); m_channels_requested.erase(id);
@@ -244,16 +228,6 @@ int Abaddon::StartGTK() {
return 1; return 1;
} }
#ifdef WITH_VOICE
m_audio = std::make_unique<AudioManager>();
if (!m_audio->OK()) {
Gtk::MessageDialog dlg(*m_main_window, "The audio engine could not be initialized!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
return 1;
}
#endif
// store must be checked before this can be called // store must be checked before this can be called
m_main_window->UpdateComponents(); m_main_window->UpdateComponents();
@@ -273,11 +247,6 @@ int Abaddon::StartGTK() {
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild)); m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings)); m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings));
#ifdef WITH_VOICE
m_main_window->GetChannelList()->signal_action_join_voice_channel().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinVoiceChannel));
m_main_window->GetChannelList()->signal_action_disconnect_voice().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnectVoice));
#endif
m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage)); m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage));
m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit)); m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit));
m_main_window->GetChatWindow()->signal_action_chat_load_history().connect(sigc::mem_fun(*this, &Abaddon::ActionChatLoadHistory)); m_main_window->GetChatWindow()->signal_action_chat_load_history().connect(sigc::mem_fun(*this, &Abaddon::ActionChatLoadHistory));
@@ -429,64 +398,6 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) {
} }
} }
#ifdef WITH_VOICE
void Abaddon::OnVoiceConnected() {
m_audio->StartCaptureDevice();
auto *wnd = new VoiceWindow(m_discord.GetVoiceChannelID());
m_voice_window = wnd;
wnd->signal_mute().connect([this](bool is_mute) {
m_discord.SetVoiceMuted(is_mute);
m_audio->SetCapture(!is_mute);
});
wnd->signal_deafen().connect([this](bool is_deaf) {
m_discord.SetVoiceDeafened(is_deaf);
m_audio->SetPlayback(!is_deaf);
});
wnd->signal_gate().connect([this](double gate) {
m_audio->SetCaptureGate(gate);
});
wnd->signal_gain().connect([this](double gain) {
m_audio->SetCaptureGain(gain);
});
wnd->signal_mute_user_cs().connect([this](Snowflake id, bool is_mute) {
if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) {
m_audio->SetMuteSSRC(*ssrc, is_mute);
}
});
wnd->signal_user_volume_changed().connect([this](Snowflake id, double volume) {
if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) {
m_audio->SetVolumeSSRC(*ssrc, volume);
}
});
wnd->set_position(Gtk::WIN_POS_CENTER);
wnd->show();
wnd->signal_hide().connect([this, wnd]() {
m_discord.DisconnectFromVoice();
m_voice_window = nullptr;
delete wnd;
delete m_user_menu;
SetupUserMenu();
});
}
void Abaddon::OnVoiceDisconnected() {
m_audio->StopCaptureDevice();
m_audio->RemoveAllSSRCs();
if (m_voice_window != nullptr) {
m_voice_window->close();
}
}
#endif
SettingsManager::Settings &Abaddon::GetSettings() { SettingsManager::Settings &Abaddon::GetSettings() {
return m_settings.GetSettings(); return m_settings.GetSettings();
} }
@@ -1005,16 +916,6 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) {
window->show(); window->show();
} }
#ifdef WITH_VOICE
void Abaddon::ActionJoinVoiceChannel(Snowflake channel_id) {
m_discord.ConnectToVoice(channel_id);
}
void Abaddon::ActionDisconnectVoice() {
m_discord.DisconnectFromVoice();
}
#endif
std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) { std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) {
TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window); TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window);
const auto code = dlg.run(); const auto code = dlg.run();
@@ -1054,24 +955,15 @@ EmojiResource &Abaddon::GetEmojis() {
return m_emojis; return m_emojis;
} }
#ifdef WITH_VOICE
AudioManager &Abaddon::GetAudio() {
return *m_audio;
}
#endif
void Abaddon::on_tray_click() { void Abaddon::on_tray_click() {
m_main_window->set_visible(!m_main_window->is_visible()); m_main_window->set_visible(!m_main_window->is_visible());
} }
void Abaddon::on_tray_menu_click() { void Abaddon::on_tray_menu_click() {
m_gtk_app->quit(); m_gtk_app->quit();
} }
void Abaddon::on_tray_popup_menu(int button, int activate_time) { void Abaddon::on_tray_popup_menu(int button, int activate_time) {
m_tray->popup_menu_at_position(*m_tray_menu, button, activate_time); m_tray->popup_menu_at_position(*m_tray_menu, button, activate_time);
} }
void Abaddon::on_window_hide() { void Abaddon::on_window_hide() {
if (!m_settings.GetSettings().HideToTray) { if (!m_settings.GetSettings().HideToTray) {
m_gtk_app->quit(); m_gtk_app->quit();
@@ -1102,12 +994,6 @@ int main(int argc, char **argv) {
if (buf[0] != '1') if (buf[0] != '1')
SetEnvironmentVariableA("GTK_CSD", "0"); SetEnvironmentVariableA("GTK_CSD", "0");
#endif #endif
spdlog::cfg::load_env_levels();
auto log_audio = spdlog::stdout_color_mt("audio");
auto log_voice = spdlog::stdout_color_mt("voice");
auto log_discord = spdlog::stdout_color_mt("discord");
Gtk::Main::init_gtkmm_internals(); // why??? Gtk::Main::init_gtkmm_internals(); // why???
return Abaddon::Get().StartGTK(); return Abaddon::Get().StartGTK();
} }

View File

@@ -12,8 +12,6 @@
#define APP_TITLE "Abaddon" #define APP_TITLE "Abaddon"
class AudioManager;
class Abaddon { class Abaddon {
private: private:
Abaddon(); Abaddon();
@@ -54,11 +52,6 @@ public:
void ActionViewPins(Snowflake channel_id); void ActionViewPins(Snowflake channel_id);
void ActionViewThreads(Snowflake channel_id); void ActionViewThreads(Snowflake channel_id);
#ifdef WITH_VOICE
void ActionJoinVoiceChannel(Snowflake channel_id);
void ActionDisconnectVoice();
#endif
std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr); std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr); bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
@@ -67,10 +60,6 @@ public:
ImageManager &GetImageManager(); ImageManager &GetImageManager();
EmojiResource &GetEmojis(); EmojiResource &GetEmojis();
#ifdef WITH_VOICE
AudioManager &GetAudio();
#endif
std::string GetDiscordToken() const; std::string GetDiscordToken() const;
bool IsDiscordActive() const; bool IsDiscordActive() const;
@@ -89,11 +78,6 @@ public:
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code); void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
void DiscordOnThreadUpdate(const ThreadUpdateData &data); void DiscordOnThreadUpdate(const ThreadUpdateData &data);
#ifdef WITH_VOICE
void OnVoiceConnected();
void OnVoiceDisconnected();
#endif
SettingsManager::Settings &GetSettings(); SettingsManager::Settings &GetSettings();
Glib::RefPtr<Gtk::CssProvider> GetStyleProvider(); Glib::RefPtr<Gtk::CssProvider> GetStyleProvider();
@@ -160,11 +144,6 @@ private:
ImageManager m_img_mgr; ImageManager m_img_mgr;
EmojiResource m_emojis; EmojiResource m_emojis;
#ifdef WITH_VOICE
std::unique_ptr<AudioManager> m_audio;
Gtk::Window *m_voice_window = nullptr;
#endif
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
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;

View File

@@ -1,121 +0,0 @@
#ifdef WITH_VOICE
// clang-format off
#include "devices.hpp"
#include <cstring>
#include <spdlog/spdlog.h>
// clang-format on
AudioDevices::AudioDevices()
: m_playback(Gtk::ListStore::create(m_playback_columns))
, m_capture(Gtk::ListStore::create(m_capture_columns)) {
}
Glib::RefPtr<Gtk::ListStore> AudioDevices::GetPlaybackDeviceModel() {
return m_playback;
}
Glib::RefPtr<Gtk::ListStore> AudioDevices::GetCaptureDeviceModel() {
return m_capture;
}
void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count) {
m_playback->clear();
for (ma_uint32 i = 0; i < playback_count; i++) {
auto &d = pPlayback[i];
auto row = *m_playback->append();
row[m_playback_columns.Name] = d.name;
row[m_playback_columns.DeviceID] = d.id;
if (d.isDefault) {
m_default_playback_iter = row;
SetActivePlaybackDevice(row);
}
}
m_capture->clear();
for (ma_uint32 i = 0; i < capture_count; i++) {
auto &d = pCapture[i];
auto row = *m_capture->append();
row[m_capture_columns.Name] = d.name;
row[m_capture_columns.DeviceID] = d.id;
if (d.isDefault) {
m_default_capture_iter = row;
SetActiveCaptureDevice(row);
}
}
if (!m_default_playback_iter) {
spdlog::get("audio")->warn("No default playback device found");
}
if (!m_default_capture_iter) {
spdlog::get("audio")->warn("No default capture device found");
}
}
std::optional<ma_device_id> AudioDevices::GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const {
if (iter) {
return static_cast<ma_device_id>((*iter)[m_playback_columns.DeviceID]);
}
return std::nullopt;
}
std::optional<ma_device_id> AudioDevices::GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const {
if (iter) {
return static_cast<ma_device_id>((*iter)[m_capture_columns.DeviceID]);
}
return std::nullopt;
}
std::optional<ma_device_id> AudioDevices::GetDefaultPlayback() const {
if (m_default_playback_iter) {
return static_cast<ma_device_id>((*m_default_playback_iter)[m_playback_columns.DeviceID]);
}
return std::nullopt;
}
std::optional<ma_device_id> AudioDevices::GetDefaultCapture() const {
if (m_default_capture_iter) {
return static_cast<ma_device_id>((*m_default_capture_iter)[m_capture_columns.DeviceID]);
}
return std::nullopt;
}
void AudioDevices::SetActivePlaybackDevice(const Gtk::TreeModel::iterator &iter) {
m_active_playback_iter = iter;
}
void AudioDevices::SetActiveCaptureDevice(const Gtk::TreeModel::iterator &iter) {
m_active_capture_iter = iter;
}
Gtk::TreeModel::iterator AudioDevices::GetActivePlaybackDevice() {
return m_active_playback_iter;
}
Gtk::TreeModel::iterator AudioDevices::GetActiveCaptureDevice() {
return m_active_capture_iter;
}
AudioDevices::PlaybackColumns::PlaybackColumns() {
add(Name);
add(DeviceID);
}
AudioDevices::CaptureColumns::CaptureColumns() {
add(Name);
add(DeviceID);
}
#endif

View File

@@ -1,58 +0,0 @@
#pragma once
#ifdef WITH_VOICE
// clang-format off
#include <gtkmm/liststore.h>
#include <miniaudio.h>
#include <optional>
// clang-format on
class AudioDevices {
public:
AudioDevices();
Glib::RefPtr<Gtk::ListStore> GetPlaybackDeviceModel();
Glib::RefPtr<Gtk::ListStore> GetCaptureDeviceModel();
void SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count);
[[nodiscard]] std::optional<ma_device_id> GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const;
[[nodiscard]] std::optional<ma_device_id> GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const;
[[nodiscard]] std::optional<ma_device_id> GetDefaultPlayback() const;
[[nodiscard]] std::optional<ma_device_id> GetDefaultCapture() const;
void SetActivePlaybackDevice(const Gtk::TreeModel::iterator &iter);
void SetActiveCaptureDevice(const Gtk::TreeModel::iterator &iter);
Gtk::TreeModel::iterator GetActivePlaybackDevice();
Gtk::TreeModel::iterator GetActiveCaptureDevice();
private:
class PlaybackColumns : public Gtk::TreeModel::ColumnRecord {
public:
PlaybackColumns();
Gtk::TreeModelColumn<Glib::ustring> Name;
Gtk::TreeModelColumn<ma_device_id> DeviceID;
};
PlaybackColumns m_playback_columns;
Glib::RefPtr<Gtk::ListStore> m_playback;
Gtk::TreeModel::iterator m_active_playback_iter;
Gtk::TreeModel::iterator m_default_playback_iter;
class CaptureColumns : public Gtk::TreeModel::ColumnRecord {
public:
CaptureColumns();
Gtk::TreeModelColumn<Glib::ustring> Name;
Gtk::TreeModelColumn<ma_device_id> DeviceID;
};
CaptureColumns m_capture_columns;
Glib::RefPtr<Gtk::ListStore> m_capture;
Gtk::TreeModel::iterator m_active_capture_iter;
Gtk::TreeModel::iterator m_default_capture_iter;
};
#endif

View File

@@ -1,466 +0,0 @@
#ifdef WITH_VOICE
// clang-format off
#ifdef _WIN32
#include <winsock2.h>
#endif
#include "manager.hpp"
#include <array>
#include <glibmm/main.h>
#include <spdlog/spdlog.h>
#define MINIAUDIO_IMPLEMENTATION
#include <miniaudio.h>
#include <opus.h>
#include <cstring>
// clang-format on
const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) {
if (buf[0] == 0xbe && buf[1] == 0xde && num_bytes > 4) {
uint64_t offset = 4 + 4 * ((buf[2] << 8) | buf[3]);
outlen = num_bytes - offset;
return buf + offset;
}
outlen = num_bytes;
return buf;
}
void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount) {
AudioManager *mgr = reinterpret_cast<AudioManager *>(pDevice->pUserData);
if (mgr == nullptr) return;
std::lock_guard<std::mutex> _(mgr->m_mutex);
auto *pOutputF32 = static_cast<float *>(pOutput);
for (auto &[ssrc, pair] : mgr->m_sources) {
double volume = 1.0;
if (const auto vol_it = mgr->m_volume_ssrc.find(ssrc); vol_it != mgr->m_volume_ssrc.end()) {
volume = vol_it->second;
}
auto &buf = pair.first;
const size_t n = std::min(static_cast<size_t>(buf.size()), static_cast<size_t>(frameCount * 2ULL));
for (size_t i = 0; i < n; i++) {
pOutputF32[i] += volume * buf[i] / 32768.F;
}
buf.erase(buf.begin(), buf.begin() + n);
}
}
void capture_data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount) {
auto *mgr = reinterpret_cast<AudioManager *>(pDevice->pUserData);
if (mgr == nullptr) return;
mgr->OnCapturedPCM(static_cast<const int16_t *>(pInput), frameCount);
}
AudioManager::AudioManager() {
m_ok = true;
int err;
m_encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_VOIP, &err);
if (err != OPUS_OK) {
spdlog::get("audio")->error("failed to initialize opus encoder: {}", err);
m_ok = false;
return;
}
opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(64000));
if (ma_context_init(nullptr, 0, nullptr, &m_context) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to initialize context");
m_ok = false;
return;
}
spdlog::get("audio")->info("Audio backend: {}", ma_get_backend_name(m_context.backend));
Enumerate();
m_playback_config = ma_device_config_init(ma_device_type_playback);
m_playback_config.playback.format = ma_format_f32;
m_playback_config.playback.channels = 2;
m_playback_config.sampleRate = 48000;
m_playback_config.dataCallback = data_callback;
m_playback_config.pUserData = this;
if (const auto playback_id = m_devices.GetDefaultPlayback(); playback_id.has_value()) {
m_playback_id = *playback_id;
m_playback_config.playback.pDeviceID = &m_playback_id;
}
if (ma_device_init(&m_context, &m_playback_config, &m_playback_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to initialize playback device");
m_ok = false;
return;
}
if (ma_device_start(&m_playback_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to start playback");
ma_device_uninit(&m_playback_device);
m_ok = false;
return;
}
m_capture_config = ma_device_config_init(ma_device_type_capture);
m_capture_config.capture.format = ma_format_s16;
m_capture_config.capture.channels = 2;
m_capture_config.sampleRate = 48000;
m_capture_config.periodSizeInFrames = 480;
m_capture_config.dataCallback = capture_data_callback;
m_capture_config.pUserData = this;
if (const auto capture_id = m_devices.GetDefaultCapture(); capture_id.has_value()) {
m_capture_id = *capture_id;
m_capture_config.capture.pDeviceID = &m_capture_id;
}
if (ma_device_init(&m_context, &m_capture_config, &m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("failed to initialize capture device");
m_ok = false;
return;
}
char playback_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1];
ma_device_get_name(&m_playback_device, ma_device_type_playback, playback_device_name, sizeof(playback_device_name), nullptr);
spdlog::get("audio")->info("using {} as playback device", playback_device_name);
char capture_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1];
ma_device_get_name(&m_capture_device, ma_device_type_capture, capture_device_name, sizeof(capture_device_name), nullptr);
spdlog::get("audio")->info("using {} as capture device", capture_device_name);
Glib::signal_timeout().connect(sigc::mem_fun(*this, &AudioManager::DecayVolumeMeters), 40);
}
AudioManager::~AudioManager() {
ma_device_uninit(&m_playback_device);
ma_device_uninit(&m_capture_device);
ma_context_uninit(&m_context);
RemoveAllSSRCs();
}
void AudioManager::AddSSRC(uint32_t ssrc) {
std::lock_guard<std::mutex> _(m_mutex);
int error;
if (m_sources.find(ssrc) == m_sources.end()) {
auto *decoder = opus_decoder_create(48000, 2, &error);
m_sources.insert(std::make_pair(ssrc, std::make_pair(std::deque<int16_t> {}, decoder)));
}
}
void AudioManager::RemoveSSRC(uint32_t ssrc) {
std::lock_guard<std::mutex> _(m_mutex);
if (auto it = m_sources.find(ssrc); it != m_sources.end()) {
opus_decoder_destroy(it->second.second);
m_sources.erase(it);
}
}
void AudioManager::RemoveAllSSRCs() {
spdlog::get("audio")->info("removing all ssrc");
std::lock_guard<std::mutex> _(m_mutex);
for (auto &[ssrc, pair] : m_sources) {
opus_decoder_destroy(pair.second);
}
m_sources.clear();
}
void AudioManager::SetOpusBuffer(uint8_t *ptr) {
m_opus_buffer = ptr;
}
void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector<uint8_t> &data) {
if (!m_should_playback) return;
std::lock_guard<std::mutex> _(m_mutex);
if (m_muted_ssrcs.find(ssrc) != m_muted_ssrcs.end()) return;
size_t payload_size = 0;
const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast<int>(data.size()), payload_size);
static std::array<opus_int16, 120 * 48 * 2> pcm;
if (auto it = m_sources.find(ssrc); it != m_sources.end()) {
int decoded = opus_decode(it->second.second, opus_encoded, static_cast<opus_int32>(payload_size), pcm.data(), 120 * 48, 0);
if (decoded <= 0) {
} else {
UpdateReceiveVolume(ssrc, pcm.data(), decoded);
auto &buf = it->second.first;
buf.insert(buf.end(), pcm.begin(), pcm.begin() + decoded * 2);
}
}
}
void AudioManager::StartCaptureDevice() {
if (ma_device_start(&m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to start capture device");
}
}
void AudioManager::StopCaptureDevice() {
if (ma_device_stop(&m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to stop capture device");
}
}
void AudioManager::SetPlaybackDevice(const Gtk::TreeModel::iterator &iter) {
spdlog::get("audio")->debug("Setting new playback device");
const auto device_id = m_devices.GetPlaybackDeviceIDFromModel(iter);
if (!device_id) {
spdlog::get("audio")->error("Requested ID from iterator is invalid");
return;
}
m_devices.SetActivePlaybackDevice(iter);
m_playback_id = *device_id;
ma_device_uninit(&m_playback_device);
m_playback_config = ma_device_config_init(ma_device_type_playback);
m_playback_config.playback.format = ma_format_f32;
m_playback_config.playback.channels = 2;
m_playback_config.playback.pDeviceID = &m_playback_id;
m_playback_config.sampleRate = 48000;
m_playback_config.dataCallback = data_callback;
m_playback_config.pUserData = this;
if (ma_device_init(&m_context, &m_playback_config, &m_playback_device) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to initialize new device");
return;
}
if (ma_device_start(&m_playback_device) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to start new device");
return;
}
}
void AudioManager::SetCaptureDevice(const Gtk::TreeModel::iterator &iter) {
spdlog::get("audio")->debug("Setting new capture device");
const auto device_id = m_devices.GetCaptureDeviceIDFromModel(iter);
if (!device_id) {
spdlog::get("audio")->error("Requested ID from iterator is invalid");
return;
}
m_devices.SetActiveCaptureDevice(iter);
m_capture_id = *device_id;
ma_device_uninit(&m_capture_device);
m_capture_config = ma_device_config_init(ma_device_type_capture);
m_capture_config.capture.format = ma_format_s16;
m_capture_config.capture.channels = 2;
m_capture_config.capture.pDeviceID = &m_capture_id;
m_capture_config.sampleRate = 48000;
m_capture_config.periodSizeInFrames = 480;
m_capture_config.dataCallback = capture_data_callback;
m_capture_config.pUserData = this;
if (ma_device_init(&m_context, &m_capture_config, &m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to initialize new device");
return;
}
// technically this should probably try and check old state but if you are in the window to change it then you are connected
if (ma_device_start(&m_capture_device) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to start new device");
return;
}
}
void AudioManager::SetCapture(bool capture) {
m_should_capture = capture;
}
void AudioManager::SetPlayback(bool playback) {
m_should_playback = playback;
}
void AudioManager::SetCaptureGate(double gate) {
m_capture_gate = gate * 0.01;
}
void AudioManager::SetCaptureGain(double gain) {
m_capture_gain = gain;
}
void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) {
std::lock_guard<std::mutex> _(m_mutex);
if (mute) {
m_muted_ssrcs.insert(ssrc);
} else {
m_muted_ssrcs.erase(ssrc);
}
}
void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) {
std::lock_guard<std::mutex> _(m_mutex);
volume *= 0.01;
constexpr const double E = 2.71828182845904523536;
m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1);
}
void AudioManager::SetEncodingApplication(int application) {
std::lock_guard<std::mutex> _(m_enc_mutex);
int prev_bitrate = 64000;
if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_BITRATE(&prev_bitrate)); err != OPUS_OK) {
spdlog::get("audio")->error("Failed to get old bitrate when reinitializing: {}", err);
}
opus_encoder_destroy(m_encoder);
int err = 0;
m_encoder = opus_encoder_create(48000, 2, application, &err);
if (err != OPUS_OK) {
spdlog::get("audio")->critical("opus_encoder_create failed: {}", err);
return;
}
if (int err = opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(prev_bitrate)); err != OPUS_OK) {
spdlog::get("audio")->error("Failed to set bitrate when reinitializing: {}", err);
}
}
int AudioManager::GetEncodingApplication() {
std::lock_guard<std::mutex> _(m_enc_mutex);
int temp = OPUS_APPLICATION_VOIP;
if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_APPLICATION(&temp)); err != OPUS_OK) {
spdlog::get("audio")->error("opus_encoder_ctl(OPUS_GET_APPLICATION) failed: {}", err);
}
return temp;
}
void AudioManager::SetSignalHint(int signal) {
std::lock_guard<std::mutex> _(m_enc_mutex);
if (int err = opus_encoder_ctl(m_encoder, OPUS_SET_SIGNAL(signal)); err != OPUS_OK) {
spdlog::get("audio")->error("opus_encoder_ctl(OPUS_SET_SIGNAL) failed: {}", err);
}
}
int AudioManager::GetSignalHint() {
std::lock_guard<std::mutex> _(m_enc_mutex);
int temp = OPUS_AUTO;
if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_SIGNAL(&temp)); err != OPUS_OK) {
spdlog::get("audio")->error("opus_encoder_ctl(OPUS_GET_SIGNAL) failed: {}", err);
}
return temp;
}
void AudioManager::SetBitrate(int bitrate) {
std::lock_guard<std::mutex> _(m_enc_mutex);
if (int err = opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(bitrate)); err != OPUS_OK) {
spdlog::get("audio")->error("opus_encoder_ctl(OPUS_SET_BITRATE) failed: {}", err);
}
}
int AudioManager::GetBitrate() {
std::lock_guard<std::mutex> _(m_enc_mutex);
int temp = 64000;
if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_BITRATE(&temp)); err != OPUS_OK) {
spdlog::get("audio")->error("opus_encoder_ctl(OPUS_GET_BITRATE) failed: {}", err);
}
return temp;
}
void AudioManager::Enumerate() {
ma_device_info *pPlaybackDeviceInfo;
ma_uint32 playbackDeviceCount;
ma_device_info *pCaptureDeviceInfo;
ma_uint32 captureDeviceCount;
spdlog::get("audio")->debug("Enumerating devices");
if (ma_context_get_devices(
&m_context,
&pPlaybackDeviceInfo,
&playbackDeviceCount,
&pCaptureDeviceInfo,
&captureDeviceCount) != MA_SUCCESS) {
spdlog::get("audio")->error("Failed to enumerate devices");
return;
}
spdlog::get("audio")->debug("Found {} playback devices and {} capture devices", playbackDeviceCount, captureDeviceCount);
m_devices.SetDevices(pPlaybackDeviceInfo, playbackDeviceCount, pCaptureDeviceInfo, captureDeviceCount);
}
void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) {
if (m_opus_buffer == nullptr || !m_should_capture) return;
const double gain = m_capture_gain;
// i have a suspicion i can cast the const away... but i wont
std::vector<int16_t> new_pcm(pcm, pcm + frames * 2);
for (auto &val : new_pcm) {
const int32_t unclamped = static_cast<int32_t>(val * gain);
val = std::clamp(unclamped, INT16_MIN, INT16_MAX);
}
UpdateCaptureVolume(new_pcm.data(), frames);
if (m_capture_peak_meter / 32768.0 < m_capture_gate) return;
m_enc_mutex.lock();
int payload_len = opus_encode(m_encoder, new_pcm.data(), 480, static_cast<unsigned char *>(m_opus_buffer), 1275);
m_enc_mutex.unlock();
if (payload_len < 0) {
spdlog::get("audio")->error("encoding error: {}", payload_len);
} else {
m_signal_opus_packet.emit(payload_len);
}
}
void AudioManager::UpdateReceiveVolume(uint32_t ssrc, const int16_t *pcm, int frames) {
std::lock_guard<std::mutex> _(m_vol_mtx);
auto &meter = m_volumes[ssrc];
for (int i = 0; i < frames * 2; i += 2) {
const int amp = std::abs(pcm[i]);
meter = std::max(meter, std::abs(amp) / 32768.0);
}
}
void AudioManager::UpdateCaptureVolume(const int16_t *pcm, ma_uint32 frames) {
for (ma_uint32 i = 0; i < frames * 2; i += 2) {
const int amp = std::abs(pcm[i]);
m_capture_peak_meter = std::max(m_capture_peak_meter.load(std::memory_order_relaxed), amp);
}
}
bool AudioManager::DecayVolumeMeters() {
m_capture_peak_meter -= 600;
if (m_capture_peak_meter < 0) m_capture_peak_meter = 0;
std::lock_guard<std::mutex> _(m_vol_mtx);
for (auto &[ssrc, meter] : m_volumes) {
meter -= 0.01;
if (meter < 0.0) meter = 0.0;
}
return true;
}
bool AudioManager::OK() const {
return m_ok;
}
double AudioManager::GetCaptureVolumeLevel() const noexcept {
return m_capture_peak_meter / 32768.0;
}
double AudioManager::GetSSRCVolumeLevel(uint32_t ssrc) const noexcept {
std::lock_guard<std::mutex> _(m_vol_mtx);
if (const auto it = m_volumes.find(ssrc); it != m_volumes.end()) {
return it->second;
}
return 0.0;
}
AudioDevices &AudioManager::GetDevices() {
return m_devices;
}
AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() {
return m_signal_opus_packet;
}
#endif

View File

@@ -1,120 +0,0 @@
#pragma once
#ifdef WITH_VOICE
// clang-format off
#include <array>
#include <atomic>
#include <deque>
#include <gtkmm/treemodel.h>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <miniaudio.h>
#include <opus.h>
#include <sigc++/sigc++.h>
#include "devices.hpp"
// clang-format on
class AudioManager {
public:
AudioManager();
~AudioManager();
void AddSSRC(uint32_t ssrc);
void RemoveSSRC(uint32_t ssrc);
void RemoveAllSSRCs();
void SetOpusBuffer(uint8_t *ptr);
void FeedMeOpus(uint32_t ssrc, const std::vector<uint8_t> &data);
void StartCaptureDevice();
void StopCaptureDevice();
void SetPlaybackDevice(const Gtk::TreeModel::iterator &iter);
void SetCaptureDevice(const Gtk::TreeModel::iterator &iter);
void SetCapture(bool capture);
void SetPlayback(bool playback);
void SetCaptureGate(double gate);
void SetCaptureGain(double gain);
void SetMuteSSRC(uint32_t ssrc, bool mute);
void SetVolumeSSRC(uint32_t ssrc, double volume);
void SetEncodingApplication(int application);
[[nodiscard]] int GetEncodingApplication();
void SetSignalHint(int signal);
[[nodiscard]] int GetSignalHint();
void SetBitrate(int bitrate);
[[nodiscard]] int GetBitrate();
void Enumerate();
[[nodiscard]] bool OK() const;
[[nodiscard]] double GetCaptureVolumeLevel() const noexcept;
[[nodiscard]] double GetSSRCVolumeLevel(uint32_t ssrc) const noexcept;
[[nodiscard]] AudioDevices &GetDevices();
private:
void OnCapturedPCM(const int16_t *pcm, ma_uint32 frames);
void UpdateReceiveVolume(uint32_t ssrc, const int16_t *pcm, int frames);
void UpdateCaptureVolume(const int16_t *pcm, ma_uint32 frames);
std::atomic<int> m_capture_peak_meter = 0;
bool DecayVolumeMeters();
friend void data_callback(ma_device *, void *, const void *, ma_uint32);
friend void capture_data_callback(ma_device *, void *, const void *, ma_uint32);
std::thread m_thread;
bool m_ok;
// playback
ma_device m_playback_device;
ma_device_config m_playback_config;
ma_device_id m_playback_id;
// capture
ma_device m_capture_device;
ma_device_config m_capture_config;
ma_device_id m_capture_id;
ma_context m_context;
mutable std::mutex m_mutex;
mutable std::mutex m_enc_mutex;
std::unordered_map<uint32_t, std::pair<std::deque<int16_t>, OpusDecoder *>> m_sources;
OpusEncoder *m_encoder;
uint8_t *m_opus_buffer = nullptr;
std::atomic<bool> m_should_capture = true;
std::atomic<bool> m_should_playback = true;
std::atomic<double> m_capture_gate = 0.0;
std::atomic<double> m_capture_gain = 1.0;
std::unordered_set<uint32_t> m_muted_ssrcs;
std::unordered_map<uint32_t, double> m_volume_ssrc;
mutable std::mutex m_vol_mtx;
std::unordered_map<uint32_t, double> m_volumes;
AudioDevices m_devices;
public:
using type_signal_opus_packet = sigc::signal<void(int payload_size)>;
type_signal_opus_packet signal_opus_packet();
private:
type_signal_opus_packet m_signal_opus_packet;
};
#endif

View File

@@ -20,17 +20,9 @@ ChannelList::ChannelList()
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
, m_menu_channel_open_tab("Open in New _Tab", true) , m_menu_channel_open_tab("Open in New _Tab", true)
, m_menu_dm_open_tab("Open in New _Tab", true) , m_menu_dm_open_tab("Open in New _Tab", true)
#endif
#ifdef WITH_VOICE
, m_menu_voice_channel_join("_Join", true)
, m_menu_voice_channel_disconnect("_Disconnect", true)
#endif #endif
, m_menu_dm_copy_id("_Copy ID", true) , m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not , m_menu_dm_close("") // changes depending on if group or not
#ifdef WITH_VOICE
, m_menu_dm_join_voice("Join _Voice", true)
, m_menu_dm_disconnect_voice("_Disconnect Voice", true)
#endif
, m_menu_thread_copy_id("_Copy ID", true) , m_menu_thread_copy_id("_Copy ID", true)
, m_menu_thread_leave("_Leave", true) , m_menu_thread_leave("_Leave", true)
, m_menu_thread_archive("_Archive", true) , m_menu_thread_archive("_Archive", true)
@@ -44,11 +36,7 @@ ChannelList::ChannelList()
const auto type = row[m_columns.m_type]; const auto type = row[m_columns.m_type];
// text channels should not be allowed to be collapsed // text channels should not be allowed to be collapsed
// maybe they should be but it seems a little difficult to handle expansion to permit this // maybe they should be but it seems a little difficult to handle expansion to permit this
#ifdef WITH_VOICE
if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) {
#else
if (type != RenderType::TextChannel) { if (type != RenderType::TextChannel) {
#endif
if (row[m_columns.m_expanded]) { if (row[m_columns.m_expanded]) {
m_view.collapse_row(path); m_view.collapse_row(path);
row[m_columns.m_expanded] = false; row[m_columns.m_expanded] = false;
@@ -58,11 +46,7 @@ ChannelList::ChannelList()
} }
} }
#ifdef WITH_VOICE
if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel) {
#else
if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) {
#endif
const auto id = static_cast<Snowflake>(row[m_columns.m_id]); const auto id = static_cast<Snowflake>(row[m_columns.m_id]);
m_signal_action_channel_item_select.emit(id); m_signal_action_channel_item_select.emit(id);
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {}); Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {});
@@ -177,21 +161,6 @@ ChannelList::ChannelList()
m_menu_channel.append(m_menu_channel_copy_id); m_menu_channel.append(m_menu_channel_copy_id);
m_menu_channel.show_all(); m_menu_channel.show_all();
#ifdef WITH_VOICE
m_menu_voice_channel_join.signal_activate().connect([this]() {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
m_signal_action_join_voice_channel.emit(id);
});
m_menu_voice_channel_disconnect.signal_activate().connect([this]() {
m_signal_action_disconnect_voice.emit();
});
m_menu_voice_channel.append(m_menu_voice_channel_join);
m_menu_voice_channel.append(m_menu_voice_channel_disconnect);
m_menu_voice_channel.show_all();
#endif
m_menu_dm_copy_id.signal_activate().connect([this] { m_menu_dm_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
}); });
@@ -223,17 +192,6 @@ ChannelList::ChannelList()
#endif #endif
m_menu_dm.append(m_menu_dm_toggle_mute); m_menu_dm.append(m_menu_dm_toggle_mute);
m_menu_dm.append(m_menu_dm_close); m_menu_dm.append(m_menu_dm_close);
#ifdef WITH_VOICE
m_menu_dm_join_voice.signal_activate().connect([this]() {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
m_signal_action_join_voice_channel.emit(id);
});
m_menu_dm_disconnect_voice.signal_activate().connect([this]() {
m_signal_action_disconnect_voice.emit();
});
m_menu_dm.append(m_menu_dm_join_voice);
m_menu_dm.append(m_menu_dm_disconnect_voice);
#endif
m_menu_dm.append(m_menu_dm_copy_id); m_menu_dm.append(m_menu_dm_copy_id);
m_menu_dm.show_all(); m_menu_dm.show_all();
@@ -621,11 +579,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel_ : *guild.Channels) { for (const auto &channel_ : *guild.Channels) {
const auto channel = discord.GetChannel(channel_.ID); const auto channel = discord.GetChannel(channel_.ID);
if (!channel.has_value()) continue; if (!channel.has_value()) continue;
#ifdef WITH_VOICE
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) {
#else
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) {
#endif
if (channel->ParentID.has_value()) if (channel->ParentID.has_value())
categories[*channel->ParentID].push_back(*channel); categories[*channel->ParentID].push_back(*channel);
else else
@@ -651,31 +605,9 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
m_tmp_channel_map[thread.ID] = CreateThreadRow(row.children(), thread); m_tmp_channel_map[thread.ID] = CreateThreadRow(row.children(), thread);
}; };
auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) {
for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) {
const auto user = discord.GetUser(user_id);
auto user_row = *m_model->append(root);
user_row[m_columns.m_type] = RenderType::VoiceParticipant;
user_row[m_columns.m_id] = user_id;
if (user.has_value()) {
user_row[m_columns.m_name] = user->GetEscapedName();
} else {
user_row[m_columns.m_name] = "<i>Unknown</i>";
}
}
};
for (const auto &channel : orphan_channels) { for (const auto &channel : orphan_channels) {
auto channel_row = *m_model->append(guild_row.children()); auto channel_row = *m_model->append(guild_row.children());
if (IsTextChannel(channel.Type))
channel_row[m_columns.m_type] = RenderType::TextChannel; channel_row[m_columns.m_type] = RenderType::TextChannel;
#ifdef WITH_VOICE
else {
channel_row[m_columns.m_type] = RenderType::VoiceChannel;
add_voice_participants(channel, channel_row->children());
}
#endif
channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
@@ -698,14 +630,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel : channels) { for (const auto &channel : channels) {
auto channel_row = *m_model->append(cat_row.children()); auto channel_row = *m_model->append(cat_row.children());
if (IsTextChannel(channel.Type))
channel_row[m_columns.m_type] = RenderType::TextChannel; channel_row[m_columns.m_type] = RenderType::TextChannel;
#ifdef WITH_VOICE
else {
channel_row[m_columns.m_type] = RenderType::VoiceChannel;
add_voice_participants(channel, channel_row->children());
}
#endif
channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position; channel_row[m_columns.m_sort] = *channel.Position;
@@ -807,11 +732,7 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const
m_last_selected = m_model->get_path(row); m_last_selected = m_model->get_path(row);
auto type = (*m_model->get_iter(path))[m_columns.m_type]; auto type = (*m_model->get_iter(path))[m_columns.m_type];
#ifdef WITH_VOICE
return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel;
#else
return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread;
#endif
} }
void ChannelList::AddPrivateChannels() { void ChannelList::AddPrivateChannels() {
@@ -935,12 +856,6 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
OnChannelSubmenuPopup(); OnChannelSubmenuPopup();
m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break; break;
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
OnVoiceChannelSubmenuPopup();
m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
#endif
case RenderType::DM: { case RenderType::DM: {
OnDMSubmenuPopup(); OnDMSubmenuPopup();
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id])); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
@@ -1032,41 +947,14 @@ void ChannelList::OnChannelSubmenuPopup() {
m_menu_channel_toggle_mute.set_label("Mute"); m_menu_channel_toggle_mute.set_label("Mute");
} }
#ifdef WITH_VOICE
void ChannelList::OnVoiceChannelSubmenuPopup() {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) {
m_menu_voice_channel_join.set_sensitive(false);
m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id);
} else {
m_menu_voice_channel_join.set_sensitive(true);
m_menu_voice_channel_disconnect.set_sensitive(false);
}
}
#endif
void ChannelList::OnDMSubmenuPopup() { void ChannelList::OnDMSubmenuPopup() {
auto iter = m_model->get_iter(m_path_for_menu); auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return; if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient(); if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
if (discord.IsChannelMuted(id))
m_menu_dm_toggle_mute.set_label("Unmute"); m_menu_dm_toggle_mute.set_label("Unmute");
else else
m_menu_dm_toggle_mute.set_label("Mute"); m_menu_dm_toggle_mute.set_label("Mute");
#ifdef WITH_VOICE
if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) {
m_menu_dm_join_voice.set_sensitive(false);
m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id);
} else {
m_menu_dm_join_voice.set_sensitive(true);
m_menu_dm_disconnect_voice.set_sensitive(false);
}
#endif
} }
void ChannelList::OnThreadSubmenuPopup() { void ChannelList::OnThreadSubmenuPopup() {
@@ -1109,16 +997,6 @@ ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new
} }
#endif #endif
#ifdef WITH_VOICE
ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() {
return m_signal_action_join_voice_channel;
}
ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() {
return m_signal_action_disconnect_voice;
}
#endif
ChannelList::ModelColumns::ModelColumns() { ChannelList::ModelColumns::ModelColumns() {
add(m_type); add(m_type);
add(m_id); add(m_id);

View File

@@ -125,20 +125,10 @@ protected:
Gtk::MenuItem m_menu_channel_open_tab; Gtk::MenuItem m_menu_channel_open_tab;
#endif #endif
#ifdef WITH_VOICE
Gtk::Menu m_menu_voice_channel;
Gtk::MenuItem m_menu_voice_channel_join;
Gtk::MenuItem m_menu_voice_channel_disconnect;
#endif
Gtk::Menu m_menu_dm; Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id; Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close; Gtk::MenuItem m_menu_dm_close;
Gtk::MenuItem m_menu_dm_toggle_mute; Gtk::MenuItem m_menu_dm_toggle_mute;
#ifdef WITH_VOICE
Gtk::MenuItem m_menu_dm_join_voice;
Gtk::MenuItem m_menu_dm_disconnect_voice;
#endif
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_dm_open_tab; Gtk::MenuItem m_menu_dm_open_tab;
@@ -158,10 +148,6 @@ protected:
void OnDMSubmenuPopup(); void OnDMSubmenuPopup();
void OnThreadSubmenuPopup(); void OnThreadSubmenuPopup();
#ifdef WITH_VOICE
void OnVoiceChannelSubmenuPopup();
#endif
bool m_updating_listing = false; bool m_updating_listing = false;
Snowflake m_active_channel; Snowflake m_active_channel;
@@ -180,14 +166,6 @@ public:
type_signal_action_open_new_tab signal_action_open_new_tab(); type_signal_action_open_new_tab signal_action_open_new_tab();
#endif #endif
#ifdef WITH_VOICE
using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>;
using type_signal_action_disconnect_voice = sigc::signal<void>;
type_signal_action_join_voice_channel signal_action_join_voice_channel();
type_signal_action_disconnect_voice signal_action_disconnect_voice();
#endif
type_signal_action_channel_item_select signal_action_channel_item_select(); type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave(); type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings(); type_signal_action_guild_settings signal_action_guild_settings();
@@ -200,9 +178,4 @@ private:
#ifdef WITH_LIBHANDY #ifdef WITH_LIBHANDY
type_signal_action_open_new_tab m_signal_action_open_new_tab; type_signal_action_open_new_tab m_signal_action_open_new_tab;
#endif #endif
#ifdef WITH_VOICE
type_signal_action_join_voice_channel m_signal_action_join_voice_channel;
type_signal_action_disconnect_voice m_signal_action_disconnect_voice;
#endif
}; };

View File

@@ -65,12 +65,6 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width); return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
case RenderType::Thread: case RenderType::Thread:
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width);
case RenderType::VoiceParticipant:
return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width);
#endif
case RenderType::DMHeader: case RenderType::DMHeader:
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width); return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
case RenderType::DM: case RenderType::DM:
@@ -88,12 +82,6 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
case RenderType::Thread: case RenderType::Thread:
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width);
case RenderType::VoiceParticipant:
return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width);
#endif
case RenderType::DMHeader: case RenderType::DMHeader:
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
case RenderType::DM: case RenderType::DM:
@@ -111,12 +99,6 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height); return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
case RenderType::Thread: case RenderType::Thread:
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height);
case RenderType::VoiceParticipant:
return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height);
#endif
case RenderType::DMHeader: case RenderType::DMHeader:
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height); return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
case RenderType::DM: case RenderType::DM:
@@ -134,12 +116,6 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
case RenderType::Thread: case RenderType::Thread:
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height);
case RenderType::VoiceParticipant:
return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height);
#endif
case RenderType::DMHeader: case RenderType::DMHeader:
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
case RenderType::DM: case RenderType::DM:
@@ -157,12 +133,6 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
return render_vfunc_channel(cr, widget, background_area, cell_area, flags); return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
case RenderType::Thread: case RenderType::Thread:
return render_vfunc_thread(cr, widget, background_area, cell_area, flags); return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags);
case RenderType::VoiceParticipant:
return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags);
#endif
case RenderType::DMHeader: case RenderType::DMHeader:
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags); return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
case RenderType::DM: case RenderType::DM:
@@ -529,76 +499,6 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Contex
} }
} }
#ifdef WITH_VOICE
// voice channel
void CellRendererChannels::get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
const int text_x = background_area.get_x() + 21;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
const int text_w = natural_size.width;
const int text_h = natural_size.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#0f0");
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
m_renderer_text.property_foreground_set() = false;
}
// voice participant
void CellRendererChannels::get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
}
void CellRendererChannels::get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
}
void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
}
void CellRendererChannels::render_vfunc_voice_participant(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
const int text_x = background_area.get_x() + 27;
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
const int text_w = natural_size.width;
const int text_h = natural_size.height;
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#f00");
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
m_renderer_text.property_foreground_set() = false;
}
#endif
// dm header // dm header
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {

View File

@@ -11,12 +11,6 @@ enum class RenderType : uint8_t {
TextChannel, TextChannel,
Thread, Thread,
// TODO: maybe enable anyways but without ability to join if no voice support
#ifdef WITH_VOICE
VoiceChannel,
VoiceParticipant,
#endif
DMHeader, DMHeader,
DM, DM,
}; };
@@ -89,30 +83,6 @@ protected:
const Gdk::Rectangle &cell_area, const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags); Gtk::CellRendererState flags);
#ifdef WITH_VOICE
// voice channel
void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
// voice channel
void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
void get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
void render_vfunc_voice_participant(const Cairo::RefPtr<Cairo::Context> &cr,
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
#endif
// dm header // dm header
void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;

View File

@@ -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);
return true; row[m_columns.m_icon] = Abaddon::Get().GetImageManager().GetPlaceholder(16);
}; // come on
Gtk::TreeRowReference ref(m_model, m_model->get_path(row));
auto add_role = [this](const std::string &name) { Abaddon::Get().GetImageManager().LoadFromURL(user.GetAvatarURL(), [this, ref = std::move(ref)](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
auto *role_row = Gtk::manage(new Gtk::ListBoxRow); if (ref.is_valid()) {
auto *role_lbl = Gtk::manage(new Gtk::Label); auto row = *m_model->get_iter(ref.get_path());
row[m_columns.m_icon] = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
role_row->get_style_context()->add_class("members-row");
role_row->get_style_context()->add_class("members-row-role");
role_lbl->get_style_context()->add_class("members-row-label");
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++) {
auto pos = it->first;
const auto &role = it->second;
add_role(role.Name);
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;
}); });
return true;
};
auto add_role = [this](const RoleData &role) -> Gtk::TreeRow {
auto row = *m_model->append();
row[m_columns.m_type] = MemberListRenderType::Role;
row[m_columns.m_id] = role.ID;
row[m_columns.m_markup] = "<b>" + role.GetEscapedName() + "</b>";
row[m_columns.m_sort] = role.Position;
return row;
};
for (auto &[pos, role] : pos_to_role) {
auto role_children = add_role(role).children();
if (auto it = pos_to_users.find(pos); it != pos_to_users.end()) {
for (const auto &user : it->second) {
if (!add_user(user, role_children)) break;
}
}
}
m_view.expand_all();
}
MemberList::ModelColumns::ModelColumns() {
add(m_type);
add(m_id);
add(m_markup);
add(m_icon);
add(m_sort);
} }

View File

@@ -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;
}; };

View 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);
}

View 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;
};

View File

@@ -11,9 +11,9 @@ public:
protected: protected:
Gtk::SizeRequestMode get_request_mode_vfunc() const override; Gtk::SizeRequestMode get_request_mode_vfunc() const override;
void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override;
void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override;
void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
void on_size_allocate(Gtk::Allocation &allocation) override; void on_size_allocate(Gtk::Allocation &allocation) override;
void on_map() override; void on_map() override;
void on_unmap() override; void on_unmap() override;

View File

@@ -1,89 +0,0 @@
#include "voiceinfobox.hpp"
#include "abaddon.hpp"
#include "util.hpp"
VoiceInfoBox::VoiceInfoBox()
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
, m_left(Gtk::ORIENTATION_VERTICAL) {
m_disconnect_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) {
spdlog::get("discord")->debug("Request disconnect from info box");
Abaddon::Get().GetDiscordClient().DisconnectFromVoice();
return true;
}
return false;
});
AddPointerCursor(m_disconnect_ev);
get_style_context()->add_class("voice-info");
m_status.get_style_context()->add_class("voice-info-status");
m_location.get_style_context()->add_class("voice-info-location");
m_disconnect_img.get_style_context()->add_class("voice-info-disconnect-image");
m_status.set_label("You shouldn't see me");
m_location.set_label("You shouldn't see me");
Abaddon::Get().GetDiscordClient().signal_voice_requested_connect().connect([this](Snowflake channel_id) {
show();
if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) {
if (channel->GuildID.has_value()) {
if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*channel->GuildID); guild.has_value()) {
m_location.set_label(*channel->Name + " / " + guild->Name);
return;
}
}
m_location.set_label(*channel->Name);
return;
}
m_location.set_label("Unknown");
});
Abaddon::Get().GetDiscordClient().signal_voice_requested_disconnect().connect([this]() {
hide();
});
Abaddon::Get().GetDiscordClient().signal_voice_client_state_update().connect([this](DiscordVoiceClient::State state) {
Glib::ustring label;
switch (state) {
case DiscordVoiceClient::State::ConnectingToWebsocket:
label = "Connecting";
break;
case DiscordVoiceClient::State::EstablishingConnection:
label = "Establishing connection";
break;
case DiscordVoiceClient::State::Connected:
label = "Connected";
break;
case DiscordVoiceClient::State::DisconnectedByServer:
case DiscordVoiceClient::State::DisconnectedByClient:
label = "Disconnected";
break;
default:
label = "Unknown";
break;
}
m_status.set_label(label);
});
m_status.set_ellipsize(Pango::ELLIPSIZE_END);
m_location.set_ellipsize(Pango::ELLIPSIZE_END);
m_disconnect_ev.add(m_disconnect_img);
m_disconnect_img.property_icon_name() = "call-stop-symbolic";
m_disconnect_img.property_icon_size() = 5;
m_disconnect_img.set_hexpand(true);
m_disconnect_img.set_halign(Gtk::ALIGN_END);
m_left.add(m_status);
m_left.add(m_location);
add(m_left);
add(m_disconnect_ev);
show_all_children();
}

View File

@@ -1,19 +0,0 @@
#pragma once
#include <gtkmm/box.h>
#include <gtkmm/eventbox.h>
#include <gtkmm/image.h>
#include <gtkmm/label.h>
class VoiceInfoBox : public Gtk::Box {
public:
VoiceInfoBox();
private:
Gtk::Box m_left;
Gtk::Label m_status;
Gtk::Label m_location;
Gtk::EventBox m_disconnect_ev;
Gtk::Image m_disconnect_img;
};

View File

@@ -1,125 +0,0 @@
#include "volumemeter.hpp"
#include <cstring>
VolumeMeter::VolumeMeter()
: Glib::ObjectBase("volumemeter")
, Gtk::Widget() {
set_has_window(true);
}
void VolumeMeter::SetVolume(double fraction) {
m_fraction = fraction;
queue_draw();
}
void VolumeMeter::SetTick(double fraction) {
m_tick = fraction;
queue_draw();
}
void VolumeMeter::SetShowTick(bool show) {
m_show_tick = show;
}
Gtk::SizeRequestMode VolumeMeter::get_request_mode_vfunc() const {
return Gtk::Widget::get_request_mode_vfunc();
}
void VolumeMeter::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const {
const int width = get_allocated_width();
minimum_width = natural_width = width;
}
void VolumeMeter::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const {
get_preferred_width_vfunc(minimum_width, natural_width);
}
void VolumeMeter::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const {
// blehhh :PPP
const int height = get_allocated_height();
minimum_height = natural_height = 4;
}
void VolumeMeter::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const {
get_preferred_height_vfunc(minimum_height, natural_height);
}
void VolumeMeter::on_size_allocate(Gtk::Allocation &allocation) {
set_allocation(allocation);
if (m_window)
m_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), allocation.get_height());
}
void VolumeMeter::on_map() {
Gtk::Widget::on_map();
}
void VolumeMeter::on_unmap() {
Gtk::Widget::on_unmap();
}
void VolumeMeter::on_realize() {
set_realized(true);
if (!m_window) {
GdkWindowAttr attributes;
std::memset(&attributes, 0, sizeof(attributes));
auto allocation = get_allocation();
attributes.x = allocation.get_x();
attributes.y = allocation.get_y();
attributes.width = allocation.get_width();
attributes.height = allocation.get_height();
attributes.event_mask = get_events() | Gdk::EXPOSURE_MASK;
attributes.window_type = GDK_WINDOW_CHILD;
attributes.wclass = GDK_INPUT_OUTPUT;
m_window = Gdk::Window::create(get_parent_window(), &attributes, GDK_WA_X | GDK_WA_Y);
set_window(m_window);
m_window->set_user_data(gobj());
}
}
void VolumeMeter::on_unrealize() {
m_window.reset();
Gtk::Widget::on_unrealize();
}
bool VolumeMeter::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
const auto allocation = get_allocation();
const auto width = allocation.get_width();
const auto height = allocation.get_height();
const double LOW_MAX = 0.7 * width;
const double MID_MAX = 0.85 * width;
const double desired_width = width * m_fraction;
const double draw_low = std::min(desired_width, LOW_MAX);
const double draw_mid = std::min(desired_width, MID_MAX);
const double draw_hi = desired_width;
cr->set_source_rgb(1.0, 0.0, 0.0);
cr->rectangle(0.0, 0.0, draw_hi, height);
cr->fill();
cr->set_source_rgb(1.0, 0.5, 0.0);
cr->rectangle(0.0, 0.0, draw_mid, height);
cr->fill();
cr->set_source_rgb(.0, 1.0, 0.0);
cr->rectangle(0.0, 0.0, draw_low, height);
cr->fill();
if (m_show_tick) {
const double tick_base = width * m_tick;
cr->set_source_rgb(0.8, 0.8, 0.8);
cr->rectangle(tick_base, 0, 4, height);
cr->fill();
}
return true;
}

View File

@@ -1,31 +0,0 @@
#pragma once
#include <gtkmm/widget.h>
class VolumeMeter : public Gtk::Widget {
public:
VolumeMeter();
void SetVolume(double fraction);
void SetTick(double fraction);
void SetShowTick(bool show);
protected:
Gtk::SizeRequestMode get_request_mode_vfunc() const override;
void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override;
void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override;
void on_size_allocate(Gtk::Allocation &allocation) override;
void on_map() override;
void on_unmap() override;
void on_realize() override;
void on_unrealize() override;
bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
private:
Glib::RefPtr<Gdk::Window> m_window;
double m_fraction = 0.0;
double m_tick = 0.0;
bool m_show_tick = false;
};

View File

@@ -1,7 +1,6 @@
#include "abaddon.hpp" #include "abaddon.hpp"
#include "discord.hpp" #include "discord.hpp"
#include "util.hpp" #include "util.hpp"
#include <spdlog/spdlog.h>
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
@@ -9,8 +8,7 @@ using namespace std::string_literals;
DiscordClient::DiscordClient(bool mem_store) DiscordClient::DiscordClient(bool mem_store)
: m_decompress_buf(InflateChunkSize) : m_decompress_buf(InflateChunkSize)
, m_store(mem_store) , m_store(mem_store) {
, m_websocket("gateway-ws") {
m_msg_dispatch.connect(sigc::mem_fun(*this, &DiscordClient::MessageDispatch)); m_msg_dispatch.connect(sigc::mem_fun(*this, &DiscordClient::MessageDispatch));
auto dispatch_cb = [this]() { auto dispatch_cb = [this]() {
m_generic_mutex.lock(); m_generic_mutex.lock();
@@ -25,17 +23,6 @@ DiscordClient::DiscordClient(bool mem_store)
m_websocket.signal_open().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketOpen)); m_websocket.signal_open().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketOpen));
m_websocket.signal_close().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketClose)); m_websocket.signal_close().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketClose));
#ifdef WITH_VOICE
m_voice.signal_connected().connect(sigc::mem_fun(*this, &DiscordClient::OnVoiceConnected));
m_voice.signal_disconnected().connect(sigc::mem_fun(*this, &DiscordClient::OnVoiceDisconnected));
m_voice.signal_speaking().connect([this](const VoiceSpeakingData &data) {
m_signal_voice_speaking.emit(data);
});
m_voice.signal_state_update().connect([this](DiscordVoiceClient::State state) {
m_signal_voice_client_state_update.emit(state);
});
#endif
LoadEventMap(); LoadEventMap();
} }
@@ -275,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())
@@ -1178,65 +1171,6 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI
}); });
} }
#ifdef WITH_VOICE
void DiscordClient::ConnectToVoice(Snowflake channel_id) {
auto channel = GetChannel(channel_id);
if (!channel.has_value()) return;
m_voice_channel_id = channel_id;
VoiceStateUpdateMessage m;
if (channel->GuildID.has_value())
m.GuildID = channel->GuildID;
m.ChannelID = channel_id;
m.PreferredRegion = "newark";
m_websocket.Send(m);
m_signal_voice_requested_connect.emit(channel_id);
}
void DiscordClient::DisconnectFromVoice() {
m_voice.Stop();
VoiceStateUpdateMessage m;
m_websocket.Send(m);
m_signal_voice_requested_disconnect.emit();
}
bool DiscordClient::IsVoiceConnected() const noexcept {
return m_voice.IsConnected();
}
bool DiscordClient::IsVoiceConnecting() const noexcept {
return m_voice.IsConnecting();
}
Snowflake DiscordClient::GetVoiceChannelID() const noexcept {
return m_voice_channel_id;
}
std::unordered_set<Snowflake> DiscordClient::GetUsersInVoiceChannel(Snowflake channel_id) {
return m_voice_state_channel_users[channel_id];
}
std::optional<uint32_t> DiscordClient::GetSSRCOfUser(Snowflake id) const {
return m_voice.GetSSRCOfUser(id);
}
std::optional<Snowflake> DiscordClient::GetVoiceState(Snowflake user_id) const {
if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) {
return it->second;
}
return std::nullopt;
}
void DiscordClient::SetVoiceMuted(bool is_mute) {
m_mute_requested = is_mute;
SendVoiceStateUpdate();
}
void DiscordClient::SetVoiceDeafened(bool is_deaf) {
m_deaf_requested = is_deaf;
SendVoiceStateUpdate();
}
#endif
void DiscordClient::SetReferringChannel(Snowflake id) { void DiscordClient::SetReferringChannel(Snowflake id) {
if (!id.IsValid()) { if (!id.IsValid()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me"); m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
@@ -1556,14 +1490,6 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::GUILD_MEMBERS_CHUNK: { case GatewayEvent::GUILD_MEMBERS_CHUNK: {
HandleGatewayGuildMembersChunk(m); HandleGatewayGuildMembersChunk(m);
} break; } break;
#ifdef WITH_VOICE
case GatewayEvent::VOICE_STATE_UPDATE: {
HandleGatewayVoiceStateUpdate(m);
} break;
case GatewayEvent::VOICE_SERVER_UPDATE: {
HandleGatewayVoiceServerUpdate(m);
} break;
#endif
} }
} break; } break;
default: default:
@@ -2174,61 +2100,6 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) {
m_store.EndTransaction(); m_store.EndTransaction();
} }
#ifdef WITH_VOICE
void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) {
VoiceState data = msg.Data;
if (data.UserID == m_user_data.ID) {
spdlog::get("discord")->debug("Voice session ID: {}", data.SessionID);
m_voice.SetSessionID(data.SessionID);
// channel_id = null means disconnect. stop cuz out of order maybe
if (!data.ChannelID.has_value() && (m_voice.IsConnected() || m_voice.IsConnecting())) {
m_voice.Stop();
}
} else {
if (data.GuildID.has_value() && data.Member.has_value()) {
if (data.Member->User.has_value()) {
m_store.SetUser(data.UserID, *data.Member->User);
}
m_store.SetGuildMember(*data.GuildID, data.UserID, *data.Member);
}
}
if (data.ChannelID.has_value()) {
const auto old_state = GetVoiceState(data.UserID);
SetVoiceState(data.UserID, *data.ChannelID);
if (old_state.has_value() && *old_state != *data.ChannelID) {
m_signal_voice_user_disconnect.emit(data.UserID, *old_state);
}
m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID);
} else {
const auto old_state = GetVoiceState(data.UserID);
ClearVoiceState(data.UserID);
if (old_state.has_value()) {
m_signal_voice_user_disconnect.emit(data.UserID, *old_state);
}
}
}
void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) {
VoiceServerUpdateData data = msg.Data;
spdlog::get("discord")->debug("Voice server endpoint: {}", data.Endpoint);
spdlog::get("discord")->debug("Voice token: {}", data.Token);
m_voice.SetEndpoint(data.Endpoint);
m_voice.SetToken(data.Token);
if (data.GuildID.has_value()) {
m_voice.SetServerID(*data.GuildID);
} else if (data.ChannelID.has_value()) {
m_voice.SetServerID(*data.ChannelID);
} else {
spdlog::get("discord")->error("No guild or channel ID in voice server?");
}
m_voice.SetUserID(m_user_data.ID);
m_voice.Start();
}
#endif
void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) {
ReadySupplementalData data = msg.Data; ReadySupplementalData data = msg.Data;
for (const auto &p : data.MergedPresences.Friends) { for (const auto &p : data.MergedPresences.Friends) {
@@ -2245,17 +2116,6 @@ void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) {
m_user_to_status[p.UserID] = PresenceStatus::DND; m_user_to_status[p.UserID] = PresenceStatus::DND;
m_signal_presence_update.emit(*user, m_user_to_status.at(p.UserID)); m_signal_presence_update.emit(*user, m_user_to_status.at(p.UserID));
} }
#ifdef WITH_VOICE
for (const auto &g : data.Guilds) {
for (const auto &s : g.VoiceStates) {
if (s.ChannelID.has_value()) {
SetVoiceState(s.UserID, *s.ChannelID);
}
}
}
#endif
m_signal_gateway_ready_supplemental.emit();
} }
void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) { void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) {
@@ -2521,8 +2381,9 @@ void DiscordClient::SetSuperPropertiesFromIdentity(const IdentifyMessage &identi
void DiscordClient::HandleSocketOpen() { void DiscordClient::HandleSocketOpen() {
} }
void DiscordClient::HandleSocketClose(const ix::WebSocketCloseInfo &info) { void DiscordClient::HandleSocketClose(uint16_t code) {
auto close_code = static_cast<GatewayCloseCode>(info.code); printf("got socket close code: %d\n", code);
auto close_code = static_cast<GatewayCloseCode>(code);
auto cb = [this, close_code]() { auto cb = [this, close_code]() {
m_heartbeat_waiter.kill(); m_heartbeat_waiter.kill();
if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join();
@@ -2687,44 +2548,6 @@ void DiscordClient::HandleReadyGuildSettings(const ReadyEventData &data) {
} }
} }
#ifdef WITH_VOICE
void DiscordClient::SendVoiceStateUpdate() {
VoiceStateUpdateMessage msg;
msg.ChannelID = m_voice_channel_id;
const auto channel = GetChannel(m_voice_channel_id);
if (channel.has_value() && channel->GuildID.has_value()) {
msg.GuildID = *channel->GuildID;
}
msg.SelfMute = m_mute_requested;
msg.SelfDeaf = m_deaf_requested;
msg.SelfVideo = false;
m_websocket.Send(msg);
}
void DiscordClient::SetVoiceState(Snowflake user_id, Snowflake channel_id) {
m_voice_state_user_channel[user_id] = channel_id;
m_voice_state_channel_users[channel_id].insert(user_id);
}
void DiscordClient::ClearVoiceState(Snowflake user_id) {
if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) {
m_voice_state_channel_users[it->second].erase(user_id);
// invalidated
m_voice_state_user_channel.erase(user_id);
}
}
void DiscordClient::OnVoiceConnected() {
m_signal_voice_connected.emit();
}
void DiscordClient::OnVoiceDisconnected() {
m_signal_voice_disconnected.emit();
}
#endif
void DiscordClient::LoadEventMap() { void DiscordClient::LoadEventMap() {
m_event_map["READY"] = GatewayEvent::READY; m_event_map["READY"] = GatewayEvent::READY;
m_event_map["MESSAGE_CREATE"] = GatewayEvent::MESSAGE_CREATE; m_event_map["MESSAGE_CREATE"] = GatewayEvent::MESSAGE_CREATE;
@@ -2770,18 +2593,12 @@ void DiscordClient::LoadEventMap() {
m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK; m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK;
m_event_map["USER_GUILD_SETTINGS_UPDATE"] = GatewayEvent::USER_GUILD_SETTINGS_UPDATE; m_event_map["USER_GUILD_SETTINGS_UPDATE"] = GatewayEvent::USER_GUILD_SETTINGS_UPDATE;
m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK; m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK;
m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE;
m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE;
} }
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {
return m_signal_gateway_ready; return m_signal_gateway_ready;
} }
DiscordClient::type_signal_gateway_ready_supplemental DiscordClient::signal_gateway_ready_supplemental() {
return m_signal_gateway_ready_supplemental;
}
DiscordClient::type_signal_message_create DiscordClient::signal_message_create() { DiscordClient::type_signal_message_create DiscordClient::signal_message_create() {
return m_signal_message_create; return m_signal_message_create;
} }
@@ -2985,37 +2802,3 @@ DiscordClient::type_signal_channel_accessibility_changed DiscordClient::signal_c
DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() { DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() {
return m_signal_message_send_fail; return m_signal_message_send_fail;
} }
#ifdef WITH_VOICE
DiscordClient::type_signal_voice_connected DiscordClient::signal_voice_connected() {
return m_signal_voice_connected;
}
DiscordClient::type_signal_voice_disconnected DiscordClient::signal_voice_disconnected() {
return m_signal_voice_disconnected;
}
DiscordClient::type_signal_voice_speaking DiscordClient::signal_voice_speaking() {
return m_signal_voice_speaking;
}
DiscordClient::type_signal_voice_user_disconnect DiscordClient::signal_voice_user_disconnect() {
return m_signal_voice_user_disconnect;
}
DiscordClient::type_signal_voice_user_connect DiscordClient::signal_voice_user_connect() {
return m_signal_voice_user_connect;
}
DiscordClient::type_signal_voice_requested_connect DiscordClient::signal_voice_requested_connect() {
return m_signal_voice_requested_connect;
}
DiscordClient::type_signal_voice_requested_disconnect DiscordClient::signal_voice_requested_disconnect() {
return m_signal_voice_requested_disconnect;
}
DiscordClient::type_signal_voice_client_state_update DiscordClient::signal_voice_client_state_update() {
return m_signal_voice_client_state_update;
}
#endif

View File

@@ -1,11 +1,9 @@
#pragma once #pragma once
#include "chatsubmitparams.hpp" #include "websocket.hpp"
#include "waiter.hpp"
#include "httpclient.hpp" #include "httpclient.hpp"
#include "objects.hpp" #include "objects.hpp"
#include "store.hpp" #include "store.hpp"
#include "voiceclient.hpp" #include "chatsubmitparams.hpp"
#include "websocket.hpp"
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <thread> #include <thread>
@@ -20,6 +18,31 @@
#undef GetMessage #undef GetMessage
#endif #endif
class HeartbeatWaiter {
public:
template<class R, class P>
bool wait_for(std::chrono::duration<R, P> const &time) const {
std::unique_lock<std::mutex> lock(m);
return !cv.wait_for(lock, time, [&] { return terminate; });
}
void kill() {
std::unique_lock<std::mutex> lock(m);
terminate = true;
cv.notify_all();
}
void revive() {
std::unique_lock<std::mutex> lock(m);
terminate = false;
}
private:
mutable std::condition_variable cv;
mutable std::mutex m;
bool terminate = false;
};
class Abaddon; class Abaddon;
class DiscordClient { class DiscordClient {
friend class Abaddon; friend class Abaddon;
@@ -53,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;
@@ -180,21 +204,6 @@ public:
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback); void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback); void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
#ifdef WITH_VOICE
void ConnectToVoice(Snowflake channel_id);
void DisconnectFromVoice();
// Is fully connected
[[nodiscard]] bool IsVoiceConnected() const noexcept;
[[nodiscard]] bool IsVoiceConnecting() const noexcept;
[[nodiscard]] Snowflake GetVoiceChannelID() const noexcept;
[[nodiscard]] std::unordered_set<Snowflake> GetUsersInVoiceChannel(Snowflake channel_id);
[[nodiscard]] std::optional<uint32_t> GetSSRCOfUser(Snowflake id) const;
[[nodiscard]] std::optional<Snowflake> GetVoiceState(Snowflake user_id) const;
void SetVoiceMuted(bool is_mute);
void SetVoiceDeafened(bool is_deaf);
#endif
void SetReferringChannel(Snowflake id); void SetReferringChannel(Snowflake id);
void SetBuildNumber(uint32_t build_number); void SetBuildNumber(uint32_t build_number);
@@ -277,12 +286,6 @@ private:
void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg);
#ifdef WITH_VOICE
void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg);
void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg);
#endif
void HeartbeatThread(); void HeartbeatThread();
void SendIdentify(); void SendIdentify();
void SendResume(); void SendResume();
@@ -291,7 +294,7 @@ private:
void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity); void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity);
void HandleSocketOpen(); void HandleSocketOpen();
void HandleSocketClose(const ix::WebSocketCloseInfo &info); void HandleSocketClose(uint16_t code);
static bool CheckCode(const http::response_type &r); static bool CheckCode(const http::response_type &r);
static bool CheckCode(const http::response_type &r, int expected); static bool CheckCode(const http::response_type &r, int expected);
@@ -335,33 +338,13 @@ private:
std::thread m_heartbeat_thread; std::thread m_heartbeat_thread;
std::atomic<int> m_last_sequence = -1; std::atomic<int> m_last_sequence = -1;
std::atomic<int> m_heartbeat_msec = 0; std::atomic<int> m_heartbeat_msec = 0;
Waiter m_heartbeat_waiter; HeartbeatWaiter m_heartbeat_waiter;
std::atomic<bool> m_heartbeat_acked = true; std::atomic<bool> m_heartbeat_acked = true;
bool m_reconnecting = false; // reconnecting either to resume or reidentify bool m_reconnecting = false; // reconnecting either to resume or reidentify
bool m_wants_resume = false; // reconnecting specifically to resume bool m_wants_resume = false; // reconnecting specifically to resume
std::string m_session_id; std::string m_session_id;
#ifdef WITH_VOICE
DiscordVoiceClient m_voice;
bool m_mute_requested = false;
bool m_deaf_requested = false;
Snowflake m_voice_channel_id;
// todo sql i guess
std::unordered_map<Snowflake, Snowflake> m_voice_state_user_channel;
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_voice_state_channel_users;
void SendVoiceStateUpdate();
void SetVoiceState(Snowflake user_id, Snowflake channel_id);
void ClearVoiceState(Snowflake user_id);
void OnVoiceConnected();
void OnVoiceDisconnected();
#endif
mutable std::mutex m_msg_mutex; mutable std::mutex m_msg_mutex;
Glib::Dispatcher m_msg_dispatch; Glib::Dispatcher m_msg_dispatch;
std::queue<std::string> m_msg_queue; std::queue<std::string> m_msg_queue;
@@ -379,7 +362,6 @@ private:
// signals // signals
public: public:
typedef sigc::signal<void> type_signal_gateway_ready; typedef sigc::signal<void> type_signal_gateway_ready;
typedef sigc::signal<void> type_signal_gateway_ready_supplemental;
typedef sigc::signal<void, Message> type_signal_message_create; typedef sigc::signal<void, Message> type_signal_message_create;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_message_delete; typedef sigc::signal<void, Snowflake, Snowflake> type_signal_message_delete;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_message_update; typedef sigc::signal<void, Snowflake, Snowflake> type_signal_message_update;
@@ -435,19 +417,7 @@ public:
typedef sigc::signal<void> type_signal_connected; typedef sigc::signal<void> type_signal_connected;
typedef sigc::signal<void, std::string, float> type_signal_message_progress; typedef sigc::signal<void, std::string, float> type_signal_message_progress;
#ifdef WITH_VOICE
using type_signal_voice_connected = sigc::signal<void()>;
using type_signal_voice_disconnected = sigc::signal<void()>;
using type_signal_voice_speaking = sigc::signal<void(VoiceSpeakingData)>;
using type_signal_voice_user_disconnect = sigc::signal<void(Snowflake, Snowflake)>;
using type_signal_voice_user_connect = sigc::signal<void(Snowflake, Snowflake)>;
using type_signal_voice_requested_connect = sigc::signal<void(Snowflake)>;
using type_signal_voice_requested_disconnect = sigc::signal<void()>;
using type_signal_voice_client_state_update = sigc::signal<void(DiscordVoiceClient::State)>;
#endif
type_signal_gateway_ready signal_gateway_ready(); type_signal_gateway_ready signal_gateway_ready();
type_signal_gateway_ready_supplemental signal_gateway_ready_supplemental();
type_signal_message_create signal_message_create(); type_signal_message_create signal_message_create();
type_signal_message_delete signal_message_delete(); type_signal_message_delete signal_message_delete();
type_signal_message_update signal_message_update(); type_signal_message_update signal_message_update();
@@ -501,20 +471,8 @@ public:
type_signal_connected signal_connected(); type_signal_connected signal_connected();
type_signal_message_progress signal_message_progress(); type_signal_message_progress signal_message_progress();
#ifdef WITH_VOICE
type_signal_voice_connected signal_voice_connected();
type_signal_voice_disconnected signal_voice_disconnected();
type_signal_voice_speaking signal_voice_speaking();
type_signal_voice_user_disconnect signal_voice_user_disconnect();
type_signal_voice_user_connect signal_voice_user_connect();
type_signal_voice_requested_connect signal_voice_requested_connect();
type_signal_voice_requested_disconnect signal_voice_requested_disconnect();
type_signal_voice_client_state_update signal_voice_client_state_update();
#endif
protected: protected:
type_signal_gateway_ready m_signal_gateway_ready; type_signal_gateway_ready m_signal_gateway_ready;
type_signal_gateway_ready_supplemental m_signal_gateway_ready_supplemental;
type_signal_message_create m_signal_message_create; type_signal_message_create m_signal_message_create;
type_signal_message_delete m_signal_message_delete; type_signal_message_delete m_signal_message_delete;
type_signal_message_update m_signal_message_update; type_signal_message_update m_signal_message_update;
@@ -567,15 +525,4 @@ protected:
type_signal_disconnected m_signal_disconnected; type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected; type_signal_connected m_signal_connected;
type_signal_message_progress m_signal_message_progress; type_signal_message_progress m_signal_message_progress;
#ifdef WITH_VOICE
type_signal_voice_connected m_signal_voice_connected;
type_signal_voice_disconnected m_signal_voice_disconnected;
type_signal_voice_speaking m_signal_voice_speaking;
type_signal_voice_user_disconnect m_signal_voice_user_disconnect;
type_signal_voice_user_connect m_signal_voice_user_connect;
type_signal_voice_requested_connect m_signal_voice_requested_connect;
type_signal_voice_requested_disconnect m_signal_voice_requested_disconnect;
type_signal_voice_client_state_update m_signal_voice_client_state_update;
#endif
}; };

View File

@@ -233,14 +233,8 @@ void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m) {
JS_D("friends", m.Friends); JS_D("friends", m.Friends);
} }
void from_json(const nlohmann::json &j, SupplementalGuildEntry &m) {
JS_D("id", m.ID);
JS_D("voice_states", m.VoiceStates);
}
void from_json(const nlohmann::json &j, ReadySupplementalData &m) { void from_json(const nlohmann::json &j, ReadySupplementalData &m) {
JS_D("merged_presences", m.MergedPresences); JS_D("merged_presences", m.MergedPresences);
JS_D("guilds", m.Guilds);
} }
void to_json(nlohmann::json &j, const IdentifyProperties &m) { void to_json(nlohmann::json &j, const IdentifyProperties &m) {
@@ -646,43 +640,3 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) {
JS_D("members", m.Members); JS_D("members", m.Members);
JS_D("guild_id", m.GuildID); JS_D("guild_id", m.GuildID);
} }
#ifdef WITH_VOICE
void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) {
j["op"] = GatewayOp::VoiceStateUpdate;
if (m.GuildID.has_value())
j["d"]["guild_id"] = *m.GuildID;
else
j["d"]["guild_id"] = nullptr;
if (m.ChannelID.has_value())
j["d"]["channel_id"] = *m.ChannelID;
else
j["d"]["channel_id"] = nullptr;
j["d"]["self_mute"] = m.SelfMute;
j["d"]["self_deaf"] = m.SelfDeaf;
j["d"]["self_video"] = m.SelfVideo;
// j["d"]["preferred_region"] = m.PreferredRegion;
}
void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) {
JS_D("token", m.Token);
JS_D("endpoint", m.Endpoint);
JS_ON("guild_id", m.GuildID);
JS_ON("channel_id", m.ChannelID);
}
#endif
void from_json(const nlohmann::json &j, VoiceState &m) {
JS_ON("guild_id", m.GuildID);
JS_N("channel_id", m.ChannelID);
JS_D("deaf", m.IsDeafened);
JS_D("mute", m.IsMuted);
JS_D("self_deaf", m.IsSelfDeafened);
JS_D("self_mute", m.IsSelfMuted);
JS_D("self_video", m.IsSelfVideo);
JS_O("self_stream", m.IsSelfStream);
JS_D("suppress", m.IsSuppressed);
JS_D("user_id", m.UserID);
JS_ON("member", m.Member);
JS_D("session_id", m.SessionID);
}

View File

@@ -100,8 +100,6 @@ enum class GatewayEvent : int {
MESSAGE_ACK, MESSAGE_ACK,
USER_GUILD_SETTINGS_UPDATE, USER_GUILD_SETTINGS_UPDATE,
GUILD_MEMBERS_CHUNK, GUILD_MEMBERS_CHUNK,
VOICE_STATE_UPDATE,
VOICE_SERVER_UPDATE,
}; };
enum class GatewayCloseCode : uint16_t { enum class GatewayCloseCode : uint16_t {
@@ -354,18 +352,8 @@ struct SupplementalMergedPresencesData {
friend void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m); friend void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m);
}; };
struct VoiceState;
struct SupplementalGuildEntry {
// std::vector<?> EmbeddedActivities;
Snowflake ID;
std::vector<VoiceState> VoiceStates;
friend void from_json(const nlohmann::json &j, SupplementalGuildEntry &m);
};
struct ReadySupplementalData { struct ReadySupplementalData {
SupplementalMergedPresencesData MergedPresences; SupplementalMergedPresencesData MergedPresences;
std::vector<SupplementalGuildEntry> Guilds;
friend void from_json(const nlohmann::json &j, ReadySupplementalData &m); friend void from_json(const nlohmann::json &j, ReadySupplementalData &m);
}; };
@@ -876,42 +864,3 @@ struct GuildMembersChunkData {
friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m); friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m);
}; };
#ifdef WITH_VOICE
struct VoiceStateUpdateMessage {
std::optional<Snowflake> GuildID;
std::optional<Snowflake> ChannelID;
bool SelfMute = false;
bool SelfDeaf = false;
bool SelfVideo = false;
std::string PreferredRegion;
friend void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m);
};
struct VoiceServerUpdateData {
std::string Token;
std::string Endpoint;
std::optional<Snowflake> GuildID;
std::optional<Snowflake> ChannelID;
friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m);
};
#endif
struct VoiceState {
std::optional<Snowflake> ChannelID;
bool IsDeafened;
bool IsMuted;
std::optional<Snowflake> GuildID;
std::optional<GuildMember> Member;
bool IsSelfDeafened;
bool IsSelfMuted;
bool IsSelfVideo;
bool IsSelfStream = false;
std::string SessionID;
bool IsSuppressed;
Snowflake UserID;
friend void from_json(const nlohmann::json &j, VoiceState &m);
};

View File

@@ -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();

View File

@@ -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);

View File

@@ -1,544 +0,0 @@
#ifdef WITH_VOICE
// clang-format off
#include "voiceclient.hpp"
#include "json.hpp"
#include <sodium.h>
#include <spdlog/spdlog.h>
#include <spdlog/fmt/bin_to_hex.h>
#include "abaddon.hpp"
#include "audio/manager.hpp"
#ifdef _WIN32
#define S_ADDR(var) (var).sin_addr.S_un.S_addr
#define socklen_t int
#else
#define S_ADDR(var) (var).sin_addr.s_addr
#endif
// clang-format on
UDPSocket::UDPSocket()
: m_socket(INVALID_SOCKET) {
}
UDPSocket::~UDPSocket() {
Stop();
}
void UDPSocket::Connect(std::string_view ip, uint16_t port) {
std::memset(&m_server, 0, sizeof(m_server));
m_server.sin_family = AF_INET;
S_ADDR(m_server) = inet_addr(ip.data());
m_server.sin_port = htons(port);
m_socket = socket(AF_INET, SOCK_DGRAM, 0);
bind(m_socket, reinterpret_cast<sockaddr *>(&m_server), sizeof(m_server));
}
void UDPSocket::Run() {
m_running = true;
m_thread = std::thread(&UDPSocket::ReadThread, this);
}
void UDPSocket::SetSecretKey(std::array<uint8_t, 32> key) {
m_secret_key = key;
}
void UDPSocket::SetSSRC(uint32_t ssrc) {
m_ssrc = ssrc;
}
void UDPSocket::SendEncrypted(const uint8_t *data, size_t len) {
m_sequence++;
m_timestamp += 480; // this is important
std::vector<uint8_t> rtp(12 + len + crypto_secretbox_MACBYTES, 0);
rtp[0] = 0x80; // ver 2
rtp[1] = 0x78; // payload type 0x78
rtp[2] = (m_sequence >> 8) & 0xFF;
rtp[3] = (m_sequence >> 0) & 0xFF;
rtp[4] = (m_timestamp >> 24) & 0xFF;
rtp[5] = (m_timestamp >> 16) & 0xFF;
rtp[6] = (m_timestamp >> 8) & 0xFF;
rtp[7] = (m_timestamp >> 0) & 0xFF;
rtp[8] = (m_ssrc >> 24) & 0xFF;
rtp[9] = (m_ssrc >> 16) & 0xFF;
rtp[10] = (m_ssrc >> 8) & 0xFF;
rtp[11] = (m_ssrc >> 0) & 0xFF;
static std::array<uint8_t, 24> nonce = {};
std::memcpy(nonce.data(), rtp.data(), 12);
crypto_secretbox_easy(rtp.data() + 12, data, len, nonce.data(), m_secret_key.data());
Send(rtp.data(), rtp.size());
}
void UDPSocket::SendEncrypted(const std::vector<uint8_t> &data) {
SendEncrypted(data.data(), data.size());
}
void UDPSocket::Send(const uint8_t *data, size_t len) {
sendto(m_socket, reinterpret_cast<const char *>(data), static_cast<int>(len), 0, reinterpret_cast<sockaddr *>(&m_server), sizeof(m_server));
}
std::vector<uint8_t> UDPSocket::Receive() {
while (true) {
sockaddr_in from;
socklen_t fromlen = sizeof(from);
static std::array<uint8_t, 4096> buf;
int n = recvfrom(m_socket, reinterpret_cast<char *>(buf.data()), sizeof(buf), 0, reinterpret_cast<sockaddr *>(&from), &fromlen);
if (n < 0) {
return {};
} else if (S_ADDR(from) == S_ADDR(m_server) && from.sin_port == m_server.sin_port) {
return { buf.begin(), buf.begin() + n };
}
}
}
void UDPSocket::Stop() {
#ifdef _WIN32
closesocket(m_socket);
#else
close(m_socket);
#endif
m_running = false;
if (m_thread.joinable()) m_thread.join();
}
void UDPSocket::ReadThread() {
timeval tv;
while (m_running) {
static std::array<uint8_t, 4096> buf;
sockaddr_in from;
socklen_t addrlen = sizeof(from);
tv.tv_sec = 0;
tv.tv_usec = 1000000;
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(m_socket, &read_fds);
if (select(m_socket + 1, &read_fds, nullptr, nullptr, &tv) > 0) {
int n = recvfrom(m_socket, reinterpret_cast<char *>(buf.data()), sizeof(buf), 0, reinterpret_cast<sockaddr *>(&from), &addrlen);
if (n > 0 && S_ADDR(from) == S_ADDR(m_server) && from.sin_port == m_server.sin_port) {
m_signal_data.emit({ buf.begin(), buf.begin() + n });
}
}
}
}
UDPSocket::type_signal_data UDPSocket::signal_data() {
return m_signal_data;
}
DiscordVoiceClient::DiscordVoiceClient()
: m_state(State::DisconnectedByClient)
, m_ws("voice-ws")
, m_log(spdlog::get("voice")) {
if (sodium_init() == -1) {
m_log->critical("sodium_init() failed");
}
m_udp.signal_data().connect([this](const std::vector<uint8_t> &data) {
OnUDPData(data);
});
m_ws.signal_open().connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnWebsocketOpen));
m_ws.signal_close().connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnWebsocketClose));
m_ws.signal_message().connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnWebsocketMessage));
m_dispatcher.connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnDispatch));
// idle or else singleton deadlock
Glib::signal_idle().connect_once([this]() {
auto &audio = Abaddon::Get().GetAudio();
audio.SetOpusBuffer(m_opus_buffer.data());
audio.signal_opus_packet().connect([this](int payload_size) {
if (IsConnected()) {
m_udp.SendEncrypted(m_opus_buffer.data(), payload_size);
}
});
});
}
DiscordVoiceClient::~DiscordVoiceClient() {
if (IsConnected() || IsConnecting()) Stop();
}
void DiscordVoiceClient::Start() {
SetState(State::ConnectingToWebsocket);
m_heartbeat_waiter.revive();
m_keepalive_waiter.revive();
m_ws.StartConnection("wss://" + m_endpoint + "/?v=7");
m_signal_connected.emit();
}
void DiscordVoiceClient::Stop() {
if (!IsConnected() && !IsConnecting()) {
m_log->warn("Requested stop while not connected (from {})", GetStateName(m_state));
return;
}
SetState(State::DisconnectedByClient);
m_ws.Stop(4014);
m_udp.Stop();
m_heartbeat_waiter.kill();
if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join();
m_keepalive_waiter.kill();
if (m_keepalive_thread.joinable()) m_keepalive_thread.join();
m_signal_disconnected.emit();
}
void DiscordVoiceClient::SetSessionID(std::string_view session_id) {
m_session_id = session_id;
}
void DiscordVoiceClient::SetEndpoint(std::string_view endpoint) {
m_endpoint = endpoint;
}
void DiscordVoiceClient::SetToken(std::string_view token) {
m_token = token;
}
void DiscordVoiceClient::SetServerID(Snowflake id) {
m_server_id = id;
}
void DiscordVoiceClient::SetUserID(Snowflake id) {
m_user_id = id;
}
std::optional<uint32_t> DiscordVoiceClient::GetSSRCOfUser(Snowflake id) const {
if (const auto it = m_ssrc_map.find(id); it != m_ssrc_map.end()) {
return it->second;
}
return std::nullopt;
}
bool DiscordVoiceClient::IsConnected() const noexcept {
return m_state == State::Connected;
}
bool DiscordVoiceClient::IsConnecting() const noexcept {
return m_state == State::ConnectingToWebsocket || m_state == State::EstablishingConnection;
}
void DiscordVoiceClient::OnGatewayMessage(const std::string &str) {
VoiceGatewayMessage msg = nlohmann::json::parse(str);
switch (msg.Opcode) {
case VoiceGatewayOp::Hello:
HandleGatewayHello(msg);
break;
case VoiceGatewayOp::Ready:
HandleGatewayReady(msg);
break;
case VoiceGatewayOp::SessionDescription:
HandleGatewaySessionDescription(msg);
break;
case VoiceGatewayOp::Speaking:
HandleGatewaySpeaking(msg);
break;
default:
m_log->warn("Unhandled opcode: {}", static_cast<int>(msg.Opcode));
}
}
const char *DiscordVoiceClient::GetStateName(State state) {
switch (state) {
case State::DisconnectedByClient:
return "DisconnectedByClient";
case State::DisconnectedByServer:
return "DisconnectedByServer";
case State::ConnectingToWebsocket:
return "ConnectingToWebsocket";
case State::EstablishingConnection:
return "EstablishingConnection";
case State::Connected:
return "Connected";
default:
return "Unknown";
}
}
void DiscordVoiceClient::HandleGatewayHello(const VoiceGatewayMessage &m) {
VoiceHelloData d = m.Data;
m_log->debug("Received hello: {}ms", d.HeartbeatInterval);
m_heartbeat_msec = d.HeartbeatInterval;
m_heartbeat_thread = std::thread(&DiscordVoiceClient::HeartbeatThread, this);
Identify();
}
void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) {
VoiceReadyData d = m.Data;
m_log->debug("Received ready: {}:{} (ssrc: {})", d.IP, d.Port, d.SSRC);
m_ip = d.IP;
m_port = d.Port;
m_ssrc = d.SSRC;
if (std::find(d.Modes.begin(), d.Modes.end(), "xsalsa20_poly1305") == d.Modes.end()) {
m_log->warn("xsalsa20_poly1305 not in modes");
}
m_udp.Connect(m_ip, m_port);
m_keepalive_thread = std::thread(&DiscordVoiceClient::KeepaliveThread, this);
Discovery();
}
void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessage &m) {
VoiceSessionDescriptionData d = m.Data;
m_log->debug("Received session description (mode: {}) (key: {:ns}) ", d.Mode, spdlog::to_hex(d.SecretKey.begin(), d.SecretKey.end()));
VoiceSpeakingMessage msg;
msg.Delay = 0;
msg.SSRC = m_ssrc;
msg.Speaking = VoiceSpeakingType::Microphone;
m_ws.Send(msg);
m_secret_key = d.SecretKey;
m_udp.SetSSRC(m_ssrc);
m_udp.SetSecretKey(m_secret_key);
m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE });
m_udp.Run();
SetState(State::Connected);
}
void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) {
VoiceSpeakingData d = m.Data;
m_ssrc_map[d.UserID] = d.SSRC;
m_signal_speaking.emit(d);
}
void DiscordVoiceClient::Identify() {
VoiceIdentifyMessage msg;
msg.ServerID = m_server_id;
msg.UserID = m_user_id;
msg.SessionID = m_session_id;
msg.Token = m_token;
msg.Video = true;
m_ws.Send(msg);
}
void DiscordVoiceClient::Discovery() {
std::vector<uint8_t> payload;
// request
payload.push_back(0x00);
payload.push_back(0x01);
// payload length (70)
payload.push_back(0x00);
payload.push_back(0x46);
// ssrc
payload.push_back((m_ssrc >> 24) & 0xFF);
payload.push_back((m_ssrc >> 16) & 0xFF);
payload.push_back((m_ssrc >> 8) & 0xFF);
payload.push_back((m_ssrc >> 0) & 0xFF);
// space for address and port
for (int i = 0; i < 66; i++) payload.push_back(0x00);
m_udp.Send(payload.data(), payload.size());
constexpr int MAX_TRIES = 100;
for (int i = 0; i < MAX_TRIES; i++) {
const auto response = m_udp.Receive();
if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) {
const char *ip = reinterpret_cast<const char *>(response.data() + 8);
uint16_t port = (response[73] << 8) | response[74];
m_log->info("Discovered IP and port: {}:{}", ip, port);
SelectProtocol(ip, port);
break;
} else {
m_log->error("Received non-discovery packet after sending request (try {}/{})", i + 1, MAX_TRIES);
}
}
}
void DiscordVoiceClient::SelectProtocol(const char *ip, uint16_t port) {
VoiceSelectProtocolMessage msg;
msg.Mode = "xsalsa20_poly1305";
msg.Address = ip;
msg.Port = port;
msg.Protocol = "udp";
m_ws.Send(msg);
}
void DiscordVoiceClient::OnWebsocketOpen() {
m_log->info("Websocket opened");
SetState(State::EstablishingConnection);
}
void DiscordVoiceClient::OnWebsocketClose(const ix::WebSocketCloseInfo &info) {
if (info.remote) {
m_log->debug("Websocket closed (remote): {} ({})", info.code, info.reason);
} else {
m_log->debug("Websocket closed (local): {} ({})", info.code, info.reason);
}
}
void DiscordVoiceClient::OnWebsocketMessage(const std::string &data) {
m_dispatch_mutex.lock();
m_dispatch_queue.push(data);
m_dispatcher.emit();
m_dispatch_mutex.unlock();
}
void DiscordVoiceClient::HeartbeatThread() {
while (true) {
if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) break;
const auto ms = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count());
m_log->trace("Heartbeat: {}", ms);
VoiceHeartbeatMessage msg;
msg.Nonce = ms;
m_ws.Send(msg);
}
}
void DiscordVoiceClient::KeepaliveThread() {
while (true) {
if (!m_heartbeat_waiter.wait_for(std::chrono::seconds(10))) break;
if (IsConnected()) {
const static uint8_t KEEPALIVE[] = { 0x13, 0x37 };
m_udp.Send(KEEPALIVE, sizeof(KEEPALIVE));
}
}
}
void DiscordVoiceClient::SetState(State state) {
m_log->debug("Changing state to {}", GetStateName(state));
m_state = state;
m_signal_state_update.emit(state);
}
void DiscordVoiceClient::OnUDPData(std::vector<uint8_t> data) {
uint8_t *payload = data.data() + 12;
uint32_t ssrc = (data[8] << 24) |
(data[9] << 16) |
(data[10] << 8) |
(data[11] << 0);
static std::array<uint8_t, 24> nonce = {};
std::memcpy(nonce.data(), data.data(), 12);
if (crypto_secretbox_open_easy(payload, payload, data.size() - 12, nonce.data(), m_secret_key.data())) {
// spdlog::get("voice")->trace("UDP payload decryption failure");
} else {
Abaddon::Get().GetAudio().FeedMeOpus(ssrc, { payload, payload + data.size() - 12 - crypto_box_MACBYTES });
}
}
void DiscordVoiceClient::OnDispatch() {
m_dispatch_mutex.lock();
if (m_dispatch_queue.empty()) {
m_dispatch_mutex.unlock();
return;
}
auto msg = std::move(m_dispatch_queue.front());
m_dispatch_queue.pop();
m_dispatch_mutex.unlock();
OnGatewayMessage(msg);
}
DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_connected() {
return m_signal_connected;
}
DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_disconnected() {
return m_signal_disconnected;
}
DiscordVoiceClient::type_signal_speaking DiscordVoiceClient::signal_speaking() {
return m_signal_speaking;
}
DiscordVoiceClient::type_signal_state_update DiscordVoiceClient::signal_state_update() {
return m_signal_state_update;
}
void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) {
JS_D("op", m.Opcode);
m.Data = j.at("d");
}
void from_json(const nlohmann::json &j, VoiceHelloData &m) {
JS_D("heartbeat_interval", m.HeartbeatInterval);
}
void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m) {
j["op"] = VoiceGatewayOp::Heartbeat;
j["d"] = m.Nonce;
}
void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m) {
j["op"] = VoiceGatewayOp::Identify;
j["d"]["server_id"] = m.ServerID;
j["d"]["user_id"] = m.UserID;
j["d"]["session_id"] = m.SessionID;
j["d"]["token"] = m.Token;
j["d"]["video"] = m.Video;
j["d"]["streams"][0]["type"] = "video";
j["d"]["streams"][0]["rid"] = "100";
j["d"]["streams"][0]["quality"] = 100;
}
void from_json(const nlohmann::json &j, VoiceReadyData::VoiceStream &m) {
JS_D("active", m.IsActive);
JS_D("quality", m.Quality);
JS_D("rid", m.RID);
JS_D("rtx_ssrc", m.RTXSSRC);
JS_D("ssrc", m.SSRC);
JS_D("type", m.Type);
}
void from_json(const nlohmann::json &j, VoiceReadyData &m) {
JS_ON("experiments", m.Experiments);
JS_D("ip", m.IP);
JS_D("modes", m.Modes);
JS_D("port", m.Port);
JS_D("ssrc", m.SSRC);
JS_ON("streams", m.Streams);
}
void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m) {
j["op"] = VoiceGatewayOp::SelectProtocol;
j["d"]["address"] = m.Address;
j["d"]["port"] = m.Port;
j["d"]["protocol"] = m.Protocol;
j["d"]["mode"] = m.Mode;
j["d"]["data"]["address"] = m.Address;
j["d"]["data"]["port"] = m.Port;
j["d"]["data"]["mode"] = m.Mode;
}
void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m) {
JS_D("mode", m.Mode);
JS_D("secret_key", m.SecretKey);
}
void to_json(nlohmann::json &j, const VoiceSpeakingMessage &m) {
j["op"] = VoiceGatewayOp::Speaking;
j["d"]["speaking"] = m.Speaking;
j["d"]["delay"] = m.Delay;
j["d"]["ssrc"] = m.SSRC;
}
void from_json(const nlohmann::json &j, VoiceSpeakingData &m) {
JS_D("user_id", m.UserID);
JS_D("ssrc", m.SSRC);
JS_D("speaking", m.Speaking);
}
#endif

View File

@@ -1,288 +0,0 @@
#pragma once
#ifdef WITH_VOICE
// clang-format off
#include "snowflake.hpp"
#include "waiter.hpp"
#include "websocket.hpp"
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <glibmm/dispatcher.h>
#include <sigc++/sigc++.h>
#include <spdlog/logger.h>
#include <unordered_map>
// clang-format on
enum class VoiceGatewayCloseCode : uint16_t {
Normal = 4000,
UnknownOpcode = 4001,
InvalidPayload = 4002,
NotAuthenticated = 4003,
AuthenticationFailed = 4004,
AlreadyAuthenticated = 4005,
SessionInvalid = 4006,
SessionTimedOut = 4009,
ServerNotFound = 4011,
UnknownProtocol = 4012,
Disconnected = 4014,
ServerCrashed = 4015,
UnknownEncryption = 4016,
};
enum class VoiceGatewayOp : int {
Identify = 0,
SelectProtocol = 1,
Ready = 2,
Heartbeat = 3,
SessionDescription = 4,
Speaking = 5,
HeartbeatAck = 6,
Resume = 7,
Hello = 8,
Resumed = 9,
ClientDisconnect = 13,
};
struct VoiceGatewayMessage {
VoiceGatewayOp Opcode;
nlohmann::json Data;
friend void from_json(const nlohmann::json &j, VoiceGatewayMessage &m);
};
struct VoiceHelloData {
int HeartbeatInterval;
friend void from_json(const nlohmann::json &j, VoiceHelloData &m);
};
struct VoiceHeartbeatMessage {
uint64_t Nonce;
friend void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m);
};
struct VoiceIdentifyMessage {
Snowflake ServerID;
Snowflake UserID;
std::string SessionID;
std::string Token;
bool Video;
// todo streams i guess?
friend void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m);
};
struct VoiceReadyData {
struct VoiceStream {
bool IsActive;
int Quality;
std::string RID;
int RTXSSRC;
int SSRC;
std::string Type;
friend void from_json(const nlohmann::json &j, VoiceStream &m);
};
std::vector<std::string> Experiments;
std::string IP;
std::vector<std::string> Modes;
uint16_t Port;
uint32_t SSRC;
std::vector<VoiceStream> Streams;
friend void from_json(const nlohmann::json &j, VoiceReadyData &m);
};
struct VoiceSelectProtocolMessage {
std::string Address;
uint16_t Port;
std::string Mode;
std::string Protocol;
friend void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m);
};
struct VoiceSessionDescriptionData {
// std::string AudioCodec;
// std::string VideoCodec;
// std::string MediaSessionID;
std::string Mode;
std::array<uint8_t, 32> SecretKey;
friend void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m);
};
enum class VoiceSpeakingType {
Microphone = 1 << 0,
Soundshare = 1 << 1,
Priority = 1 << 2,
};
struct VoiceSpeakingMessage {
VoiceSpeakingType Speaking;
int Delay;
uint32_t SSRC;
friend void to_json(nlohmann::json &j, const VoiceSpeakingMessage &m);
};
struct VoiceSpeakingData {
Snowflake UserID;
uint32_t SSRC;
VoiceSpeakingType Speaking;
friend void from_json(const nlohmann::json &j, VoiceSpeakingData &m);
};
class UDPSocket {
public:
UDPSocket();
~UDPSocket();
void Connect(std::string_view ip, uint16_t port);
void Run();
void SetSecretKey(std::array<uint8_t, 32> key);
void SetSSRC(uint32_t ssrc);
void SendEncrypted(const uint8_t *data, size_t len);
void SendEncrypted(const std::vector<uint8_t> &data);
void Send(const uint8_t *data, size_t len);
std::vector<uint8_t> Receive();
void Stop();
private:
void ReadThread();
#ifdef _WIN32
SOCKET m_socket;
#else
int m_socket;
#endif
sockaddr_in m_server;
std::atomic<bool> m_running = false;
std::thread m_thread;
std::array<uint8_t, 32> m_secret_key;
uint32_t m_ssrc;
uint16_t m_sequence = 0;
uint32_t m_timestamp = 0;
public:
using type_signal_data = sigc::signal<void, std::vector<uint8_t>>;
type_signal_data signal_data();
private:
type_signal_data m_signal_data;
};
class DiscordVoiceClient {
public:
DiscordVoiceClient();
~DiscordVoiceClient();
void Start();
void Stop();
void SetSessionID(std::string_view session_id);
void SetEndpoint(std::string_view endpoint);
void SetToken(std::string_view token);
void SetServerID(Snowflake id);
void SetUserID(Snowflake id);
[[nodiscard]] std::optional<uint32_t> GetSSRCOfUser(Snowflake id) const;
// Is a websocket and udp connection fully established
[[nodiscard]] bool IsConnected() const noexcept;
[[nodiscard]] bool IsConnecting() const noexcept;
enum class State {
ConnectingToWebsocket,
EstablishingConnection,
Connected,
DisconnectedByClient,
DisconnectedByServer,
};
private:
static const char *GetStateName(State state);
void OnGatewayMessage(const std::string &msg);
void HandleGatewayHello(const VoiceGatewayMessage &m);
void HandleGatewayReady(const VoiceGatewayMessage &m);
void HandleGatewaySessionDescription(const VoiceGatewayMessage &m);
void HandleGatewaySpeaking(const VoiceGatewayMessage &m);
void Identify();
void Discovery();
void SelectProtocol(const char *ip, uint16_t port);
void OnWebsocketOpen();
void OnWebsocketClose(const ix::WebSocketCloseInfo &info);
void OnWebsocketMessage(const std::string &str);
void HeartbeatThread();
void KeepaliveThread();
void SetState(State state);
void OnUDPData(std::vector<uint8_t> data);
std::string m_session_id;
std::string m_endpoint;
std::string m_token;
Snowflake m_server_id;
Snowflake m_channel_id;
Snowflake m_user_id;
std::unordered_map<Snowflake, uint32_t> m_ssrc_map;
std::array<uint8_t, 32> m_secret_key;
std::string m_ip;
uint16_t m_port;
uint32_t m_ssrc;
int m_heartbeat_msec;
Waiter m_heartbeat_waiter;
std::thread m_heartbeat_thread;
Waiter m_keepalive_waiter;
std::thread m_keepalive_thread;
Websocket m_ws;
UDPSocket m_udp;
Glib::Dispatcher m_dispatcher;
std::queue<std::string> m_dispatch_queue;
std::mutex m_dispatch_mutex;
void OnDispatch();
std::array<uint8_t, 1275> m_opus_buffer;
std::shared_ptr<spdlog::logger> m_log;
std::atomic<State> m_state;
using type_signal_connected = sigc::signal<void()>;
using type_signal_disconnected = sigc::signal<void()>;
using type_signal_speaking = sigc::signal<void(VoiceSpeakingData)>;
using type_signal_state_update = sigc::signal<void(State)>;
type_signal_connected m_signal_connected;
type_signal_disconnected m_signal_disconnected;
type_signal_speaking m_signal_speaking;
type_signal_state_update m_signal_state_update;
public:
type_signal_connected signal_connected();
type_signal_disconnected signal_disconnected();
type_signal_speaking signal_speaking();
type_signal_state_update signal_state_update();
};
#endif

View File

@@ -1,29 +0,0 @@
#pragma once
#include <chrono>
#include <condition_variable>
#include <mutex>
class Waiter {
public:
template<class R, class P>
bool wait_for(std::chrono::duration<R, P> const &time) const {
std::unique_lock<std::mutex> lock(m);
return !cv.wait_for(lock, time, [&] { return terminate; });
}
void kill() {
std::unique_lock<std::mutex> lock(m);
terminate = true;
cv.notify_all();
}
void revive() {
std::unique_lock<std::mutex> lock(m);
terminate = false;
}
private:
mutable std::condition_variable cv;
mutable std::mutex m;
bool terminate = false;
};

View File

@@ -1,33 +1,14 @@
#include "websocket.hpp" #include "websocket.hpp"
#include <spdlog/sinks/stdout_color_sinks.h>
#include <utility> #include <utility>
Websocket::Websocket(const std::string &id) Websocket::Websocket() = default;
: m_close_info { 1000, "Normal", false } {
if (m_log = spdlog::get(id); !m_log) {
m_log = spdlog::stdout_color_mt(id);
}
m_open_dispatcher.connect([this]() {
m_signal_open.emit();
});
m_close_dispatcher.connect([this]() {
Stop();
m_signal_close.emit(m_close_info);
});
}
void Websocket::StartConnection(const std::string &url) { void Websocket::StartConnection(const std::string &url) {
m_log->debug("Starting connection to {}", url); m_websocket.disableAutomaticReconnection();
m_websocket.setUrl(url);
m_websocket = std::make_unique<ix::WebSocket>(); m_websocket.setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward<decltype(msg)>(msg)); });
m_websocket.setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent } }); // idk if this actually works
m_websocket->disableAutomaticReconnection(); m_websocket.start();
m_websocket->setUrl(url);
m_websocket->setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward<decltype(msg)>(msg)); });
m_websocket->setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent } }); // idk if this actually works
m_websocket->start();
} }
void Websocket::SetUserAgent(std::string agent) { void Websocket::SetUserAgent(std::string agent) {
@@ -43,19 +24,17 @@ void Websocket::SetPrintMessages(bool show) noexcept {
} }
void Websocket::Stop() { void Websocket::Stop() {
m_log->debug("Stopping with default close code");
Stop(ix::WebSocketCloseConstants::kNormalClosureCode); Stop(ix::WebSocketCloseConstants::kNormalClosureCode);
} }
void Websocket::Stop(uint16_t code) { void Websocket::Stop(uint16_t code) {
m_log->debug("Stopping with close code {}", code); m_websocket.stop(code);
m_websocket-> stop(code);
} }
void Websocket::Send(const std::string &str) { void Websocket::Send(const std::string &str) {
if (m_print_messages) if (m_print_messages)
m_log->trace("Send: {}", str); printf("sending %s\n", str.c_str());
m_websocket->sendText(str); m_websocket.sendText(str);
} }
void Websocket::Send(const nlohmann::json &j) { void Websocket::Send(const nlohmann::json &j) {
@@ -65,13 +44,10 @@ void Websocket::Send(const nlohmann::json &j) {
void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) { void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) {
switch (msg->type) { switch (msg->type) {
case ix::WebSocketMessageType::Open: { case ix::WebSocketMessageType::Open: {
m_log->debug("Received open frame, dispatching"); m_signal_open.emit();
m_open_dispatcher.emit();
} break; } break;
case ix::WebSocketMessageType::Close: { case ix::WebSocketMessageType::Close: {
m_log->debug("Received close frame, dispatching. {} ({}){}", msg->closeInfo.code, msg->closeInfo.reason, msg->closeInfo.remote ? " Remote" : ""); m_signal_close.emit(msg->closeInfo.code);
m_close_info = msg->closeInfo;
m_close_dispatcher.emit();
} break; } break;
case ix::WebSocketMessageType::Message: { case ix::WebSocketMessageType::Message: {
m_signal_message.emit(msg->str); m_signal_message.emit(msg->str);

View File

@@ -3,14 +3,12 @@
#include <ixwebsocket/IXWebSocket.h> #include <ixwebsocket/IXWebSocket.h>
#include <string> #include <string>
#include <functional> #include <functional>
#include <glibmm.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
#include <spdlog/spdlog.h>
class Websocket { class Websocket {
public: public:
Websocket(const std::string &id); Websocket();
void StartConnection(const std::string &url); void StartConnection(const std::string &url);
void SetUserAgent(std::string agent); void SetUserAgent(std::string agent);
@@ -26,12 +24,12 @@ public:
private: private:
void OnMessage(const ix::WebSocketMessagePtr &msg); void OnMessage(const ix::WebSocketMessagePtr &msg);
std::unique_ptr<ix::WebSocket> m_websocket; ix::WebSocket m_websocket;
std::string m_agent; std::string m_agent;
public: public:
using type_signal_open = sigc::signal<void>; using type_signal_open = sigc::signal<void>;
using type_signal_close = sigc::signal<void, ix::WebSocketCloseInfo>; using type_signal_close = sigc::signal<void, uint16_t>;
using type_signal_message = sigc::signal<void, std::string>; using type_signal_message = sigc::signal<void, std::string>;
type_signal_open signal_open(); type_signal_open signal_open();
@@ -44,10 +42,4 @@ private:
type_signal_message m_signal_message; type_signal_message m_signal_message;
bool m_print_messages = true; bool m_print_messages = true;
Glib::Dispatcher m_open_dispatcher;
Glib::Dispatcher m_close_dispatcher;
ix::WebSocketCloseInfo m_close_info;
std::shared_ptr<spdlog::logger> m_log;
}; };

View File

@@ -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,7 +45,6 @@ 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); SMBOOL("discord", "autoconnect", Autoconnect);
@@ -61,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
@@ -92,7 +126,6 @@ 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); SMBOOL("discord", "autoconnect", Autoconnect);
@@ -117,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

View File

@@ -1,12 +1,13 @@
#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)
, m_chan_content_paned(Gtk::ORIENTATION_HORIZONTAL) , m_chan_content_paned(Gtk::ORIENTATION_HORIZONTAL)
, m_content_members_paned(Gtk::ORIENTATION_HORIZONTAL) , m_content_members_paned(Gtk::ORIENTATION_HORIZONTAL)
, m_left_pane(Gtk::ORIENTATION_VERTICAL)
, m_accels(Gtk::AccelGroup::create()) { , m_accels(Gtk::AccelGroup::create()) {
set_default_size(1200, 800); set_default_size(1200, 800);
get_style_context()->add_class("app-window"); get_style_context()->add_class("app-window");
@@ -21,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);
@@ -38,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);
@@ -52,29 +52,23 @@ MainWindow::MainWindow()
m_content_stack.set_visible_child("chat"); m_content_stack.set_visible_child("chat");
m_content_stack.show(); m_content_stack.show();
m_voice_info.show(); m_chan_content_paned.pack1(m_channel_list);
m_left_pane.add(m_channel_list);
m_left_pane.add(m_voice_info);
m_left_pane.show();
m_chan_content_paned.pack1(m_left_pane);
m_chan_content_paned.pack2(m_content_members_paned); m_chan_content_paned.pack2(m_content_members_paned);
m_chan_content_paned.child_property_shrink(m_content_members_paned) = true; m_chan_content_paned.child_property_shrink(m_content_members_paned) = true;
m_chan_content_paned.child_property_resize(m_content_members_paned) = true; m_chan_content_paned.child_property_resize(m_content_members_paned) = true;
m_chan_content_paned.child_property_shrink(m_left_pane) = true; m_chan_content_paned.child_property_shrink(m_channel_list) = true;
m_chan_content_paned.child_property_resize(m_left_pane) = true; m_chan_content_paned.child_property_resize(m_channel_list) = true;
m_chan_content_paned.set_position(200); m_chan_content_paned.set_position(200);
m_chan_content_paned.show(); m_chan_content_paned.show();
m_content_box.add(m_chan_content_paned); m_content_box.add(m_chan_content_paned);
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);
@@ -272,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");
@@ -282,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);
@@ -361,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();

View File

@@ -1,9 +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/voiceinfobox.hpp" #include "components/memberlist.hpp"
#include <gtkmm.h> #include <gtkmm.h>
class MainWindow : public Gtk::Window { class MainWindow : public Gtk::Window {
@@ -54,9 +53,6 @@ private:
ChatWindow m_chat; ChatWindow m_chat;
MemberList m_members; MemberList m_members;
FriendsList m_friends; FriendsList m_friends;
VoiceInfoBox m_voice_info;
Gtk::Box m_left_pane;
Gtk::Stack m_content_stack; Gtk::Stack m_content_stack;
@@ -83,6 +79,8 @@ 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;

View File

@@ -1,125 +0,0 @@
#ifdef WITH_VOICE
// clang-format off
#include "voicesettingswindow.hpp"
#include "abaddon.hpp"
#include "audio/manager.hpp"
#include <spdlog/spdlog.h>
// clang-format on
VoiceSettingsWindow::VoiceSettingsWindow()
: m_main(Gtk::ORIENTATION_VERTICAL) {
get_style_context()->add_class("app-window");
set_default_size(300, 300);
m_encoding_mode.append("Voice");
m_encoding_mode.append("Music");
m_encoding_mode.append("Restricted");
m_encoding_mode.set_tooltip_text(
"Sets the coding mode for the Opus encoder\n"
"Voice - Optimize for voice quality\n"
"Music - Optimize for non-voice signals incl. music\n"
"Restricted - Optimize for non-voice, low latency. Not recommended");
const auto mode = Abaddon::Get().GetAudio().GetEncodingApplication();
if (mode == OPUS_APPLICATION_VOIP) {
m_encoding_mode.set_active(0);
} else if (mode == OPUS_APPLICATION_AUDIO) {
m_encoding_mode.set_active(1);
} else if (mode == OPUS_APPLICATION_RESTRICTED_LOWDELAY) {
m_encoding_mode.set_active(2);
}
m_encoding_mode.signal_changed().connect([this]() {
const auto mode = m_encoding_mode.get_active_text();
auto &audio = Abaddon::Get().GetAudio();
spdlog::get("audio")->debug("Chose encoding mode: {}", mode.c_str());
if (mode == "Voice") {
audio.SetEncodingApplication(OPUS_APPLICATION_VOIP);
} else if (mode == "Music") {
spdlog::get("audio")->debug("music/audio");
audio.SetEncodingApplication(OPUS_APPLICATION_AUDIO);
} else if (mode == "Restricted") {
audio.SetEncodingApplication(OPUS_APPLICATION_RESTRICTED_LOWDELAY);
}
});
m_signal.append("Auto");
m_signal.append("Voice");
m_signal.append("Music");
m_signal.set_tooltip_text(
"Signal hint. Tells Opus what the current signal is\n"
"Auto - Let Opus figure it out\n"
"Voice - Tell Opus it's a voice signal\n"
"Music - Tell Opus it's a music signal");
const auto signal = Abaddon::Get().GetAudio().GetSignalHint();
if (signal == OPUS_AUTO) {
m_signal.set_active(0);
} else if (signal == OPUS_SIGNAL_VOICE) {
m_signal.set_active(1);
} else if (signal == OPUS_SIGNAL_MUSIC) {
m_signal.set_active(2);
}
m_signal.signal_changed().connect([this]() {
const auto signal = m_signal.get_active_text();
auto &audio = Abaddon::Get().GetAudio();
spdlog::get("audio")->debug("Chose signal hint: {}", signal.c_str());
if (signal == "Auto") {
audio.SetSignalHint(OPUS_AUTO);
} else if (signal == "Voice") {
audio.SetSignalHint(OPUS_SIGNAL_VOICE);
} else if (signal == "Music") {
audio.SetSignalHint(OPUS_SIGNAL_MUSIC);
}
});
// exponential scale for bitrate because higher bitrates dont sound much different
constexpr static auto MAX_BITRATE = 128000.0;
constexpr static auto MIN_BITRATE = 2400.0;
const auto bitrate_scale = [this](double value) -> double {
value /= 100.0;
return (MAX_BITRATE - MIN_BITRATE) * value * value * value + MIN_BITRATE;
};
const auto bitrate_scale_r = [this](double value) -> double {
return 100.0 * std::cbrt((value - MIN_BITRATE) / (MAX_BITRATE - MIN_BITRATE));
};
m_bitrate.set_range(0.0, 100.0);
m_bitrate.set_value_pos(Gtk::POS_TOP);
m_bitrate.set_value(bitrate_scale_r(Abaddon::Get().GetAudio().GetBitrate()));
m_bitrate.signal_format_value().connect([this, bitrate_scale](double value) {
const auto scaled = bitrate_scale(value);
if (value <= 99.9) {
return Glib::ustring(std::to_string(static_cast<int>(scaled)));
} else {
return Glib::ustring("MAX");
}
});
m_bitrate.signal_value_changed().connect([this, bitrate_scale]() {
const auto value = m_bitrate.get_value();
const auto scaled = bitrate_scale(value);
if (value <= 99.9) {
Abaddon::Get().GetAudio().SetBitrate(static_cast<int>(scaled));
} else {
Abaddon::Get().GetAudio().SetBitrate(OPUS_BITRATE_MAX);
}
});
m_main.add(m_encoding_mode);
m_main.add(m_signal);
m_main.add(m_bitrate);
add(m_main);
show_all_children();
// no need to bring in ManageHeapWindow, no user menu
signal_hide().connect([this]() {
delete this;
});
}
#endif

View File

@@ -1,25 +0,0 @@
#pragma once
#ifdef WITH_VOICE
// clang-format off
#include <gtkmm/box.h>
#include <gtkmm/comboboxtext.h>
#include <gtkmm/scale.h>
#include <gtkmm/window.h>
// clang-format on
class VoiceSettingsWindow : public Gtk::Window {
public:
VoiceSettingsWindow();
Gtk::Box m_main;
Gtk::ComboBoxText m_encoding_mode;
Gtk::ComboBoxText m_signal;
Gtk::Scale m_bitrate;
private:
};
#endif

View File

@@ -1,254 +0,0 @@
#ifdef WITH_VOICE
// clang-format off
#include "voicewindow.hpp"
#include "components/lazyimage.hpp"
#include "abaddon.hpp"
#include "audio/manager.hpp"
#include "voicesettingswindow.hpp"
// clang-format on
class VoiceWindowUserListEntry : public Gtk::ListBoxRow {
public:
VoiceWindowUserListEntry(Snowflake id)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_horz(Gtk::ORIENTATION_HORIZONTAL)
, m_avatar(32, 32)
, m_mute("Mute") {
m_name.set_halign(Gtk::ALIGN_START);
m_name.set_hexpand(true);
m_mute.set_halign(Gtk::ALIGN_END);
m_volume.set_range(0.0, 200.0);
m_volume.set_value_pos(Gtk::POS_LEFT);
m_volume.set_value(100.0);
m_volume.signal_value_changed().connect([this]() {
m_signal_volume.emit(m_volume.get_value());
});
m_horz.add(m_avatar);
m_horz.add(m_name);
m_horz.add(m_mute);
m_main.add(m_horz);
m_main.add(m_volume);
m_main.add(m_meter);
add(m_main);
show_all_children();
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(id);
if (user.has_value()) {
m_name.set_text(user->Username);
m_avatar.SetURL(user->GetAvatarURL("png", "32"));
} else {
m_name.set_text("Unknown user");
}
m_mute.signal_toggled().connect([this]() {
m_signal_mute_cs.emit(m_mute.get_active());
});
}
void SetVolumeMeter(double frac) {
m_meter.SetVolume(frac);
}
private:
Gtk::Box m_main;
Gtk::Box m_horz;
LazyImage m_avatar;
Gtk::Label m_name;
Gtk::CheckButton m_mute;
Gtk::Scale m_volume;
VolumeMeter m_meter;
public:
using type_signal_mute_cs = sigc::signal<void(bool)>;
using type_signal_volume = sigc::signal<void(double)>;
type_signal_mute_cs signal_mute_cs() {
return m_signal_mute_cs;
}
type_signal_volume signal_volume() {
return m_signal_volume;
}
private:
type_signal_mute_cs m_signal_mute_cs;
type_signal_volume m_signal_volume;
};
VoiceWindow::VoiceWindow(Snowflake channel_id)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_controls(Gtk::ORIENTATION_HORIZONTAL)
, m_mute("Mute")
, m_deafen("Deafen")
, m_channel_id(channel_id)
, m_menu_view("View")
, m_menu_view_settings("More _Settings", true) {
get_style_context()->add_class("app-window");
set_default_size(300, 300);
auto &discord = Abaddon::Get().GetDiscordClient();
SetUsers(discord.GetUsersInVoiceChannel(m_channel_id));
discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect));
discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserConnect));
m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged));
m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged));
m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
m_scroll.set_hexpand(true);
m_scroll.set_vexpand(true);
m_capture_volume.SetShowTick(true);
m_capture_gate.set_range(0.0, 100.0);
m_capture_gate.set_value_pos(Gtk::POS_LEFT);
m_capture_gate.set_value(0.0);
m_capture_gate.signal_value_changed().connect([this]() {
// todo this should probably emit 0-1 i dont think the mgr should be responsible for scaling down
const double val = m_capture_gate.get_value();
m_signal_gate.emit(val);
m_capture_volume.SetTick(val / 100.0);
});
m_capture_gain.set_range(0.0, 200.0);
m_capture_gain.set_value_pos(Gtk::POS_LEFT);
m_capture_gain.set_value(100.0);
m_capture_gain.signal_value_changed().connect([this]() {
const double val = m_capture_gain.get_value();
m_signal_gain.emit(val / 100.0);
});
auto *playback_renderer = Gtk::make_managed<Gtk::CellRendererText>();
m_playback_combo.set_valign(Gtk::ALIGN_END);
m_playback_combo.set_hexpand(true);
m_playback_combo.set_halign(Gtk::ALIGN_FILL);
m_playback_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetPlaybackDeviceModel());
m_playback_combo.set_active(Abaddon::Get().GetAudio().GetDevices().GetActivePlaybackDevice());
m_playback_combo.pack_start(*playback_renderer);
m_playback_combo.add_attribute(*playback_renderer, "text", 0);
m_playback_combo.signal_changed().connect([this]() {
Abaddon::Get().GetAudio().SetPlaybackDevice(m_playback_combo.get_active());
});
auto *capture_renderer = Gtk::make_managed<Gtk::CellRendererText>();
m_capture_combo.set_valign(Gtk::ALIGN_END);
m_capture_combo.set_hexpand(true);
m_capture_combo.set_halign(Gtk::ALIGN_FILL);
m_capture_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetCaptureDeviceModel());
m_capture_combo.set_active(Abaddon::Get().GetAudio().GetDevices().GetActiveCaptureDevice());
m_capture_combo.pack_start(*capture_renderer);
m_capture_combo.add_attribute(*capture_renderer, "text", 0);
m_capture_combo.signal_changed().connect([this]() {
Abaddon::Get().GetAudio().SetCaptureDevice(m_capture_combo.get_active());
});
m_menu_bar.append(m_menu_view);
m_menu_view.set_submenu(m_menu_view_sub);
m_menu_view_sub.append(m_menu_view_settings);
m_menu_view_settings.signal_activate().connect([this]() {
auto *window = new VoiceSettingsWindow;
window->show();
});
m_scroll.add(m_user_list);
m_controls.add(m_mute);
m_controls.add(m_deafen);
m_main.add(m_menu_bar);
m_main.add(m_controls);
m_main.add(m_capture_volume);
m_main.add(m_capture_gate);
m_main.add(m_capture_gain);
m_main.add(m_scroll);
m_main.add(m_playback_combo);
m_main.add(m_capture_combo);
add(m_main);
show_all_children();
Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40);
}
void VoiceWindow::SetUsers(const std::unordered_set<Snowflake> &user_ids) {
for (auto id : user_ids) {
m_user_list.add(*CreateRow(id));
}
}
Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) {
auto *row = Gtk::make_managed<VoiceWindowUserListEntry>(id);
m_rows[id] = row;
row->signal_mute_cs().connect([this, id](bool is_muted) {
m_signal_mute_user_cs.emit(id, is_muted);
});
row->signal_volume().connect([this, id](double volume) {
m_signal_user_volume_changed.emit(id, volume);
});
row->show_all();
return row;
}
void VoiceWindow::OnMuteChanged() {
m_signal_mute.emit(m_mute.get_active());
}
void VoiceWindow::OnDeafenChanged() {
m_signal_deafen.emit(m_deafen.get_active());
}
bool VoiceWindow::UpdateVoiceMeters() {
m_capture_volume.SetVolume(Abaddon::Get().GetAudio().GetCaptureVolumeLevel());
for (auto [id, row] : m_rows) {
const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id);
if (ssrc.has_value()) {
row->SetVolumeMeter(Abaddon::Get().GetAudio().GetSSRCVolumeLevel(*ssrc));
}
}
return true;
}
void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) {
if (m_channel_id == to_channel_id) {
if (auto it = m_rows.find(user_id); it == m_rows.end()) {
m_user_list.add(*CreateRow(user_id));
}
}
}
void VoiceWindow::OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id) {
if (m_channel_id == from_channel_id) {
if (auto it = m_rows.find(user_id); it != m_rows.end()) {
delete it->second;
m_rows.erase(it);
}
}
}
VoiceWindow::type_signal_mute VoiceWindow::signal_mute() {
return m_signal_mute;
}
VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() {
return m_signal_deafen;
}
VoiceWindow::type_signal_gate VoiceWindow::signal_gate() {
return m_signal_gate;
}
VoiceWindow::type_signal_gate VoiceWindow::signal_gain() {
return m_signal_gain;
}
VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() {
return m_signal_mute_user_cs;
}
VoiceWindow::type_signal_user_volume_changed VoiceWindow::signal_user_volume_changed() {
return m_signal_user_volume_changed;
}
#endif

View File

@@ -1,85 +0,0 @@
#pragma once
#ifdef WITH_VOICE
// clang-format off
#include "components/volumemeter.hpp"
#include "discord/snowflake.hpp"
#include <gtkmm/box.h>
#include <gtkmm/checkbutton.h>
#include <gtkmm/combobox.h>
#include <gtkmm/listbox.h>
#include <gtkmm/menubar.h>
#include <gtkmm/progressbar.h>
#include <gtkmm/scale.h>
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/window.h>
#include <unordered_set>
// clang-format on
class VoiceWindowUserListEntry;
class VoiceWindow : public Gtk::Window {
public:
VoiceWindow(Snowflake channel_id);
private:
void SetUsers(const std::unordered_set<Snowflake> &user_ids);
Gtk::ListBoxRow *CreateRow(Snowflake id);
void OnUserConnect(Snowflake user_id, Snowflake to_channel_id);
void OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id);
void OnMuteChanged();
void OnDeafenChanged();
bool UpdateVoiceMeters();
Gtk::Box m_main;
Gtk::Box m_controls;
Gtk::CheckButton m_mute;
Gtk::CheckButton m_deafen;
Gtk::ScrolledWindow m_scroll;
Gtk::ListBox m_user_list;
VolumeMeter m_capture_volume;
Gtk::Scale m_capture_gate;
Gtk::Scale m_capture_gain;
Gtk::ComboBox m_playback_combo;
Gtk::ComboBox m_capture_combo;
Snowflake m_channel_id;
std::unordered_map<Snowflake, VoiceWindowUserListEntry *> m_rows;
Gtk::MenuBar m_menu_bar;
Gtk::MenuItem m_menu_view;
Gtk::Menu m_menu_view_sub;
Gtk::MenuItem m_menu_view_settings;
public:
using type_signal_mute = sigc::signal<void(bool)>;
using type_signal_deafen = sigc::signal<void(bool)>;
using type_signal_gate = sigc::signal<void(double)>;
using type_signal_gain = sigc::signal<void(double)>;
using type_signal_mute_user_cs = sigc::signal<void(Snowflake, bool)>;
using type_signal_user_volume_changed = sigc::signal<void(Snowflake, double)>;
type_signal_mute signal_mute();
type_signal_deafen signal_deafen();
type_signal_gate signal_gate();
type_signal_gain signal_gain();
type_signal_mute_user_cs signal_mute_user_cs();
type_signal_user_volume_changed signal_user_volume_changed();
private:
type_signal_mute m_signal_mute;
type_signal_deafen m_signal_deafen;
type_signal_gate m_signal_gate;
type_signal_gain m_signal_gain;
type_signal_mute_user_cs m_signal_mute_user_cs;
type_signal_user_volume_changed m_signal_user_volume_changed;
};
#endif

1
subprojects/keychain Submodule

Submodule subprojects/keychain added at 44b517d096