172 Commits

Author SHA1 Message Date
ouwou
67e924e538 display users in voice in channel list 2023-01-06 18:40:11 -05:00
ouwou
c4590f8b23 start voice info box 2023-01-03 22:52:41 -05:00
ouwou
dff93e103a actually go to connected state and transmit data 2023-01-03 19:57:38 -05:00
ouwou
02583b8512 re-add ssrc map 2023-01-03 19:47:56 -05:00
ouwou
4740965f4c rewrite DiscordVoiceClient and stuff 2023-01-03 19:01:33 -05:00
ouwou
6ff2563e36 move ixwebsocket to fork 2022-12-26 22:13:09 -05:00
ouwou
afaba05293 actually reconstruct websocket on voice connect 2022-11-15 15:38:39 -05:00
ouwou
929ebf1a60 mess with some websocket stuff to try and fix things
to be honest, im not sure what ive done here. whatever memory i have of the issue i was trying to fix has long disappeared by the time im committing this. theres still some issues with being force disconnected and i really dont understand it
ill figure it out eventually... maybe :/
2022-11-15 02:15:21 -05:00
ouwou
38c5230a1d add window to change more stuff with opus 2022-11-14 01:28:07 -05:00
ouwou
e2784cd97b model stuff to track active device
also minor refactor
2022-11-09 19:03:53 -05:00
ouwou
0471688732 add ability to set capture device 2022-11-08 04:01:54 -05:00
ouwou
f97a6ff266 fix up CI:
add libhandy as dependency
change ubuntu actions environment
update nlohmann/json to latest release
add preprocessor checks
2022-11-08 02:32:45 -05:00
ouwou
28c3ec417f add spdlog to ci 2022-11-07 21:53:05 -05:00
ouwou
f8f9a907c9 add basic combobox to choose output device, start using spdlog 2022-11-05 02:32:43 -04:00
ouwou
cb690b6def only enable microphone when in a voice channel 2022-10-24 22:10:50 -04:00
ouwou
f751037717 Merge branch 'master' into voice 2022-10-24 02:48:57 -04:00
ouwou
64245bf745 add option to autoconnect (closes #114) 2022-10-23 18:23:11 -04:00
ouwou
772598996c Add option to hide the menu bar behind alt key (#115) 2022-10-23 02:56:07 +00:00
ouwou
e888306272 add gain slider (how 2 loudmic? 🤓) 2022-10-21 01:23:37 -04:00
ouwou
848e75f577 use new volume meter for other users 2022-10-20 02:18:01 -04:00
ouwou
e2110c22ee store user data from voice state updates 2022-10-18 18:34:14 -04:00
ouwou
cf53831b2a decay capture meter faster 2022-10-18 18:34:02 -04:00
ouwou
88f2e63eeb custom draw capture volume with gate indicator 2022-10-18 02:53:11 -04:00
Addison Snelling
ccb82c1676 readme: list exact depedency package names for Ubuntu (#109)
Co-authored-by: Addison Snelling <fwd+gpg@ext.asnell.io>
2022-10-18 05:48:45 +00:00
ouwou
5a3bce7498 basic voice gate 2022-10-18 01:47:43 -04:00
ouwou
621beb1344 basic volume meters 2022-10-16 23:12:26 -04:00
ouwou
cd900cdfee update msys dependencies 2022-10-13 17:03:20 -04:00
ouwou
17e7478bb4 add user row on voice connect 2022-10-12 01:51:32 -04:00
ouwou
78a5b9599c remove user from list on disconnect 2022-10-10 00:27:47 -04:00
ouwou
5588c46d14 Merge branch 'master' into voice 2022-10-09 23:01:09 -04:00
ouwou
1767575728 make CURLOPT_ACCEPT_ENCODING automatic 2022-10-09 17:31:15 -04:00
ouwou
c30d17ebb2 show avatar in voice window 2022-10-07 20:43:41 -04:00
ouwou
fd9d1ffb33 Merge branch 'master' into voice 2022-10-07 20:34:02 -04:00
ouwou
0a34c04b44 remove ability to join guilds
because 1. joining a guild seems to often require captchas now which are never going to be supported and 2. joining guilds is one of the things that upsets discords spam filter the most, so it kinda makes sense to remove anyways just like open dm
2022-10-06 03:08:54 -04:00
ouwou
7e85168576 remove a bunch of unnecessary fields from user settings 2022-10-06 02:34:45 -04:00
ouwou
dfcfe4353a center voice window 2022-10-05 22:01:23 -04:00
ouwou
9edac78380 put voice member list in a scrolled window 2022-10-05 21:54:07 -04:00
ouwou
d2c9985c57 one mutex is enough 2022-10-05 21:39:18 -04:00
ouwou
92c70bda08 add per user volume slider 2022-10-05 18:43:44 -04:00
ouwou
9dc2e863e8 temp mindeps build fix 2022-10-04 02:08:48 -04:00
ouwou
9394ac9b93 support voice text channels 2022-10-03 16:05:31 -04:00
ouwou
05acb8c857 try and handle voice socket closure properly maybe 2022-10-03 00:16:56 -04:00
ouwou
d8d9f1b857 close voice window on context menu disconnect 2022-10-02 02:50:48 -04:00
ouwou
e08e3106d6 rudimentary dm voice call support 2022-10-01 17:46:10 -04:00
ouwou
3e3afde223 try to fix mindeps build 2022-09-30 01:23:58 -04:00
ouwou
0438b11c91 dont dispatch udp to main 2022-09-30 01:09:51 -04:00
ouwou
f8ae99ee7b fix crash on disconnect 2022-09-29 22:47:10 -04:00
ouwou
b735feb901 add udp keepalive 2022-09-29 22:47:00 -04:00
ouwou
dc127d15fb display user list, client side mute 2022-09-29 21:46:15 -04:00
ouwou
a96d96b3aa basic mute/deafen 2022-09-28 22:10:36 -04:00
ouwou
d57d822aa9 manage decoders with ssrc updates 2022-09-28 20:44:52 -04:00
ouwou
a79b2d418e synchronize ws close/open to creating thread 2022-09-28 20:44:33 -04:00
ouwou
0571a05497 Merge branch 'master' into voice 2022-09-27 00:36:11 -04:00
ouwou
3027e00905 open browser on mouse release (fixes #108) 2022-09-25 01:44:09 -04:00
ouwou
2ecbacc924 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-09-09 02:52:17 -04:00
ouwou
84eb56d6b1 store user from interaction even if member is not present 2022-09-09 02:51:59 -04:00
ouwou
f3e5dcbe65 fix some potential crashes because of optionals 2022-09-09 02:40:33 -04:00
KnightMurloc
a78fdd386f add opt-in hide to system tray icon (#99) 2022-09-09 05:03:55 +00:00
ouwou
90437de2c0 make voice disconnect/reconnect work 2022-09-06 03:29:13 -04:00
ouwou
654e225093 try to fix shutdown with select 2022-09-06 03:25:24 -04:00
ouwou
e93b8715f9 basic voice capture + transmission 2022-09-05 02:21:37 -04:00
ouwou
b7fffb8691 fix min call 2022-09-02 01:47:13 -04:00
ouwou
0a04985678 make compile work if voice support is disabled 2022-09-02 01:25:33 -04:00
ouwou
9c8d9e54fe handle multiple speakers properly 2022-09-02 00:38:59 -04:00
ouwou
1f4070e52f basic buffering (i think) 2022-08-31 20:42:14 -04:00
ouwou
2e9beaaa30 dont send preferred region 2022-08-31 20:03:24 -04:00
ouwou
21d640cea3 try to fix mindeps run 2022-08-31 17:55:03 -04:00
ouwou
352c0fd1c1 fix opus include path (pt 2) 2022-08-31 17:07:43 -04:00
ouwou
12a5fcfcd3 fix opus include path 2022-08-31 16:58:17 -04:00
ouwou
f2f8afa368 fix compilation maybe 2022-08-31 16:44:30 -04:00
ouwou
c393cc9707 add deps to ci 2022-08-31 03:01:48 -04:00
ouwou
0fa33915da rudimentary voice implementation 2022-08-31 01:51:02 -04:00
ouwou
634f51fb41 add miniaudio submodule 2022-08-28 16:58:09 -04:00
ouwou
348c1cb965 remove curl error buffer
it was useless anyways
2022-08-21 14:10:28 -04:00
ouwou
32fc7def7c fetch cookies and build number on startup 2022-08-17 01:42:30 -04:00
ouwou
14602a7384 make cmake unity build work 2022-08-16 17:34:55 -04:00
ouwou
c683ef9ad9 update msys-deps again 2022-08-15 23:24:40 -04:00
ouwou
039cc7458d copy over new dlls for win build 2022-08-14 22:32:33 -04:00
ouwou
d99f16f82d fix ci run on mac 2022-08-14 00:02:19 -04:00
dragontamer8740
243e48e609 remove unnecessary cstdio include (#101) 2022-08-14 03:36:56 +00:00
dragontamer8740
fac9f1ba58 Big-endian 'emojis.bin' decoding fix (#100)
* Big-endian patch: fixes emojis.bin reading on big-endian systems (tested only on 32-bit PowerPC linux)
2022-08-14 02:59:08 +00:00
ouwou
6a5ecb4d95 Merge branch 'attachments' 2022-08-12 18:35:58 -04:00
ouwou
dc28eae95a spoof a bunch of headers like the web client 2022-08-11 23:37:39 -04:00
ouwou
baf96da80c add copy url menu item to attachments (closes #96)
also refactor menu popup to fit over entire message width
2022-08-11 20:53:36 -04:00
ouwou
31ca6d9fd2 update capabilities and client build number 2022-08-10 23:38:12 -04:00
ouwou
04befeb180 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-08-10 23:29:10 -04:00
ouwou
a4c8a2290d remove ability to create dms 2022-08-10 23:29:00 -04:00
ouwou
96ec5bb665 fix removing roles from members (maybe) 2022-08-10 23:20:27 -04:00
ouwou
f60cea2216 control icon pos in css 2022-08-10 00:03:04 -04:00
ouwou
02741f2c1b remove unnecessary verbosity 2022-08-08 02:41:54 -04:00
ouwou
955b9239b9 hide browse icon when not in channel with perms 2022-08-08 00:40:20 -04:00
ouwou
53ac853367 fix some permission checks 2022-08-08 00:29:18 -04:00
ouwou
1c38671356 add SEND_MESSAGES permission check finally 2022-08-07 19:56:01 -04:00
ouwou
91527fbd0d pull chat input permission check out of signal 2022-08-07 19:54:39 -04:00
ouwou
537d4163c2 add some Get()s that i forgot somehow 2022-08-07 19:19:52 -04:00
ouwou
c0e4a3a988 give focus to input after choosing file 2022-08-07 19:18:05 -04:00
ouwou
860049fad5 Update README.md 2022-08-07 20:15:47 +00:00
ouwou
344f269414 add file picker to chat input 2022-08-07 02:16:20 -04:00
ouwou
3487353fc7 css tweaks 2022-08-07 02:14:26 -04:00
ouwou
86fc8f4186 Merge branch 'master' into attachments 2022-08-06 02:27:37 -04:00
ouwou
acb80da387 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-08-06 02:23:15 -04:00
ouwou
d99d8443ee dont override expansion state because of active channel 2022-08-06 02:23:08 -04:00
dragontamer8740
319f9c392c fixed text input box to not resize when typing (#89) 2022-08-05 04:22:59 +00:00
ouwou
a61a630ee6 handle null from std::locale::locale (fixes #88) 2022-08-03 02:27:43 -04:00
ouwou
e8260c164f stop printing every event type 2022-07-31 17:53:55 -04:00
ouwou
4e4986f670 grey out leave button if user owns the guild 2022-07-31 17:23:00 -04:00
ouwou
3610a2508b limit how often progress bar can update 2022-07-25 01:10:31 -04:00
ouwou
8396d07d9e try to align stuff a little better 2022-07-25 00:26:04 -04:00
ouwou
59acd0f82f handle max message payload + show filename label 2022-07-23 18:34:33 -04:00
ouwou
544ae6f915 fix potential deadlock 2022-07-14 04:12:17 -04:00
ouwou
111399cf4a move progress bar so it doesnt conflict with other stuff 2022-07-14 03:13:27 -04:00
ouwou
fba5cee519 try to fix compile again 2022-07-10 02:38:19 +00:00
ouwou
f95d79129e handle premium server upload size limits 2022-07-09 03:05:48 -04:00
ouwou
02ce353c6d check nitro size restriction + fix replying border 2022-07-09 01:57:56 -04:00
ouwou
241d9a2140 fix compile 2022-07-08 02:27:21 -04:00
ouwou
849ebf17f1 try to properly clear chat list
im sure this will break something somehow
2022-07-08 02:27:09 -04:00
ouwou
41776fbd02 add upload progress bar 2022-07-07 03:09:54 -04:00
ouwou
5c7631e713 fix checks for is bot 2022-07-05 03:53:00 -04:00
ouwou
41e2478a6f grab focus when adding attachment 2022-07-04 03:29:30 -04:00
ouwou
a9d35dcccd fix compile 2022-07-03 18:58:48 -04:00
ouwou
e87766f106 dnd'd files show preview images if displayable 2022-06-30 04:20:42 -04:00
ouwou
a038f47a25 add icon to attachments without preview 2022-06-23 01:51:30 -04:00
ouwou
d841a2c862 add change filename 2022-06-23 00:48:00 -04:00
ouwou
4ee7025ab0 add file upload via dnd + rework http 2022-06-17 02:46:55 -04:00
ouwou
d0fa308f6e preserve attachment insertion order 2022-06-16 01:09:54 -04:00
ouwou
4456c8771d refactor send message params into one struct 2022-06-14 02:36:04 -04:00
ouwou
caa551a469 Merge branch 'master' into attachments 2022-06-14 02:23:22 -04:00
ouwou
ccf5afbba9 Merge branch 'master' of https://github.com/uowuo/abaddon 2022-06-14 02:16:03 -04:00
ouwou
2474ffc2ba hoisted_role can be missing from guild member list update 2022-06-14 02:15:08 -04:00
ouwou
5cf4d8e160 dont use tmpnam because its stupid 2022-06-10 01:28:27 -04:00
ouwou
49ff9a249e remove temp attachment files when theyre actually done being uploaded 2022-06-09 01:48:24 -04:00
betty "reenii" bessa
abc448eca0 readme: Simplify submodule install process. (#81)
* readme: Simplify submodule install process.

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

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

This was probably added by accident.

* gitignore: Ignore archive files

Archives don't belong in the repo, so make sure they don't get
accidentally added again.
2022-05-23 16:55:27 +00:00
ouwou
de3b53c676 add include 2022-05-22 00:03:51 -04:00
ouwou
3ac993bae4 always quietly try and include fontconfig if present 2022-05-21 22:59:53 -04:00
ouwou
756de57919 save state per-user 2022-05-20 02:29:32 -04:00
ouwou
d9cf989813 remove thing i shouldnt have committed 2022-05-19 03:15:27 -04:00
ouwou
ffc69576f2 fix role updates (fixes #69, fixes #70) 2022-05-19 03:13:02 -04:00
ouwou
2bed7f161b only save state when actually disconnecting (fixes #65) 2022-05-17 20:47:01 -04:00
ouwou
607607ef0a update css 2022-05-16 01:17:34 -04:00
ouwou
b2ba7709df make it look sort of better i guess 2022-05-15 23:38:10 -04:00
ouwou
8b488a5ca9 add icons to dms 2022-05-11 15:05:32 -04:00
ouwou
1d8ef79da6 add ctrl+number key shortcuts for tabs 2022-05-10 01:09:42 -04:00
ouwou
bbf32730cd add ctrl(+shift)+tab keybinds 2022-05-09 01:33:09 -04:00
ouwou
f58ca39e8c request channels again if accessibility was lost 2022-05-08 02:40:27 -04:00
ouwou
3b5f4ded31 stop sending messages to inaccessible channels 2022-05-08 00:27:48 -04:00
ouwou
f6fdfeb95f compile oopsy 2022-05-06 04:13:29 -04:00
ouwou
2c25319fb8 clear tabs when access lost, show blanks for missing channels 2022-05-06 01:14:15 -04:00
ouwou
7daa0a250c Merge branch 'master' into tabs 2022-05-03 18:26:17 -04:00
ouwou
121c2585c8 improve resizing behavior (fixes #67) 2022-05-03 18:19:26 -04:00
ouwou
b18b94818a respect muted state for tab indicator 2022-05-02 02:38:41 -04:00
ouwou
63db16a711 open channel if its the first opened tab 2022-05-02 02:31:15 -04:00
ouwou
c30ab91738 add menu+accelerator for go back/forward 2022-04-27 17:40:37 -04:00
ouwou
e8f16292d1 add back/forward history to tabs
also lots of reformatting in .cmake because clion is weird and did that for some reason
2022-04-27 16:24:11 -04:00
ouwou
db28abaa44 dont show attention indicator on checked tab 2022-04-23 15:41:31 -04:00
ouwou
bfb2490938 dont expand channel list when changing tabs 2022-04-23 15:33:54 -04:00
ouwou
b4ab88f708 add opened tabs to state 2022-04-21 14:41:45 -04:00
ouwou
2dab595476 add open dm in new tab 2022-04-21 14:19:21 -04:00
ouwou
a98967fccc add ci run for minimal dependencies 2022-04-20 21:50:57 -04:00
ouwou
32e4540464 install libhandy dep on msys 2022-04-17 02:26:21 -04:00
ouwou
02dc28e89c Merge branch 'master' into tabs 2022-04-17 02:20:34 -04:00
ouwou
47545d9d32 fix menu bar updates again (fixes #61) 2022-04-15 02:14:25 -04:00
ouwou
5670dfc1d5 update nightly.link download url 2022-04-15 03:26:27 +00:00
ouwou
34f8af599d fix tab titles 2022-04-14 00:11:39 -04:00
ouwou
d36fe4d0e9 add server icons to channels 2022-04-09 03:33:56 -04:00
ouwou
44317e2d34 more tab work
- only one tab for any channel can be open
- rudimentary unread indicators
- add some css
2022-04-09 02:45:09 -04:00
ouwou
5b806a2589 basic tabs system 2022-04-08 23:47:12 -04:00
ouwou
5a13c7fef7 pull in libhandy optionally 2022-04-08 14:50:11 -04:00
92 changed files with 6256 additions and 708 deletions

View File

@@ -1,6 +1,6 @@
name: Abaddon CI
on: [push, pull_request]
on: [ push, pull_request ]
jobs:
msys2:
@@ -8,7 +8,11 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
buildtype: [ Debug, RelWithDebInfo, MinSizeRel ]
mindeps: [ false ]
include:
- buildtype: RelWithDebInfo
mindeps: true
defaults:
run:
shell: msys2 {0}
@@ -17,12 +21,12 @@ jobs:
with:
submodules: true
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
- name: Setup MSYS2 (1)
uses: haya14busa/action-cond@v1
id: setupmsys
with:
msystem: mingw64
update: true
install: >-
cond: ${{ matrix.mindeps == true }}
if_true: >-
git
make
mingw-w64-x86_64-toolchain
@@ -33,11 +37,44 @@ jobs:
mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3
mingw-w64-x86_64-spdlog
if_false: >-
git
make
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-cmake
mingw-w64-x86_64-ninja
mingw-w64-x86_64-sqlite3
mingw-w64-x86_64-nlohmann-json
mingw-w64-x86_64-curl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-gtkmm3
mingw-w64-x86_64-libhandy
mingw-w64-x86_64-opus
mingw-w64-x86_64-libsodium
mingw-w64-x86_64-spdlog
- name: Build
run: |
cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }}
cmake --build build
- name: Setup MSYS2 (2)
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
update: true
install: ${{ steps.setupmsys.outputs.value }}
- name: Build (1)
uses: haya14busa/action-cond@v1
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 --build build
- name: Build (2)
run: ${{ steps.buildcmd.outputs.value }}
- name: Setup Artifact
run: |
@@ -49,12 +86,32 @@ jobs:
cp -r /mingw64/lib/gdk-pixbuf-2.0 build/artifactdir/lib
cp -r res/css res/res res/fonts build/artifactdir/bin
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
cp /usr/bin/msys-ffi-8.dll build/artifactdir/bin/libffi-8.dll
mkdir -p build/artifactdir/share/icons/Adwaita
cd build/artifactdir/share/icons/Adwaita
mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions
cd ../../../../../
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/16x16/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/24x24/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/32x32/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/48x48/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/64x64/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/96x96/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/actions/%.svg build/artifactdir/share/icons/Adwaita/scalable/actions || :
- name: Upload build
- name: Upload build (1)
uses: haya14busa/action-cond@v1
id: buildname
with:
cond: ${{ matrix.mindeps == true }}
if_true: "${{ matrix.buildtype }}-mindeps"
if_false: "${{ matrix.buildtype }}"
- name: Upload build (2)
uses: actions/upload-artifact@v2
with:
name: build-windows-msys2-${{ matrix.buildtype }}
name: build-windows-msys2-${{ steps.buildname.outputs.value }}
path: build/artifactdir
mac:
@@ -62,7 +119,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo]
buildtype: [ Debug, RelWithDebInfo ]
steps:
- uses: actions/checkout@v1
with:
@@ -75,6 +132,11 @@ jobs:
run: |
brew install gtkmm3
brew install nlohmann-json
brew install jpeg
brew install opus
brew install libsodium
brew install spdlog
brew install libhandy
- name: Build
uses: lukka/run-cmake@v3
@@ -97,10 +159,10 @@ jobs:
linux:
name: linux-${{ matrix.buildtype }}
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
buildtype: [Debug, RelWithDebInfo, MinSizeRel]
buildtype: [ Debug, RelWithDebInfo, MinSizeRel ]
steps:
- uses: actions/checkout@v1
with:
@@ -116,7 +178,7 @@ jobs:
cd deps
git clone https://github.com/nlohmann/json
cd json
git checkout db78ac1d7716f56fc9f1b030b715f872f93964e4
git checkout bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d
mkdir build
cd build
cmake ..
@@ -124,6 +186,10 @@ jobs:
sudo make install
sudo apt-get install libgtkmm-3.0-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
uses: lukka/run-cmake@v3

6
.gitignore vendored
View File

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

5
.gitmodules vendored
View File

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

View File

@@ -7,6 +7,9 @@ set(ABADDON_RESOURCE_DIR "/usr/share/abaddon" CACHE PATH "Fallback directory for
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
option(USE_LIBHANDY "Enable features that require libhandy (default)" ON)
option(ENABLE_VOICE "Enable voice suppport" ON)
find_package(nlohmann_json REQUIRED)
find_package(CURL)
find_package(ZLIB REQUIRED)
@@ -17,30 +20,33 @@ set(USE_TLS TRUE)
set(USE_OPEN_SSL TRUE)
find_package(IXWebSocket QUIET)
if (NOT IXWebSocket_FOUND)
message("ixwebsocket was not found and will be included as a submodule")
add_subdirectory(subprojects/ixwebsocket)
include_directories(IXWEBSOCKET_INCLUDE_DIRS)
endif()
message("ixwebsocket was not found and will be included as a submodule")
add_subdirectory(subprojects/ixwebsocket)
include_directories(IXWEBSOCKET_INCLUDE_DIRS)
endif ()
if(MINGW OR WIN32)
link_libraries(ws2_32)
endif()
if (MINGW OR WIN32)
link_libraries(ws2_32)
endif ()
if(WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
if (WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
endif ()
find_package(Fontconfig REQUIRED)
link_libraries(${Fontconfig_LIBRARIES})
endif()
include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if (IS_BIG_ENDIAN)
add_compile_definitions(ABADDON_IS_BIG_ENDIAN)
endif ()
configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/config.h)
file(GLOB_RECURSE ABADDON_SOURCES
"src/*.h"
"src/*.hpp"
"src/*.cpp"
)
"src/*.h"
"src/*.hpp"
"src/*.cpp"
)
add_executable(abaddon ${ABADDON_SOURCES})
target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src)
@@ -51,36 +57,71 @@ target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
target_link_libraries(abaddon stdc++fs)
endif()
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
((CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_CXX_COMPILER_VERSION LESS 9))))
target_link_libraries(abaddon stdc++fs)
endif ()
if (IXWebSocket_LIBRARIES)
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
find_library(MBEDTLS_X509_LIBRARY mbedx509)
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
if (MBEDTLS_TLS_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
endif()
if (MBEDTLS_X509_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
endif()
if (MBEDTLS_CRYPTO_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
endif()
else()
target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
endif()
target_link_libraries(abaddon ${IXWebSocket_LIBRARIES})
find_library(MBEDTLS_X509_LIBRARY mbedx509)
find_library(MBEDTLS_TLS_LIBRARY mbedtls)
find_library(MBEDTLS_CRYPTO_LIBRARY mbedcrypto)
if (MBEDTLS_TLS_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_TLS_LIBRARY})
endif ()
if (MBEDTLS_X509_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_X509_LIBRARY})
endif ()
if (MBEDTLS_CRYPTO_LIBRARY)
target_link_libraries(abaddon ${MBEDTLS_CRYPTO_LIBRARY})
endif ()
else ()
target_link_libraries(abaddon $<BUILD_INTERFACE:ixwebsocket>)
endif ()
find_package(Threads)
if (Threads_FOUND)
target_link_libraries(abaddon Threads::Threads)
endif()
target_link_libraries(abaddon Threads::Threads)
endif ()
find_package(Fontconfig QUIET)
if (Fontconfig_FOUND)
target_link_libraries(abaddon Fontconfig::Fontconfig)
endif ()
find_package(spdlog REQUIRED)
target_link_libraries(abaddon spdlog::spdlog)
target_link_libraries(abaddon ${SQLite3_LIBRARIES})
target_link_libraries(abaddon ${GTKMM_LIBRARIES})
target_link_libraries(abaddon ${CURL_LIBRARIES})
target_link_libraries(abaddon ${ZLIB_LIBRARY})
target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES})
if (USE_LIBHANDY)
find_package(libhandy)
if (NOT libhandy_FOUND)
message("libhandy could not be found. features requiring it have been disabled")
set(USE_LIBHANDY OFF)
else ()
target_include_directories(abaddon PUBLIC ${libhandy_INCLUDE_DIRS})
target_link_libraries(abaddon ${libhandy_LIBRARIES})
target_compile_definitions(abaddon PRIVATE WITH_LIBHANDY)
endif ()
endif ()
if (ENABLE_VOICE)
target_compile_definitions(abaddon PRIVATE WITH_VOICE)
find_package(PkgConfig)
target_include_directories(abaddon PUBLIC subprojects/miniaudio)
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus)
target_link_libraries(abaddon PkgConfig::Opus)
pkg_check_modules(libsodium REQUIRED IMPORTED_TARGET libsodium)
target_link_libraries(abaddon PkgConfig::libsodium)
target_link_libraries(abaddon ${CMAKE_DL_LIBS})
endif ()

View File

@@ -25,13 +25,15 @@ Current features:
* Thread support<sup>3</sup>
* Animated avatars, server icons, emojis (can be turned off)
1 - Abaddon tries its best to make Discord think it's a legitimate web client. Some of the things done to do this
1 - Abaddon tries its best (though is not perfect) to make Discord think it's a legitimate web client. Some of the
things done to do this
include: using a browser user agent, sending the same IDENTIFY message that the official web client does, using API v9
endpoints in all cases, and not using endpoints the web client does not normally use. There are still a few smaller
inconsistencies, however. For example the web client sends lots of telemetry via the `/science` endpoint (uBlock origin
stops this) as well as in the headers of all requests. **In any case,** you should use an official client for joining
servers, sending new DMs, or managing your friends list if you are afraid of being caught in Discord's spam filters
(unlikely).
stops this) as well as in the headers of all requests.<br>
**See [here](#the-spam-filter)** for things you might want to avoid if you are worried about being caught in the spam
filter.
2 - Unicode emojis are substituted manually as opposed to rendered by GTK on non-Windows platforms. This can be changed
with the `stock_emojis` setting as shown at the bottom of this README. A CBDT-based font using Twemoji is provided to
@@ -52,6 +54,7 @@ the result of fundamental issues with Discord's thread implementation.
* mingw-w64-x86_64-curl
* mingw-w64-x86_64-zlib
* mingw-w64-x86_64-gtkmm3
* mingw-w64-x86_64-libhandy
2. `git clone --recurse-submodules="subprojects" https://github.com/uowuo/abaddon && cd abaddon`
3. `mkdir build && cd build`
4. `cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..`
@@ -59,18 +62,20 @@ the result of fundamental issues with Discord's thread implementation.
#### Mac:
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `brew install gtkmm3 nlohmann-json`
3. `git submodule update --init subprojects`
4. `mkdir build && cd build`
5. `cmake ..`
6. `make`
1. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
2. `brew install gtkmm3 nlohmann-json libhandy`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
#### Linux:
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`,
and [nlohmann-json](https://github.com/nlohmann/json)
2. `git clone https://github.com/uowuo/abaddon && cd abaddon`
1. Install dependencies
* On Ubuntu 20.04 (Focal) and newer:
```Shell
$ sudo apt install g++ cmake libgtkmm-3.0-dev libcurl4-gnutls-dev libsqlite3-dev libssl-dev nlohmann-json3-dev
```
2. `git clone https://github.com/uowuo/abaddon --recurse-submodules="subprojects" && cd abaddon`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
@@ -81,7 +86,7 @@ Latest release version: https://github.com/uowuo/abaddon/releases/latest
**CI:**
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-RelWithDebInfo.zip)
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-msys2-MinSizeRel.zip)
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned,
unpackaged, requires gtkmm3 (e.g. from homebrew)
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now),
@@ -94,6 +99,21 @@ On Linux, `css` and `res` can also be loaded from `~/.local/share/abaddon` or `/
`abaddon.ini` will also be automatically used if located at `~/.config/abaddon/abaddon.ini` and there is
no `abaddon.ini` in the working directory
#### The Spam Filter
Discord likes disabling accounts/forcing them to reset their passwords if they think the user is a spam bot or
potentially had their account compromised. While the official client still often gets users caught in the spam filter,
third party clients tend to upset the spam filter more often. If you get caught by it, you can
usually [appeal](https://support.discord.com/hc/en-us/requests/new?ticket_form_id=360000029731) it and get it restored.
Here are some things you might want to do with the official client instead if you are particularly afraid of evoking the
spam filter's wrath:
* Joining or leaving servers (usually main cause of getting caught)
* Frequently disconnecting and reconnecting
* Starting new DMs with people
* Managing your friends list
* Managing your user profile while connected to a third party client
#### Dependencies:
* [gtkmm](https://www.gtkmm.org/en/)
@@ -102,6 +122,7 @@ no `abaddon.ini` in the working directory
* [libcurl](https://curl.se/)
* [zlib](https://zlib.net/)
* [SQLite3](https://www.sqlite.org/index.html)
* [libhandy](https://gnome.pages.gitlab.gnome.org/libhandy/) (optional)
### TODO:
@@ -238,6 +259,9 @@ For example, memory_db would be set by adding `memory_db = true` under the line
over
* owner_crown (true or false, default true) - show a crown next to the owner
* unreads (true or false, default true) - show unread indicators and mention badges
* save_state (true or false, default true) - save the state of the gui (active channels, tabs, expanded channels)
* alt_menu (true or false, default false) - keep the menu hidden unless revealed with alt key
* hide_to_tray (true or false, default false) - hide abaddon to the system tray on window close
#### style

View File

@@ -14,7 +14,7 @@
/bin/libdeflate.dll
/bin/libepoxy-0.dll
/bin/libexpat-1.dll
/bin/libffi-7.dll
/bin/libffi-8.dll
/bin/libfontconfig-1.dll
/bin/libfreetype-6.dll
/bin/libfribidi-0.dll
@@ -31,6 +31,7 @@
/bin/libgraphite2.dll
/bin/libgtk-3-0.dll
/bin/libgtkmm-3.0-1.dll
/bin/libhandy-1-0.dll
/bin/libharfbuzz-0.dll
/bin/libiconv-2.dll
/bin/libidn2-0.dll
@@ -41,11 +42,12 @@
/bin/libpangoft2-1.0-0.dll
/bin/libpangomm-1.4-1.dll
/bin/libpangowin32-1.0-0.dll
/bin/libpcre-1.dll
/bin/libpcre2-8-0.dll
/bin/libpixman-1-0.dll
/bin/libpng16-16.dll
/bin/libpsl-5.dll
/bin/libsigc-2.0-0.dll
/bin/libspdlog.dll
/bin/libsqlite3-0.dll
/bin/libssh2-1.dll
/bin/libssl-1_1-x64.dll
@@ -55,3 +57,4 @@
/bin/libwinpthread-1.dll
/bin/libzstd.dll
/bin/zlib1.dll
/../usr/bin/msys-2.0.dll

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

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

37
cmake/Findgdk.cmake Normal file
View File

@@ -0,0 +1,37 @@
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
pkg_check_modules(PC_gdk QUIET gdk-3.0)
set(gdk_DEFINITIONS ${PC_gdk_CFLAGS_OTHER})
endif ()
set(gdk_INCLUDE_HINTS ${PC_gdk_INCLUDEDIR} ${PC_gdk_INCLUDE_DIRS})
set(gdk_LIBRARY_HINTS ${PC_gdk_LIBDIR} ${PC_gdk_LIBRARY_DIRS})
find_path(gdk_INCLUDE_DIR
NAMES gdk/gdk.h
HINTS ${gdk_INCLUDE_HINTS}
/usr/include
/usr/local/include
/opt/local/include
PATH_SUFFIXES gdk-3.0)
find_library(gdk_LIBRARY
NAMES gdk-3.0
gdk-3
gdk
HINTS ${gdk_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib)
set(gdk_LIBRARIES ${gdk_LIBRARY})
set(gdk_INCLUDE_DIRS ${gdk_INCLUDE_DIR})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gdk
REQUIRED_VARS
gdk_LIBRARY
gdk_INCLUDE_DIR
VERSION_VAR gdk_VERSION)
mark_as_advanced(gdk_INCLUDE_DIR gdk_LIBRARY)

View File

@@ -1,48 +1,50 @@
set(GDKMM_LIBRARY_NAME gdkmm-3.0)
set(gdkmm_LIBRARY_NAME gdkmm-3.0)
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
pkg_check_modules(PKGCONFIG_GDKMM QUIET ${GDKMM_LIBRARY_NAME})
set(GDKMM_DEFINITIONS ${PKGCONFIG_GDKMM_CFLAGS_OTHER})
pkg_check_modules(PKGCONFIG_gdkmm QUIET ${gdkmm_LIBRARY_NAME})
set(gdkmm_DEFINITIONS ${PKGCONFIG_gdkmm_CFLAGS_OTHER})
endif (PKG_CONFIG_FOUND)
set(GDKMM_INCLUDE_HINTS ${PKGCONFIG_GDKMM_INCLUDEDIR} ${PKGCONFIG_GDKMM_INCLUDE_DIRS})
set(GDKMM_LIBRARY_HINTS ${PKGCONFIG_GDKMM_LIBDIR} ${PKGCONFIG_GDKMM_LIBRARY_DIRS})
set(gdkmm_INCLUDE_HINTS ${PKGCONFIG_gdkmm_INCLUDEDIR} ${PKGCONFIG_gdkmm_INCLUDE_DIRS})
set(gdkmm_LIBRARY_HINTS ${PKGCONFIG_gdkmm_LIBDIR} ${PKGCONFIG_gdkmm_LIBRARY_DIRS})
find_path(GDKMM_INCLUDE_DIR
find_path(gdkmm_INCLUDE_DIR
NAMES gdkmm.h
HINTS ${GDKMM_INCLUDE_HINTS}
HINTS ${gdkmm_INCLUDE_HINTS}
/usr/include
/usr/local/include
/opt/local/include
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME})
PATH_SUFFIXES ${gdkmm_LIBRARY_NAME})
find_path(GDKMM_CONFIG_INCLUDE_DIR
find_path(gdkmm_CONFIG_INCLUDE_DIR
NAMES gdkmmconfig.h
HINTS ${GDKMM_LIBRARY_HINTS}
HINTS ${gdkmm_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}/include)
PATH_SUFFIXES ${gdkmm_LIBRARY_NAME}/include)
find_library(GDKMM_LIBRARY
NAMES ${GDKMM_LIBRARY_NAME}
gdkmm
HINTS ${GDKMM_LIBRARY_HINTS}
find_library(gdkmm_LIBRARY
NAMES ${gdkmm_LIBRARY_NAME}
gdkmm
HINTS ${gdkmm_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}
${GDKMM_LIBRARY_NAME}/include)
PATH_SUFFIXES ${gdkmm_LIBRARY_NAME}
${gdkmm_LIBRARY_NAME}/include)
set(GDKMM_LIBRARIES ${GDKMM_LIBRARY})
set(GDKMM_INCLUDE_DIRS ${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIRS};${GDKMM_CONFIG_INCLUDE_DIR})
find_package(gdk)
set(gdkmm_LIBRARIES ${gdkmm_LIBRARY};${gdk_LIBRARIES})
set(gdkmm_INCLUDE_DIRS ${gdkmm_INCLUDE_DIR};${gdkmm_CONFIG_INCLUDE_DIRS};${gdkmm_CONFIG_INCLUDE_DIR};${gdk_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gdkmm
REQUIRED_VARS
GDKMM_LIBRARY
GDKMM_INCLUDE_DIRS
VERSION_VAR GDKMM_VERSION)
gdkmm_LIBRARY
gdkmm_INCLUDE_DIRS
VERSION_VAR gdkmm_VERSION)
mark_as_advanced(GDKMM_INCLUDE_DIR GDKMM_LIBRARY)
mark_as_advanced(gdkmm_INCLUDE_DIR gdkmm_LIBRARY)

View File

@@ -2,56 +2,70 @@ find_package(PkgConfig)
pkg_check_modules(PC_GLIB2 QUIET glib-2.0)
find_path(GLIB_INCLUDE_DIR
NAMES glib.h
HINTS ${PC_GLIB2_INCLUDEDIR}
${PC_GLIB2_INCLUDE_DIRS}
$ENV{GLIB2_HOME}/include
$ENV{GLIB2_ROOT}/include
/usr/local/include
/usr/include
/glib2/include
/glib-2.0/include
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
)
NAMES glib.h
HINTS ${PC_GLIB2_INCLUDEDIR}
${PC_GLIB2_INCLUDE_DIRS}
$ENV{GLIB2_HOME}/include
$ENV{GLIB2_ROOT}/include
/usr/local/include
/usr/include
/glib2/include
/glib-2.0/include
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
)
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIR})
find_library(GLIB_LIBRARIES
NAMES glib2
glib-2.0
HINTS ${PC_GLIB2_LIBDIR}
${PC_GLIB2_LIBRARY_DIRS}
$ENV{GLIB2_HOME}/lib
$ENV{GLIB2_ROOT}/lib
/usr/local/lib
/usr/lib
/lib
/glib-2.0/lib
PATH_SUFFIXES glib2 glib-2.0
)
NAMES glib2
glib-2.0
HINTS ${PC_GLIB2_LIBDIR}
${PC_GLIB2_LIBRARY_DIRS}
$ENV{GLIB2_HOME}/lib
$ENV{GLIB2_ROOT}/lib
/usr/local/lib
/usr/lib
/lib
/glib-2.0/lib
PATH_SUFFIXES glib2 glib-2.0
)
find_library(glib_GOBJECT_LIBRARIES
NAMES gobject-2.0
HINTS ${PC_GLIB2_LIBDIR}
${PC_GLIB2_LIBRARY_DIRS}
)
find_library(glib_GIO_LIBRARIES
NAMES gio-2.0
HINTS ${PC_GLIB2_LIBDIR}
${PC_GLIB2_LIBRARY_DIRS}
)
get_filename_component(_GLIB2_LIB_DIR "${GLIB_LIBRARIES}" PATH)
find_path(GLIB_CONFIG_INCLUDE_DIR
NAMES glibconfig.h
HINTS ${PC_GLIB2_INCLUDEDIR}
${PC_GLIB2_INCLUDE_DIRS}
$ENV{GLIB2_HOME}/include
$ENV{GLIB2_ROOT}/include
/usr/local/include
/usr/include
/glib2/include
/glib-2.0/include
${_GLIB2_LIB_DIR}
${CMAKE_SYSTEM_LIBRARY_PATH}
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
)
NAMES glibconfig.h
HINTS ${PC_GLIB2_INCLUDEDIR}
${PC_GLIB2_INCLUDE_DIRS}
$ENV{GLIB2_HOME}/include
$ENV{GLIB2_ROOT}/include
/usr/local/include
/usr/include
/glib2/include
/glib-2.0/include
${_GLIB2_LIB_DIR}
${CMAKE_SYSTEM_LIBRARY_PATH}
PATH_SUFFIXES glib2 glib-2.0 glib-2.0/include
)
if (GLIB_CONFIG_INCLUDE_DIR)
set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIRS} ${GLIB_CONFIG_INCLUDE_DIR})
endif()
endif ()
set(GLIB_LIBRARIES ${GLIB_LIBRARIES} ${glib_GOBJECT_LIBRARIES} ${glib_GIO_LIBRARIES})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(glib
REQUIRED_VARS
GLIB_LIBRARIES
GLIB_INCLUDE_DIRS
VERSION_VAR GLIB_VERSION)
mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR)
REQUIRED_VARS
GLIB_LIBRARIES
GLIB_INCLUDE_DIRS
VERSION_VAR GLIB_VERSION)
mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR glib_GOBJECT_LIBRARIES)

View File

@@ -1,13 +1,13 @@
set(GTKMM_LIBRARY_NAME gtkmm-3.0)
set(GDKMM_LIBRARY_NAME gdkmm-3.0)
set(GTKMM_LIBRARY_NAME gtkmm-3.0)
set(GDKMM_LIBRARY_NAME gdkmm-3.0)
find_package(PkgConfig)
if(PKG_CONFIG_FOUND)
if (PKG_CONFIG_FOUND)
pkg_check_modules(PC_GTKMM QUIET ${GTKMM_LIBRARY_NAME})
pkg_check_modules(PC_GDKMM QUIET ${GDKMM_LIBRARY_NAME})
pkg_check_modules(PC_PANGOMM QUIET ${PANGOMM_LIBRARY_NAME})
set(GTKMM_DEFINITIONS ${PC_GTKMM_CFLAGS_OTHER})
endif()
set(GTKMM_DEFINITIONS ${PC_GTKMM_CFLAGS_OTHER})
endif ()
find_package(gtk)
find_package(glibmm)
@@ -46,14 +46,14 @@ find_path(GDKMM_CONFIG_INCLUDE_DIR
HINTS ${GDKMM_INCLUDE_HINTS}
PATH_SUFFIXES ${GDKMM_LIBRARY_NAME}/include)
set(GTKMM_LIBRARIES ${GTKMM_LIB};${GDKMM_LIBRARY};${GTK_LIBRARIES};${GLIBMM_LIBRARIES};${PANGOMM_LIBRARIES};${CAIROMM_LIBRARIES};${ATKMM_LIBRARIES};${SIGC++_LIBRARIES})
set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${GDKMM_INCLUDE_DIR};${GDKMM_CONFIG_INCLUDE_DIR};${GTK_INCLUDE_DIRS};${GLIBMM_INCLUDE_DIRS};${PANGOMM_INCLUDE_DIRS};${CAIROMM_INCLUDE_DIRS};${ATKMM_INCLUDE_DIRS};${SIGC++_INCLUDE_DIRS})
set(GTKMM_LIBRARIES ${GTKMM_LIB};${gdkmm_LIBRARIES};${GTK_LIBRARIES};${GLIBMM_LIBRARIES};${PANGOMM_LIBRARIES};${CAIROMM_LIBRARIES};${ATKMM_LIBRARIES};${SIGC++_LIBRARIES})
set(GTKMM_INCLUDE_DIRS ${GTKMM_INCLUDE_DIR};${GTKMM_CONFIG_INCLUDE_DIR};${gdkmm_INCLUDE_DIRS};${gdkmm_CONFIG_INCLUDE_DIR};${GTK_INCLUDE_DIRS};${GLIBMM_INCLUDE_DIRS};${PANGOMM_INCLUDE_DIRS};${CAIROMM_INCLUDE_DIRS};${ATKMM_INCLUDE_DIRS};${SIGC++_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(gtkmm
REQUIRED_VARS
GTKMM_LIB
GTKMM_INCLUDE_DIRS
GTKMM_LIB
GTKMM_INCLUDE_DIRS
VERSION_VAR GTKMM_VERSION)
mark_as_advanced(GTKMM_INCLUDE_DIR GTKMM_LIBRARY)

39
cmake/Findlibhandy.cmake Normal file
View File

@@ -0,0 +1,39 @@
set(libhandy_LIBRARY_NAME libhandy-1)
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
pkg_check_modules(PC_libhandy QUIET ${libhandy_LIBRARY_NAME})
set(libhandy_DEfINITIONS ${PC_libhandy_CFLAGS_OTHER})
endif (PKG_CONFIG_FOUND)
set(libhandy_INCLUDE_HINTS ${PC_libhandy_INCLUDEDIR} ${PC_libhandy_INCLUDE_DIRS})
set(libhandy_LIBRARY_HINTS ${PC_libhandy_LIBDIR} ${PC_libhandy_LIBRARY_DIRS})
find_path(libhandy_INCLUDE_DIR
NAMES handy.h
HINTS ${libhandy_INCLUDE_HINTS}
/usr/include
/usr/local/include
/opt/local/include
PATH_SUFFIXES ${libhandy_LIBRARY_NAME})
find_library(libhandy_LIBRARY
NAMES ${libhandy_LIBRARY_NAME} handy-1
HINTS ${libhandy_LIBRARY_HINTS}
/usr/lib
/usr/local/lib
/opt/local/lib
PATH_SUFFIXES ${libhandy_LIBRARY_NAME}
${libhandy_LIBRARY_NAME}/include)
set(libhandy_LIBRARIES ${libhandy_LIBRARY})
set(libhandy_INCLUDE_DIRS ${libhandy_INCLUDE_DIR};${libhandy_CONFIG_INCLUDE_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(libhandy
REQUIRED_VARS
libhandy_LIBRARY
libhandy_INCLUDE_DIR
VERSION_VAR libhandy_VERSION)
mark_as_advanced(libhandy_INCLUDE_DIR libhandy_LIBRARY)

View File

@@ -44,7 +44,7 @@ has to be separate to allow main.css to override certain things
background: @secondary_color;
}
.app-popup list {
.app-window list, .app-popup list {
background: @secondary_color;
}
@@ -87,3 +87,11 @@ has to be separate to allow main.css to override certain things
.app-window colorswatch {
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

@@ -101,17 +101,38 @@
.message-input, .message-input textview, .message-input textview text {
background-color: #242424;
color: #adadad;
border-radius: 15px;
border-radius: 3px;
border: 1px solid transparent;
}
.message-input {
border: 1px solid #444444;
margin-right: 15px;
}
.message-input.replying {
border: 1px solid #026FB9;
}
.message-input {
.message-input.bad-input {
border: 1px solid #dd3300;
}
.message-input-browse-icon {
color: #b9bbbe;
margin-left: 5px;
margin-top: 11px;
}
/* i dont think theres a way to circumvent having to do this to adjust around the browse icon */
.message-input:not(.with-browser-icon) {
padding: 0px 0px 0px 5px;
}
.message-input.with-browse-icon {
padding: 0px 0px 0px 30px;
}
.members {
background-color: @background_color;
}
@@ -282,3 +303,78 @@
.friends-list-row-bot {
color: #ff0000;
}
.channel-tab-switcher .box {
margin: -7px -1px -7px -1px;
background: #2a2a2a;
border: 1px solid black;
}
.channel-tab-switcher tab:hover {
box-shadow: inset 0 -6px #17633e;
}
.channel-tab-switcher tab:checked {
box-shadow: inset 0 -6px #2feb90;
}
.channel-tab-switcher tab {
background: #1A1A1A;
border: 1px solid #808080;
min-height: 35px;
}
.channel-tab-switcher tab.needs-attention:not(:checked) {
font-weight: bold;
animation: 150ms ease-in;
/* background-image: radial-gradient(ellipse at bottom, #FF5370, #1A1A1A 30%); */
box-shadow: inset 0 -6px red;
}
.channel-tab-switcher tab > button {
border: none;
padding: 0;
margin: 5px;
min-width: 16px;
min-height: 16px;
color: #FF5370;
background-color: rgba(0.21, 0.21, 0.21, 0.5);
}
.channel-tab-switcher tab > button:hover {
background-color: alpha(#ff0000, 0.5);
}
.message-progress {
border: none;
margin-bottom: -8px;
}
.message-progress trough {
border: none;
background-color: transparent;
}
.message-progress progress {
border: none;
background-color: #dd3300;
margin-left: 1px;
}
.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,21 +1,31 @@
#include <gtkmm.h>
#include <memory>
#include <spdlog/spdlog.h>
#include <spdlog/cfg/env.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <string>
#include <algorithm>
#include "platform.hpp"
#include "audio/manager.hpp"
#include "discord/discord.hpp"
#include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp"
#include "dialogs/joinguild.hpp"
#include "dialogs/confirm.hpp"
#include "dialogs/setstatus.hpp"
#include "dialogs/friendpicker.hpp"
#include "dialogs/verificationgate.hpp"
#include "dialogs/textinput.hpp"
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
#include "windows/voicewindow.hpp"
#include "startup.hpp"
#ifdef WITH_LIBHANDY
#include <handy.h>
#endif
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
@@ -31,7 +41,8 @@ Abaddon::Abaddon()
std::string ua = GetSettings().UserAgent;
m_discord.SetUserAgent(ua);
m_discord.signal_gateway_ready().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady));
// todo rename funcs
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_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete));
m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate));
@@ -43,6 +54,20 @@ Abaddon::Abaddon()
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_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) {
if (!accessible)
m_channels_requested.erase(id);
});
if (GetSettings().Prefetch)
m_discord.signal_message_create().connect([this](const Message &message) {
if (message.Author.HasAvatar())
@@ -59,9 +84,102 @@ Abaddon &Abaddon::Get() {
return instance;
}
#ifdef _WIN32
constexpr static guint BUTTON_BACK = 4;
constexpr static guint BUTTON_FORWARD = 5;
#else
constexpr static guint BUTTON_BACK = 8;
constexpr static guint BUTTON_FORWARD = 9;
#endif
static bool HandleButtonEvents(GdkEvent *event, MainWindow *main_window) {
if (event->type != GDK_BUTTON_PRESS) return false;
auto *widget = gtk_get_event_widget(event);
if (widget == nullptr) return false;
auto *window = gtk_widget_get_toplevel(widget);
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false; // is this the right way???
#ifdef WITH_LIBHANDY
switch (event->button.button) {
case BUTTON_BACK:
main_window->GoBack();
break;
case BUTTON_FORWARD:
main_window->GoForward();
break;
}
#endif
return false;
}
static bool HandleKeyEvents(GdkEvent *event, MainWindow *main_window) {
if (event->type != GDK_KEY_PRESS) return false;
auto *widget = gtk_get_event_widget(event);
if (widget == nullptr) return false;
auto *window = gtk_widget_get_toplevel(widget);
if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return false;
const bool ctrl = (event->key.state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK;
const bool shft = (event->key.state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK;
constexpr static guint EXCLUDE_STATES = GDK_CONTROL_MASK | GDK_SHIFT_MASK;
if (!(event->key.state & EXCLUDE_STATES) && event->key.keyval == GDK_KEY_Alt_L) {
if (Abaddon::Get().GetSettings().AltMenu) {
main_window->ToggleMenuVisibility();
}
}
#ifdef WITH_LIBHANDY
if (ctrl) {
switch (event->key.keyval) {
case GDK_KEY_Tab:
case GDK_KEY_KP_Tab:
case GDK_KEY_ISO_Left_Tab:
if (shft)
main_window->GoToPreviousTab();
else
main_window->GoToNextTab();
return true;
case GDK_KEY_1:
case GDK_KEY_2:
case GDK_KEY_3:
case GDK_KEY_4:
case GDK_KEY_5:
case GDK_KEY_6:
case GDK_KEY_7:
case GDK_KEY_8:
case GDK_KEY_9:
main_window->GoToTab(event->key.keyval - GDK_KEY_1);
return true;
case GDK_KEY_0:
main_window->GoToTab(9);
return true;
}
}
#endif
return false;
}
static void MainEventHandler(GdkEvent *event, void *main_window) {
if (HandleButtonEvents(event, static_cast<MainWindow *>(main_window))) return;
if (HandleKeyEvents(event, static_cast<MainWindow *>(main_window))) return;
gtk_main_do_event(event);
}
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
#ifdef WITH_LIBHANDY
m_gtk_app->signal_activate().connect([] {
hdy_init();
});
#endif
m_css_provider = Gtk::CssProvider::create();
m_css_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
@@ -103,6 +221,10 @@ int Abaddon::StartGTK() {
m_main_window->set_title(APP_TITLE);
m_main_window->set_position(Gtk::WIN_POS_CENTER);
#ifdef WITH_LIBHANDY
gdk_event_handler_set(&MainEventHandler, m_main_window.get(), nullptr);
#endif
if (!m_settings.IsValid()) {
Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be opened!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
@@ -122,6 +244,16 @@ int Abaddon::StartGTK() {
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
m_main_window->UpdateComponents();
@@ -132,16 +264,20 @@ int Abaddon::StartGTK() {
m_main_window->signal_action_disconnect().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnect));
m_main_window->signal_action_set_token().connect(sigc::mem_fun(*this, &Abaddon::ActionSetToken));
m_main_window->signal_action_reload_css().connect(sigc::mem_fun(*this, &Abaddon::ActionReloadCSS));
m_main_window->signal_action_join_guild().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinGuildDialog));
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads));
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true));
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));
#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_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));
@@ -151,10 +287,29 @@ int Abaddon::StartGTK() {
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));
ActionReloadCSS();
if (m_settings.GetSettings().HideToTray) {
m_tray = Gtk::StatusIcon::create("discord");
m_tray->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_tray_click));
m_tray->signal_popup_menu().connect(sigc::mem_fun(*this, &Abaddon::on_tray_popup_menu));
}
m_tray_menu = Gtk::make_managed<Gtk::Menu>();
m_tray_exit = Gtk::make_managed<Gtk::MenuItem>("Quit", false);
m_tray_exit->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_tray_menu_click));
m_tray_menu->append(*m_tray_exit);
m_tray_menu->show_all();
m_main_window->signal_hide().connect(sigc::mem_fun(*this, &Abaddon::on_window_hide));
m_gtk_app->signal_shutdown().connect(sigc::mem_fun(*this, &Abaddon::OnShutdown), false);
m_main_window->UpdateMenus();
m_gtk_app->hold();
m_main_window->show();
RunFirstTimeDiscordStartup();
return m_gtk_app->run(*m_main_window);
}
@@ -173,11 +328,13 @@ void Abaddon::LoadFromSettings() {
void Abaddon::StartDiscord() {
m_discord.Start();
m_main_window->UpdateMenus();
}
void Abaddon::StopDiscord() {
m_discord.Stop();
SaveState();
if (m_discord.Stop())
SaveState();
m_main_window->UpdateMenus();
}
bool Abaddon::IsDiscordActive() const {
@@ -272,6 +429,64 @@ 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() {
return m_settings.GetSettings();
}
@@ -290,6 +505,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
for (const auto child : m_user_menu_roles_submenu->get_children())
delete child;
if (guild.has_value() && user.has_value()) {
const auto roles = user->GetSortedRoles();
m_user_menu_roles->set_visible(!roles.empty());
@@ -312,7 +528,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
if (me == id) {
m_user_menu_ban->set_visible(false);
m_user_menu_kick->set_visible(false);
m_user_menu_open_dm->set_visible(false);
m_user_menu_open_dm->set_sensitive(false);
} else {
const bool has_kick = m_discord.HasGuildPermission(me, guild_id, Permission::KICK_MEMBERS);
const bool has_ban = m_discord.HasGuildPermission(me, guild_id, Permission::BAN_MEMBERS);
@@ -320,7 +536,7 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
m_user_menu_kick->set_visible(has_kick && can_manage);
m_user_menu_ban->set_visible(has_ban && can_manage);
m_user_menu_open_dm->set_visible(true);
m_user_menu_open_dm->set_sensitive(m_discord.FindDM(id).has_value());
}
m_user_menu_remove_recipient->hide();
@@ -334,6 +550,48 @@ void Abaddon::ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_
m_user_menu->popup_at_pointer(event);
}
void Abaddon::RunFirstTimeDiscordStartup() {
DiscordStartupDialog dlg(*m_main_window);
dlg.set_position(Gtk::WIN_POS_CENTER);
std::optional<std::string> cookie;
std::optional<uint32_t> build_number;
dlg.signal_response().connect([&](int response) {
if (response == Gtk::RESPONSE_OK) {
cookie = dlg.GetCookie();
build_number = dlg.GetBuildNumber();
}
});
dlg.run();
Glib::signal_idle().connect_once([this, cookie, build_number]() {
if (cookie.has_value()) {
m_discord.SetCookie(*cookie);
} else {
ConfirmDialog confirm(*m_main_window);
confirm.SetConfirmText("Cookies could not be fetched. This may increase your chances of being flagged by Discord's anti-spam");
confirm.SetAcceptOnly(true);
confirm.run();
}
if (build_number.has_value()) {
m_discord.SetBuildNumber(*build_number);
} else {
ConfirmDialog confirm(*m_main_window);
confirm.SetConfirmText("Build number could not be fetched. This may increase your chances of being flagged by Discord's anti-spam");
confirm.SetAcceptOnly(true);
confirm.run();
}
// autoconnect
if (cookie.has_value() && build_number.has_value() && GetSettings().Autoconnect && !GetDiscordToken().empty()) {
ActionConnect();
}
});
}
void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) {
VerificationGateDialog dlg(*m_main_window, guild_id);
if (dlg.run() == Gtk::RESPONSE_OK) {
@@ -368,7 +626,7 @@ void Abaddon::SetupUserMenu() {
m_user_menu_ban = Gtk::manage(new Gtk::MenuItem("Ban"));
m_user_menu_kick = Gtk::manage(new Gtk::MenuItem("Kick"));
m_user_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Open DM"));
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Go to DM"));
m_user_menu_roles = Gtk::manage(new Gtk::MenuItem("Roles"));
m_user_menu_info = Gtk::manage(new Gtk::MenuItem("View Profile"));
m_user_menu_remove_recipient = Gtk::manage(new Gtk::MenuItem("Remove From Group"));
@@ -410,6 +668,9 @@ void Abaddon::SaveState() {
AbaddonApplicationState state;
state.ActiveChannel = m_main_window->GetChatActiveChannel();
state.Expansion = m_main_window->GetChannelList()->GetExpansionState();
#ifdef WITH_LIBHANDY
state.Tabs = m_main_window->GetChatWindow()->GetTabsState();
#endif
const auto path = GetStateCachePath();
if (!util::IsFolder(path)) {
@@ -417,7 +678,8 @@ void Abaddon::SaveState() {
std::filesystem::create_directories(path, ec);
}
auto *fp = std::fopen(GetStateCachePath("/state.json").c_str(), "wb");
auto file_name = "/" + std::to_string(m_discord.GetUserData().ID) + ".json";
auto *fp = std::fopen(GetStateCachePath(file_name).c_str(), "wb");
if (fp == nullptr) return;
const auto s = nlohmann::json(state).dump(4);
std::fwrite(s.c_str(), 1, s.size(), fp);
@@ -431,12 +693,16 @@ void Abaddon::LoadState() {
return;
}
const auto data = ReadWholeFile(GetStateCachePath("/state.json"));
auto file_name = "/" + std::to_string(m_discord.GetUserData().ID) + ".json";
const auto data = ReadWholeFile(GetStateCachePath(file_name));
if (data.empty()) return;
try {
AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end());
m_main_window->GetChannelList()->UseExpansionState(state.Expansion);
ActionChannelOpened(state.ActiveChannel);
#ifdef WITH_LIBHANDY
m_main_window->GetChatWindow()->UseTabsState(state.Tabs);
#endif
ActionChannelOpened(state.ActiveChannel, false);
} catch (const std::exception &e) {
printf("failed to load application state: %s\n", e.what());
}
@@ -471,18 +737,9 @@ void Abaddon::on_user_menu_copy_id() {
void Abaddon::on_user_menu_open_dm() {
const auto existing = m_discord.FindDM(m_shown_user_menu_id);
if (existing.has_value())
if (existing.has_value()) {
ActionChannelOpened(*existing);
else
m_discord.CreateDM(m_shown_user_menu_id, [this](DiscordError code, Snowflake channel_id) {
if (code == DiscordError::NONE) {
// give the gateway a little window to send CHANNEL_CREATE
auto cb = [this, channel_id] {
ActionChannelOpened(channel_id);
};
Glib::signal_timeout().connect_once(sigc::track_obj(cb, *this), 200);
}
});
}
}
void Abaddon::on_user_menu_remove_recipient() {
@@ -535,24 +792,25 @@ void Abaddon::ActionSetToken() {
m_main_window->UpdateComponents();
GetSettings().DiscordToken = m_discord_token;
}
m_main_window->UpdateMenus();
}
void Abaddon::ActionJoinGuildDialog() {
JoinGuildDialog dlg(*m_main_window);
auto response = dlg.run();
if (response == Gtk::RESPONSE_OK) {
auto code = dlg.GetCode();
m_discord.JoinGuild(code);
void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) {
if (!id.IsValid()) {
m_discord.SetReferringChannel(Snowflake::Invalid);
return;
}
}
void Abaddon::ActionChannelOpened(Snowflake id) {
if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return;
if (id == m_main_window->GetChatActiveChannel()) return;
m_main_window->GetChatWindow()->SetTopic("");
const auto channel = m_discord.GetChannel(id);
if (!channel.has_value()) return;
if (!channel.has_value()) {
m_discord.SetReferringChannel(Snowflake::Invalid);
m_main_window->UpdateChatActiveChannel(Snowflake::Invalid, false);
m_main_window->UpdateChatWindowContents();
return;
}
const bool can_access = channel->IsDM() || m_discord.HasChannelPermission(m_discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
@@ -569,7 +827,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
display = "Empty group";
m_main_window->set_title(std::string(APP_TITLE) + " - " + display);
}
m_main_window->UpdateChatActiveChannel(id);
m_main_window->UpdateChatActiveChannel(id, expand_to);
if (m_channels_requested.find(id) == m_channels_requested.end()) {
// dont fire requests we know will fail
if (can_access) {
@@ -595,6 +853,9 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
ShowGuildVerificationGateDialog(*channel->GuildID);
}
}
m_main_window->UpdateMenus();
m_discord.SetReferringChannel(id);
}
void Abaddon::ActionChatLoadHistory(Snowflake id) {
@@ -629,13 +890,13 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
});
}
void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message) {
if (msg.substr(0, 7) == "/shrug " || msg == "/shrug")
msg = msg.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
if (referenced_message.IsValid())
m_discord.SendChatMessage(msg, channel, referenced_message);
else
m_discord.SendChatMessage(msg, channel);
void Abaddon::ActionChatInputSubmit(ChatSubmitParams data) {
if (data.Message.substr(0, 7) == "/shrug " || data.Message == "/shrug")
data.Message = data.Message.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, data.ChannelID, Permission::VIEW_CHANNEL)) return;
m_discord.SendChatMessage(data, NOOP_CALLBACK);
}
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
@@ -744,6 +1005,25 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) {
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) {
TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window);
const auto code = dlg.run();
if (code == Gtk::RESPONSE_OK)
return dlg.GetInput();
else
return {};
}
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
dlg.SetConfirmText(prompt);
@@ -774,17 +1054,45 @@ EmojiResource &Abaddon::GetEmojis() {
return m_emojis;
}
#ifdef WITH_VOICE
AudioManager &Abaddon::GetAudio() {
return *m_audio;
}
#endif
void Abaddon::on_tray_click() {
m_main_window->set_visible(!m_main_window->is_visible());
}
void Abaddon::on_tray_menu_click() {
m_gtk_app->quit();
}
void Abaddon::on_tray_popup_menu(int button, int activate_time) {
m_tray->popup_menu_at_position(*m_tray_menu, button, activate_time);
}
void Abaddon::on_window_hide() {
if (!m_settings.GetSettings().HideToTray) {
m_gtk_app->quit();
}
}
int main(int argc, char **argv) {
if (std::getenv("ABADDON_NO_FC") == nullptr)
Platform::SetupFonts();
char *systemLocale = std::setlocale(LC_ALL, "");
try {
std::locale::global(std::locale(systemLocale));
if (systemLocale != nullptr) {
std::locale::global(std::locale(systemLocale));
}
} catch (...) {
try {
std::locale::global(std::locale::classic());
std::setlocale(LC_ALL, systemLocale);
if (systemLocale != nullptr) {
std::setlocale(LC_ALL, systemLocale);
}
} catch (...) {}
}
@@ -794,6 +1102,12 @@ int main(int argc, char **argv) {
if (buf[0] != '1')
SetEnvironmentVariableA("GTK_CSD", "0");
#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???
return Abaddon::Get().StartGTK();
}

View File

@@ -1,3 +1,4 @@
#pragma once
#include <gtkmm.h>
#include <memory>
#include <mutex>
@@ -11,6 +12,8 @@
#define APP_TITLE "Abaddon"
class AudioManager;
class Abaddon {
private:
Abaddon();
@@ -35,8 +38,8 @@ public:
void ActionDisconnect();
void ActionSetToken();
void ActionJoinGuildDialog();
void ActionChannelOpened(Snowflake id);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChannelOpened(Snowflake id, bool expand_to = true);
void ActionChatInputSubmit(ChatSubmitParams data);
void ActionChatLoadHistory(Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
void ActionInsertMention(Snowflake id);
@@ -51,6 +54,12 @@ public:
void ActionViewPins(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);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
void ActionReloadCSS();
@@ -58,6 +67,10 @@ public:
ImageManager &GetImageManager();
EmojiResource &GetEmojis();
#ifdef WITH_VOICE
AudioManager &GetAudio();
#endif
std::string GetDiscordToken() const;
bool IsDiscordActive() const;
@@ -76,6 +89,11 @@ public:
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
void DiscordOnThreadUpdate(const ThreadUpdateData &data);
#ifdef WITH_VOICE
void OnVoiceConnected();
void OnVoiceDisconnected();
#endif
SettingsManager::Settings &GetSettings();
Glib::RefPtr<Gtk::CssProvider> GetStyleProvider();
@@ -92,6 +110,8 @@ public:
static std::string GetStateCachePath(const std::string &path);
protected:
void RunFirstTimeDiscordStartup();
void ShowGuildVerificationGateDialog(Snowflake guild_id);
void CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs);
@@ -113,6 +133,8 @@ protected:
Gtk::MenuItem *m_user_menu_roles;
Gtk::MenuItem *m_user_menu_remove_recipient;
Gtk::Menu *m_user_menu_roles_submenu;
Gtk::Menu *m_tray_menu;
Gtk::MenuItem *m_tray_exit;
void on_user_menu_insert_mention();
void on_user_menu_ban();
@@ -120,6 +142,10 @@ protected:
void on_user_menu_copy_id();
void on_user_menu_open_dm();
void on_user_menu_remove_recipient();
void on_tray_click();
void on_tray_popup_menu(int button, int activate_time);
void on_tray_menu_click();
void on_window_hide();
private:
SettingsManager m_settings;
@@ -134,9 +160,15 @@ private:
ImageManager m_img_mgr;
EmojiResource m_emojis;
#ifdef WITH_VOICE
std::unique_ptr<AudioManager> m_audio;
Gtk::Window *m_voice_window = nullptr;
#endif
mutable std::mutex m_mutex;
Glib::RefPtr<Gtk::Application> m_gtk_app;
Glib::RefPtr<Gtk::CssProvider> m_css_provider;
Glib::RefPtr<Gtk::CssProvider> m_css_low_provider; // registered with a lower priority to allow better customization
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
Glib::RefPtr<Gtk::StatusIcon> m_tray;
std::unique_ptr<MainWindow> m_main_window; // wah wah cant create a gtkstylecontext fuck you
};

121
src/audio/devices.cpp Normal file
View File

@@ -0,0 +1,121 @@
#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

58
src/audio/devices.hpp Normal file
View File

@@ -0,0 +1,58 @@
#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

466
src/audio/manager.cpp Normal file
View File

@@ -0,0 +1,466 @@
#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

120
src/audio/manager.hpp Normal file
View File

@@ -0,0 +1,120 @@
#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

@@ -17,8 +17,20 @@ ChannelList::ChannelList()
, m_menu_category_copy_id("_Copy ID", true)
, m_menu_channel_copy_id("_Copy ID", true)
, m_menu_channel_mark_as_read("Mark as _Read", true)
#ifdef WITH_LIBHANDY
, m_menu_channel_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
, m_menu_dm_copy_id("_Copy ID", true)
, 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_leave("_Leave", true)
, m_menu_thread_archive("_Archive", true)
@@ -32,7 +44,11 @@ ChannelList::ChannelList()
const auto type = row[m_columns.m_type];
// 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
#ifdef WITH_VOICE
if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) {
#else
if (type != RenderType::TextChannel) {
#endif
if (row[m_columns.m_expanded]) {
m_view.collapse_row(path);
row[m_columns.m_expanded] = false;
@@ -42,7 +58,11 @@ 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) {
#endif
const auto id = static_cast<Snowflake>(row[m_columns.m_id]);
m_signal_action_channel_item_select.emit(id);
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {});
@@ -143,11 +163,35 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
#ifdef WITH_LIBHANDY
m_menu_channel_open_tab.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_open_new_tab.emit(id);
});
m_menu_channel.append(m_menu_channel_open_tab);
#endif
m_menu_channel.append(m_menu_channel_mark_as_read);
m_menu_channel.append(m_menu_channel_toggle_mute);
m_menu_channel.append(m_menu_channel_copy_id);
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] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
@@ -170,8 +214,26 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
#ifdef WITH_LIBHANDY
m_menu_dm_open_tab.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_open_new_tab.emit(id);
});
m_menu_dm.append(m_menu_dm_open_tab);
#endif
m_menu_dm.append(m_menu_dm_toggle_mute);
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.show_all();
@@ -442,7 +504,7 @@ void ChannelList::OnGuildUnmute(Snowflake id) {
// create a temporary channel row for non-joined threads
// and delete them when the active channel switches off of them if still not joined
void ChannelList::SetActiveChannel(Snowflake id) {
void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) {
// mark channel as read when switching off
if (m_active_channel.IsValid())
Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {});
@@ -459,11 +521,12 @@ void ChannelList::SetActiveChannel(Snowflake id) {
const auto channel_iter = GetIteratorForChannelFromID(id);
if (channel_iter) {
m_view.expand_to_path(m_model->get_path(channel_iter));
if (expand_to) {
m_view.expand_to_path(m_model->get_path(channel_iter));
}
m_view.get_selection()->select(channel_iter);
} else {
m_view.get_selection()->unselect_all();
// SetActiveChannel should probably just take the channel object
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (!channel.has_value() || !channel->IsThread()) return;
auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID);
@@ -558,7 +621,11 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel_ : *guild.Channels) {
const auto channel = discord.GetChannel(channel_.ID);
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) {
#endif
if (channel->ParentID.has_value())
categories[*channel->ParentID].push_back(*channel);
else
@@ -584,9 +651,31 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
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) {
auto channel_row = *m_model->append(guild_row.children());
channel_row[m_columns.m_type] = RenderType::TextChannel;
if (IsTextChannel(channel.Type))
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_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
@@ -609,7 +698,14 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel : channels) {
auto channel_row = *m_model->append(cat_row.children());
channel_row[m_columns.m_type] = RenderType::TextChannel;
if (IsTextChannel(channel.Type))
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_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position;
@@ -711,7 +807,11 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const
m_last_selected = m_model->get_path(row);
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;
#endif
}
void ChannelList::AddPrivateChannels() {
@@ -835,6 +935,12 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
OnChannelSubmenuPopup();
m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
#ifdef WITH_VOICE
case RenderType::VoiceChannel:
OnVoiceChannelSubmenuPopup();
m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
#endif
case RenderType::DM: {
OnDMSubmenuPopup();
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
@@ -890,10 +996,15 @@ void ChannelList::OnGuildSubmenuPopup() {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsGuildMuted(id))
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsGuildMuted(id))
m_menu_guild_toggle_mute.set_label("Unmute");
else
m_menu_guild_toggle_mute.set_label("Mute");
const auto guild = discord.GetGuild(id);
const auto self_id = discord.GetUserData().ID;
m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id));
}
void ChannelList::OnCategorySubmenuPopup() {
@@ -910,20 +1021,52 @@ void ChannelList::OnChannelSubmenuPopup() {
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
auto &discord = Abaddon::Get().GetDiscordClient();
#ifdef WITH_LIBHANDY
const auto perms = discord.HasChannelPermission(discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
m_menu_channel_open_tab.set_sensitive(perms);
#endif
if (discord.IsChannelMuted(id))
m_menu_channel_toggle_mute.set_label("Unmute");
else
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() {
auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsChannelMuted(id))
m_menu_dm_toggle_mute.set_label("Unmute");
else
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() {
@@ -960,6 +1103,22 @@ ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_
return m_signal_action_guild_settings;
}
#ifdef WITH_LIBHANDY
ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() {
return m_signal_action_open_new_tab;
}
#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() {
add(m_type);
add(m_id);

View File

@@ -19,7 +19,7 @@ public:
ChannelList();
void UpdateListing();
void SetActiveChannel(Snowflake id);
void SetActiveChannel(Snowflake id, bool expand_to);
// channel list should be populated when this is called
void UseExpansionState(const ExpansionStateRoot &state);
@@ -121,10 +121,28 @@ protected:
Gtk::MenuItem m_menu_channel_mark_as_read;
Gtk::MenuItem m_menu_channel_toggle_mute;
#ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_channel_open_tab;
#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::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
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
Gtk::MenuItem m_menu_dm_open_tab;
#endif
Gtk::Menu m_menu_thread;
Gtk::MenuItem m_menu_thread_copy_id;
@@ -140,6 +158,10 @@ protected:
void OnDMSubmenuPopup();
void OnThreadSubmenuPopup();
#ifdef WITH_VOICE
void OnVoiceChannelSubmenuPopup();
#endif
bool m_updating_listing = false;
Snowflake m_active_channel;
@@ -149,16 +171,38 @@ protected:
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
public:
typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
#ifdef WITH_LIBHANDY
using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
type_signal_action_open_new_tab signal_action_open_new_tab();
#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_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
protected:
private:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
#ifdef WITH_LIBHANDY
type_signal_action_open_new_tab m_signal_action_open_new_tab;
#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,6 +65,12 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
case RenderType::Thread:
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:
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
case RenderType::DM:
@@ -82,6 +88,12 @@ 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);
case RenderType::Thread:
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:
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
case RenderType::DM:
@@ -99,6 +111,12 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
case RenderType::Thread:
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:
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
case RenderType::DM:
@@ -116,6 +134,12 @@ 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);
case RenderType::Thread:
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:
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
case RenderType::DM:
@@ -133,6 +157,12 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
case RenderType::Thread:
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:
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
case RenderType::DM:
@@ -499,6 +529,76 @@ 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
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {

View File

@@ -11,6 +11,12 @@ enum class RenderType : uint8_t {
TextChannel,
Thread,
// TODO: maybe enable anyways but without ability to join if no voice support
#ifdef WITH_VOICE
VoiceChannel,
VoiceParticipant,
#endif
DMHeader,
DM,
};
@@ -83,6 +89,30 @@ protected:
const Gdk::Rectangle &cell_area,
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
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;

View File

@@ -0,0 +1,246 @@
#ifdef WITH_LIBHANDY
#include "channeltabswitcherhandy.hpp"
#include "abaddon.hpp"
void selected_page_notify_cb(HdyTabView *view, GParamSpec *pspec, ChannelTabSwitcherHandy *switcher) {
auto *page = hdy_tab_view_get_selected_page(view);
if (auto it = switcher->m_pages_rev.find(page); it != switcher->m_pages_rev.end()) {
switcher->m_signal_channel_switched_to.emit(it->second);
}
}
gboolean close_page_cb(HdyTabView *view, HdyTabPage *page, ChannelTabSwitcherHandy *switcher) {
switcher->ClearPage(page);
hdy_tab_view_close_page_finish(view, page, true);
return GDK_EVENT_STOP;
}
ChannelTabSwitcherHandy::ChannelTabSwitcherHandy() {
m_tab_bar = hdy_tab_bar_new();
m_tab_bar_wrapped = Glib::wrap(GTK_WIDGET(m_tab_bar));
m_tab_view = hdy_tab_view_new();
m_tab_view_wrapped = Glib::wrap(GTK_WIDGET(m_tab_view));
m_tab_bar_wrapped->get_style_context()->add_class("channel-tab-switcher");
g_signal_connect(m_tab_view, "notify::selected-page", G_CALLBACK(selected_page_notify_cb), this);
g_signal_connect(m_tab_view, "close-page", G_CALLBACK(close_page_cb), this);
hdy_tab_bar_set_view(m_tab_bar, m_tab_view);
add(*m_tab_bar_wrapped);
m_tab_bar_wrapped->show();
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_message_create().connect([this](const Message &data) {
CheckUnread(data.ChannelID);
});
discord.signal_message_ack().connect([this](const MessageAckData &data) {
CheckUnread(data.ChannelID);
});
discord.signal_channel_accessibility_changed().connect(sigc::mem_fun(*this, &ChannelTabSwitcherHandy::OnChannelAccessibilityChanged));
}
void ChannelTabSwitcherHandy::AddChannelTab(Snowflake id) {
if (m_pages.find(id) != m_pages.end()) return;
auto &discord = Abaddon::Get().GetDiscordClient();
const auto channel = discord.GetChannel(id);
if (!channel.has_value()) return;
auto *dummy = Gtk::make_managed<Gtk::Box>(); // minimal
auto *page = hdy_tab_view_append(m_tab_view, GTK_WIDGET(dummy->gobj()));
hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
hdy_tab_page_set_tooltip(page, nullptr);
m_pages[id] = page;
m_pages_rev[page] = id;
CheckUnread(id);
CheckPageIcon(page, *channel);
AppendPageHistory(page, id);
}
void ChannelTabSwitcherHandy::ReplaceActiveTab(Snowflake id) {
auto *page = hdy_tab_view_get_selected_page(m_tab_view);
if (page == nullptr) {
AddChannelTab(id);
} else if (auto it = m_pages.find(id); it != m_pages.end()) {
hdy_tab_view_set_selected_page(m_tab_view, it->second);
} else {
auto &discord = Abaddon::Get().GetDiscordClient();
const auto channel = discord.GetChannel(id);
if (!channel.has_value()) return;
hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
ClearPage(page);
m_pages[id] = page;
m_pages_rev[page] = id;
CheckUnread(id);
CheckPageIcon(page, *channel);
AppendPageHistory(page, id);
}
}
TabsState ChannelTabSwitcherHandy::GetTabsState() {
TabsState state;
const gint num_pages = hdy_tab_view_get_n_pages(m_tab_view);
for (gint i = 0; i < num_pages; i++) {
auto *page = hdy_tab_view_get_nth_page(m_tab_view, i);
if (page != nullptr) {
if (const auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
state.Channels.push_back(it->second);
}
}
}
return state;
}
void ChannelTabSwitcherHandy::UseTabsState(const TabsState &state) {
for (auto id : state.Channels) {
AddChannelTab(id);
}
}
void ChannelTabSwitcherHandy::GoBackOnCurrent() {
AdvanceOnCurrent(-1);
}
void ChannelTabSwitcherHandy::GoForwardOnCurrent() {
AdvanceOnCurrent(1);
}
void ChannelTabSwitcherHandy::GoToPreviousTab() {
if (!hdy_tab_view_select_previous_page(m_tab_view)) {
if (const auto num_pages = hdy_tab_view_get_n_pages(m_tab_view); num_pages > 1) {
hdy_tab_view_set_selected_page(m_tab_view, hdy_tab_view_get_nth_page(m_tab_view, num_pages - 1));
}
}
}
void ChannelTabSwitcherHandy::GoToNextTab() {
if (!hdy_tab_view_select_next_page(m_tab_view)) {
if (hdy_tab_view_get_n_pages(m_tab_view) > 1) {
hdy_tab_view_set_selected_page(m_tab_view, hdy_tab_view_get_nth_page(m_tab_view, 0));
}
}
}
void ChannelTabSwitcherHandy::GoToTab(int idx) {
if (hdy_tab_view_get_n_pages(m_tab_view) >= idx + 1)
hdy_tab_view_set_selected_page(m_tab_view, hdy_tab_view_get_nth_page(m_tab_view, idx));
}
int ChannelTabSwitcherHandy::GetNumberOfTabs() const {
return hdy_tab_view_get_n_pages(m_tab_view);
}
void ChannelTabSwitcherHandy::CheckUnread(Snowflake id) {
if (auto it = m_pages.find(id); it != m_pages.end()) {
auto &discord = Abaddon::Get().GetDiscordClient();
const bool has_unreads = discord.GetUnreadStateForChannel(id) > -1;
const bool show_indicator = has_unreads && !discord.IsChannelMuted(id);
hdy_tab_page_set_needs_attention(it->second, show_indicator);
}
}
void ChannelTabSwitcherHandy::ClearPage(HdyTabPage *page) {
if (auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
m_pages.erase(it->second);
}
m_pages_rev.erase(page);
m_page_icons.erase(page);
}
void ChannelTabSwitcherHandy::OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb) {
auto new_pb = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
m_page_icons[page] = new_pb;
hdy_tab_page_set_icon(page, G_ICON(new_pb->gobj()));
}
void ChannelTabSwitcherHandy::CheckPageIcon(HdyTabPage *page, const ChannelData &data) {
std::optional<std::string> icon_url;
if (data.GuildID.has_value()) {
if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data.GuildID); guild.has_value() && guild->HasIcon()) {
icon_url = guild->GetIconURL("png", "16");
}
} else if (data.IsDM()) {
icon_url = data.GetIconURL();
}
if (icon_url.has_value()) {
auto *child_widget = hdy_tab_page_get_child(page);
if (child_widget == nullptr) return; // probably wont happen :---)
// i think this works???
auto *trackable = Glib::wrap(GTK_WIDGET(child_widget));
Abaddon::Get().GetImageManager().LoadFromURL(
*icon_url,
sigc::track_obj([this, page](const Glib::RefPtr<Gdk::Pixbuf> &pb) { OnPageIconLoad(page, pb); },
*trackable));
return;
}
hdy_tab_page_set_icon(page, nullptr);
}
void ChannelTabSwitcherHandy::AppendPageHistory(HdyTabPage *page, Snowflake channel) {
auto it = m_page_history.find(page);
if (it == m_page_history.end()) {
m_page_history[page] = PageHistory { { channel }, 0 };
return;
}
// drop everything beyond current position
it->second.Visited.resize(++it->second.CurrentVisitedIndex);
it->second.Visited.push_back(channel);
}
void ChannelTabSwitcherHandy::AdvanceOnCurrent(size_t by) {
auto *current = hdy_tab_view_get_selected_page(m_tab_view);
if (current == nullptr) return;
auto history = m_page_history.find(current);
if (history == m_page_history.end()) return;
if (by + history->second.CurrentVisitedIndex < 0 || by + history->second.CurrentVisitedIndex >= history->second.Visited.size()) return;
history->second.CurrentVisitedIndex += by;
const auto to_id = history->second.Visited.at(history->second.CurrentVisitedIndex);
// temporarily point current index to the end so that it doesnt fuck up the history
// remove it immediately after cuz the emit will call ReplaceActiveTab
const auto real = history->second.CurrentVisitedIndex;
history->second.CurrentVisitedIndex = history->second.Visited.size() - 1;
m_signal_channel_switched_to.emit(to_id);
// iterator might not be valid
history = m_page_history.find(current);
if (history != m_page_history.end()) {
history->second.Visited.pop_back();
}
history->second.CurrentVisitedIndex = real;
}
void ChannelTabSwitcherHandy::OnChannelAccessibilityChanged(Snowflake id, bool accessibility) {
if (accessibility) return;
if (auto it = m_pages.find(id); it != m_pages.end()) {
if (hdy_tab_page_get_selected(it->second))
if (!hdy_tab_view_select_previous_page(m_tab_view))
hdy_tab_view_select_next_page(m_tab_view);
hdy_tab_view_close_page(m_tab_view, it->second);
ClearPage(it->second);
}
}
ChannelTabSwitcherHandy::type_signal_channel_switched_to ChannelTabSwitcherHandy::signal_channel_switched_to() {
return m_signal_channel_switched_to;
}
#endif

View File

@@ -0,0 +1,71 @@
#pragma once
// perhaps this should be conditionally included within cmakelists?
#ifdef WITH_LIBHANDY
#include <gtkmm/box.h>
#include <unordered_map>
#include <handy.h>
#include "discord/snowflake.hpp"
#include "state.hpp"
class ChannelData;
// thin wrapper over c api
// HdyTabBar + invisible HdyTabView since it needs one
class ChannelTabSwitcherHandy : public Gtk::Box {
public:
ChannelTabSwitcherHandy();
// no-op if already added
void AddChannelTab(Snowflake id);
// switches to existing tab if it exists
void ReplaceActiveTab(Snowflake id);
TabsState GetTabsState();
void UseTabsState(const TabsState &state);
void GoBackOnCurrent();
void GoForwardOnCurrent();
void GoToPreviousTab();
void GoToNextTab();
void GoToTab(int idx);
[[nodiscard]] int GetNumberOfTabs() const;
private:
void CheckUnread(Snowflake id);
void ClearPage(HdyTabPage *page);
void OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb);
void CheckPageIcon(HdyTabPage *page, const ChannelData &data);
void AppendPageHistory(HdyTabPage *page, Snowflake channel);
void AdvanceOnCurrent(size_t by);
void OnChannelAccessibilityChanged(Snowflake id, bool accessibility);
HdyTabBar *m_tab_bar;
Gtk::Widget *m_tab_bar_wrapped;
HdyTabView *m_tab_view;
Gtk::Widget *m_tab_view_wrapped;
std::unordered_map<Snowflake, HdyTabPage *> m_pages;
std::unordered_map<HdyTabPage *, Snowflake> m_pages_rev;
// need to hold a reference to the pixbuf data
std::unordered_map<HdyTabPage *, Glib::RefPtr<Gdk::Pixbuf>> m_page_icons;
struct PageHistory {
std::vector<Snowflake> Visited;
size_t CurrentVisitedIndex;
};
std::unordered_map<HdyTabPage *, PageHistory> m_page_history;
friend void selected_page_notify_cb(HdyTabView *, GParamSpec *, ChannelTabSwitcherHandy *);
friend gboolean close_page_cb(HdyTabView *, HdyTabPage *, ChannelTabSwitcherHandy *);
public:
using type_signal_channel_switched_to = sigc::signal<void, Snowflake>;
type_signal_channel_switched_to signal_channel_switched_to();
private:
type_signal_channel_switched_to m_signal_channel_switched_to;
};
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,9 +4,14 @@
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
#include "chatlist.hpp"
#include "constants.hpp"
#ifdef WITH_LIBHANDY
#include "channeltabswitcherhandy.hpp"
#endif
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_chat = Gtk::manage(new ChatList);
@@ -15,6 +20,13 @@ ChatWindow::ChatWindow() {
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
#ifdef WITH_LIBHANDY
m_tab_switcher = Gtk::make_managed<ChannelTabSwitcherHandy>();
m_tab_switcher->signal_channel_switched_to().connect([this](Snowflake id) {
m_signal_action_channel_click.emit(id, false);
});
#endif
m_rate_limit_indicator->set_margin_end(5);
m_rate_limit_indicator->set_hexpand(true);
m_rate_limit_indicator->set_halign(Gtk::ALIGN_END);
@@ -35,6 +47,8 @@ ChatWindow::ChatWindow() {
m_topic_text.set_halign(Gtk::ALIGN_START);
m_topic_text.show();
m_input->set_valign(Gtk::ALIGN_END);
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() {
if (m_is_replying)
@@ -44,25 +58,22 @@ ChatWindow::ChatWindow() {
m_input->show();
m_completer.SetBuffer(m_input->GetBuffer());
m_completer.SetGetChannelID([this]() -> auto {
m_completer.SetGetChannelID([this]() {
return m_active_channel;
});
m_completer.SetGetRecentAuthors([this]() -> auto {
m_completer.SetGetRecentAuthors([this]() {
return m_chat->GetRecentAuthors();
});
m_completer.show();
m_chat->signal_action_channel_click().connect([this](Snowflake id) {
m_signal_action_channel_click.emit(id);
m_signal_action_channel_click.emit(id, true);
});
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
m_signal_action_chat_load_history.emit(id);
});
m_chat->signal_action_chat_submit().connect([this](const std::string &str, Snowflake channel_id, Snowflake referenced_id) {
m_signal_action_chat_submit.emit(str, channel_id, referenced_id);
});
m_chat->signal_action_insert_mention().connect([this](Snowflake id) {
// lowkey gross
m_signal_action_insert_mention.emit(id);
@@ -88,11 +99,19 @@ ChatWindow::ChatWindow() {
m_meta->add(*m_input_indicator);
m_meta->add(*m_rate_limit_indicator);
// m_scroll->add(*m_list);
#ifdef WITH_LIBHANDY
m_main->add(*m_tab_switcher);
m_tab_switcher->show();
#endif
m_main->add(m_topic);
m_main->add(*m_chat);
m_main->add(m_completer);
m_main->add(*m_input);
m_main->add(*m_meta);
m_main->add(m_progress);
m_progress.show();
m_main->show();
}
@@ -111,10 +130,15 @@ void ChatWindow::SetMessages(const std::vector<Message> &msgs) {
void ChatWindow::SetActiveChannel(Snowflake id) {
m_active_channel = id;
m_chat->SetActiveChannel(id);
m_input->SetActiveChannel(id);
m_input_indicator->SetActiveChannel(id);
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
#ifdef WITH_LIBHANDY
m_tab_switcher->ReplaceActiveTab(id);
#endif
}
void ChatWindow::AddNewMessage(const Message &data) {
@@ -150,19 +174,110 @@ void ChatWindow::SetTopic(const std::string &text) {
m_topic.set_visible(text.length() > 0);
}
void ChatWindow::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
m_input->AddAttachment(file);
}
#ifdef WITH_LIBHANDY
void ChatWindow::OpenNewTab(Snowflake id) {
// open if its the first tab (in which case it really isnt a tab but whatever)
if (m_tab_switcher->GetNumberOfTabs() == 0) {
m_signal_action_channel_click.emit(id, false);
}
m_tab_switcher->AddChannelTab(id);
}
TabsState ChatWindow::GetTabsState() {
return m_tab_switcher->GetTabsState();
}
void ChatWindow::UseTabsState(const TabsState &state) {
m_tab_switcher->UseTabsState(state);
}
void ChatWindow::GoBack() {
m_tab_switcher->GoBackOnCurrent();
}
void ChatWindow::GoForward() {
m_tab_switcher->GoForwardOnCurrent();
}
void ChatWindow::GoToPreviousTab() {
m_tab_switcher->GoToPreviousTab();
}
void ChatWindow::GoToNextTab() {
m_tab_switcher->GoToNextTab();
}
void ChatWindow::GoToTab(int idx) {
m_tab_switcher->GoToTab(idx);
}
#endif
Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
bool ChatWindow::OnInputSubmit(ChatSubmitParams data) {
auto &discord = Abaddon::Get().GetDiscordClient();
if (!discord.HasSelfChannelPermission(m_active_channel, Permission::SEND_MESSAGES)) return false;
if (!data.Attachments.empty() && !discord.HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES)) return false;
int nitro_restriction = BaseAttachmentSizeLimit;
const auto nitro = discord.GetUserData().PremiumType;
if (!nitro.has_value() || nitro == EPremiumType::None) {
nitro_restriction = BaseAttachmentSizeLimit;
} else if (nitro == EPremiumType::NitroClassic) {
nitro_restriction = NitroClassicAttachmentSizeLimit;
} else if (nitro == EPremiumType::Nitro) {
nitro_restriction = NitroAttachmentSizeLimit;
}
int guild_restriction = BaseAttachmentSizeLimit;
if (const auto channel = discord.GetChannel(m_active_channel); channel.has_value() && channel->GuildID.has_value()) {
if (const auto guild = discord.GetGuild(*channel->GuildID); guild.has_value()) {
if (!guild->PremiumTier.has_value() || guild->PremiumTier == GuildPremiumTier::NONE || guild->PremiumTier == GuildPremiumTier::TIER_1) {
guild_restriction = BaseAttachmentSizeLimit;
} else if (guild->PremiumTier == GuildPremiumTier::TIER_2) {
guild_restriction = BoostLevel2AttachmentSizeLimit;
} else if (guild->PremiumTier == GuildPremiumTier::TIER_3) {
guild_restriction = BoostLevel3AttachmentSizeLimit;
}
}
}
int restriction = std::max(nitro_restriction, guild_restriction);
goffset total_size = 0;
for (const auto &attachment : data.Attachments) {
const auto info = attachment.File->query_info();
if (info) {
const auto size = info->get_size();
if (size > restriction) {
m_input->IndicateTooLarge();
return false;
}
total_size += size;
if (total_size > MaxMessagePayloadSize) {
m_input->IndicateTooLarge();
return false;
}
}
}
if (!m_rate_limit_indicator->CanSpeak())
return false;
if (text.empty())
if (data.Message.empty() && data.Attachments.empty())
return false;
data.ChannelID = m_active_channel;
data.InReplyToID = m_replying_to;
if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler
m_signal_action_chat_submit.emit(data); // m_replying_to is checked for invalid in the handler
if (m_is_replying)
StopReplying();
@@ -185,8 +300,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
const auto author = discord.GetUser(message.Author.ID);
m_replying_to = message_id;
m_is_replying = true;
m_input->grab_focus();
m_input->get_style_context()->add_class("replying");
m_input->StartReplying();
if (author.has_value())
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>());
else
@@ -196,7 +310,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
void ChatWindow::StopReplying() {
m_is_replying = false;
m_replying_to = Snowflake::Invalid;
m_input->get_style_context()->remove_class("replying");
m_input->StopReplying();
m_input_indicator->ClearCustom();
}

View File

@@ -3,7 +3,14 @@
#include <string>
#include <set>
#include "discord/discord.hpp"
#include "discord/chatsubmitparams.hpp"
#include "completer.hpp"
#include "state.hpp"
#include "progressbar.hpp"
#ifdef WITH_LIBHANDY
class ChannelTabSwitcherHandy;
#endif
class ChatMessageHeader;
class ChatMessageItemContainer;
@@ -25,10 +32,22 @@ public:
void DeleteMessage(Snowflake id); // add [deleted] indicator
void UpdateMessage(Snowflake id); // add [edited] indicator
void AddNewHistory(const std::vector<Message> &msgs); // prepend messages
void InsertChatInput(const std::string& text);
void InsertChatInput(const std::string &text);
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
void AddAttachment(const Glib::RefPtr<Gio::File> &file);
#ifdef WITH_LIBHANDY
void OpenNewTab(Snowflake id);
TabsState GetTabsState();
void UseTabsState(const TabsState &state);
void GoBack();
void GoForward();
void GoToPreviousTab();
void GoToNextTab();
void GoToTab(int idx);
#endif
protected:
bool m_is_replying = false;
@@ -39,7 +58,7 @@ protected:
Snowflake m_active_channel;
bool OnInputSubmit(const Glib::ustring &text);
bool OnInputSubmit(ChatSubmitParams data);
bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos);
@@ -61,15 +80,20 @@ protected:
ChatInputIndicator *m_input_indicator;
RateLimitIndicator *m_rate_limit_indicator;
Gtk::Box *m_meta;
MessageUploadProgressBar m_progress;
#ifdef WITH_LIBHANDY
ChannelTabSwitcherHandy *m_tab_switcher;
#endif
public:
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
typedef sigc::signal<void, std::string, Snowflake, Snowflake> type_signal_action_chat_submit;
typedef sigc::signal<void, Snowflake> type_signal_action_chat_load_history;
typedef sigc::signal<void, Snowflake> type_signal_action_channel_click;
typedef sigc::signal<void, Snowflake> type_signal_action_insert_mention;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_remove;
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
using type_signal_action_chat_submit = sigc::signal<void, ChatSubmitParams>;
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
using type_signal_action_channel_click = sigc::signal<void, Snowflake, bool>;
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
using type_signal_action_reaction_add = sigc::signal<void, Snowflake, Glib::ustring>;
using type_signal_action_reaction_remove = sigc::signal<void, Snowflake, Glib::ustring>;
type_signal_action_message_edit signal_action_message_edit();
type_signal_action_chat_submit signal_action_chat_submit();

View File

@@ -160,10 +160,11 @@ void MemberList::UpdateMemberList() {
}
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;
auto add_user = [this, &num_rows, guild](const UserData &data) -> bool {
if (num_rows++ > MaxMemberListRows) return false;
auto *row = Gtk::manage(new MemberListUserRow(guild, data));
auto *row = Gtk::manage(new MemberListUserRow(*guild, data));
m_id_to_row[data.ID] = row;
AttachUserMenuHandler(row, data.ID);
m_listbox->add(*row);

View File

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

View File

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

View File

@@ -11,9 +11,9 @@ public:
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_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 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;

View File

@@ -0,0 +1,89 @@
#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

@@ -0,0 +1,19 @@
#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

@@ -0,0 +1,125 @@
#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

@@ -0,0 +1,31 @@
#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,4 +1,12 @@
#pragma once
#include <cstdint>
constexpr static uint64_t SnowflakeSplitDifference = 600;
constexpr static int MaxMessagesForChatCull = 50; // this has to be 50 (for now) cuz that magic number is used in a couple other places and i dont feel like replacing them
constexpr static int AttachmentItemSize = 120;
constexpr static int BaseAttachmentSizeLimit = 8 * 1024 * 1024;
constexpr static int NitroClassicAttachmentSizeLimit = 50 * 1024 * 1024;
constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024;
constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;
constexpr static int BoostLevel3AttachmentSizeLimit = 100 * 1024 * 1024;
constexpr static int MaxMessagePayloadSize = 199 * 1024 * 1024;

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

@@ -89,7 +89,28 @@ bool ChannelData::HasIcon() const noexcept {
}
std::string ChannelData::GetIconURL() const {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
if (HasIcon()) {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
} else {
const auto recipients = GetDMRecipients();
if (!recipients.empty())
return recipients[0].GetAvatarURL("png", "32");
else
return "https://cdn.discordapp.com/embed/avatars/0.png";
}
}
std::string ChannelData::GetDisplayName() const {
if (Name.has_value()) {
return "#" + *Name;
} else {
const auto recipients = GetDMRecipients();
if (Type == ChannelType::DM && !recipients.empty())
return recipients[0].Username;
else if (Type == ChannelType::GROUP_DM)
return std::to_string(recipients.size()) + " members";
}
return "Unknown";
}
std::vector<Snowflake> ChannelData::GetChildIDs() const {

View File

@@ -102,6 +102,7 @@ struct ChannelData {
[[nodiscard]] bool IsText() const noexcept;
[[nodiscard]] bool HasIcon() const noexcept;
[[nodiscard]] std::string GetIconURL() const;
[[nodiscard]] std::string GetDisplayName() const;
[[nodiscard]] std::vector<Snowflake> GetChildIDs() const;
[[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
[[nodiscard]] std::vector<UserData> GetDMRecipients() const;

View File

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

View File

@@ -1,6 +1,7 @@
#include "abaddon.hpp"
#include "discord.hpp"
#include "util.hpp"
#include <spdlog/spdlog.h>
#include <cinttypes>
#include <utility>
@@ -8,7 +9,8 @@ using namespace std::string_literals;
DiscordClient::DiscordClient(bool mem_store)
: 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));
auto dispatch_cb = [this]() {
m_generic_mutex.lock();
@@ -23,6 +25,17 @@ DiscordClient::DiscordClient(bool mem_store)
m_websocket.signal_open().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketOpen));
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();
}
@@ -30,6 +43,7 @@ void DiscordClient::Start() {
if (m_client_started) return;
m_http.SetBase(GetAPIURL());
SetHeaders();
std::memset(&m_zstream, 0, sizeof(m_zstream));
inflateInit2(&m_zstream, MAX_WBITS + 32);
@@ -41,7 +55,7 @@ void DiscordClient::Start() {
m_websocket.StartConnection(GetGatewayURL());
}
void DiscordClient::Stop() {
bool DiscordClient::Stop() {
if (m_client_started) {
inflateEnd(&m_zstream);
m_compressed_buf.clear();
@@ -55,9 +69,15 @@ void DiscordClient::Stop() {
m_guild_to_users.clear();
m_websocket.Stop();
m_client_started = false;
return true;
}
m_client_started = false;
return false;
}
bool DiscordClient::IsStarted() const {
@@ -312,6 +332,10 @@ bool DiscordClient::HasGuildPermission(Snowflake user_id, Snowflake guild_id, Pe
return (base & perm) == perm;
}
bool DiscordClient::HasSelfChannelPermission(Snowflake channel_id, Permission perm) const {
return HasChannelPermission(m_user_data.ID, channel_id, perm);
}
bool DiscordClient::HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const {
const auto channel = m_store.GetChannel(channel_id);
if (!channel.has_value() || !channel->GuildID.has_value()) return false;
@@ -403,7 +427,7 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
return actor_highest->Position > target_highest->Position;
}
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response) {
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError)> &callback) {
if (!CheckCode(response)) {
if (response.status_code == http::TooManyRequests) {
try { // not sure if this body is guaranteed
@@ -415,54 +439,108 @@ void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::re
} else {
m_signal_message_send_fail.emit(nonce, 0);
}
// todo actually callback with correct error code (not necessary rn)
callback(DiscordError::GENERIC);
} else {
callback(DiscordError::NONE);
}
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
// @([^@#]{1,32})#(\\d{4})
void DiscordClient::SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
const auto nonce = std::to_string(Snowflake::FromNow());
CreateMessageObject obj;
obj.Content = content;
obj.Content = params.Message;
obj.Nonce = nonce;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
// dummy data so the content can be shown while waiting for MESSAGE_CREATE
if (params.InReplyToID.IsValid())
obj.MessageReference.emplace().MessageID = params.InReplyToID;
m_http.MakePOST("/channels/" + std::to_string(params.ChannelID) + "/messages",
nlohmann::json(obj).dump(),
[this, nonce, callback](const http::response_type &r) {
ChatMessageCallback(nonce, r, callback);
});
// dummy preview data
Message tmp;
tmp.Content = content;
tmp.Content = params.Message;
tmp.ID = nonce;
tmp.ChannelID = channel;
tmp.ChannelID = params.ChannelID;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
tmp.Nonce = obj.Nonce;
tmp.Nonce = nonce;
tmp.IsPending = true;
m_store.SetMessage(tmp.ID, tmp);
m_signal_message_sent.emit(tmp);
m_signal_message_create.emit(tmp);
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
void DiscordClient::SendChatMessageAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
const auto nonce = std::to_string(Snowflake::FromNow());
CreateMessageObject obj;
obj.Content = content;
obj.Content = params.Message;
obj.Nonce = nonce;
obj.MessageReference.emplace().MessageID = referenced_message;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
if (params.InReplyToID.IsValid())
obj.MessageReference.emplace().MessageID = params.InReplyToID;
auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(params.ChannelID) + "/messages");
m_progress_cb_timer.start();
req.set_progress_callback([this, nonce](curl_off_t ultotal, curl_off_t ulnow) {
if (m_progress_cb_timer.elapsed() < 0.0417) return; // try to prevent it from blocking ui
m_progress_cb_timer.start();
m_generic_mutex.lock();
m_generic_queue.push([this, nonce, ultotal, ulnow] {
m_signal_message_progress.emit(
nonce,
static_cast<float>(ulnow) / static_cast<float>(ultotal));
});
m_generic_mutex.unlock();
m_generic_dispatch.emit();
});
req.make_form();
req.add_field("payload_json", nlohmann::json(obj).dump().c_str(), CURL_ZERO_TERMINATED);
for (size_t i = 0; i < params.Attachments.size(); i++) {
const auto field_name = "files[" + std::to_string(i) + "]";
req.add_file(field_name, params.Attachments.at(i).File, params.Attachments.at(i).Filename);
}
m_http.Execute(std::move(req), [this, params, nonce, callback](const http::response_type &res) {
for (const auto &attachment : params.Attachments) {
if (attachment.Type == ChatSubmitParams::AttachmentType::PastedImage) {
attachment.File->remove();
}
}
ChatMessageCallback(nonce, res, callback);
});
// dummy preview data
Message tmp;
tmp.Content = content;
tmp.Content = params.Message;
tmp.ID = nonce;
tmp.ChannelID = channel;
tmp.ChannelID = params.ChannelID;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
tmp.Nonce = obj.Nonce;
tmp.Nonce = nonce;
tmp.IsPending = true;
m_store.SetMessage(tmp.ID, tmp);
m_signal_message_sent.emit(tmp);
m_signal_message_create.emit(tmp);
}
void DiscordClient::SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
if (params.Attachments.empty())
SendChatMessageNoAttachments(params, callback);
else
SendChatMessageAttachments(params, callback);
}
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
@@ -508,10 +586,6 @@ void DiscordClient::SendThreadLazyLoad(Snowflake id) {
m_websocket.Send(msg);
}
void DiscordClient::JoinGuild(const std::string &code) {
m_http.MakePOST("/invites/" + code, "{}", [](auto) {});
}
void DiscordClient::LeaveGuild(Snowflake id) {
m_http.MakeDELETE("/users/@me/guilds/" + std::to_string(id), [](auto) {});
}
@@ -546,19 +620,6 @@ void DiscordClient::UpdateStatus(PresenceStatus status, bool is_afk, const Activ
m_signal_presence_update.emit(GetUserData(), status);
}
void DiscordClient::CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback) {
CreateDMObject obj;
obj.Recipients.push_back(user_id);
m_http.MakePOST("/users/@me/channels", nlohmann::json(obj).dump(), [callback](const http::response &response) {
if (!CheckCode(response)) {
callback(DiscordError::NONE, Snowflake::Invalid);
return;
}
auto channel = nlohmann::json::parse(response.text).get<ChannelData>();
callback(GetCodeFromResponse(response), channel.ID);
});
}
void DiscordClient::CloseDM(Snowflake channel_id) {
m_http.MakeDELETE("/channels/" + std::to_string(channel_id), [](const http::response &response) {
CheckCode(response);
@@ -1117,6 +1178,92 @@ 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) {
if (!id.IsValid()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
} else {
const auto channel = GetChannel(id);
if (channel.has_value()) {
if (channel->IsDM()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me/" + std::to_string(id));
} else if (channel->GuildID.has_value()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/" + std::to_string(*channel->GuildID) + "/" + std::to_string(id));
} else {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
}
} else {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
}
}
}
void DiscordClient::SetBuildNumber(uint32_t build_number) {
m_build_number = build_number;
}
void DiscordClient::SetCookie(std::string_view cookie) {
m_http.SetCookie(cookie);
}
void DiscordClient::UpdateToken(const std::string &token) {
if (!IsStarted()) {
m_token = token;
@@ -1409,6 +1556,14 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::GUILD_MEMBERS_CHUNK: {
HandleGatewayGuildMembersChunk(m);
} break;
#ifdef WITH_VOICE
case GatewayEvent::VOICE_STATE_UPDATE: {
HandleGatewayVoiceStateUpdate(m);
} break;
case GatewayEvent::VOICE_SERVER_UPDATE: {
HandleGatewayVoiceServerUpdate(m);
} break;
#endif
}
} break;
default:
@@ -1622,18 +1777,27 @@ void DiscordClient::HandleGatewayChannelDelete(const GatewayMessage &msg) {
it->second.erase(id);
m_store.ClearChannel(id);
m_signal_channel_delete.emit(id);
m_signal_channel_accessibility_changed.emit(id, false);
}
void DiscordClient::HandleGatewayChannelUpdate(const GatewayMessage &msg) {
const auto id = msg.Data.at("id").get<Snowflake>();
auto cur = m_store.GetChannel(id);
if (cur.has_value()) {
const bool old_perms = HasChannelPermission(m_user_data.ID, id, Permission::VIEW_CHANNEL);
cur->update_from_json(msg.Data);
m_store.SetChannel(id, *cur);
if (cur->PermissionOverwrites.has_value())
for (const auto &p : *cur->PermissionOverwrites)
m_store.SetPermissionOverwrite(id, p.ID, p);
m_signal_channel_update.emit(id);
const bool new_perms = HasChannelPermission(m_user_data.ID, id, Permission::VIEW_CHANNEL);
if (old_perms && !new_perms)
m_signal_channel_accessibility_changed.emit(id, false);
else if (!old_perms && new_perms)
m_signal_channel_accessibility_changed.emit(id, true);
}
}
@@ -1660,29 +1824,37 @@ void DiscordClient::HandleGatewayGuildUpdate(const GatewayMessage &msg) {
void DiscordClient::HandleGatewayGuildRoleUpdate(const GatewayMessage &msg) {
GuildRoleUpdateObject data = msg.Data;
const auto channels = GetChannelsInGuild(data.GuildID);
std::unordered_set<Snowflake> accessible;
for (auto channel : channels) {
if (HasChannelPermission(m_user_data.ID, channel, Permission::VIEW_CHANNEL))
accessible.insert(channel);
}
m_store.SetRole(data.GuildID, data.Role);
m_signal_role_update.emit(data.GuildID, data.Role.ID);
for (auto channel : channels) {
const bool old_perms = accessible.find(channel) != accessible.end();
const bool new_perms = HasChannelPermission(m_user_data.ID, channel, Permission::VIEW_CHANNEL);
if (old_perms && !new_perms) {
m_signal_channel_accessibility_changed.emit(channel, false);
} else if (!old_perms && new_perms) {
m_signal_channel_accessibility_changed.emit(channel, true);
}
}
}
void DiscordClient::HandleGatewayGuildRoleCreate(const GatewayMessage &msg) {
GuildRoleCreateObject data = msg.Data;
auto guild = *m_store.GetGuild(data.GuildID);
guild.Roles->push_back(data.Role);
m_store.BeginTransaction();
m_store.SetRole(guild.ID, data.Role);
m_store.SetGuild(guild.ID, guild);
m_store.EndTransaction();
m_store.SetRole(data.GuildID, data.Role);
m_signal_role_create.emit(data.GuildID, data.Role.ID);
}
void DiscordClient::HandleGatewayGuildRoleDelete(const GatewayMessage &msg) {
GuildRoleDeleteObject data = msg.Data;
auto guild = *m_store.GetGuild(data.GuildID);
const auto pred = [id = data.RoleID](const RoleData &role) -> bool {
return role.ID == id;
};
guild.Roles->erase(std::remove_if(guild.Roles->begin(), guild.Roles->end(), pred), guild.Roles->end());
m_store.SetGuild(guild.ID, guild);
m_store.ClearRole(data.RoleID);
m_signal_role_delete.emit(data.GuildID, data.RoleID);
}
@@ -2002,6 +2174,61 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) {
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) {
ReadySupplementalData data = msg.Data;
for (const auto &p : data.MergedPresences.Friends) {
@@ -2018,6 +2245,17 @@ void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) {
m_user_to_status[p.UserID] = PresenceStatus::DND;
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) {
@@ -2152,7 +2390,7 @@ void DiscordClient::HandleGatewayGuildCreate(const GatewayMessage &msg) {
void DiscordClient::HandleGatewayGuildDelete(const GatewayMessage &msg) {
Snowflake id = msg.Data.at("id");
bool unavailable = msg.Data.contains("unavilable") && msg.Data.at("unavailable").get<bool>();
bool unavailable = msg.Data.contains("unavailable") && msg.Data.at("unavailable").get<bool>();
if (unavailable)
printf("guild %" PRIu64 " became unavailable\n", static_cast<uint64_t>(id));
@@ -2165,9 +2403,12 @@ void DiscordClient::HandleGatewayGuildDelete(const GatewayMessage &msg) {
}
m_store.ClearGuild(id);
if (guild->Channels.has_value())
for (const auto &c : *guild->Channels)
if (guild->Channels.has_value()) {
for (const auto &c : *guild->Channels) {
m_store.ClearChannel(c.ID);
m_signal_channel_accessibility_changed.emit(c.ID, false);
}
}
m_signal_guild_delete.emit(id);
}
@@ -2210,7 +2451,7 @@ void DiscordClient::HeartbeatThread() {
void DiscordClient::SendIdentify() {
IdentifyMessage msg;
msg.Token = m_token;
msg.Capabilities = 125; // no idea what this is
msg.Capabilities = 509; // no idea what this is
msg.Properties.OS = "Windows";
msg.Properties.Browser = "Chrome";
msg.Properties.Device = "";
@@ -2223,7 +2464,7 @@ void DiscordClient::SendIdentify() {
msg.Properties.ReferrerCurrent = "";
msg.Properties.ReferringDomainCurrent = "";
msg.Properties.ReleaseChannel = "stable";
msg.Properties.ClientBuildNumber = 105691;
msg.Properties.ClientBuildNumber = m_build_number;
msg.Properties.ClientEventSource = "";
msg.Presence.Status = "online";
msg.Presence.Since = 0;
@@ -2232,6 +2473,7 @@ void DiscordClient::SendIdentify() {
msg.ClientState.HighestLastMessageID = "0";
msg.ClientState.ReadStateVersion = 0;
msg.ClientState.UserGuildSettingsVersion = -1;
SetSuperPropertiesFromIdentity(msg);
const bool b = m_websocket.GetPrintMessages();
m_websocket.SetPrintMessages(false);
m_websocket.Send(msg);
@@ -2246,12 +2488,41 @@ void DiscordClient::SendResume() {
m_websocket.Send(msg);
}
void DiscordClient::SetHeaders() {
m_http.SetPersistentHeader("Sec-Fetch-Dest", "empty");
m_http.SetPersistentHeader("Sec-Fetch-Mode", "cors");
m_http.SetPersistentHeader("Sec-Fetch-Site", "same-origin");
m_http.SetPersistentHeader("X-Debug-Options", "bugReporterEnabled");
m_http.SetPersistentHeader("Accept-Language", "en-US,en;q=0.9");
SetReferringChannel(Snowflake::Invalid);
}
void DiscordClient::SetSuperPropertiesFromIdentity(const IdentifyMessage &identity) {
nlohmann::ordered_json j;
j["os"] = identity.Properties.OS;
j["browser"] = identity.Properties.Browser;
j["device"] = identity.Properties.Device;
j["system_locale"] = identity.Properties.SystemLocale;
j["browser_user_agent"] = identity.Properties.BrowserUserAgent;
j["browser_version"] = identity.Properties.BrowserVersion;
j["os_version"] = identity.Properties.OSVersion;
j["referrer"] = identity.Properties.Referrer;
j["referring_domain"] = identity.Properties.ReferringDomain;
j["referrer_current"] = identity.Properties.ReferrerCurrent;
j["referring_domain_current"] = identity.Properties.ReferringDomainCurrent;
j["release_channel"] = identity.Properties.ReleaseChannel;
j["client_build_number"] = identity.Properties.ClientBuildNumber;
j["client_event_source"] = nullptr; // probably will never be non-null ("") anyways
m_http.SetPersistentHeader("X-Super-Properties", Glib::Base64::encode(j.dump()));
m_http.SetPersistentHeader("X-Discord-Locale", identity.Properties.SystemLocale);
}
void DiscordClient::HandleSocketOpen() {
}
void DiscordClient::HandleSocketClose(uint16_t code) {
printf("got socket close code: %d\n", code);
auto close_code = static_cast<GatewayCloseCode>(code);
void DiscordClient::HandleSocketClose(const ix::WebSocketCloseInfo &info) {
auto close_code = static_cast<GatewayCloseCode>(info.code);
auto cb = [this, close_code]() {
m_heartbeat_waiter.kill();
if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join();
@@ -2311,9 +2582,11 @@ void DiscordClient::StoreMessageData(Message &msg) {
if (msg.Member.has_value())
m_store.SetGuildMember(*msg.GuildID, msg.Author.ID, *msg.Member);
if (msg.Interaction.has_value() && msg.Interaction->Member.has_value()) {
if (msg.Interaction.has_value()) {
m_store.SetUser(msg.Interaction->User.ID, msg.Interaction->User);
m_store.SetGuildMember(*msg.GuildID, msg.Interaction->User.ID, *msg.Interaction->Member);
if (msg.Interaction->Member.has_value()) {
m_store.SetGuildMember(*msg.GuildID, msg.Interaction->User.ID, *msg.Interaction->Member);
}
}
m_store.EndTransaction();
@@ -2414,6 +2687,44 @@ 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() {
m_event_map["READY"] = GatewayEvent::READY;
m_event_map["MESSAGE_CREATE"] = GatewayEvent::MESSAGE_CREATE;
@@ -2459,12 +2770,18 @@ void DiscordClient::LoadEventMap() {
m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK;
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["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() {
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() {
return m_signal_message_create;
}
@@ -2513,6 +2830,10 @@ DiscordClient::type_signal_connected DiscordClient::signal_connected() {
return m_signal_connected;
}
DiscordClient::type_signal_message_progress DiscordClient::signal_message_progress() {
return m_signal_message_progress;
}
DiscordClient::type_signal_role_update DiscordClient::signal_role_update() {
return m_signal_role_update;
}
@@ -2657,6 +2978,44 @@ DiscordClient::type_signal_guild_unmuted DiscordClient::signal_guild_unmuted() {
return m_signal_guild_unmuted;
}
DiscordClient::type_signal_channel_accessibility_changed DiscordClient::signal_channel_accessibility_changed() {
return m_signal_channel_accessibility_changed;
}
DiscordClient::type_signal_message_send_fail DiscordClient::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,8 +1,11 @@
#pragma once
#include "websocket.hpp"
#include "chatsubmitparams.hpp"
#include "waiter.hpp"
#include "httpclient.hpp"
#include "objects.hpp"
#include "store.hpp"
#include "voiceclient.hpp"
#include "websocket.hpp"
#include <sigc++/sigc++.h>
#include <nlohmann/json.hpp>
#include <thread>
@@ -17,31 +20,6 @@
#undef GetMessage
#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 DiscordClient {
friend class Abaddon;
@@ -49,7 +27,7 @@ class DiscordClient {
public:
DiscordClient(bool mem_store = false);
void Start();
void Stop();
bool Stop();
bool IsStarted() const;
bool IsStoreValid() const;
@@ -96,27 +74,27 @@ public:
bool IsThreadJoined(Snowflake thread_id) const;
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
bool HasSelfChannelPermission(Snowflake channel_id, Permission perm) const;
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
bool HasChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const;
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
void ChatMessageCallback(const std::string &nonce, const http::response_type &response);
void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessageAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessage(const std::string &content, Snowflake channel);
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
void SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void DeleteMessage(Snowflake channel_id, Snowflake id);
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id);
void SendThreadLazyLoad(Snowflake id);
void JoinGuild(const std::string &code);
void LeaveGuild(Snowflake id);
void KickUser(Snowflake user_id, Snowflake guild_id);
void BanUser(Snowflake user_id, Snowflake guild_id); // todo: reason, delete messages
void UpdateStatus(PresenceStatus status, bool is_afk);
void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj);
void CreateDM(Snowflake user_id, const sigc::slot<void(DiscordError code, Snowflake channel_id)> &callback);
void CloseDM(Snowflake channel_id);
std::optional<Snowflake> FindDM(Snowflake user_id); // wont find group dms
void AddReaction(Snowflake id, Glib::ustring param);
@@ -202,6 +180,26 @@ public:
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
#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 SetBuildNumber(uint32_t build_number);
void SetCookie(std::string_view cookie);
void UpdateToken(const std::string &token);
void SetUserAgent(const std::string &agent);
@@ -279,12 +277,21 @@ private:
void HandleGatewayReadySupplemental(const GatewayMessage &msg);
void HandleGatewayReconnect(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 SendIdentify();
void SendResume();
void SetHeaders();
void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity);
void HandleSocketOpen();
void HandleSocketClose(uint16_t code);
void HandleSocketClose(const ix::WebSocketCloseInfo &info);
static bool CheckCode(const http::response_type &r);
static bool CheckCode(const http::response_type &r, int expected);
@@ -296,6 +303,8 @@ private:
std::string m_token;
uint32_t m_build_number = 142000;
void AddUserToGuild(Snowflake user_id, Snowflake guild_id);
std::map<Snowflake, std::set<Snowflake>> m_guild_to_users;
std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels;
@@ -326,13 +335,33 @@ private:
std::thread m_heartbeat_thread;
std::atomic<int> m_last_sequence = -1;
std::atomic<int> m_heartbeat_msec = 0;
HeartbeatWaiter m_heartbeat_waiter;
Waiter m_heartbeat_waiter;
std::atomic<bool> m_heartbeat_acked = true;
bool m_reconnecting = false; // reconnecting either to resume or reidentify
bool m_wants_resume = false; // reconnecting specifically to resume
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;
Glib::Dispatcher m_msg_dispatch;
std::queue<std::string> m_msg_queue;
@@ -342,12 +371,15 @@ private:
Glib::Dispatcher m_generic_dispatch;
std::queue<std::function<void()>> m_generic_queue;
Glib::Timer m_progress_cb_timer;
std::set<Snowflake> m_channels_pinned_requested;
std::set<Snowflake> m_channels_lazy_loaded;
// signals
public:
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, Snowflake, Snowflake> type_signal_message_delete;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_message_update;
@@ -396,12 +428,26 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_channel_unmuted;
typedef sigc::signal<void, Snowflake> type_signal_guild_muted;
typedef sigc::signal<void, Snowflake> type_signal_guild_unmuted;
typedef sigc::signal<void, Snowflake, bool> type_signal_channel_accessibility_changed;
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
typedef sigc::signal<void, std::string, float> type_signal_message_progress;
#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_supplemental signal_gateway_ready_supplemental();
type_signal_message_create signal_message_create();
type_signal_message_delete signal_message_delete();
type_signal_message_update signal_message_update();
@@ -449,12 +495,26 @@ public:
type_signal_channel_unmuted signal_channel_unmuted();
type_signal_guild_muted signal_guild_muted();
type_signal_guild_unmuted signal_guild_unmuted();
type_signal_channel_accessibility_changed signal_channel_accessibility_changed();
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
type_signal_message_progress signal_message_progress();
#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:
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_delete m_signal_message_delete;
type_signal_message_update m_signal_message_update;
@@ -502,7 +562,20 @@ protected:
type_signal_channel_unmuted m_signal_channel_unmuted;
type_signal_guild_muted m_signal_guild_muted;
type_signal_guild_unmuted m_signal_guild_unmuted;
type_signal_channel_accessibility_changed m_signal_channel_accessibility_changed;
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;
type_signal_message_progress m_signal_message_progress;
#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

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage::MemberItem
JS_D("mute", m.IsMuted);
JS_D("joined_at", m.JoinedAt);
JS_D("deaf", m.IsDefeaned);
JS_N("hoisted_role", m.HoistedRole);
JS_ON("hoisted_role", m.HoistedRole);
JS_ON("premium_since", m.PremiumSince);
JS_ON("nick", m.Nickname);
JS_ON("presence", m.Presence);
@@ -233,8 +233,14 @@ void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m) {
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) {
JS_D("merged_presences", m.MergedPresences);
JS_D("guilds", m.Guilds);
}
void to_json(nlohmann::json &j, const IdentifyProperties &m) {
@@ -262,6 +268,7 @@ void to_json(nlohmann::json &j, const ClientStateProperties &m) {
j["highest_last_message_id"] = m.HighestLastMessageID;
j["read_state_version"] = m.ReadStateVersion;
j["user_guild_settings_version"] = m.UserGuildSettingsVersion;
j["user_settings_version"] = m.UserSettingsVersion;
}
void to_json(nlohmann::json &j, const IdentifyMessage &m) {
@@ -639,3 +646,43 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) {
JS_D("members", m.Members);
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,6 +100,8 @@ enum class GatewayEvent : int {
MESSAGE_ACK,
USER_GUILD_SETTINGS_UPDATE,
GUILD_MEMBERS_CHUNK,
VOICE_STATE_UPDATE,
VOICE_SERVER_UPDATE,
};
enum class GatewayCloseCode : uint16_t {
@@ -352,8 +354,18 @@ struct SupplementalMergedPresencesData {
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 {
SupplementalMergedPresencesData MergedPresences;
std::vector<SupplementalGuildEntry> Guilds;
friend void from_json(const nlohmann::json &j, ReadySupplementalData &m);
};
@@ -382,6 +394,7 @@ struct ClientStateProperties {
std::string HighestLastMessageID = "0";
int ReadStateVersion = 0;
int UserGuildSettingsVersion = -1;
int UserSettingsVersion = -1;
friend void to_json(nlohmann::json &j, const ClientStateProperties &m);
};
@@ -863,3 +876,42 @@ struct GuildMembersChunkData {
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

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

View File

@@ -54,6 +54,7 @@ public:
void ClearChannel(Snowflake id);
void ClearBan(Snowflake guild_id, Snowflake user_id);
void ClearRecipient(Snowflake channel_id, Snowflake user_id);
void ClearRole(Snowflake id);
std::unordered_set<Snowflake> GetChannels() const;
std::unordered_set<Snowflake> GetGuilds() const;
@@ -280,6 +281,7 @@ private:
STMT(set_interaction);
STMT(set_member_roles);
STMT(get_member_roles);
STMT(clr_member_roles);
STMT(set_guild_emoji);
STMT(get_guild_emojis);
STMT(clr_guild_emoji);
@@ -305,5 +307,6 @@ private:
STMT(get_reactions);
STMT(get_chan_ids_parent);
STMT(get_guild_member_ids);
STMT(clr_role);
#undef STMT
};

View File

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

View File

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

View File

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

View File

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

544
src/discord/voiceclient.cpp Normal file
View File

@@ -0,0 +1,544 @@
#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

288
src/discord/voiceclient.hpp Normal file
View File

@@ -0,0 +1,288 @@
#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

29
src/discord/waiter.hpp Normal file
View File

@@ -0,0 +1,29 @@
#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,14 +1,33 @@
#include "websocket.hpp"
#include <spdlog/sinks/stdout_color_sinks.h>
#include <utility>
Websocket::Websocket() = default;
Websocket::Websocket(const std::string &id)
: 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) {
m_websocket.disableAutomaticReconnection();
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();
m_log->debug("Starting connection to {}", url);
m_websocket = std::make_unique<ix::WebSocket>();
m_websocket->disableAutomaticReconnection();
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) {
@@ -24,17 +43,19 @@ void Websocket::SetPrintMessages(bool show) noexcept {
}
void Websocket::Stop() {
m_log->debug("Stopping with default close code");
Stop(ix::WebSocketCloseConstants::kNormalClosureCode);
}
void Websocket::Stop(uint16_t code) {
m_websocket.stop(code);
m_log->debug("Stopping with close code {}", code);
m_websocket-> stop(code);
}
void Websocket::Send(const std::string &str) {
if (m_print_messages)
printf("sending %s\n", str.c_str());
m_websocket.sendText(str);
m_log->trace("Send: {}", str);
m_websocket->sendText(str);
}
void Websocket::Send(const nlohmann::json &j) {
@@ -44,10 +65,13 @@ void Websocket::Send(const nlohmann::json &j) {
void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) {
switch (msg->type) {
case ix::WebSocketMessageType::Open: {
m_signal_open.emit();
m_log->debug("Received open frame, dispatching");
m_open_dispatcher.emit();
} break;
case ix::WebSocketMessageType::Close: {
m_signal_close.emit(msg->closeInfo.code);
m_log->debug("Received close frame, dispatching. {} ({}){}", msg->closeInfo.code, msg->closeInfo.reason, msg->closeInfo.remote ? " Remote" : "");
m_close_info = msg->closeInfo;
m_close_dispatcher.emit();
} break;
case ix::WebSocketMessageType::Message: {
m_signal_message.emit(msg->str);

View File

@@ -3,12 +3,14 @@
#include <ixwebsocket/IXWebSocket.h>
#include <string>
#include <functional>
#include <glibmm.h>
#include <nlohmann/json.hpp>
#include <sigc++/sigc++.h>
#include <spdlog/spdlog.h>
class Websocket {
public:
Websocket();
Websocket(const std::string &id);
void StartConnection(const std::string &url);
void SetUserAgent(std::string agent);
@@ -24,12 +26,12 @@ public:
private:
void OnMessage(const ix::WebSocketMessagePtr &msg);
ix::WebSocket m_websocket;
std::unique_ptr<ix::WebSocket> m_websocket;
std::string m_agent;
public:
using type_signal_open = sigc::signal<void>;
using type_signal_close = sigc::signal<void, uint16_t>;
using type_signal_close = sigc::signal<void, ix::WebSocketCloseInfo>;
using type_signal_message = sigc::signal<void, std::string>;
type_signal_open signal_open();
@@ -42,4 +44,10 @@ private:
type_signal_message m_signal_message;
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

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

View File

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

View File

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

View File

@@ -39,6 +39,7 @@ void SettingsManager::ReadSettings() {
SMSTR("discord", "token", DiscordToken);
SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect);
SMSTR("gui", "css", MainCSS);
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
SMBOOL("gui", "animations", ShowAnimations);
@@ -48,6 +49,8 @@ void SettingsManager::ReadSettings() {
SMBOOL("gui", "save_state", SaveState);
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
SMBOOL("gui", "unreads", Unreads);
SMBOOL("gui", "alt_menu", AltMenu);
SMBOOL("gui", "hide_to_tray", HideToTray);
SMINT("http", "concurrent", CacheHTTPConcurrency);
SMSTR("http", "user_agent", UserAgent);
SMSTR("style", "expandercolor", ChannelsExpanderColor);
@@ -92,6 +95,7 @@ void SettingsManager::Close() {
SMSTR("discord", "token", DiscordToken);
SMBOOL("discord", "memory_db", UseMemoryDB);
SMBOOL("discord", "prefetch", Prefetch);
SMBOOL("discord", "autoconnect", Autoconnect);
SMSTR("gui", "css", MainCSS);
SMBOOL("gui", "animated_guild_hover_only", AnimatedGuildHoverOnly);
SMBOOL("gui", "animations", ShowAnimations);
@@ -101,6 +105,8 @@ void SettingsManager::Close() {
SMBOOL("gui", "save_state", SaveState);
SMBOOL("gui", "stock_emojis", ShowStockEmojis);
SMBOOL("gui", "unreads", Unreads);
SMBOOL("gui", "alt_menu", AltMenu);
SMBOOL("gui", "hide_to_tray", HideToTray);
SMINT("http", "concurrent", CacheHTTPConcurrency);
SMSTR("http", "user_agent", UserAgent);
SMSTR("style", "expandercolor", ChannelsExpanderColor);

View File

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

126
src/startup.cpp Normal file
View File

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

25
src/startup.hpp Normal file
View File

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

View File

@@ -24,9 +24,18 @@ void from_json(const nlohmann::json &j, ExpansionState &m) {
j.at("c").get_to(m.Children);
}
void to_json(nlohmann::json &j, const TabsState &m) {
j = m.Channels;
}
void from_json(const nlohmann::json &j, TabsState &m) {
j.get_to(m.Channels);
}
void to_json(nlohmann::json &j, const AbaddonApplicationState &m) {
j["active_channel"] = m.ActiveChannel;
j["expansion"] = m.Expansion;
j["tabs"] = m.Tabs;
}
void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
@@ -34,4 +43,6 @@ void from_json(const nlohmann::json &j, AbaddonApplicationState &m) {
j.at("active_channel").get_to(m.ActiveChannel);
if (j.contains("expansion"))
j.at("expansion").get_to(m.Expansion);
if (j.contains("tabs"))
j.at("tabs").get_to(m.Tabs);
}

View File

@@ -1,3 +1,4 @@
#pragma once
#include <vector>
#include <nlohmann/json.hpp>
#include "discord/snowflake.hpp"
@@ -18,9 +19,17 @@ struct ExpansionState {
friend void from_json(const nlohmann::json &j, ExpansionState &m);
};
struct TabsState {
std::vector<Snowflake> Channels;
friend void to_json(nlohmann::json &j, const TabsState &m);
friend void from_json(const nlohmann::json &j, TabsState &m);
};
struct AbaddonApplicationState {
Snowflake ActiveChannel;
ExpansionStateRoot Expansion;
TabsState Tabs;
friend void to_json(nlohmann::json &j, const AbaddonApplicationState &m);
friend void from_json(const nlohmann::json &j, AbaddonApplicationState &m);

View File

@@ -1,5 +1,6 @@
#include "util.hpp"
#include <array>
#include <cstring>
#include <filesystem>
void LaunchBrowser(const Glib::ustring &url) {

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ MainWindow::MainWindow()
, m_content_box(Gtk::ORIENTATION_HORIZONTAL)
, m_chan_content_paned(Gtk::ORIENTATION_HORIZONTAL)
, m_content_members_paned(Gtk::ORIENTATION_HORIZONTAL)
, m_left_pane(Gtk::ORIENTATION_VERTICAL)
, m_accels(Gtk::AccelGroup::create()) {
set_default_size(1200, 800);
get_style_context()->add_class("app-window");
@@ -27,6 +28,12 @@ MainWindow::MainWindow()
chat->set_hexpand(true);
chat->show();
#ifdef WITH_LIBHANDY
m_channel_list.signal_action_open_new_tab().connect([this](Snowflake id) {
m_chat.OpenNewTab(id);
});
#endif
m_channel_list.set_vexpand(true);
m_channel_list.set_size_request(-1, -1);
m_channel_list.show();
@@ -45,10 +52,18 @@ MainWindow::MainWindow()
m_content_stack.set_visible_child("chat");
m_content_stack.show();
m_chan_content_paned.pack1(m_channel_list);
m_voice_info.show();
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.child_property_shrink(m_channel_list) = false;
m_chan_content_paned.child_property_resize(m_channel_list) = false;
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_shrink(m_left_pane) = true;
m_chan_content_paned.child_property_resize(m_left_pane) = true;
m_chan_content_paned.set_position(200);
m_chan_content_paned.show();
m_content_box.add(m_chan_content_paned);
@@ -56,8 +71,10 @@ MainWindow::MainWindow()
m_content_members_paned.pack1(m_content_stack);
m_content_members_paned.pack2(*member_list);
m_content_members_paned.child_property_shrink(*member_list) = false;
m_content_members_paned.child_property_resize(*member_list) = false;
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_shrink(*member_list) = true;
m_content_members_paned.child_property_resize(*member_list) = true;
int w, h;
get_default_size(w, h); // :s
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
@@ -66,6 +83,7 @@ MainWindow::MainWindow()
add(m_main_box);
SetupMenu();
SetupDND();
}
void MainWindow::UpdateComponents() {
@@ -95,10 +113,10 @@ void MainWindow::UpdateChatWindowContents() {
m_members.UpdateMemberList();
}
void MainWindow::UpdateChatActiveChannel(Snowflake id) {
void MainWindow::UpdateChatActiveChannel(Snowflake id, bool expand_to) {
m_chat.SetActiveChannel(id);
m_members.SetActiveChannel(id);
m_channel_list.SetActiveChannel(id);
m_channel_list.SetActiveChannel(id, expand_to);
m_content_stack.set_visible_child("chat");
}
@@ -142,6 +160,37 @@ void MainWindow::UpdateChatReactionRemove(Snowflake id, const Glib::ustring &par
m_chat.UpdateReactions(id);
}
void MainWindow::UpdateMenus() {
OnDiscordSubmenuPopup();
OnViewSubmenuPopup();
}
void MainWindow::ToggleMenuVisibility() {
m_menu_bar.set_visible(!m_menu_bar.get_visible());
}
#ifdef WITH_LIBHANDY
void MainWindow::GoBack() {
m_chat.GoBack();
}
void MainWindow::GoForward() {
m_chat.GoForward();
}
void MainWindow::GoToPreviousTab() {
m_chat.GoToPreviousTab();
}
void MainWindow::GoToNextTab() {
m_chat.GoToNextTab();
}
void MainWindow::GoToTab(int idx) {
m_chat.GoToTab(idx);
}
#endif
void MainWindow::OnDiscordSubmenuPopup() {
auto &discord = Abaddon::Get().GetDiscordClient();
auto channel_id = GetChatActiveChannel();
@@ -157,7 +206,6 @@ void MainWindow::OnDiscordSubmenuPopup() {
std::string token = Abaddon::Get().GetDiscordToken();
m_menu_discord_connect.set_sensitive(!token.empty() && !discord_active);
m_menu_discord_disconnect.set_sensitive(discord_active);
m_menu_discord_join_guild.set_sensitive(discord_active);
m_menu_discord_set_token.set_sensitive(!discord_active);
m_menu_discord_set_status.set_sensitive(discord_active);
}
@@ -200,15 +248,12 @@ void MainWindow::SetupMenu() {
m_menu_discord_disconnect.set_label("Disconnect");
m_menu_discord_disconnect.set_sensitive(false);
m_menu_discord_set_token.set_label("Set Token");
m_menu_discord_join_guild.set_label("Accept Invite");
m_menu_discord_join_guild.set_sensitive(false);
m_menu_discord_set_status.set_label("Set Status");
m_menu_discord_set_status.set_sensitive(false);
m_menu_discord_add_recipient.set_label("Add user to DM");
m_menu_discord_sub.append(m_menu_discord_connect);
m_menu_discord_sub.append(m_menu_discord_disconnect);
m_menu_discord_sub.append(m_menu_discord_set_token);
m_menu_discord_sub.append(m_menu_discord_join_guild);
m_menu_discord_sub.append(m_menu_discord_set_status);
m_menu_discord_sub.append(m_menu_discord_add_recipient);
m_menu_discord.set_submenu(m_menu_discord_sub);
@@ -227,21 +272,43 @@ void MainWindow::SetupMenu() {
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.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE);
#ifdef WITH_LIBHANDY
m_menu_view_go_back.set_label("Go Back");
m_menu_view_go_forward.set_label("Go Forward");
m_menu_view_go_back.add_accelerator("activate", m_accels, GDK_KEY_Left, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
m_menu_view_go_forward.add_accelerator("activate", m_accels, GDK_KEY_Right, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE);
#endif
m_menu_view_sub.append(m_menu_view_friends);
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_mark_guild_as_read);
#ifdef WITH_LIBHANDY
m_menu_view_sub.append(m_menu_view_go_back);
m_menu_view_sub.append(m_menu_view_go_forward);
#endif
m_menu_bar.append(m_menu_file);
m_menu_bar.append(m_menu_discord);
m_menu_bar.append(m_menu_view);
m_menu_bar.show_all();
m_menu_bar.signal_event().connect([this](GdkEvent *ev) -> bool {
OnViewSubmenuPopup();
OnDiscordSubmenuPopup();
return false;
});
if (Abaddon::Get().GetSettings().AltMenu) {
auto set_hide_cb = [this](Gtk::Menu &menu) {
for (auto *child : menu.get_children()) {
auto *item = dynamic_cast<Gtk::MenuItem *>(child);
if (item != nullptr) {
item->signal_activate().connect([this]() {
m_menu_bar.hide();
});
}
}
};
set_hide_cb(m_menu_discord_sub);
set_hide_cb(m_menu_file_sub);
set_hide_cb(m_menu_view_sub);
m_menu_bar.show_all_children();
} else {
m_menu_bar.show_all();
}
m_menu_discord_connect.signal_activate().connect([this] {
m_signal_action_connect.emit();
@@ -255,10 +322,6 @@ void MainWindow::SetupMenu() {
m_signal_action_set_token.emit();
});
m_menu_discord_join_guild.signal_activate().connect([this] {
m_signal_action_join_guild.emit();
});
m_menu_file_reload_css.signal_activate().connect([this] {
m_signal_action_reload_css.emit();
});
@@ -276,7 +339,7 @@ void MainWindow::SetupMenu() {
});
m_menu_view_friends.signal_activate().connect([this] {
UpdateChatActiveChannel(Snowflake::Invalid);
UpdateChatActiveChannel(Snowflake::Invalid, true);
m_members.UpdateMemberList();
m_content_stack.set_visible_child("friends");
});
@@ -297,6 +360,32 @@ void MainWindow::SetupMenu() {
discord.MarkGuildAsRead(*channel->GuildID, NOOP_CALLBACK);
}
});
#ifdef WITH_LIBHANDY
m_menu_view_go_back.signal_activate().connect([this] {
GoBack();
});
m_menu_view_go_forward.signal_activate().connect([this] {
GoForward();
});
#endif
}
void MainWindow::SetupDND() {
std::vector<Gtk::TargetEntry> targets;
targets.emplace_back("text/uri-list", Gtk::TargetFlags(0), 0);
drag_dest_set(targets, Gtk::DEST_DEFAULT_DROP | Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_HIGHLIGHT, Gdk::DragAction::ACTION_COPY);
signal_drag_data_received().connect([this](const Glib::RefPtr<Gdk::DragContext> &ctx, int x, int y, const Gtk::SelectionData &selection, guint info, guint time) {
HandleDroppedURIs(selection);
});
}
void MainWindow::HandleDroppedURIs(const Gtk::SelectionData &selection) {
for (const auto &uri : selection.get_uris()) {
// not using Glib::get_filename_for_uri or whatever because the conversion is BAD (on windows at least)
m_chat.AddAttachment(Gio::File::create_for_uri(uri));
}
}
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {
@@ -315,10 +404,6 @@ MainWindow::type_signal_action_reload_css MainWindow::signal_action_reload_css()
return m_signal_action_reload_css;
}
MainWindow::type_signal_action_join_guild MainWindow::signal_action_join_guild() {
return m_signal_action_join_guild;
}
MainWindow::type_signal_action_set_status MainWindow::signal_action_set_status() {
return m_signal_action_set_status;
}
@@ -333,4 +418,4 @@ MainWindow::type_signal_action_view_pins MainWindow::signal_action_view_pins() {
MainWindow::type_signal_action_view_threads MainWindow::signal_action_view_threads() {
return m_signal_action_view_threads;
}
}

View File

@@ -3,6 +3,7 @@
#include "components/chatwindow.hpp"
#include "components/memberlist.hpp"
#include "components/friendslist.hpp"
#include "components/voiceinfobox.hpp"
#include <gtkmm.h>
class MainWindow : public Gtk::Window {
@@ -13,7 +14,7 @@ public:
void UpdateMembers();
void UpdateChannelListing();
void UpdateChatWindowContents();
void UpdateChatActiveChannel(Snowflake id);
void UpdateChatActiveChannel(Snowflake id, bool expand_to);
Snowflake GetChatActiveChannel() const;
void UpdateChatNewMessage(const Message &data);
void UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id);
@@ -23,6 +24,16 @@ public:
Snowflake GetChatOldestListedMessage();
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param);
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
void UpdateMenus();
void ToggleMenuVisibility();
#ifdef WITH_LIBHANDY
void GoBack();
void GoForward();
void GoToPreviousTab();
void GoToNextTab();
void GoToTab(int idx);
#endif
ChannelList *GetChannelList();
ChatWindow *GetChatWindow();
@@ -30,6 +41,9 @@ public:
private:
void SetupMenu();
void SetupDND();
void HandleDroppedURIs(const Gtk::SelectionData &selection);
Gtk::Box m_main_box;
Gtk::Box m_content_box;
@@ -40,6 +54,9 @@ private:
ChatWindow m_chat;
MemberList m_members;
FriendsList m_friends;
VoiceInfoBox m_voice_info;
Gtk::Box m_left_pane;
Gtk::Stack m_content_stack;
@@ -51,7 +68,6 @@ private:
Gtk::MenuItem m_menu_discord_connect;
Gtk::MenuItem m_menu_discord_disconnect;
Gtk::MenuItem m_menu_discord_set_token;
Gtk::MenuItem m_menu_discord_join_guild;
Gtk::MenuItem m_menu_discord_set_status;
Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day
void OnDiscordSubmenuPopup();
@@ -67,6 +83,11 @@ private:
Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads;
Gtk::MenuItem m_menu_view_mark_guild_as_read;
#ifdef WITH_LIBHANDY
Gtk::MenuItem m_menu_view_go_back;
Gtk::MenuItem m_menu_view_go_forward;
#endif
void OnViewSubmenuPopup();
public:
@@ -74,7 +95,6 @@ public:
typedef sigc::signal<void> type_signal_action_disconnect;
typedef sigc::signal<void> type_signal_action_set_token;
typedef sigc::signal<void> type_signal_action_reload_css;
typedef sigc::signal<void> type_signal_action_join_guild;
typedef sigc::signal<void> type_signal_action_set_status;
// this should probably be removed
typedef sigc::signal<void, Snowflake> type_signal_action_add_recipient; // channel id
@@ -85,7 +105,6 @@ public:
type_signal_action_disconnect signal_action_disconnect();
type_signal_action_set_token signal_action_set_token();
type_signal_action_reload_css signal_action_reload_css();
type_signal_action_join_guild signal_action_join_guild();
type_signal_action_set_status signal_action_set_status();
type_signal_action_add_recipient signal_action_add_recipient();
type_signal_action_view_pins signal_action_view_pins();
@@ -96,7 +115,6 @@ private:
type_signal_action_disconnect m_signal_action_disconnect;
type_signal_action_set_token m_signal_action_set_token;
type_signal_action_reload_css m_signal_action_reload_css;
type_signal_action_join_guild m_signal_action_join_guild;
type_signal_action_set_status m_signal_action_set_status;
type_signal_action_add_recipient m_signal_action_add_recipient;
type_signal_action_view_pins m_signal_action_view_pins;

View File

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

View File

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

View File

@@ -0,0 +1,125 @@
#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

@@ -0,0 +1,25 @@
#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

254
src/windows/voicewindow.cpp Normal file
View File

@@ -0,0 +1,254 @@
#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

@@ -0,0 +1,85 @@
#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/miniaudio Submodule

Submodule subprojects/miniaudio added at 4dfe7c4c31