diff --git a/.CI/CreateAppImage.sh b/.CI/CreateAppImage.sh index 486c14ff6..d245ec9f1 100755 --- a/.CI/CreateAppImage.sh +++ b/.CI/CreateAppImage.sh @@ -57,3 +57,6 @@ exec "$here/usr/bin/chatterino" "$@"' > appdir/AppRun chmod a+x appdir/AppRun ./appimagetool-x86_64.AppImage appdir + +# TODO: Create appimage in a unique directory instead maybe idk? +rm -rf appdir diff --git a/.CI/CreateUbuntuDeb.sh b/.CI/CreateUbuntuDeb.sh index 752f211b3..ac627d649 100755 --- a/.CI/CreateUbuntuDeb.sh +++ b/.CI/CreateUbuntuDeb.sh @@ -1,6 +1,37 @@ #!/bin/sh + set -e +breakline() { + printf "================================================================================\n\n" +} + +# Configured in the CI step +install_prefix="appdir/usr" + +# The directory we finally pack into our .deb package +packaging_dir="package" + +# Get the Ubuntu Release (e.g. 20.04 or 22.04) +ubuntu_release="$(lsb_release -rs)" + +# Refactor opportunity: +case "$ubuntu_release" in + 20.04) + dependencies="libc6, libstdc++6, libqt5core5a, libqt5concurrent5, libqt5dbus5, libqt5gui5, libqt5network5, libqt5svg5, libqt5widgets5, qt5-image-formats-plugins, libboost-filesystem1.71.0" + ;; + 22.04) + dependencies="libc6, libstdc++6, libqt5core5a, libqt5concurrent5, libqt5dbus5, libqt5gui5, libqt5network5, libqt5svg5, libqt5widgets5, qt5-image-formats-plugins, libboost-filesystem1.74.0" + ;; + *) + echo "Unsupported Ubuntu release $ubuntu_release" + exit 1 + ;; +esac + +echo "Building Ubuntu .deb file on '$ubuntu_release'" +echo "Dependencies: $dependencies" + if [ ! -f ./bin/chatterino ] || [ ! -x ./bin/chatterino ]; then echo "ERROR: No chatterino binary file found. This script must be run in the build folder, and chatterino must be built first." exit 1 @@ -8,33 +39,53 @@ fi chatterino_version=$(git describe 2>/dev/null | cut -c 2-) || true if [ -z "$chatterino_version" ]; then + # Fall back to this in case the build happened outside of a git repo chatterino_version="0.0.0-dev" - echo "Falling back to setting the version to '$chatterino_version'" -else - echo "Found Chatterino version $chatterino_version via git" fi -rm -vrf "./package" || true # delete any old packaging dir +# Make sure no old remnants of a previous packaging remains +rm -vrf "$packaging_dir" -# create ./package/ from scratch -mkdir package/DEBIAN -p -packaging_dir="$(realpath ./package)" +mkdir -p "$packaging_dir/DEBIAN" echo "Making control file" cat >> "$packaging_dir/DEBIAN/control" << EOF Package: chatterino -Section: net -Priority: optional +Version: $chatterino_version Architecture: amd64 Maintainer: Mm2PL -Description: Testing out chatterino as a Ubuntu package -Depends: libc6, libqt5concurrent5, libqt5core5a, libqt5dbus5, libqt5gui5, libqt5multimedia5, libqt5network5, libqt5svg5, libqt5widgets5, libssl1.1, libstdc++6 +Depends: $dependencies +Section: net +Priority: optional +Homepage: https://github.com/Chatterino/chatterino2 +Description: Ubuntu package built for $ubuntu_release EOF -echo "Version: $chatterino_version" >> "$packaging_dir/DEBIAN/control" +cat "$packaging_dir/DEBIAN/control" +breakline -echo "Running make install in package dir" -DESTDIR="$packaging_dir" make INSTALL_ROOT="$packaging_dir" -j"$(nproc)" install; find "$packaging_dir/" -echo "" -echo "Building package..." +echo "Running make install" +make install +find "$install_prefix" +breakline + + +echo "Merge install into packaging dir" +cp -rv "$install_prefix/" "$packaging_dir/" +find "$packaging_dir" +breakline + + +echo "Building package" dpkg-deb --build "$packaging_dir" "Chatterino-x86_64.deb" +breakline + + +echo "Package info" +dpkg --info Chatterino-x86_64.deb +breakline + + +echo "Package contents" +dpkg --contents Chatterino-x86_64.deb # Shows folders and files inside .deb file +breakline diff --git a/.docker/Dockerfile-ubuntu-20.04-build b/.docker/Dockerfile-ubuntu-20.04-build new file mode 100644 index 000000000..f5a8ffa7c --- /dev/null +++ b/.docker/Dockerfile-ubuntu-20.04-build @@ -0,0 +1,54 @@ +FROM ubuntu:20.04 + +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && apt-get -y install --no-install-recommends \ + cmake \ + virtualenv \ + rapidjson-dev \ + libfuse2 \ + libssl-dev \ + libboost-dev \ + libxcb-randr0-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libpulse-dev \ + libxkbcommon-x11-0 \ + build-essential \ + libgl1-mesa-dev \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinerama0 + +RUN apt-get -y install \ + git \ + lsb-release \ + python3-pip && \ + apt-get clean all + +# Install Qt as we do in CI + +RUN pip3 install -U pip && \ + pip3 install aqtinstall && \ + aqt install-qt linux desktop 5.12.12 && \ + mkdir -p /opt/qt512 && \ + mv /5.12.12/gcc_64/* /opt/qt512 + +ADD . /src + +RUN mkdir /src/build + +# cmake +RUN cd /src/build && \ + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_PREFIX_PATH=/opt/qt512/lib/cmake \ + -DBUILD_WITH_QTKEYCHAIN=OFF \ + .. + +# build +RUN cd /src/build && \ + make -j8 diff --git a/.docker/Dockerfile-ubuntu-20.04-package b/.docker/Dockerfile-ubuntu-20.04-package new file mode 100644 index 000000000..6c41156f3 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-20.04-package @@ -0,0 +1,13 @@ +FROM chatterino-ubuntu-20.04-build + +ADD .CI /src/.CI + +WORKDIR /src/build + +# RUN apt-get install -y wget + +# create appimage +# RUN pwd && ./../.CI/CreateAppImage.sh + +# package deb +RUN pwd && ./../.CI/CreateUbuntuDeb.sh diff --git a/.docker/Dockerfile-ubuntu-22.04-build b/.docker/Dockerfile-ubuntu-22.04-build new file mode 100644 index 000000000..21f2ceb15 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-build @@ -0,0 +1,57 @@ +FROM ubuntu:22.04 + +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && apt-get -y install --no-install-recommends \ + cmake \ + virtualenv \ + rapidjson-dev \ + libfuse2 \ + libssl-dev \ + libboost-dev \ + libxcb-randr0-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libpulse-dev \ + libxkbcommon-x11-0 \ + build-essential \ + libgl1-mesa-dev \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinerama0 + +RUN apt-get -y install \ + git \ + lsb-release \ + python3-pip && \ + apt-get clean all + +# Install Qt as we do in CI + +RUN pip3 install -U pip && \ + pip3 install aqtinstall && \ + aqt install-qt linux desktop 5.15.2 && \ + mkdir -p /opt/qt515 && \ + mv /5.15.2/gcc_64/* /opt/qt515 + +ADD . /src + +RUN mkdir /src/build + +# Apply Qt patches +RUN patch "/opt/qt515/include/QtConcurrent/qtconcurrentthreadengine.h" /src/.patches/qt5-on-newer-gcc.patch + +# cmake +RUN cd /src/build && \ + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_PREFIX_PATH=/opt/qt515/lib/cmake \ + -DBUILD_WITH_QTKEYCHAIN=OFF \ + .. + +# build +RUN cd /src/build && \ + make -j8 diff --git a/.docker/Dockerfile-ubuntu-22.04-package b/.docker/Dockerfile-ubuntu-22.04-package new file mode 100644 index 000000000..193c666a2 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-package @@ -0,0 +1,8 @@ +FROM chatterino-ubuntu-22.04-build + +ADD .CI /src/.CI + +WORKDIR /src/build + +# package deb +RUN ./../.CI/CreateUbuntuDeb.sh diff --git a/.docker/README.md b/.docker/README.md new file mode 100644 index 000000000..869a1e391 --- /dev/null +++ b/.docker/README.md @@ -0,0 +1,29 @@ +## Groups + +### Ubuntu 20.04 package + +`Dockerfile-ubuntu-20.04-package` relies on `Dockerfile-ubuntu-20.04-build` + +To build, from the repo root + +1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 20.04 + `docker build -t chatterino-ubuntu-20.04-build -f .docker/Dockerfile-ubuntu-20.04-build .` +1. Build a docker image that uses the above-built image & packages it into a .deb file + `docker build -t chatterino-ubuntu-20.04-package -f .docker/Dockerfile-ubuntu-20.04-package .` + +To extract the final package, you can run the following command: +`docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-20.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"` + +### Ubuntu 22.04 package + +`Dockerfile-ubuntu-22.04-package` relies on `Dockerfile-ubuntu-22.04-build` + +To build, from the repo root + +1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 22.04 + `docker build -t chatterino-ubuntu-22.04-build -f .docker/Dockerfile-ubuntu-22.04-build .` +1. Build a docker image that uses the above-built image & packages it into a .deb file + `docker build -t chatterino-ubuntu-22.04-package -f .docker/Dockerfile-ubuntu-22.04-package .` + +To extract the final package, you can run the following command: +`docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-22.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"` diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..1d2b1be66 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +build* +.mypy_cache +.cache +.docker diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8355de004..cd7a57cfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,19 +17,41 @@ env: jobs: build: + name: "Build ${{ matrix.os }}, Qt ${{ matrix.qt-version }} (PCH:${{ matrix.pch }}, LTO:${{ matrix.force-lto }})" runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-20.04, macos-latest] + os: [windows-latest, macos-latest] qt-version: [5.15.2, 5.12.12] pch: [true] force-lto: [false] plugins: [false] + skip_artifact: ["no"] + crashpad: [true] include: + # Ubuntu 20.04, Qt 5.12 - os: ubuntu-20.04 + qt-version: 5.12.12 + pch: true + force-lto: false + # Ubuntu 22.04, Qt 5.15 + - os: ubuntu-22.04 + qt-version: 5.15.2 + pch: true + force-lto: false + # Test for disabling Precompiled Headers & enabling link-time optimization + - os: ubuntu-22.04 qt-version: 5.15.2 pch: false force-lto: true + skip_artifact: "yes" + # Test for disabling crashpad on Windows + - os: windows-latest + qt-version: 5.15.2 + pch: false + force-lto: true + skip_artifact: "yes" + crashpad: false - os: ubuntu-20.04 qt-version: 5.15.2 pch: true @@ -45,7 +67,6 @@ jobs: pch: true force-lto: false plugins: true - fail-fast: false steps: @@ -59,6 +80,18 @@ jobs: if: matrix.plugins == true run: | echo "C2_PLUGINS=ON" >> "$GITHUB_ENV" + echo "artifact_descr=plugins" >> "$GITHUB_ENV" + shell: bash + + - name: Disable plugin support + if: matrix.plugins == false + echo "artifact_descr=no-plugins" >> "$GITHUB_ENV" + shell: bash + + - name: Set Crashpad + if: matrix.crashpad == true + run: | + echo "C2_ENABLE_CRASHPAD=ON" >> "$GITHUB_ENV" shell: bash - name: Set environment variables for windows-latest @@ -69,7 +102,7 @@ jobs: - uses: actions/checkout@v3 with: - submodules: true + submodules: recursive fetch-depth: 0 # allows for tags access - name: Install Qt @@ -85,14 +118,14 @@ jobs: if: startsWith(matrix.os, 'windows') uses: actions/cache@v3 with: - key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.txt') }} + key: ${{ runner.os }}-${{ matrix.crashpad }}-conan-user-${{ hashFiles('**/conanfile.txt') }} path: ~/.conan/ - name: Cache conan packages part 2 if: startsWith(matrix.os, 'windows') uses: actions/cache@v3 with: - key: ${{ runner.os }}-conan-root-${{ hashFiles('**/conanfile.txt') }} + key: ${{ runner.os }}-${{ matrix.crashpad }}-conan-root-${{ hashFiles('**/conanfile.txt') }} path: C:/.conan/ - name: Add Conan to path @@ -106,7 +139,7 @@ jobs: - name: Enable Developer Command Prompt if: startsWith(matrix.os, 'windows') - uses: ilammy/msvc-dev-cmd@v1.12.0 + uses: ilammy/msvc-dev-cmd@v1.12.1 - name: Setup Conan (Windows) if: startsWith(matrix.os, 'windows') @@ -119,34 +152,50 @@ jobs: run: | mkdir build cd build - conan install .. -s build_type=Release -b missing -pr:b=default + conan install .. -s build_type=RelWithDebInfo -b missing -pr:b=default cmake ` -G"NMake Makefiles" ` - -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" ` + -DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" ` -DCHATTERINO_LTO="$Env:C2_ENABLE_LTO" ` -DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" ` .. set cl=/MP nmake /S /NOLOGO + + - name: Build crashpad (Windows) + if: startsWith(matrix.os, 'windows') && matrix.crashpad + run: | + cd build + set cl=/MP + nmake /S /NOLOGO crashpad_handler + mkdir Chatterino2/crashpad + cp bin/crashpad/crashpad_handler.exe Chatterino2/crashpad/crashpad_handler.exe + 7z a bin/chatterino.pdb.7z bin/chatterino.pdb + + - name: Package (windows) + if: startsWith(matrix.os, 'windows') + run: | + cd build windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/ cp bin/chatterino.exe Chatterino2/ echo nightly > Chatterino2/modes 7z a chatterino-windows-x86-64.zip Chatterino2/ - - name: Upload artifact (Windows) - if: startsWith(matrix.os, 'windows') && matrix.plugins == false + - name: Upload artifact (Windows - binary) + if: startsWith(matrix.os, 'windows') && matrix.skip_artifact != 'yes' uses: actions/upload-artifact@v3 with: - name: chatterino-windows-x86-64-${{ matrix.qt-version }}.zip + name: chatterino-windows-x86-64-${{ matrix.qt-version }}-${{ artifact_descr }}.zip path: build/chatterino-windows-x86-64.zip - - name: Upload artifact (Windows, plugins) - if: startsWith(matrix.os, 'windows') && matrix.plugins == true + - name: Upload artifact (Windows - symbols) + if: startsWith(matrix.os, 'windows') && matrix.skip_artifact != 'yes' uses: actions/upload-artifact@v3 with: - name: chatterino-windows-x86-64-${{ matrix.qt-version }}-plugins.zip - path: build/chatterino-windows-x86-64.zip + name: chatterino-windows-x86-64-${{ matrix.qt-version }}-symbols.pdb.7z + path: build/bin/chatterino.pdb.7z - name: Clean Conan pkgs if: startsWith(matrix.os, 'windows') @@ -170,7 +219,6 @@ jobs: libboost-filesystem-dev \ libpulse-dev \ libxkbcommon-x11-0 \ - libgstreamer-plugins-base1.0-0 \ build-essential \ libgl1-mesa-dev \ libxcb-icccm4 \ @@ -179,12 +227,18 @@ jobs: libxcb-render-util0 \ libxcb-xinerama0 + - name: Apply Qt patches (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.qt-version, '5.') + run: | + patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch + shell: bash + - name: Build (Ubuntu) if: startsWith(matrix.os, 'ubuntu') run: | mkdir build cd build - cmake \ + CXXFLAGS=-fno-sized-deallocation cmake \ -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ -DCMAKE_BUILD_TYPE=Release \ -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ @@ -215,24 +269,24 @@ jobs: clang-tidy-review-metadata.json - name: Package - AppImage (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') + if: startsWith(matrix.os, 'ubuntu') && matrix.skip_artifact != 'yes' run: | cd build sh ./../.CI/CreateAppImage.sh shell: bash - name: Package - .deb (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') + if: startsWith(matrix.os, 'ubuntu') && matrix.skip_artifact != 'yes' run: | cd build sh ./../.CI/CreateUbuntuDeb.sh shell: bash - name: Upload artifact - AppImage (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') && matrix.plugins == false + if: startsWith(matrix.os, 'ubuntu') && matrix.skip_artifact != 'yes' uses: actions/upload-artifact@v3 with: - name: Chatterino-x86_64-${{ matrix.qt-version }}.AppImage + name: Chatterino-x86_64-${{ matrix.qt-version }}-${{ artifact_descr }}.AppImage path: build/Chatterino-x86_64.AppImage - name: Upload artifact - AppImage (Ubuntu, plugins) @@ -243,17 +297,10 @@ jobs: path: build/Chatterino-x86_64.AppImage - name: Upload artifact - .deb (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') && matrix.plugins == false + if: startsWith(matrix.os, 'ubuntu') && matrix.skip_artifact != 'yes' uses: actions/upload-artifact@v3 with: - name: Chatterino-${{ matrix.qt-version }}.deb - path: build/Chatterino-x86_64.deb - - - name: Upload artifact - .deb (Ubuntu, plugins) - if: startsWith(matrix.os, 'ubuntu') && matrix.plugins == true - uses: actions/upload-artifact@v3 - with: - name: Chatterino-${{ matrix.qt-version }}-plugins.deb + name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}-${{ artifact_descr }}.deb path: build/Chatterino-x86_64.deb # MACOS @@ -290,17 +337,10 @@ jobs: shell: bash - name: Upload artifact (MacOS) - if: startsWith(matrix.os, 'macos') && matrix.plugins == false + if: startsWith(matrix.os, 'macos') uses: actions/upload-artifact@v3 with: - name: chatterino-osx-${{ matrix.qt-version }}.dmg - path: build/chatterino-osx.dmg - - - name: Upload artifact (MacOS, plugins) - if: startsWith(matrix.os, 'macos') && matrix.plugins == true - uses: actions/upload-artifact@v3 - with: - name: chatterino-osx-${{ matrix.qt-version }}.dmg + name: chatterino-osx-${{ matrix.qt-version }}-${{ artifact_descr }}.dmg path: build/chatterino-osx.dmg create-release: needs: build @@ -320,7 +360,12 @@ jobs: - uses: actions/download-artifact@v3 with: - name: Chatterino-5.15.2.deb + name: Chatterino-ubuntu-20.04-Qt-5.12.12.deb + path: release-artifacts/ + + - uses: actions/download-artifact@v3 + with: + name: Chatterino-ubuntu-22.04-Qt-5.15.2.deb path: release-artifacts/ - uses: actions/download-artifact@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84aae8c17..e1c9df85b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:master + TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.6 concurrency: group: test-${{ github.ref }} diff --git a/.gitmodules b/.gitmodules index 310596ada..571cc0f44 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,9 +2,6 @@ path = lib/libcommuni url = https://github.com/Chatterino/libcommuni branch = chatterino-cmake -[submodule "lib/qBreakpad"] - path = lib/qBreakpad - url = https://github.com/jiakuan/qBreakpad.git [submodule "lib/WinToast"] path = lib/WinToast url = https://github.com/mohabouje/WinToast.git @@ -41,3 +38,6 @@ [submodule "lib/lua/src"] path = lib/lua/src url = https://github.com/lua/lua +[submodule "lib/crashpad"] + path = lib/crashpad + url = https://github.com/getsentry/crashpad diff --git a/.patches/qt5-on-newer-gcc.patch b/.patches/qt5-on-newer-gcc.patch new file mode 100644 index 000000000..2abdc120f --- /dev/null +++ b/.patches/qt5-on-newer-gcc.patch @@ -0,0 +1,20 @@ +This patch ensures Qt 5.15 in particular can build with modern compilers + +See https://bugreports.qt.io/browse/QTBUG-91909 and https://codereview.qt-project.org/c/qt/qtbase/+/339417 +--- + +diff --git a/src/concurrent/qtconcurrentthreadengine.h b/src/concurrent/qtconcurrentthreadengine.h +index cbd8ad04..4cd5b85 100644 +--- a/src/concurrent/qtconcurrentthreadengine.h ++++ b/src/concurrent/qtconcurrentthreadengine.h +@@ -256,8 +256,8 @@ + class ThreadEngineStarter : public ThreadEngineStarterBase + { + public: +- ThreadEngineStarter(ThreadEngine *_threadEngine) +- :ThreadEngineStarterBase(_threadEngine) {} ++ ThreadEngineStarter(ThreadEngine *_threadEngine) ++ : ThreadEngineStarterBase(_threadEngine) {} + + void startBlocking() + { diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2866b8f..04f001b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unversioned - Major: Added live emote updates for BTTV (#4147) +- Minor: Added option to highlight your own messages in Highlights page under Users tab. (#3833) - Minor: Change the highlight order to prioritize Message highlights over User highlights. (#4303) - Minor: Added ability to negate search options by prefixing it with an exclamation mark (e.g. `!badge:mod` to search for messages where the author does not have the moderator badge). (#4207) - Minor: Search window input will automatically use currently selected text if present. (#4178) @@ -11,6 +12,9 @@ - Minor: Added link to streamlink docs for easier user setup. (#4217) - Minor: Added setting to turn off rendering of reply context. (#4224) - Minor: Added setting to select which channels to log. (#4302) +- Minor: Added support for HTTP and Socks5 proxies through environment variables. (#4321) +- Minor: Remove sending part of the multipart emoji workaround (#4361) +- Minor: Added crashpad to capture crashes on Windows locally. See PR for build/crash analysis instructions. (#4351) - Bugfix: Fixed crash that would occur when performing certain actions after removing all tabs. (#4271) - Bugfix: Fixed highlight sounds not reloading on change properly. (#4194) - Bugfix: Fixed CTRL + C not working in reply thread popups. (#4209) @@ -27,6 +31,7 @@ - Bugfix: Fixed the split "Search" menu action not opening the correct search window. (#4305) - Bugfix: Fixed an issue on Windows when opening links in incognito mode that contained forward slashes in hash (#4307) - Bugfix: Fixed an issue where beta versions wouldn't update to stable versions correctly. (#4329) +- Bugfix: Avoided crash that could occur when receiving channel point reward information. (#4360) - Dev: Changed sound backend from Qt to miniaudio. (#4334) - Dev: Remove protocol from QApplication's Organization Domain (so changed from `https://www.chatterino.com` to `chatterino.com`). (#4256) - Dev: Ignore `WM_SHOWWINDOW` hide events, causing fewer attempted rescales. (#4198) @@ -44,6 +49,10 @@ - Dev: Added CMake Install Support on Windows. (#4300) - Dev: Changed conan generator to [`CMakeDeps`](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmakedeps.html) and [`CMakeToolchain`](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmaketoolchain.html). See PR for migration notes. (#4335) - Dev: Add scripting capabilities with Lua (#4341) +- Dev: Refactored 7TV EventAPI implementation. (#4342) +- Dev: Disabled ImageExpirationPool in tests. (#4363) +- Dev: Don't rely on undocumented registry keys to find the default browser on Windows. (#4362) +- Dev: Use `QEnterEvent` for `QWidget::enterEvent` on Qt 6. (#4365) ## 2.4.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index ba0bddf63..7e8c584d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ option(USE_SYSTEM_PAJLADA_SETTINGS "Use system pajlada settings library" OFF) option(USE_SYSTEM_LIBCOMMUNI "Use system communi library" OFF) option(USE_SYSTEM_QTKEYCHAIN "Use system QtKeychain library" OFF) option(BUILD_WITH_QTKEYCHAIN "Build Chatterino with support for your system key chain" ON) +option(BUILD_WITH_CRASHPAD "Build chatterino with crashpad" OFF) option(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF) option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF) @@ -149,11 +150,16 @@ else() add_subdirectory("${CMAKE_SOURCE_DIR}/lib/settings" EXCLUDE_FROM_ALL) endif() + if (CHATTERINO_PLUGINS) set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src") add_subdirectory(lib/lua) endif() +if (BUILD_WITH_CRASHPAD) + add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL) +endif() + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/cmake/sanitizers-cmake b/cmake/sanitizers-cmake index a6748f4f5..c3dc841af 160000 --- a/cmake/sanitizers-cmake +++ b/cmake/sanitizers-cmake @@ -1 +1 @@ -Subproject commit a6748f4f51273d86312e3d27ebe5277c9b1ff870 +Subproject commit c3dc841af4dbf44669e65b82cb68a575864326bd diff --git a/lib/crashpad b/lib/crashpad new file mode 160000 index 000000000..918fd319d --- /dev/null +++ b/lib/crashpad @@ -0,0 +1 @@ +Subproject commit 918fd319d679306c8c95ee92376c6fa6ef3407a0 diff --git a/lib/qBreakpad b/lib/qBreakpad deleted file mode 160000 index a4626c12e..000000000 --- a/lib/qBreakpad +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a4626c12e9ae6f02fc1ca7a4e399bd8307424103 diff --git a/resources/licenses/crashpad.txt b/resources/licenses/crashpad.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/resources/licenses/crashpad.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/Application.cpp b/src/Application.cpp index 464549690..b794a7966 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -22,8 +22,8 @@ #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/ffz/FfzBadges.hpp" #include "providers/irc/Irc2.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp" -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" #include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/ChannelPointReward.hpp" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 44dd3f032..0638e489b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -200,10 +200,14 @@ set(SOURCE_FILES messages/search/SubtierPredicate.cpp messages/search/SubtierPredicate.hpp + providers/Crashpad.cpp + providers/Crashpad.hpp providers/IvrApi.cpp providers/IvrApi.hpp providers/LinkResolver.cpp providers/LinkResolver.hpp + providers/NetworkConfigurationProvider.cpp + providers/NetworkConfigurationProvider.hpp providers/RecentMessagesApi.cpp providers/RecentMessagesApi.hpp @@ -259,14 +263,14 @@ set(SOURCE_FILES providers/seventv/SeventvEventAPI.cpp providers/seventv/SeventvEventAPI.hpp - providers/seventv/eventapi/SeventvEventAPIClient.cpp - providers/seventv/eventapi/SeventvEventAPIClient.hpp - providers/seventv/eventapi/SeventvEventAPIDispatch.cpp - providers/seventv/eventapi/SeventvEventAPIDispatch.hpp - providers/seventv/eventapi/SeventvEventAPIMessage.cpp - providers/seventv/eventapi/SeventvEventAPIMessage.hpp - providers/seventv/eventapi/SeventvEventAPISubscription.cpp - providers/seventv/eventapi/SeventvEventAPISubscription.hpp + providers/seventv/eventapi/Client.cpp + providers/seventv/eventapi/Client.hpp + providers/seventv/eventapi/Dispatch.cpp + providers/seventv/eventapi/Dispatch.hpp + providers/seventv/eventapi/Message.cpp + providers/seventv/eventapi/Message.hpp + providers/seventv/eventapi/Subscription.cpp + providers/seventv/eventapi/Subscription.hpp providers/twitch/ChannelPointReward.cpp providers/twitch/ChannelPointReward.hpp @@ -651,6 +655,22 @@ else() ) endif() +# Set the output of TARGET to be +# - CMAKE_BIN_DIR/lib for libraries +# - CMAKE_BIN_DIR/bin for BINARIES +# an additional argument specifies the subdirectory. +function(set_target_directory_hierarchy TARGET) + set_target_properties(${TARGET} + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/${ARGV1}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + ) +endfunction() + if (BUILD_APP) if (APPLE) add_executable(${EXECUTABLE_PROJECT} ${MACOS_BUNDLE_ICON_FILE} main.cpp) @@ -663,15 +683,7 @@ if (BUILD_APP) target_link_libraries(${EXECUTABLE_PROJECT} PUBLIC ${LIBRARY_PROJECT}) - set_target_properties(${EXECUTABLE_PROJECT} - PROPERTIES - ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" - RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" - RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin" - ) + set_target_directory_hierarchy(${EXECUTABLE_PROJECT}) if (WIN32) if (NOT WINDEPLOYQT_PATH) @@ -851,8 +863,29 @@ if (LIBRT) ) endif () +if (BUILD_WITH_CRASHPAD) + target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD) + target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client) + set_target_directory_hierarchy(crashpad_handler crashpad) +endif() + # Configure compiler warnings if (MSVC) + # Change flags for RelWithDebInfo + + # Default: "/debug /INCREMENTAL" + # Changes: + # - Disable incremental linking to reduce padding + # - Enable all optimizations - by default when /DEBUG is specified, + # these optimizations will be disabled. We need /DEBUG to generate a PDB. + # See https://gitlab.kitware.com/cmake/cmake/-/issues/20812 for more details. + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /INCREMENTAL:NO /OPT:REF,ICF,LBR") + + # Use the function inlining level from 'Release' mode (2). + string(REPLACE "/Ob1" "/Ob2" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + + # Configure warnings + # Someone adds /W3 before we add /W4. # This makes sure, only /W4 is specified. string(REPLACE "/W3" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") @@ -881,6 +914,7 @@ if (MSVC) /wd4100 /wd4267 ) + # Disable min/max macros from Windows.h target_compile_definitions(${LIBRARY_PROJECT} PUBLIC NOMINMAX) else () target_compile_options(${LIBRARY_PROJECT} PUBLIC diff --git a/src/RunGui.cpp b/src/RunGui.cpp index f5d64087f..fdf557e61 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -162,7 +162,7 @@ namespace { // true. void initSignalHandler() { -#ifdef NDEBUG +#if defined(NDEBUG) && !defined(CHATTERINO_WITH_CRASHPAD) signalsInitTime = std::chrono::steady_clock::now(); signal(SIGSEGV, handleSignal); diff --git a/src/common/Env.cpp b/src/common/Env.cpp index 36757b841..605e49247 100644 --- a/src/common/Env.cpp +++ b/src/common/Env.cpp @@ -44,6 +44,17 @@ namespace { return defaultValue; } + boost::optional readOptionalStringEnv(const char *envName) + { + auto envString = std::getenv(envName); + if (envString != nullptr) + { + return QString(envString); + } + + return boost::none; + } + uint16_t readPortEnv(const char *envName, uint16_t defaultValue) { auto envString = std::getenv(envName); @@ -89,6 +100,7 @@ Env::Env() readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv")) , twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443)) , twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true)) + , proxyUrl(readOptionalStringEnv("CHATTERINO2_PROXY_URL")) { } diff --git a/src/common/Env.hpp b/src/common/Env.hpp index b334e8e96..97e5040d8 100644 --- a/src/common/Env.hpp +++ b/src/common/Env.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace chatterino { @@ -16,6 +17,7 @@ public: const QString twitchServerHost; const uint16_t twitchServerPort; const bool twitchServerSecure; + const boost::optional proxyUrl; }; } // namespace chatterino diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 2d58fa4ad..11b39897e 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -29,6 +29,7 @@ Q_LOGGING_CATEGORY(chatterinoMain, "chatterino.main", logThreshold); Q_LOGGING_CATEGORY(chatterinoMessage, "chatterino.message", logThreshold); Q_LOGGING_CATEGORY(chatterinoNativeMessage, "chatterino.nativemessage", logThreshold); +Q_LOGGING_CATEGORY(chatterinoNetwork, "chatterino.network", logThreshold); Q_LOGGING_CATEGORY(chatterinoNotification, "chatterino.notification", logThreshold); Q_LOGGING_CATEGORY(chatterinoNuulsuploader, "chatterino.nuulsuploader", diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index 1922133ee..d3585f18c 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -23,6 +23,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoLua); Q_DECLARE_LOGGING_CATEGORY(chatterinoMain); Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage); +Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork); Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification); Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); diff --git a/src/common/Version.cpp b/src/common/Version.cpp index 9a5d4e978..bbd99e176 100644 --- a/src/common/Version.cpp +++ b/src/common/Version.cpp @@ -100,6 +100,9 @@ QStringList Version::buildTags() const #ifdef _MSC_FULL_VER tags.append("MSVC " + QString::number(_MSC_FULL_VER, 10)); #endif +#ifdef CHATTERINO_WITH_CRASHPAD + tags.append("Crashpad"); +#endif return tags; } diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 1af99f5c2..bb17850b5 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -85,12 +85,6 @@ void sendWhisperMessage(const QString &text) auto app = getApp(); QString toSend = text.simplified(); - // This is to make sure that combined emoji go through properly, see - // https://github.com/Chatterino/chatterino2/issues/3384 and - // https://mm2pl.github.io/emoji_rfc.pdf for more details - // Constants used here are defined in TwitchChannel.hpp - toSend.replace(ZERO_WIDTH_JOINER, ESCAPE_TAG); - app->twitch->sendMessage("jtv", toSend); } diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index 6e94ebdca..bd517863f 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -210,6 +210,35 @@ void rebuildUserHighlights(Settings &settings, { auto userHighlights = settings.highlightedUsers.readOnly(); + if (settings.enableSelfMessageHighlight) + { + bool showInMentions = settings.showSelfMessageHighlightInMentions; + + checks.emplace_back(HighlightCheck{ + [showInMentions]( + const auto &args, const auto &badges, const auto &senderName, + const auto &originalMessage, const auto &flags, + const auto self) -> boost::optional { + (void)args; //unused + (void)badges; //unused + (void)senderName; //unused + (void)flags; //unused + (void)originalMessage; //unused + + if (!self) + { + return boost::none; + } + + // Highlight color is provided by the ColorProvider and will be updated accordingly + auto highlightColor = ColorProvider::instance().color( + ColorType::SelfMessageHighlight); + + return HighlightResult{false, false, (QUrl) nullptr, + highlightColor, showInMentions}; + }}); + } + for (const auto &highlight : *userHighlights) { checks.emplace_back(HighlightCheck{ @@ -391,6 +420,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/) this->rebuildListener_.addSetting(settings.enableSubHighlight); this->rebuildListener_.addSetting(settings.enableSubHighlightSound); this->rebuildListener_.addSetting(settings.enableSubHighlightTaskbar); + this->rebuildListener_.addSetting(settings.enableSelfMessageHighlight); + this->rebuildListener_.addSetting( + settings.showSelfMessageHighlightInMentions); + // We do not need to rebuild the listener for the selfMessagesHighlightColor + // The color is dynamically fetched any time the self message highlight is triggered this->rebuildListener_.addSetting(settings.subHighlightSoundUrl); this->rebuildListener_.addSetting(settings.enableThreadHighlight); diff --git a/src/controllers/highlights/HighlightModel.hpp b/src/controllers/highlights/HighlightModel.hpp index 7f20a9c6c..4966950a1 100644 --- a/src/controllers/highlights/HighlightModel.hpp +++ b/src/controllers/highlights/HighlightModel.hpp @@ -36,6 +36,10 @@ public: ThreadMessageRow = 6, }; + enum UserHighlightRowIndexes { + SelfMessageRow = 0, + }; + protected: // turn a vector item into a model row virtual HighlightPhrase getItemFromRow( diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp index d7b20e16f..c8963e59c 100644 --- a/src/controllers/highlights/HighlightPhrase.cpp +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -10,6 +10,8 @@ namespace { } // namespace QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127); +QColor HighlightPhrase::FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR = + QColor(0, 118, 221, 115); QColor HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR = QColor(28, 126, 141, 60); QColor HighlightPhrase::FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR = diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index 392f13e28..56d3499cc 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -79,6 +79,8 @@ public: * Qt>=5.13. */ static QColor FALLBACK_HIGHLIGHT_COLOR; + // Used for automatic self messages highlighing + static QColor FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR; static QColor FALLBACK_REDEEMED_HIGHLIGHT_COLOR; static QColor FALLBACK_SUB_COLOR; static QColor FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR; diff --git a/src/controllers/highlights/UserHighlightModel.cpp b/src/controllers/highlights/UserHighlightModel.cpp index 42aeb819f..15ca70163 100644 --- a/src/controllers/highlights/UserHighlightModel.cpp +++ b/src/controllers/highlights/UserHighlightModel.cpp @@ -3,7 +3,9 @@ #include "Application.hpp" #include "controllers/highlights/HighlightModel.hpp" #include "controllers/highlights/HighlightPhrase.hpp" +#include "providers/colors/ColorProvider.hpp" #include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { @@ -37,6 +39,86 @@ HighlightPhrase UserHighlightModel::getItemFromRow( highlightColor}; } +void UserHighlightModel::afterInit() +{ + // User highlight settings for your own messages + std::vector messagesRow = this->createRow(); + setBoolItem(messagesRow[Column::Pattern], + getSettings()->enableSelfMessageHighlight.getValue(), true, + false); + messagesRow[Column::Pattern]->setData("Your messages (automatic)", + Qt::DisplayRole); + setBoolItem(messagesRow[Column::ShowInMentions], + getSettings()->showSelfMessageHighlightInMentions.getValue(), + true, false); + messagesRow[Column::FlashTaskbar]->setFlags({}); + messagesRow[Column::PlaySound]->setFlags({}); + messagesRow[Column::UseRegex]->setFlags({}); + messagesRow[Column::CaseSensitive]->setFlags({}); + messagesRow[Column::SoundPath]->setFlags({}); + + auto selfColor = + ColorProvider::instance().color(ColorType::SelfMessageHighlight); + setColorItem(messagesRow[Column::Color], *selfColor, false); + + this->insertCustomRow( + messagesRow, HighlightModel::UserHighlightRowIndexes::SelfMessageRow); +} + +void UserHighlightModel::customRowSetData( + const std::vector &row, int column, const QVariant &value, + int role, int rowIndex) +{ + switch (column) + { + case Column::Pattern: { + if (role == Qt::CheckStateRole) + { + if (rowIndex == + HighlightModel::UserHighlightRowIndexes::SelfMessageRow) + { + getSettings()->enableSelfMessageHighlight.setValue( + value.toBool()); + } + } + } + break; + case Column::ShowInMentions: { + if (role == Qt::CheckStateRole) + { + if (rowIndex == + HighlightModel::UserHighlightRowIndexes::SelfMessageRow) + { + getSettings()->showSelfMessageHighlightInMentions.setValue( + value.toBool()); + } + } + } + break; + case Column::Color: { + // Custom color + if (role == Qt::DecorationRole) + { + auto colorName = value.value().name(QColor::HexArgb); + if (rowIndex == + HighlightModel::UserHighlightRowIndexes::SelfMessageRow) + { + // Update the setting with the new value + getSettings()->selfMessageHighlightColor.setValue( + colorName); + // Update the color provider with the new color to be used for future + const_cast(ColorProvider::instance()) + .updateColor(ColorType::SelfMessageHighlight, + QColor(colorName)); + } + } + } + break; + } + + getApp()->windows->forceLayoutChannelViews(); +} + // row into vector item void UserHighlightModel::getRowFromItem(const HighlightPhrase &item, std::vector &row) diff --git a/src/controllers/highlights/UserHighlightModel.hpp b/src/controllers/highlights/UserHighlightModel.hpp index fa2811ddc..928d4931d 100644 --- a/src/controllers/highlights/UserHighlightModel.hpp +++ b/src/controllers/highlights/UserHighlightModel.hpp @@ -22,6 +22,12 @@ protected: virtual void getRowFromItem(const HighlightPhrase &item, std::vector &row) override; + + virtual void afterInit() override; + + virtual void customRowSetData(const std::vector &row, + int column, const QVariant &value, int role, + int rowIndex) override; }; } // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index 8ad92e3da..303d8ea22 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,17 @@ #include "BrowserExtension.hpp" #include "common/Args.hpp" +#include "common/Env.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" +#include "providers/Crashpad.hpp" #include "providers/IvrApi.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/api/Helix.hpp" #include "RunGui.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "util/AttachToConsole.hpp" -#include "util/IncognitoBrowser.hpp" #include #include @@ -57,6 +59,10 @@ int main(int argc, char **argv) initArgs(a); +#ifdef CHATTERINO_WITH_CRASHPAD + const auto crashpadHandler = installCrashHandler(); +#endif + // run in gui mode or browser extension host mode if (getArgs().shouldRunBrowserExtensionHost) { @@ -81,6 +87,8 @@ int main(int argc, char **argv) attachToConsole(); } + NetworkConfigurationProvider::applyFromEnv(Env::get()); + IvrApi::initialize(); Helix::initialize(); diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 7c6d13086..952b61465 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -272,7 +272,9 @@ namespace detail { // IMAGE2 Image::~Image() { +#ifndef DISABLE_IMAGE_EXPIRATION_POOL ImageExpirationPool::instance().removeImagePtr(this); +#endif if (this->empty_ && !this->frames_) { @@ -425,7 +427,9 @@ void Image::load() const Image *this2 = const_cast(this); this2->shouldLoad_ = false; this2->actuallyLoad(); +#ifndef DISABLE_IMAGE_EXPIRATION_POOL ImageExpirationPool::instance().addImagePtr(this2->shared_from_this()); +#endif } } @@ -551,6 +555,8 @@ void Image::expireFrames() this->shouldLoad_ = true; // Mark as needing load again } +#ifndef DISABLE_IMAGE_EXPIRATION_POOL + ImageExpirationPool::ImageExpirationPool() { QObject::connect(&this->freeTimer_, &QTimer::timeout, [this] { @@ -593,10 +599,10 @@ void ImageExpirationPool::freeOld() { std::lock_guard lock(this->mutex_); -#ifndef NDEBUG +# ifndef NDEBUG size_t numExpired = 0; size_t eligible = 0; -#endif +# endif auto now = std::chrono::steady_clock::now(); for (auto it = this->allImages_.begin(); it != this->allImages_.end();) @@ -617,17 +623,17 @@ void ImageExpirationPool::freeOld() continue; } -#ifndef NDEBUG +# ifndef NDEBUG ++eligible; -#endif +# endif // Check if image has expired and, if so, expire its frame data auto diff = now - img->lastUsed_; if (diff > IMAGE_POOL_IMAGE_LIFETIME) { -#ifndef NDEBUG +# ifndef NDEBUG ++numExpired; -#endif +# endif img->expireFrames(); // erase without mutex locking issue it = this->allImages_.erase(it); @@ -637,10 +643,12 @@ void ImageExpirationPool::freeOld() ++it; } -#ifndef NDEBUG +# ifndef NDEBUG qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/" << eligible << "eligible images"; -#endif +# endif } +#endif + } // namespace chatterino diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 6bd20ea37..5ac7fafe5 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -19,6 +19,14 @@ #include #include +#ifdef CHATTERINO_TEST +// When running tests, the ImageExpirationPool destructor can be called before +// all images are deleted, leading to a use-after-free of its mutex. This +// happens despite the lifetime of the ImageExpirationPool being (apparently) +// static. Therefore, just disable it during testing. +# define DISABLE_IMAGE_EXPIRATION_POOL +#endif + namespace chatterino { namespace detail { template @@ -105,6 +113,8 @@ private: // forward-declarable function that calls Image::getEmpty() under the hood. ImagePtr getEmptyImagePtr(); +#ifndef DISABLE_IMAGE_EXPIRATION_POOL + class ImageExpirationPool { private: @@ -131,4 +141,6 @@ private: std::mutex mutex_; }; +#endif + } // namespace chatterino diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index fcecef211..bea40a1b1 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace chatterino { diff --git a/src/providers/Crashpad.cpp b/src/providers/Crashpad.cpp new file mode 100644 index 000000000..4c2fb9760 --- /dev/null +++ b/src/providers/Crashpad.cpp @@ -0,0 +1,95 @@ +#ifdef CHATTERINO_WITH_CRASHPAD +# include "providers/Crashpad.hpp" + +# include "common/QLogging.hpp" +# include "singletons/Paths.hpp" + +# include +# include +# include + +# include +# include + +namespace { + +/// The name of the crashpad handler executable. +/// This varies across platforms +# if defined(Q_OS_UNIX) +const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad_handler"); +# elif defined(Q_OS_WINDOWS) +const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad_handler.exe"); +# else +# error Unsupported platform +# endif + +/// Converts a QString into the platform string representation. +# if defined(Q_OS_UNIX) +std::string nativeString(const QString &s) +{ + return s.toStdString(); +} +# elif defined(Q_OS_WINDOWS) +std::wstring nativeString(const QString &s) +{ + return s.toStdWString(); +} +# else +# error Unsupported platform +# endif + +} // namespace + +namespace chatterino { + +std::unique_ptr installCrashHandler() +{ + // Currently, the following directory layout is assumed: + // [applicationDirPath] + // │ + // ├─chatterino + // │ + // ╰─[crashpad] + // │ + // ╰─crashpad_handler + // TODO: The location of the binary might vary across platforms + auto crashpadBinDir = QDir(QApplication::applicationDirPath()); + + if (!crashpadBinDir.cd("crashpad")) + { + qCDebug(chatterinoApp) << "Cannot find crashpad directory"; + return nullptr; + } + if (!crashpadBinDir.exists(CRASHPAD_EXECUTABLE_NAME)) + { + qCDebug(chatterinoApp) << "Cannot find crashpad handler executable"; + return nullptr; + } + + const auto handlerPath = base::FilePath(nativeString( + crashpadBinDir.absoluteFilePath(CRASHPAD_EXECUTABLE_NAME))); + + // Argument passed in --database + // > Crash reports are written to this database, and if uploads are enabled, + // uploaded from this database to a crash report collection server. + const auto databaseDir = + base::FilePath(nativeString(getPaths()->crashdumpDirectory)); + + auto client = std::make_unique(); + + // See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md + // for documentation on available options. + if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, true, + false)) + { + qCDebug(chatterinoApp) << "Failed to start crashpad handler"; + return nullptr; + } + + qCDebug(chatterinoApp) << "Started crashpad handler"; + return client; +} + +} // namespace chatterino + +#endif diff --git a/src/providers/Crashpad.hpp b/src/providers/Crashpad.hpp new file mode 100644 index 000000000..d15f3fcb7 --- /dev/null +++ b/src/providers/Crashpad.hpp @@ -0,0 +1,14 @@ +#pragma once + +#ifdef CHATTERINO_WITH_CRASHPAD +# include + +# include + +namespace chatterino { + +std::unique_ptr installCrashHandler(); + +} // namespace chatterino + +#endif diff --git a/src/providers/NetworkConfigurationProvider.cpp b/src/providers/NetworkConfigurationProvider.cpp new file mode 100644 index 000000000..57291ac25 --- /dev/null +++ b/src/providers/NetworkConfigurationProvider.cpp @@ -0,0 +1,76 @@ +#include "providers/NetworkConfigurationProvider.hpp" + +#include "common/Env.hpp" +#include "common/QLogging.hpp" + +#include +#include +#include + +namespace { +/** + * Creates a QNetworkProxy from a given URL. + * + * Creates an HTTP proxy by default, a Socks5 will be created if the scheme is 'socks5'. + */ +QNetworkProxy createProxyFromUrl(const QUrl &url) +{ + QNetworkProxy proxy; + proxy.setHostName(url.host(QUrl::FullyEncoded)); + proxy.setUser(url.userName(QUrl::FullyEncoded)); + proxy.setPassword(url.password(QUrl::FullyEncoded)); + proxy.setPort(url.port(1080)); + + if (url.scheme().compare(QStringLiteral("socks5"), Qt::CaseInsensitive) == + 0) + { + proxy.setType(QNetworkProxy::Socks5Proxy); + } + else + { + proxy.setType(QNetworkProxy::HttpProxy); + if (!proxy.user().isEmpty() || !proxy.password().isEmpty()) + { + // for some reason, Qt doesn't set the Proxy-Authorization header + const auto auth = proxy.user() + ":" + proxy.password(); + const auto base64 = auth.toUtf8().toBase64(); + proxy.setRawHeader("Proxy-Authorization", + QByteArray("Basic ").append(base64)); + } + } + + return proxy; +} + +/** + * Attempts to apply the proxy specified by `url` as the application proxy. + */ +void applyProxy(const QString &url) +{ + auto proxyUrl = QUrl(url); + if (!proxyUrl.isValid() || proxyUrl.isEmpty()) + { + qCDebug(chatterinoNetwork) + << "Invalid or empty proxy url: " << proxyUrl; + return; + } + + const auto proxy = createProxyFromUrl(proxyUrl); + + QNetworkProxy::setApplicationProxy(proxy); + qCDebug(chatterinoNetwork) << "Set application proxy to" << proxy; +} + +} // namespace + +namespace chatterino { + +void NetworkConfigurationProvider::applyFromEnv(const Env &env) +{ + if (env.proxyUrl) + { + applyProxy(env.proxyUrl.get()); + } +} + +} // namespace chatterino diff --git a/src/providers/NetworkConfigurationProvider.hpp b/src/providers/NetworkConfigurationProvider.hpp new file mode 100644 index 000000000..6e277cfd8 --- /dev/null +++ b/src/providers/NetworkConfigurationProvider.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "common/QLogging.hpp" + +#include +#include + +#include + +namespace chatterino { + +class Env; + +/** This class manipulates the global network configuration (e.g. proxies). */ +class NetworkConfigurationProvider +{ +public: + /** This class should never be instantiated. */ + NetworkConfigurationProvider() = delete; + + /** + * Applies the configuration requested from the environment variables. + * + * Currently a proxy is applied if configured. + */ + static void applyFromEnv(const Env &env); + + static void applyToWebSocket(const auto &connection) + { + const auto applicationProxy = QNetworkProxy::applicationProxy(); + if (applicationProxy.type() != QNetworkProxy::HttpProxy) + { + return; + } + std::string url = "http://"; + url += applicationProxy.hostName().toStdString(); + url += ":"; + url += std::to_string(applicationProxy.port()); + websocketpp::lib::error_code ec; + connection->set_proxy(url, ec); + if (ec) + { + qCDebug(chatterinoNetwork) + << "Couldn't set websocket proxy:" << ec.value(); + return; + } + + connection->set_proxy_basic_auth( + applicationProxy.user().toStdString(), + applicationProxy.password().toStdString(), ec); + if (ec) + { + qCDebug(chatterinoNetwork) + << "Couldn't set websocket proxy auth:" << ec.value(); + } + } +}; + +} // namespace chatterino diff --git a/src/providers/RecentMessagesApi.cpp b/src/providers/RecentMessagesApi.cpp index 1544b74d2..dad429d3c 100644 --- a/src/providers/RecentMessagesApi.cpp +++ b/src/providers/RecentMessagesApi.cpp @@ -84,6 +84,9 @@ namespace { for (const auto jsonMessage : jsonMessages) { auto content = jsonMessage.toString(); + + // For explanation of why this exists, see src/providers/twitch/TwitchChannel.hpp, + // where these constants are defined content.replace(COMBINED_FIXER, ZERO_WIDTH_JOINER); auto message = diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp index 94fb44451..5f47c2f68 100644 --- a/src/providers/colors/ColorProvider.cpp +++ b/src/providers/colors/ColorProvider.cpp @@ -81,6 +81,20 @@ void ColorProvider::initTypeColorMap() HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)}); } + customColor = getSettings()->selfMessageHighlightColor; + if (QColor(customColor).isValid()) + { + this->typeColorMap_.insert({ColorType::SelfMessageHighlight, + std::make_shared(customColor)}); + } + else + { + this->typeColorMap_.insert( + {ColorType::SelfMessageHighlight, + std::make_shared( + HighlightPhrase::FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR)}); + } + customColor = getSettings()->subHighlightColor; if (QColor(customColor).isValid()) { diff --git a/src/providers/colors/ColorProvider.hpp b/src/providers/colors/ColorProvider.hpp index f6fdcde50..12745371d 100644 --- a/src/providers/colors/ColorProvider.hpp +++ b/src/providers/colors/ColorProvider.hpp @@ -16,6 +16,8 @@ enum class ColorType { FirstMessageHighlight, ElevatedMessageHighlight, ThreadMessageHighlight, + // Used in automatic highlights of your own messages + SelfMessageHighlight, }; class ColorProvider diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp index 793e8b9a9..915fbc6b9 100644 --- a/src/providers/irc/Irc2.hpp +++ b/src/providers/irc/Irc2.hpp @@ -4,6 +4,8 @@ #include +#include + class QAbstractTableModel; namespace chatterino { diff --git a/src/providers/liveupdates/BasicPubSubManager.hpp b/src/providers/liveupdates/BasicPubSubManager.hpp index f849eefda..d596866bc 100644 --- a/src/providers/liveupdates/BasicPubSubManager.hpp +++ b/src/providers/liveupdates/BasicPubSubManager.hpp @@ -4,6 +4,7 @@ #include "common/Version.hpp" #include "providers/liveupdates/BasicPubSubClient.hpp" #include "providers/liveupdates/BasicPubSubWebsocket.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/PubSubHelpers.hpp" #include "util/DebugCount.hpp" #include "util/ExponentialBackoff.hpp" @@ -336,6 +337,8 @@ private: return; } + NetworkConfigurationProvider::applyToWebSocket(con); + this->websocketClient_.connect(con); } diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 8f8ee18d6..2f7883abc 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -7,7 +7,7 @@ #include "messages/Image.hpp" #include "messages/ImageSet.hpp" #include "messages/MessageBuilder.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" @@ -30,6 +30,7 @@ namespace { using namespace chatterino; +using namespace seventv::eventapi; // These declarations won't throw an exception. const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes."); @@ -224,7 +225,7 @@ EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal) } EmotePtr createUpdatedEmote(const EmotePtr &oldEmote, - const SeventvEventAPIEmoteUpdateDispatch &dispatch) + const EmoteUpdateDispatch &dispatch) { bool toNonAliased = oldEmote->baseName.has_value() && dispatch.emoteName == oldEmote->baseName->string; @@ -245,6 +246,8 @@ EmotePtr createUpdatedEmote(const EmotePtr &oldEmote, namespace chatterino { +using namespace seventv::eventapi; + SeventvEmotes::SeventvEmotes() : global_(std::make_shared()) { @@ -401,7 +404,7 @@ void SeventvEmotes::loadChannelEmotes( boost::optional SeventvEmotes::addEmote( Atomic> &map, - const SeventvEventAPIEmoteAddDispatch &dispatch) + const EmoteAddDispatch &dispatch) { // Check for visibility first, so we don't copy the map. auto emoteData = dispatch.emoteJson["data"].toObject(); @@ -429,7 +432,7 @@ boost::optional SeventvEmotes::addEmote( boost::optional SeventvEmotes::updateEmote( Atomic> &map, - const SeventvEventAPIEmoteUpdateDispatch &dispatch) + const EmoteUpdateDispatch &dispatch) { auto oldMap = map.get(); auto oldEmote = oldMap->findEmote(dispatch.emoteName, dispatch.emoteID); @@ -451,7 +454,7 @@ boost::optional SeventvEmotes::updateEmote( boost::optional SeventvEmotes::removeEmote( Atomic> &map, - const SeventvEventAPIEmoteRemoveDispatch &dispatch) + const EmoteRemoveDispatch &dispatch) { // This copies the map. EmoteMap updatedMap = *map.get(); diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index 023e99555..f978337be 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -10,9 +10,12 @@ namespace chatterino { class Channel; -struct SeventvEventAPIEmoteAddDispatch; -struct SeventvEventAPIEmoteUpdateDispatch; -struct SeventvEventAPIEmoteRemoveDispatch; + +namespace seventv::eventapi { + struct EmoteAddDispatch; + struct EmoteUpdateDispatch; + struct EmoteRemoveDispatch; +} // namespace seventv::eventapi // https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36 enum class SeventvActiveEmoteFlag : int64_t { @@ -86,7 +89,7 @@ public: */ static boost::optional addEmote( Atomic> &map, - const SeventvEventAPIEmoteAddDispatch &dispatch); + const seventv::eventapi::EmoteAddDispatch &dispatch); /** * Updates an emote in this `map`. @@ -97,7 +100,7 @@ public: */ static boost::optional updateEmote( Atomic> &map, - const SeventvEventAPIEmoteUpdateDispatch &dispatch); + const seventv::eventapi::EmoteUpdateDispatch &dispatch); /** * Removes an emote from this `map`. @@ -108,7 +111,7 @@ public: */ static boost::optional removeEmote( Atomic> &map, - const SeventvEventAPIEmoteRemoveDispatch &dispatch); + const seventv::eventapi::EmoteRemoveDispatch &dispatch); /** Fetches an emote-set by its id */ static void getEmoteSet( diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp index c4be244dc..5cec6ed30 100644 --- a/src/providers/seventv/SeventvEventAPI.cpp +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -1,8 +1,8 @@ #include "providers/seventv/SeventvEventAPI.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp" +#include "providers/seventv/eventapi/Client.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Message.hpp" #include @@ -10,6 +10,8 @@ namespace chatterino { +using namespace seventv::eventapi; + SeventvEventAPI::SeventvEventAPI( QString host, std::chrono::milliseconds defaultHeartbeatInterval) : BasicPubSubManager(std::move(host)) @@ -22,13 +24,14 @@ void SeventvEventAPI::subscribeUser(const QString &userID, { if (!userID.isEmpty() && this->subscribedUsers_.insert(userID).second) { - this->subscribe({userID, SeventvEventAPISubscriptionType::UpdateUser}); + this->subscribe( + {ObjectIDCondition{userID}, SubscriptionType::UpdateUser}); } if (!emoteSetID.isEmpty() && this->subscribedEmoteSets_.insert(emoteSetID).second) { this->subscribe( - {emoteSetID, SeventvEventAPISubscriptionType::UpdateEmoteSet}); + {ObjectIDCondition{emoteSetID}, SubscriptionType::UpdateEmoteSet}); } } @@ -37,7 +40,7 @@ void SeventvEventAPI::unsubscribeEmoteSet(const QString &id) if (this->subscribedEmoteSets_.erase(id) > 0) { this->unsubscribe( - {id, SeventvEventAPISubscriptionType::UpdateEmoteSet}); + {ObjectIDCondition{id}, SubscriptionType::UpdateEmoteSet}); } } @@ -45,27 +48,27 @@ void SeventvEventAPI::unsubscribeUser(const QString &id) { if (this->subscribedUsers_.erase(id) > 0) { - this->unsubscribe({id, SeventvEventAPISubscriptionType::UpdateUser}); + this->unsubscribe( + {ObjectIDCondition{id}, SubscriptionType::UpdateUser}); } } -std::shared_ptr> - SeventvEventAPI::createClient(liveupdates::WebsocketClient &client, - websocketpp::connection_hdl hdl) +std::shared_ptr> SeventvEventAPI::createClient( + liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl) { - auto shared = std::make_shared( - client, hdl, this->heartbeatInterval_); - return std::static_pointer_cast< - BasicPubSubClient>(std::move(shared)); + auto shared = + std::make_shared(client, hdl, this->heartbeatInterval_); + return std::static_pointer_cast>( + std::move(shared)); } void SeventvEventAPI::onMessage( websocketpp::connection_hdl hdl, - BasicPubSubManager::WebsocketMessagePtr msg) + BasicPubSubManager::WebsocketMessagePtr msg) { const auto &payload = QString::fromStdString(msg->get_payload()); - auto pMessage = parseSeventvEventAPIBaseMessage(payload); + auto pMessage = parseBaseMessage(payload); if (!pMessage) { @@ -76,11 +79,10 @@ void SeventvEventAPI::onMessage( auto message = *pMessage; switch (message.op) { - case SeventvEventAPIOpcode::Hello: { + case Opcode::Hello: { if (auto client = this->findClient(hdl)) { - if (auto *stvClient = - dynamic_cast(client.get())) + if (auto *stvClient = dynamic_cast(client.get())) { stvClient->setHeartbeatInterval( message.data["heartbeat_interval"].toInt()); @@ -88,19 +90,18 @@ void SeventvEventAPI::onMessage( } } break; - case SeventvEventAPIOpcode::Heartbeat: { + case Opcode::Heartbeat: { if (auto client = this->findClient(hdl)) { - if (auto *stvClient = - dynamic_cast(client.get())) + if (auto *stvClient = dynamic_cast(client.get())) { stvClient->handleHeartbeat(); } } } break; - case SeventvEventAPIOpcode::Dispatch: { - auto dispatch = message.toInner(); + case Opcode::Dispatch: { + auto dispatch = message.toInner(); if (!dispatch) { qCDebug(chatterinoSeventvEventAPI) @@ -110,133 +111,37 @@ void SeventvEventAPI::onMessage( this->handleDispatch(*dispatch); } break; - case SeventvEventAPIOpcode::Reconnect: { + case Opcode::Reconnect: { if (auto client = this->findClient(hdl)) { - if (auto *stvClient = - dynamic_cast(client.get())) + if (auto *stvClient = dynamic_cast(client.get())) { stvClient->close("Reconnecting"); } } } break; + case Opcode::Ack: { + // unhandled + } + break; default: { - qCDebug(chatterinoSeventvEventAPI) << "Unhandled op: " << payload; + qCDebug(chatterinoSeventvEventAPI) << "Unhandled op:" << payload; } break; } } -void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch) +void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) { switch (dispatch.type) { - case SeventvEventAPISubscriptionType::UpdateEmoteSet: { - // dispatchBody: { - // pushed: Array<{ key, value }>, - // pulled: Array<{ key, old_value }>, - // updated: Array<{ key, value, old_value }>, - // } - for (const auto pushedRef : dispatch.body["pushed"].toArray()) - { - auto pushed = pushedRef.toObject(); - if (pushed["key"].toString() != "emotes") - { - continue; - } - - SeventvEventAPIEmoteAddDispatch added( - dispatch, pushed["value"].toObject()); - - if (added.validate()) - { - this->signals_.emoteAdded.invoke(added); - } - else - { - qCDebug(chatterinoSeventvEventAPI) - << "Invalid dispatch" << dispatch.body; - } - } - for (const auto updatedRef : dispatch.body["updated"].toArray()) - { - auto updated = updatedRef.toObject(); - if (updated["key"].toString() != "emotes") - { - continue; - } - - SeventvEventAPIEmoteUpdateDispatch update( - dispatch, updated["old_value"].toObject(), - updated["value"].toObject()); - - if (update.validate()) - { - this->signals_.emoteUpdated.invoke(update); - } - else - { - qCDebug(chatterinoSeventvEventAPI) - << "Invalid dispatch" << dispatch.body; - } - } - for (const auto pulledRef : dispatch.body["pulled"].toArray()) - { - auto pulled = pulledRef.toObject(); - if (pulled["key"].toString() != "emotes") - { - continue; - } - - SeventvEventAPIEmoteRemoveDispatch removed( - dispatch, pulled["old_value"].toObject()); - - if (removed.validate()) - { - this->signals_.emoteRemoved.invoke(removed); - } - else - { - qCDebug(chatterinoSeventvEventAPI) - << "Invalid dispatch" << dispatch.body; - } - } + case SubscriptionType::UpdateEmoteSet: { + this->onEmoteSetUpdate(dispatch); } break; - case SeventvEventAPISubscriptionType::UpdateUser: { - // dispatchBody: { - // updated: Array<{ key, value: Array<{key, value}> }> - // } - for (const auto updatedRef : dispatch.body["updated"].toArray()) - { - auto updated = updatedRef.toObject(); - if (updated["key"].toString() != "connections") - { - continue; - } - for (const auto valueRef : updated["value"].toArray()) - { - auto value = valueRef.toObject(); - if (value["key"].toString() != "emote_set") - { - continue; - } - - SeventvEventAPIUserConnectionUpdateDispatch update( - dispatch, value, (size_t)updated["index"].toInt()); - - if (update.validate()) - { - this->signals_.userUpdated.invoke(update); - } - else - { - qCDebug(chatterinoSeventvEventAPI) - << "Invalid dispatch" << dispatch.body; - } - } - } + case SubscriptionType::UpdateUser: { + this->onUserUpdate(dispatch); } break; default: { @@ -248,4 +153,112 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch) } } +void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch) +{ + // dispatchBody: { + // pushed: Array<{ key, value }>, + // pulled: Array<{ key, old_value }>, + // updated: Array<{ key, value, old_value }>, + // } + for (const auto pushedRef : dispatch.body["pushed"].toArray()) + { + auto pushed = pushedRef.toObject(); + if (pushed["key"].toString() != "emotes") + { + continue; + } + + const EmoteAddDispatch added(dispatch, pushed["value"].toObject()); + + if (added.validate()) + { + this->signals_.emoteAdded.invoke(added); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } + for (const auto updatedRef : dispatch.body["updated"].toArray()) + { + auto updated = updatedRef.toObject(); + if (updated["key"].toString() != "emotes") + { + continue; + } + + const EmoteUpdateDispatch update(dispatch, + updated["old_value"].toObject(), + updated["value"].toObject()); + + if (update.validate()) + { + this->signals_.emoteUpdated.invoke(update); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } + for (const auto pulledRef : dispatch.body["pulled"].toArray()) + { + auto pulled = pulledRef.toObject(); + if (pulled["key"].toString() != "emotes") + { + continue; + } + + const EmoteRemoveDispatch removed(dispatch, + pulled["old_value"].toObject()); + + if (removed.validate()) + { + this->signals_.emoteRemoved.invoke(removed); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } +} + +void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch) +{ + // dispatchBody: { + // updated: Array<{ key, value: Array<{key, value}> }> + // } + for (const auto updatedRef : dispatch.body["updated"].toArray()) + { + auto updated = updatedRef.toObject(); + if (updated["key"].toString() != "connections") + { + continue; + } + for (const auto valueRef : updated["value"].toArray()) + { + auto value = valueRef.toObject(); + if (value["key"].toString() != "emote_set") + { + continue; + } + + const UserConnectionUpdateDispatch update( + dispatch, value, (size_t)updated["index"].toInt()); + + if (update.validate()) + { + this->signals_.userUpdated.invoke(update); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } + } +} + } // namespace chatterino diff --git a/src/providers/seventv/SeventvEventAPI.hpp b/src/providers/seventv/SeventvEventAPI.hpp index 946e147cb..5672e59b8 100644 --- a/src/providers/seventv/SeventvEventAPI.hpp +++ b/src/providers/seventv/SeventvEventAPI.hpp @@ -8,14 +8,17 @@ namespace chatterino { -struct SeventvEventAPISubscription; -struct SeventvEventAPIDispatch; -struct SeventvEventAPIEmoteAddDispatch; -struct SeventvEventAPIEmoteUpdateDispatch; -struct SeventvEventAPIEmoteRemoveDispatch; -struct SeventvEventAPIUserConnectionUpdateDispatch; +namespace seventv::eventapi { + struct Subscription; + struct Dispatch; + struct EmoteAddDispatch; + struct EmoteUpdateDispatch; + struct EmoteRemoveDispatch; + struct UserConnectionUpdateDispatch; +} // namespace seventv::eventapi -class SeventvEventAPI : public BasicPubSubManager +class SeventvEventAPI + : public BasicPubSubManager { template using Signal = @@ -27,10 +30,10 @@ public: std::chrono::milliseconds(25000)); struct { - Signal emoteAdded; - Signal emoteUpdated; - Signal emoteRemoved; - Signal userUpdated; + Signal emoteAdded; + Signal emoteUpdated; + Signal emoteRemoved; + Signal userUpdated; } signals_; // NOLINT(readability-identifier-naming) /** @@ -48,18 +51,23 @@ public: void unsubscribeEmoteSet(const QString &id); protected: - std::shared_ptr> + std::shared_ptr> createClient(liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl) override; void onMessage( websocketpp::connection_hdl hdl, - BasicPubSubManager::WebsocketMessagePtr + BasicPubSubManager::WebsocketMessagePtr msg) override; private: - void handleDispatch(const SeventvEventAPIDispatch &dispatch); + void handleDispatch(const seventv::eventapi::Dispatch &dispatch); + void onEmoteSetUpdate(const seventv::eventapi::Dispatch &dispatch); + void onUserUpdate(const seventv::eventapi::Dispatch &dispatch); + + /** emote-set ids */ std::unordered_set subscribedEmoteSets_; + /** user ids */ std::unordered_set subscribedUsers_; std::chrono::milliseconds heartbeatInterval_; }; diff --git a/src/providers/seventv/eventapi/SeventvEventAPIClient.cpp b/src/providers/seventv/eventapi/Client.cpp similarity index 64% rename from src/providers/seventv/eventapi/SeventvEventAPIClient.cpp rename to src/providers/seventv/eventapi/Client.cpp index e1fcf2d02..f266478ce 100644 --- a/src/providers/seventv/eventapi/SeventvEventAPIClient.cpp +++ b/src/providers/seventv/eventapi/Client.cpp @@ -1,44 +1,42 @@ -#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp" +#include "providers/seventv/eventapi/Client.hpp" -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" #include "providers/twitch/PubSubHelpers.hpp" #include -namespace chatterino { +namespace chatterino::seventv::eventapi { -SeventvEventAPIClient::SeventvEventAPIClient( - liveupdates::WebsocketClient &websocketClient, - liveupdates::WebsocketHandle handle, - std::chrono::milliseconds heartbeatInterval) - : BasicPubSubClient(websocketClient, - std::move(handle)) +Client::Client(liveupdates::WebsocketClient &websocketClient, + liveupdates::WebsocketHandle handle, + std::chrono::milliseconds heartbeatInterval) + : BasicPubSubClient(websocketClient, std::move(handle)) , lastHeartbeat_(std::chrono::steady_clock::now()) , heartbeatInterval_(heartbeatInterval) { } -void SeventvEventAPIClient::onConnectionEstablished() +void Client::onConnectionEstablished() { this->lastHeartbeat_.store(std::chrono::steady_clock::now(), std::memory_order_release); this->checkHeartbeat(); } -void SeventvEventAPIClient::setHeartbeatInterval(int intervalMs) +void Client::setHeartbeatInterval(int intervalMs) { qCDebug(chatterinoSeventvEventAPI) << "Setting expected heartbeat interval to" << intervalMs << "ms"; this->heartbeatInterval_ = std::chrono::milliseconds(intervalMs); } -void SeventvEventAPIClient::handleHeartbeat() +void Client::handleHeartbeat() { this->lastHeartbeat_.store(std::chrono::steady_clock::now(), std::memory_order_release); } -void SeventvEventAPIClient::checkHeartbeat() +void Client::checkHeartbeat() { // Following the heartbeat docs, a connection is dead // after three missed heartbeats. @@ -54,8 +52,7 @@ void SeventvEventAPIClient::checkHeartbeat() return; } - auto self = std::dynamic_pointer_cast( - this->shared_from_this()); + auto self = std::dynamic_pointer_cast(this->shared_from_this()); runAfter(this->websocketClient_.get_io_service(), this->heartbeatInterval_, [self](auto) { @@ -67,4 +64,4 @@ void SeventvEventAPIClient::checkHeartbeat() }); } -} // namespace chatterino +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/SeventvEventAPIClient.hpp b/src/providers/seventv/eventapi/Client.hpp similarity index 55% rename from src/providers/seventv/eventapi/SeventvEventAPIClient.hpp rename to src/providers/seventv/eventapi/Client.hpp index f33256d42..11683edcf 100644 --- a/src/providers/seventv/eventapi/SeventvEventAPIClient.hpp +++ b/src/providers/seventv/eventapi/Client.hpp @@ -2,18 +2,22 @@ #include "providers/liveupdates/BasicPubSubClient.hpp" // this needs to be included for the specialization -// of std::hash for SeventvEventAPISubscription -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" +// of std::hash for Subscription +#include "providers/seventv/eventapi/Subscription.hpp" namespace chatterino { +class SeventvEventAPI; -class SeventvEventAPIClient - : public BasicPubSubClient +} // namespace chatterino + +namespace chatterino::seventv::eventapi { + +class Client : public BasicPubSubClient { public: - SeventvEventAPIClient(liveupdates::WebsocketClient &websocketClient, - liveupdates::WebsocketHandle handle, - std::chrono::milliseconds heartbeatInterval); + Client(liveupdates::WebsocketClient &websocketClient, + liveupdates::WebsocketHandle handle, + std::chrono::milliseconds heartbeatInterval); void setHeartbeatInterval(int intervalMs); void handleHeartbeat(); @@ -29,7 +33,7 @@ private: // This will be set once on the welcome message. std::chrono::milliseconds heartbeatInterval_; - friend class SeventvEventAPI; + friend SeventvEventAPI; }; -} // namespace chatterino +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/SeventvEventAPIDispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp similarity index 62% rename from src/providers/seventv/eventapi/SeventvEventAPIDispatch.cpp rename to src/providers/seventv/eventapi/Dispatch.cpp index 52d4bde6f..bb4b4fa1d 100644 --- a/src/providers/seventv/eventapi/SeventvEventAPIDispatch.cpp +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -1,21 +1,20 @@ -#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" #include -namespace chatterino { +namespace chatterino::seventv::eventapi { -SeventvEventAPIDispatch::SeventvEventAPIDispatch(QJsonObject obj) - : type(magic_enum::enum_cast( +Dispatch::Dispatch(QJsonObject obj) + : type(magic_enum::enum_cast( obj["type"].toString().toStdString()) - .value_or(SeventvEventAPISubscriptionType::INVALID)) + .value_or(SubscriptionType::INVALID)) , body(obj["body"].toObject()) , id(this->body["id"].toString()) , actorName(this->body["actor"].toObject()["display_name"].toString()) { } -SeventvEventAPIEmoteAddDispatch::SeventvEventAPIEmoteAddDispatch( - const SeventvEventAPIDispatch &dispatch, QJsonObject emote) +EmoteAddDispatch::EmoteAddDispatch(const Dispatch &dispatch, QJsonObject emote) : emoteSetID(dispatch.id) , actorName(dispatch.actorName) , emoteJson(std::move(emote)) @@ -23,7 +22,7 @@ SeventvEventAPIEmoteAddDispatch::SeventvEventAPIEmoteAddDispatch( { } -bool SeventvEventAPIEmoteAddDispatch::validate() const +bool EmoteAddDispatch::validate() const { bool validValues = !this->emoteSetID.isEmpty() && !this->emoteJson.isEmpty(); @@ -43,8 +42,8 @@ bool SeventvEventAPIEmoteAddDispatch::validate() const emoteData.contains("owner"); } -SeventvEventAPIEmoteRemoveDispatch::SeventvEventAPIEmoteRemoveDispatch( - const SeventvEventAPIDispatch &dispatch, QJsonObject emote) +EmoteRemoveDispatch::EmoteRemoveDispatch(const Dispatch &dispatch, + QJsonObject emote) : emoteSetID(dispatch.id) , actorName(dispatch.actorName) , emoteName(emote["name"].toString()) @@ -52,15 +51,15 @@ SeventvEventAPIEmoteRemoveDispatch::SeventvEventAPIEmoteRemoveDispatch( { } -bool SeventvEventAPIEmoteRemoveDispatch::validate() const +bool EmoteRemoveDispatch::validate() const { return !this->emoteSetID.isEmpty() && !this->emoteName.isEmpty() && !this->emoteID.isEmpty(); } -SeventvEventAPIEmoteUpdateDispatch::SeventvEventAPIEmoteUpdateDispatch( - const SeventvEventAPIDispatch &dispatch, QJsonObject oldValue, - QJsonObject value) +EmoteUpdateDispatch::EmoteUpdateDispatch(const Dispatch &dispatch, + QJsonObject oldValue, + QJsonObject value) : emoteSetID(dispatch.id) , actorName(dispatch.actorName) , emoteID(value["id"].toString()) @@ -69,17 +68,15 @@ SeventvEventAPIEmoteUpdateDispatch::SeventvEventAPIEmoteUpdateDispatch( { } -bool SeventvEventAPIEmoteUpdateDispatch::validate() const +bool EmoteUpdateDispatch::validate() const { return !this->emoteSetID.isEmpty() && !this->emoteID.isEmpty() && !this->oldEmoteName.isEmpty() && !this->emoteName.isEmpty() && this->oldEmoteName != this->emoteName; } -SeventvEventAPIUserConnectionUpdateDispatch:: - SeventvEventAPIUserConnectionUpdateDispatch( - const SeventvEventAPIDispatch &dispatch, const QJsonObject &update, - size_t connectionIndex) +UserConnectionUpdateDispatch::UserConnectionUpdateDispatch( + const Dispatch &dispatch, const QJsonObject &update, size_t connectionIndex) : userID(dispatch.id) , actorName(dispatch.actorName) , oldEmoteSetID(update["old_value"].toObject()["id"].toString()) @@ -88,10 +85,10 @@ SeventvEventAPIUserConnectionUpdateDispatch:: { } -bool SeventvEventAPIUserConnectionUpdateDispatch::validate() const +bool UserConnectionUpdateDispatch::validate() const { return !this->userID.isEmpty() && !this->oldEmoteSetID.isEmpty() && !this->emoteSetID.isEmpty(); } -} // namespace chatterino +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Dispatch.hpp b/src/providers/seventv/eventapi/Dispatch.hpp new file mode 100644 index 000000000..666f5c28a --- /dev/null +++ b/src/providers/seventv/eventapi/Dispatch.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "providers/seventv/eventapi/Subscription.hpp" + +#include +#include + +namespace chatterino::seventv::eventapi { + +// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#message-payload +struct Dispatch { + SubscriptionType type; + QJsonObject body; + QString id; + // it's okay for this to be empty + QString actorName; + + Dispatch(QJsonObject obj); +}; + +struct EmoteAddDispatch { + QString emoteSetID; + QString actorName; + QJsonObject emoteJson; + QString emoteID; + + EmoteAddDispatch(const Dispatch &dispatch, QJsonObject emote); + + bool validate() const; +}; + +struct EmoteRemoveDispatch { + QString emoteSetID; + QString actorName; + QString emoteName; + QString emoteID; + + EmoteRemoveDispatch(const Dispatch &dispatch, QJsonObject emote); + + bool validate() const; +}; + +struct EmoteUpdateDispatch { + QString emoteSetID; + QString actorName; + QString emoteID; + QString oldEmoteName; + QString emoteName; + + EmoteUpdateDispatch(const Dispatch &dispatch, QJsonObject oldValue, + QJsonObject value); + + bool validate() const; +}; + +struct UserConnectionUpdateDispatch { + QString userID; + QString actorName; + QString oldEmoteSetID; + QString emoteSetID; + size_t connectionIndex; + + UserConnectionUpdateDispatch(const Dispatch &dispatch, + const QJsonObject &update, + size_t connectionIndex); + + bool validate() const; +}; + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Message.cpp b/src/providers/seventv/eventapi/Message.cpp new file mode 100644 index 000000000..a216a72b4 --- /dev/null +++ b/src/providers/seventv/eventapi/Message.cpp @@ -0,0 +1,11 @@ +#include "providers/seventv/eventapi/Message.hpp" + +namespace chatterino::seventv::eventapi { + +Message::Message(QJsonObject _json) + : data(_json["d"].toObject()) + , op(Opcode(_json["op"].toInt())) +{ +} + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/SeventvEventAPIMessage.hpp b/src/providers/seventv/eventapi/Message.hpp similarity index 50% rename from src/providers/seventv/eventapi/SeventvEventAPIMessage.hpp rename to src/providers/seventv/eventapi/Message.hpp index f18d07f31..1b857f9ea 100644 --- a/src/providers/seventv/eventapi/SeventvEventAPIMessage.hpp +++ b/src/providers/seventv/eventapi/Message.hpp @@ -1,6 +1,6 @@ #pragma once -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" #include #include @@ -8,27 +8,26 @@ #include #include -namespace chatterino { +namespace chatterino::seventv::eventapi { -struct SeventvEventAPIMessage { +struct Message { QJsonObject data; - SeventvEventAPIOpcode op; + Opcode op; - SeventvEventAPIMessage(QJsonObject _json); + Message(QJsonObject _json); template boost::optional toInner(); }; template -boost::optional SeventvEventAPIMessage::toInner() +boost::optional Message::toInner() { return InnerClass{this->data}; } -static boost::optional parseSeventvEventAPIBaseMessage( - const QString &blob) +static boost::optional parseBaseMessage(const QString &blob) { QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8())); @@ -37,7 +36,7 @@ static boost::optional parseSeventvEventAPIBaseMessage( return boost::none; } - return SeventvEventAPIMessage(jsonDoc.object()); + return Message(jsonDoc.object()); } -} // namespace chatterino +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/SeventvEventAPIDispatch.hpp b/src/providers/seventv/eventapi/SeventvEventAPIDispatch.hpp deleted file mode 100644 index a11b8471e..000000000 --- a/src/providers/seventv/eventapi/SeventvEventAPIDispatch.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" - -#include -#include - -namespace chatterino { - -// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#message-payload -struct SeventvEventAPIDispatch { - SeventvEventAPISubscriptionType type; - QJsonObject body; - QString id; - // it's okay for this to be empty - QString actorName; - - SeventvEventAPIDispatch(QJsonObject obj); -}; - -struct SeventvEventAPIEmoteAddDispatch { - QString emoteSetID; - QString actorName; - QJsonObject emoteJson; - QString emoteID; - - SeventvEventAPIEmoteAddDispatch(const SeventvEventAPIDispatch &dispatch, - QJsonObject emote); - - bool validate() const; -}; - -struct SeventvEventAPIEmoteRemoveDispatch { - QString emoteSetID; - QString actorName; - QString emoteName; - QString emoteID; - - SeventvEventAPIEmoteRemoveDispatch(const SeventvEventAPIDispatch &dispatch, - QJsonObject emote); - - bool validate() const; -}; - -struct SeventvEventAPIEmoteUpdateDispatch { - QString emoteSetID; - QString actorName; - QString emoteID; - QString oldEmoteName; - QString emoteName; - - SeventvEventAPIEmoteUpdateDispatch(const SeventvEventAPIDispatch &dispatch, - QJsonObject oldValue, QJsonObject value); - - bool validate() const; -}; - -struct SeventvEventAPIUserConnectionUpdateDispatch { - QString userID; - QString actorName; - QString oldEmoteSetID; - QString emoteSetID; - size_t connectionIndex; - - SeventvEventAPIUserConnectionUpdateDispatch( - const SeventvEventAPIDispatch &dispatch, const QJsonObject &update, - size_t connectionIndex); - - bool validate() const; -}; - -} // namespace chatterino diff --git a/src/providers/seventv/eventapi/SeventvEventAPIMessage.cpp b/src/providers/seventv/eventapi/SeventvEventAPIMessage.cpp deleted file mode 100644 index 69b3b2b22..000000000 --- a/src/providers/seventv/eventapi/SeventvEventAPIMessage.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp" - -namespace chatterino { - -SeventvEventAPIMessage::SeventvEventAPIMessage(QJsonObject _json) - : data(_json["d"].toObject()) - , op(SeventvEventAPIOpcode(_json["op"].toInt())) -{ -} - -} // namespace chatterino diff --git a/src/providers/seventv/eventapi/SeventvEventAPISubscription.cpp b/src/providers/seventv/eventapi/SeventvEventAPISubscription.cpp deleted file mode 100644 index 6ae3b9363..000000000 --- a/src/providers/seventv/eventapi/SeventvEventAPISubscription.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" - -#include -#include -#include - -#include - -namespace { - -using namespace chatterino; - -const char *typeToString(SeventvEventAPISubscriptionType type) -{ - switch (type) - { - case SeventvEventAPISubscriptionType::UpdateEmoteSet: - return "emote_set.update"; - case SeventvEventAPISubscriptionType::UpdateUser: - return "user.update"; - default: - return ""; - } -} - -QJsonObject createDataJson(const char *typeName, const QString &condition) -{ - QJsonObject data; - data["type"] = typeName; - { - QJsonObject conditionObj; - conditionObj["object_id"] = condition; - data["condition"] = conditionObj; - } - return data; -} - -} // namespace - -namespace chatterino { - -bool SeventvEventAPISubscription::operator==( - const SeventvEventAPISubscription &rhs) const -{ - return std::tie(this->condition, this->type) == - std::tie(rhs.condition, rhs.type); -} - -bool SeventvEventAPISubscription::operator!=( - const SeventvEventAPISubscription &rhs) const -{ - return !(rhs == *this); -} - -QByteArray SeventvEventAPISubscription::encodeSubscribe() const -{ - const auto *typeName = typeToString(this->type); - QJsonObject root; - root["op"] = (int)SeventvEventAPIOpcode::Subscribe; - root["d"] = createDataJson(typeName, this->condition); - return QJsonDocument(root).toJson(); -} - -QByteArray SeventvEventAPISubscription::encodeUnsubscribe() const -{ - const auto *typeName = typeToString(this->type); - QJsonObject root; - root["op"] = (int)SeventvEventAPIOpcode::Unsubscribe; - root["d"] = createDataJson(typeName, this->condition); - return QJsonDocument(root).toJson(); -} - -QDebug &operator<<(QDebug &dbg, const SeventvEventAPISubscription &subscription) -{ - dbg << "SeventvEventAPISubscription{ condition:" << subscription.condition - << "type:" << (int)subscription.type << '}'; - return dbg; -} - -} // namespace chatterino diff --git a/src/providers/seventv/eventapi/SeventvEventAPISubscription.hpp b/src/providers/seventv/eventapi/SeventvEventAPISubscription.hpp deleted file mode 100644 index 55c233da7..000000000 --- a/src/providers/seventv/eventapi/SeventvEventAPISubscription.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace chatterino { - -// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types -enum class SeventvEventAPISubscriptionType { - UpdateEmoteSet, - UpdateUser, - - INVALID, -}; - -// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#opcodes -enum class SeventvEventAPIOpcode { - Dispatch = 0, - Hello = 1, - Heartbeat = 2, - Reconnect = 4, - Ack = 5, - Error = 6, - EndOfStream = 7, - Identify = 33, - Resume = 34, - Subscribe = 35, - Unsubscribe = 36, - Signal = 37, -}; - -struct SeventvEventAPISubscription { - bool operator==(const SeventvEventAPISubscription &rhs) const; - bool operator!=(const SeventvEventAPISubscription &rhs) const; - QString condition; - SeventvEventAPISubscriptionType type; - - QByteArray encodeSubscribe() const; - QByteArray encodeUnsubscribe() const; - - friend QDebug &operator<<(QDebug &dbg, - const SeventvEventAPISubscription &subscription); -}; - -} // namespace chatterino - -template <> -constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< - chatterino::SeventvEventAPISubscriptionType>( - chatterino::SeventvEventAPISubscriptionType value) noexcept -{ - switch (value) - { - case chatterino::SeventvEventAPISubscriptionType::UpdateEmoteSet: - return "emote_set.update"; - case chatterino::SeventvEventAPISubscriptionType::UpdateUser: - return "user.update"; - - default: - return default_tag; - } -} - -namespace std { - -template <> -struct hash { - size_t operator()(const chatterino::SeventvEventAPISubscription &sub) const - { - return (size_t)qHash(sub.condition, qHash((int)sub.type)); - } -}; - -} // namespace std diff --git a/src/providers/seventv/eventapi/Subscription.cpp b/src/providers/seventv/eventapi/Subscription.cpp new file mode 100644 index 000000000..1de1f667e --- /dev/null +++ b/src/providers/seventv/eventapi/Subscription.cpp @@ -0,0 +1,105 @@ +#include "providers/seventv/eventapi/Subscription.hpp" + +#include +#include +#include + +#include +#include + +namespace { + +using namespace chatterino::seventv::eventapi; + +const char *typeToString(SubscriptionType type) +{ + return magic_enum::enum_name(type).data(); +} + +QJsonObject createDataJson(const char *typeName, const Condition &condition) +{ + QJsonObject data; + data["type"] = typeName; + data["condition"] = std::visit( + [](const auto &c) { + return c.encode(); + }, + condition); + return data; +} + +} // namespace + +namespace chatterino::seventv::eventapi { + +bool Subscription::operator==(const Subscription &rhs) const +{ + return std::tie(this->condition, this->type) == + std::tie(rhs.condition, rhs.type); +} + +bool Subscription::operator!=(const Subscription &rhs) const +{ + return !(rhs == *this); +} + +QByteArray Subscription::encodeSubscribe() const +{ + const auto *typeName = typeToString(this->type); + QJsonObject root; + root["op"] = (int)Opcode::Subscribe; + root["d"] = createDataJson(typeName, this->condition); + return QJsonDocument(root).toJson(); +} + +QByteArray Subscription::encodeUnsubscribe() const +{ + const auto *typeName = typeToString(this->type); + QJsonObject root; + root["op"] = (int)Opcode::Unsubscribe; + root["d"] = createDataJson(typeName, this->condition); + return QJsonDocument(root).toJson(); +} + +QDebug &operator<<(QDebug &dbg, const Subscription &subscription) +{ + std::visit( + [&](const auto &cond) { + dbg << "Subscription{ condition:" << cond + << "type:" << magic_enum::enum_name(subscription.type).data() + << '}'; + }, + subscription.condition); + return dbg; +} + +ObjectIDCondition::ObjectIDCondition(QString objectID) + : objectID(std::move(objectID)) +{ +} + +QJsonObject ObjectIDCondition::encode() const +{ + QJsonObject obj; + obj["object_id"] = this->objectID; + + return obj; +} + +bool ObjectIDCondition::operator==(const ObjectIDCondition &rhs) const +{ + return this->objectID == rhs.objectID; +} + +bool ObjectIDCondition::operator!=(const ObjectIDCondition &rhs) const +{ + return !(*this == rhs); +} + +QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition) +{ + dbg << "{ objectID:" << condition.objectID << "}"; + return dbg; +} + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Subscription.hpp b/src/providers/seventv/eventapi/Subscription.hpp new file mode 100644 index 000000000..53143fbd8 --- /dev/null +++ b/src/providers/seventv/eventapi/Subscription.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace chatterino::seventv::eventapi { + +// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types +enum class SubscriptionType { + UpdateEmoteSet, + UpdateUser, + + INVALID, +}; + +// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#opcodes +enum class Opcode { + Dispatch = 0, + Hello = 1, + Heartbeat = 2, + Reconnect = 4, + Ack = 5, + Error = 6, + EndOfStream = 7, + Identify = 33, + Resume = 34, + Subscribe = 35, + Unsubscribe = 36, + Signal = 37, +}; + +struct ObjectIDCondition { + ObjectIDCondition(QString objectID); + + QString objectID; + + QJsonObject encode() const; + + friend QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition); + bool operator==(const ObjectIDCondition &rhs) const; + bool operator!=(const ObjectIDCondition &rhs) const; +}; + +using Condition = std::variant; + +struct Subscription { + bool operator==(const Subscription &rhs) const; + bool operator!=(const Subscription &rhs) const; + Condition condition; + SubscriptionType type; + + QByteArray encodeSubscribe() const; + QByteArray encodeUnsubscribe() const; + + friend QDebug &operator<<(QDebug &dbg, const Subscription &subscription); +}; + +} // namespace chatterino::seventv::eventapi + +template <> +constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< + chatterino::seventv::eventapi::SubscriptionType>( + chatterino::seventv::eventapi::SubscriptionType value) noexcept +{ + using chatterino::seventv::eventapi::SubscriptionType; + switch (value) + { + case SubscriptionType::UpdateEmoteSet: + return "emote_set.update"; + case SubscriptionType::UpdateUser: + return "user.update"; + + default: + return default_tag; + } +} + +namespace std { + +template <> +struct hash { + size_t operator()( + const chatterino::seventv::eventapi::ObjectIDCondition &c) const + { + return (size_t)qHash(c.objectID); + } +}; + +template <> +struct hash { + size_t operator()( + const chatterino::seventv::eventapi::Subscription &sub) const + { + const size_t conditionHash = + std::hash{}( + sub.condition); + return (size_t)qHash(conditionHash, qHash((int)sub.type)); + } +}; + +} // namespace std diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 15d6a4898..1c04eac4e 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -311,10 +311,11 @@ std::vector IrcMessageHandler::parsePrivMessage( void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchIrcServer &server) { - // This is to make sure that combined emoji go through properly, see - // https://github.com/Chatterino/chatterino2/issues/3384 and + // This is for compatibility with older Chatterino versions. Twitch didn't use + // to allow ZERO WIDTH JOINER unicode character, so Chatterino used ESCAPE_TAG + // instead. + // See https://github.com/Chatterino/chatterino2/issues/3384 and // https://mm2pl.github.io/emoji_rfc.pdf for more details - // Constants used here are defined in TwitchChannel.hpp this->addMessage( message, message->target(), diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index 6bfc67d10..644e494cb 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -1,6 +1,7 @@ #include "providers/twitch/PubSubManager.hpp" #include "common/QLogging.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubClient.hpp" #include "providers/twitch/PubSubHelpers.hpp" @@ -514,6 +515,8 @@ void PubSub::addClient() return; } + NetworkConfigurationProvider::applyToWebSocket(con); + this->websocketClient.connect(con); } diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 5e3892ed6..89972c8d0 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -18,7 +18,7 @@ #include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/RecentMessagesApi.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/SeventvEmotes.hpp" #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/api/Helix.hpp" @@ -121,16 +121,6 @@ TwitchChannel::TwitchChannel(const QString &name) this->loadRecentMessagesReconnect(); }); - this->destroyed.connect([this]() { - getApp()->twitch->dropSeventvChannel(this->seventvUserID_, - this->seventvEmoteSetID_); - - if (getApp()->twitch->bttvLiveUpdates) - { - getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId()); - } - }); - this->messageRemovedFromStart.connect([this](MessagePtr &msg) { if (msg->replyThread) { @@ -169,6 +159,17 @@ TwitchChannel::TwitchChannel(const QString &name) #endif } +TwitchChannel::~TwitchChannel() +{ + getApp()->twitch->dropSeventvChannel(this->seventvUserID_, + this->seventvEmoteSetID_); + + if (getApp()->twitch->bttvLiveUpdates) + { + getApp()->twitch->bttvLiveUpdates->partChannel(this->roomId()); + } +} + void TwitchChannel::initialize() { this->fetchDisplayName(); @@ -303,7 +304,21 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) << "[TwitchChannel" << this->getName() << "] Channel point reward added:" << reward.id << "," << reward.title << "," << reward.isUserInputRequired; - this->channelPointRewardAdded.invoke(reward); + + // TODO: There's an underlying bug here. This bug should be fixed. + // This only attempts to prevent a crash when invoking the signal. + try + { + this->channelPointRewardAdded.invoke(reward); + } + catch (const std::bad_function_call &) + { + qCWarning(chatterinoTwitch).nospace() + << "[TwitchChannel " << this->getName() + << "] Caught std::bad_function_call when adding channel point " + "reward ChannelPointReward{ id: " + << reward.id << ", title: " << reward.title << " }."; + } } } @@ -355,10 +370,6 @@ QString TwitchChannel::prepareMessage(const QString &message) const auto app = getApp(); QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); - // This is to make sure that combined emoji go through properly, see - // https://github.com/Chatterino/chatterino2/issues/3384 and - // https://mm2pl.github.io/emoji_rfc.pdf for more details - parsedMessage.replace(ZERO_WIDTH_JOINER, ESCAPE_TAG); parsedMessage = parsedMessage.simplified(); if (parsedMessage.isEmpty()) @@ -691,7 +702,7 @@ void TwitchChannel::removeBttvEmote( } void TwitchChannel::addSeventvEmote( - const SeventvEventAPIEmoteAddDispatch &dispatch) + const seventv::eventapi::EmoteAddDispatch &dispatch) { if (!SeventvEmotes::addEmote(this->seventvEmotes_, dispatch)) { @@ -703,7 +714,7 @@ void TwitchChannel::addSeventvEmote( } void TwitchChannel::updateSeventvEmote( - const SeventvEventAPIEmoteUpdateDispatch &dispatch) + const seventv::eventapi::EmoteUpdateDispatch &dispatch) { if (!SeventvEmotes::updateEmote(this->seventvEmotes_, dispatch)) { @@ -717,7 +728,7 @@ void TwitchChannel::updateSeventvEmote( } void TwitchChannel::removeSeventvEmote( - const SeventvEventAPIEmoteRemoveDispatch &dispatch) + const seventv::eventapi::EmoteRemoveDispatch &dispatch) { auto removed = SeventvEmotes::removeEmote(this->seventvEmotes_, dispatch); if (!removed) @@ -730,7 +741,7 @@ void TwitchChannel::removeSeventvEmote( } void TwitchChannel::updateSeventvUser( - const SeventvEventAPIUserConnectionUpdateDispatch &dispatch) + const seventv::eventapi::UserConnectionUpdateDispatch &dispatch) { if (dispatch.connectionIndex != this->seventvUserTwitchConnectionIndex_) { diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 7435f06d8..746b54ed6 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -22,8 +22,10 @@ namespace chatterino { -// This is to make sure that combined emoji go through properly, see -// https://github.com/Chatterino/chatterino2/issues/3384 and +// This is for compatibility with older Chatterino versions. Twitch didn't use +// to allow ZERO WIDTH JOINER unicode character, so Chatterino used ESCAPE_TAG +// instead. +// See https://github.com/Chatterino/chatterino2/issues/3384 and // https://mm2pl.github.io/emoji_rfc.pdf for more details const QString ZERO_WIDTH_JOINER = QString(QChar(0x200D)); @@ -49,11 +51,15 @@ class FfzEmotes; class BttvEmotes; struct BttvLiveUpdateEmoteUpdateAddMessage; struct BttvLiveUpdateEmoteRemoveMessage; + class SeventvEmotes; -struct SeventvEventAPIEmoteAddDispatch; -struct SeventvEventAPIEmoteUpdateDispatch; -struct SeventvEventAPIEmoteRemoveDispatch; -struct SeventvEventAPIUserConnectionUpdateDispatch; +namespace seventv::eventapi { + struct EmoteAddDispatch; + struct EmoteUpdateDispatch; + struct EmoteRemoveDispatch; + struct UserConnectionUpdateDispatch; +} // namespace seventv::eventapi + struct ChannelPointReward; class MessageThread; struct CheerEmoteSet; @@ -98,6 +104,7 @@ public: }; explicit TwitchChannel(const QString &channelName); + ~TwitchChannel() override; void initialize(); @@ -149,14 +156,16 @@ public: void removeBttvEmote(const BttvLiveUpdateEmoteRemoveMessage &message); /** Adds a 7TV channel emote to this channel. */ - void addSeventvEmote(const SeventvEventAPIEmoteAddDispatch &dispatch); + void addSeventvEmote(const seventv::eventapi::EmoteAddDispatch &dispatch); /** Updates a 7TV channel emote's name in this channel */ - void updateSeventvEmote(const SeventvEventAPIEmoteUpdateDispatch &dispatch); + void updateSeventvEmote( + const seventv::eventapi::EmoteUpdateDispatch &dispatch); /** Removes a 7TV channel emote from this channel */ - void removeSeventvEmote(const SeventvEventAPIEmoteRemoveDispatch &dispatch); + void removeSeventvEmote( + const seventv::eventapi::EmoteRemoveDispatch &dispatch); /** Updates the current 7TV user. Currently, only the emote-set is updated. */ void updateSeventvUser( - const SeventvEventAPIUserConnectionUpdateDispatch &dispatch); + const seventv::eventapi::UserConnectionUpdateDispatch &dispatch); // Update the channel's 7TV information (the channel's 7TV user ID and emote set ID) void updateSeventvData(const QString &newUserID, diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 70d197f69..c608f9e0b 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -7,7 +7,7 @@ #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "providers/bttv/BttvLiveUpdates.hpp" -#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" #include "providers/seventv/SeventvEventAPI.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/ChannelPointReward.hpp" diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 30d62d84a..c866d9675 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -9,6 +9,8 @@ #include #include +#include + namespace chatterino { struct Emote; diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index 8c6c42660..79344ac72 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -142,6 +142,7 @@ void Paths::initSubDirectories() this->miscDirectory = makePath("Misc"); this->twitchProfileAvatars = makePath("ProfileAvatars"); this->pluginsDirectory = makePath("Plugins"); + this->crashdumpDirectory = makePath("Crashes"); //QDir().mkdir(this->twitchProfileAvatars + "/twitch"); } diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index 5cada9594..f20195fef 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -25,6 +25,9 @@ public: // Directory for miscellaneous files. Same as /Misc QString miscDirectory; + // Directory for crashdumps. Same as /Crashes + QString crashdumpDirectory; + // Hash of QCoreApplication::applicationFilePath() QString applicationFilePathHash; diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 289faeeba..4adc419e1 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -288,6 +288,13 @@ public: QStringSetting selfHighlightColor = {"/highlighting/selfHighlightColor", ""}; + BoolSetting enableSelfMessageHighlight = { + "/highlighting/selfMessageHighlight/enabled", false}; + BoolSetting showSelfMessageHighlightInMentions = { + "/highlighting/selfMessageHighlight/showInMentions", false}; + QStringSetting selfMessageHighlightColor = { + "/highlighting/selfMessageHighlight/color", ""}; + BoolSetting enableWhisperHighlight = { "/highlighting/whisperHighlight/whispersHighlighted", true}; BoolSetting enableWhisperHighlightSound = { diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index 0092d3284..074f07c1b 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -1,15 +1,17 @@ #include "util/IncognitoBrowser.hpp" +#ifdef USEWINSDK +# include "util/WindowsHelper.hpp" +#endif #include #include -#include #include namespace { using namespace chatterino; -#ifdef Q_OS_WIN +#ifdef USEWINSDK QString injectPrivateSwitch(QString command) { // list of command line switches to turn on private browsing in browsers @@ -47,23 +49,27 @@ QString injectPrivateSwitch(QString command) QString getCommand() { - // get default browser prog id - auto browserId = QSettings("HKEY_CURRENT_" - "USER\\Software\\Microsoft\\Windows\\Shell\\" - "Associations\\UrlAssociatio" - "ns\\http\\UserChoice", - QSettings::NativeFormat) - .value("Progid") - .toString(); + // get default browser start command, by protocol if possible, falling back to extension if not + QString command = + getAssociatedCommand(AssociationQueryType::Protocol, L"http"); - // get default browser start command - auto command = - QSettings("HKEY_CLASSES_ROOT\\" + browserId + "\\shell\\open\\command", - QSettings::NativeFormat) - .value("Default") - .toString(); if (command.isNull()) { + // failed to fetch default browser by protocol, try by file extension instead + command = + getAssociatedCommand(AssociationQueryType::FileExtension, L".html"); + } + + if (command.isNull()) + { + // also try the equivalent .htm extension + command = + getAssociatedCommand(AssociationQueryType::FileExtension, L".htm"); + } + + if (command.isNull()) + { + // failed to find browser command return QString(); } @@ -84,7 +90,7 @@ namespace chatterino { bool supportsIncognitoLinks() { -#ifdef Q_OS_WIN +#ifdef USEWINSDK return !getCommand().isNull(); #else return false; @@ -93,7 +99,7 @@ bool supportsIncognitoLinks() bool openLinkIncognito(const QString &link) { -#ifdef Q_OS_WIN +#ifdef USEWINSDK auto command = getCommand(); // TODO: split command into program path and incognito argument diff --git a/src/util/WindowsHelper.cpp b/src/util/WindowsHelper.cpp index 06fc0dd79..d46d29158 100644 --- a/src/util/WindowsHelper.cpp +++ b/src/util/WindowsHelper.cpp @@ -6,6 +6,9 @@ #ifdef USEWINSDK +# include +# include + namespace chatterino { typedef enum MONITOR_DPI_TYPE { @@ -17,6 +20,8 @@ typedef enum MONITOR_DPI_TYPE { typedef HRESULT(CALLBACK *GetDpiForMonitor_)(HMONITOR, MONITOR_DPI_TYPE, UINT *, UINT *); +typedef HRESULT(CALLBACK *AssocQueryString_)(ASSOCF, ASSOCSTR, LPCWSTR, LPCWSTR, + LPWSTR, DWORD *); boost::optional getWindowDpi(HWND hwnd) { @@ -83,6 +88,67 @@ void setRegisteredForStartup(bool isRegistered) } } +QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query) +{ + static HINSTANCE shlwapi = LoadLibrary(L"shlwapi"); + if (shlwapi == nullptr) + { + return QString(); + } + + static auto assocQueryString = + AssocQueryString_(GetProcAddress(shlwapi, "AssocQueryStringW")); + if (assocQueryString == nullptr) + { + return QString(); + } + + // always error out instead of returning a truncated string when the + // buffer is too small - avoids race condition when the user changes their + // default browser between calls to AssocQueryString + ASSOCF flags = ASSOCF_NOTRUNCATE; + + if (queryType == AssociationQueryType::Protocol) + { + // ASSOCF_IS_PROTOCOL was introduced in Windows 8 + if (IsWindows8OrGreater()) + { + flags |= ASSOCF_IS_PROTOCOL; + } + else + { + return QString(); + } + } + + DWORD resultSize = 0; + if (FAILED(assocQueryString(flags, ASSOCSTR_COMMAND, query, nullptr, + nullptr, &resultSize))) + { + return QString(); + } + + if (resultSize <= 1) + { + // resultSize includes the null terminator. if resultSize is 1, the + // returned value would be the empty string. + return QString(); + } + + QString result; + auto buf = new wchar_t[resultSize]; + if (SUCCEEDED(assocQueryString(flags, ASSOCSTR_COMMAND, query, nullptr, buf, + &resultSize))) + { + // QString::fromWCharArray expects the length in characters *not + // including* the null terminator, but AssocQueryStringW calculates + // length including the null terminator + result = QString::fromWCharArray(buf, resultSize - 1); + } + delete[] buf; + return result; +} + } // namespace chatterino #endif diff --git a/src/util/WindowsHelper.hpp b/src/util/WindowsHelper.hpp index f39e66512..478368b81 100644 --- a/src/util/WindowsHelper.hpp +++ b/src/util/WindowsHelper.hpp @@ -7,12 +7,16 @@ namespace chatterino { +enum class AssociationQueryType { Protocol, FileExtension }; + boost::optional getWindowDpi(HWND hwnd); void flushClipboard(); bool isRegisteredForStartup(); void setRegisteredForStartup(bool isRegistered); +QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query); + } // namespace chatterino #endif diff --git a/src/widgets/helper/Button.cpp b/src/widgets/helper/Button.cpp index 4b86742ff..d1e92af97 100644 --- a/src/widgets/helper/Button.cpp +++ b/src/widgets/helper/Button.cpp @@ -217,7 +217,11 @@ void Button::fancyPaint(QPainter &painter) } } -void Button::enterEvent(QEvent *) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void Button::enterEvent(QEnterEvent * /*event*/) +#else +void Button::enterEvent(QEvent * /*event*/) +#endif { this->mouseOver_ = true; } diff --git a/src/widgets/helper/Button.hpp b/src/widgets/helper/Button.hpp index 79ca95767..cac8c532f 100644 --- a/src/widgets/helper/Button.hpp +++ b/src/widgets/helper/Button.hpp @@ -57,7 +57,11 @@ signals: protected: virtual void paintEvent(QPaintEvent *) override; - virtual void enterEvent(QEvent *) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent * /*event*/) override; +#else + void enterEvent(QEvent * /*event*/) override; +#endif virtual void leaveEvent(QEvent *) override; virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent *event) override; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 3a2c71db4..ac8b153c9 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -1440,7 +1440,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } } -void ChannelView::enterEvent(QEvent *) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void ChannelView::enterEvent(QEnterEvent * /*event*/) +#else +void ChannelView::enterEvent(QEvent * /*event*/) +#endif { } diff --git a/src/widgets/helper/ChannelView.hpp b/src/widgets/helper/ChannelView.hpp index 0d5e1b3e3..3fb47c04d 100644 --- a/src/widgets/helper/ChannelView.hpp +++ b/src/widgets/helper/ChannelView.hpp @@ -167,7 +167,11 @@ protected: void paintEvent(QPaintEvent *) override; void wheelEvent(QWheelEvent *event) override; - void enterEvent(QEvent *) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent * /*event*/) override; +#else + void enterEvent(QEvent * /*event*/) override; +#endif void leaveEvent(QEvent *) override; void mouseMoveEvent(QMouseEvent *event) override; diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index 7d8bd0003..539d44263 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -674,7 +674,11 @@ void NotebookTab::mouseDoubleClickEvent(QMouseEvent *event) } } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void NotebookTab::enterEvent(QEnterEvent *event) +#else void NotebookTab::enterEvent(QEvent *event) +#endif { this->mouseOver_ = true; diff --git a/src/widgets/helper/NotebookTab.hpp b/src/widgets/helper/NotebookTab.hpp index 37cf1b3b6..36d622624 100644 --- a/src/widgets/helper/NotebookTab.hpp +++ b/src/widgets/helper/NotebookTab.hpp @@ -61,7 +61,11 @@ protected: virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent *event) override; virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void enterEvent(QEvent *) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *event) override; +#else + void enterEvent(QEvent *event) override; +#endif virtual void leaveEvent(QEvent *) override; virtual void dragEnterEvent(QDragEnterEvent *event) override; diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 291b0f440..ede1fa381 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -119,6 +119,11 @@ AboutPage::AboutPage() addLicense(form.getElement(), "Fluent icons", "https://github.com/microsoft/fluentui-system-icons", ":/licenses/fluenticons.txt"); +#endif +#ifdef CHATTERINO_WITH_CRASHPAD + addLicense(form.getElement(), "sentry-crashpad", + "https://github.com/getsentry/crashpad", + ":/licenses/crashpad.txt"); #endif } diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index a55c2571d..0fd258025 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -822,7 +822,11 @@ void Split::resizeEvent(QResizeEvent *event) this->overlay_->setGeometry(this->rect()); } -void Split::enterEvent(QEvent *event) +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void Split::enterEvent(QEnterEvent * /*event*/) +#else +void Split::enterEvent(QEvent * /*event*/) +#endif { this->isMouseOver_ = true; diff --git a/src/widgets/splits/Split.hpp b/src/widgets/splits/Split.hpp index 209930045..833bfffe4 100644 --- a/src/widgets/splits/Split.hpp +++ b/src/widgets/splits/Split.hpp @@ -105,7 +105,11 @@ protected: void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; void resizeEvent(QResizeEvent *event) override; - void enterEvent(QEvent *event) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent * /*event*/) override; +#else + void enterEvent(QEvent * /*event*/) override; +#endif void leaveEvent(QEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 796992378..a7781119b 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -945,7 +945,11 @@ void SplitHeader::mouseDoubleClickEvent(QMouseEvent *event) this->doubleClicked_ = true; } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void SplitHeader::enterEvent(QEnterEvent *event) +#else void SplitHeader::enterEvent(QEvent *event) +#endif { if (!this->tooltipText_.isEmpty()) { diff --git a/src/widgets/splits/SplitHeader.hpp b/src/widgets/splits/SplitHeader.hpp index 6b730703a..7e7d9e8c8 100644 --- a/src/widgets/splits/SplitHeader.hpp +++ b/src/widgets/splits/SplitHeader.hpp @@ -42,7 +42,11 @@ protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *event) override; +#else void enterEvent(QEvent *event) override; +#endif void leaveEvent(QEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; diff --git a/tests/src/SeventvEventAPI.cpp b/tests/src/SeventvEventAPI.cpp index bdde3207a..4c47e34c3 100644 --- a/tests/src/SeventvEventAPI.cpp +++ b/tests/src/SeventvEventAPI.cpp @@ -1,14 +1,15 @@ #include "providers/seventv/SeventvEventAPI.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIClient.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp" -#include "providers/seventv/eventapi/SeventvEventAPIMessage.hpp" +#include "providers/seventv/eventapi/Client.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Message.hpp" #include #include #include using namespace chatterino; +using namespace chatterino::seventv::eventapi; using namespace std::chrono_literals; const QString EMOTE_SET_A = "60b39e943e203cc169dfc106"; @@ -21,10 +22,10 @@ TEST(SeventvEventAPI, AllEvents) auto *eventAPI = new SeventvEventAPI(host, std::chrono::milliseconds(1000)); eventAPI->start(); - boost::optional addDispatch; - boost::optional updateDispatch; - boost::optional removeDispatch; - boost::optional userDispatch; + boost::optional addDispatch; + boost::optional updateDispatch; + boost::optional removeDispatch; + boost::optional userDispatch; eventAPI->signals_.emoteAdded.connect([&](const auto &d) { addDispatch = d; diff --git a/vcpkg.json b/vcpkg.json index 403ddcdbe..b6a60506a 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,8 +1,8 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "chatterino", "version": "2.0.0", - "builtin-baseline": "5ba2b95aea2a39aa89444949c7a047af38c401c1", + "builtin-baseline": "43f56137beabcd470ac2650cdf3954761f65b70e", "dependencies": [ "benchmark", "boost-asio", @@ -14,7 +14,6 @@ "boost-variant", "gtest", "openssl", - "qt5-multimedia", "qt5-tools" ], "overrides": [