mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Merge branch 'feature/lua_scripting' of github.com:Chatterino/chatterino2 into feature/lua_scripting
This commit is contained in:
commit
9c8b4b3240
107 changed files with 2259 additions and 861 deletions
|
@ -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
|
||||
|
|
|
@ -1,40 +1,95 @@
|
|||
#!/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)"
|
||||
|
||||
# The final path where we'll save the .deb package
|
||||
deb_path="Chatterino-ubuntu-${ubuntu_release}-x86_64.deb"
|
||||
|
||||
# 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
|
||||
fi
|
||||
|
||||
chatterino_version=$(git describe 2>/dev/null | cut -c 2-) || true
|
||||
if [ -z "$chatterino_version" ]; then
|
||||
chatterino_version="0.0.0-dev"
|
||||
echo "Falling back to setting the version to '$chatterino_version'"
|
||||
chatterino_version=$(git describe 2>/dev/null) || true
|
||||
if [ "$(echo "$chatterino_version" | cut -c1-1)" = 'v' ]; then
|
||||
chatterino_version="$(echo "$chatterino_version" | cut -c2-)"
|
||||
else
|
||||
echo "Found Chatterino version $chatterino_version via git"
|
||||
chatterino_version="0.0.0-dev"
|
||||
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 <mm2pl@kotmisia.pl>
|
||||
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..."
|
||||
dpkg-deb --build "$packaging_dir" "Chatterino-x86_64.deb"
|
||||
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" "$deb_path"
|
||||
breakline
|
||||
|
||||
|
||||
echo "Package info"
|
||||
dpkg --info "$deb_path"
|
||||
breakline
|
||||
|
||||
|
||||
echo "Package contents"
|
||||
dpkg --contents "$deb_path"
|
||||
breakline
|
||||
|
|
54
.docker/Dockerfile-ubuntu-20.04-build
Normal file
54
.docker/Dockerfile-ubuntu-20.04-build
Normal file
|
@ -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
|
13
.docker/Dockerfile-ubuntu-20.04-package
Normal file
13
.docker/Dockerfile-ubuntu-20.04-package
Normal file
|
@ -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
|
57
.docker/Dockerfile-ubuntu-22.04-build
Normal file
57
.docker/Dockerfile-ubuntu-22.04-build
Normal file
|
@ -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
|
8
.docker/Dockerfile-ubuntu-22.04-package
Normal file
8
.docker/Dockerfile-ubuntu-22.04-package
Normal file
|
@ -0,0 +1,8 @@
|
|||
FROM chatterino-ubuntu-22.04-build
|
||||
|
||||
ADD .CI /src/.CI
|
||||
|
||||
WORKDIR /src/build
|
||||
|
||||
# package deb
|
||||
RUN ./../.CI/CreateUbuntuDeb.sh
|
29
.docker/README.md
Normal file
29
.docker/README.md
Normal file
|
@ -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/"`
|
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
build*
|
||||
.mypy_cache
|
||||
.cache
|
||||
.docker
|
142
.github/workflows/build.yml
vendored
142
.github/workflows/build.yml
vendored
|
@ -18,18 +18,40 @@ env:
|
|||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build ${{ matrix.os }}, Qt ${{ matrix.qt-version }} (PCH:${{ matrix.pch }}, LTO:${{ matrix.force-lto }})"
|
||||
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,19 @@ 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
|
||||
run: |
|
||||
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 +103,7 @@ jobs:
|
|||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- name: Install Qt
|
||||
|
@ -85,14 +119,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 +140,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 +153,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 }}-${{ env.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 +220,6 @@ jobs:
|
|||
libboost-filesystem-dev \
|
||||
libpulse-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
build-essential \
|
||||
libgl1-mesa-dev \
|
||||
libxcb-icccm4 \
|
||||
|
@ -179,12 +228,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 +270,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 }}-${{ env.artifact_descr }}.AppImage
|
||||
path: build/Chatterino-x86_64.AppImage
|
||||
|
||||
- name: Upload artifact - AppImage (Ubuntu, plugins)
|
||||
|
@ -243,18 +298,11 @@ 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
|
||||
path: build/Chatterino-x86_64.deb
|
||||
name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}-${{ env.artifact_descr }}.deb
|
||||
path: build/Chatterino-${{ matrix.is }}-x86_64.deb
|
||||
|
||||
# MACOS
|
||||
- name: Install dependencies (MacOS)
|
||||
|
@ -290,17 +338,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 }}-${{ env.artifact_descr }}.dmg
|
||||
path: build/chatterino-osx.dmg
|
||||
create-release:
|
||||
needs: build
|
||||
|
@ -308,11 +349,19 @@ jobs:
|
|||
if: (github.event_name == 'push' && github.ref == 'refs/heads/master')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # allows for tags access
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: chatterino-windows-x86-64-5.15.2.zip
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: chatterino-windows-x86-64-5.15.2-symbols.pdb.7z
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chatterino-x86_64-5.15.2.AppImage
|
||||
|
@ -320,7 +369,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
|
||||
|
@ -339,3 +393,9 @@ jobs:
|
|||
prerelease: true
|
||||
name: Nightly Release
|
||||
tag: nightly-build
|
||||
|
||||
- name: Update nightly-build tag
|
||||
run: |
|
||||
git tag -f nightly-build
|
||||
git push -f origin nightly-build
|
||||
shell: bash
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -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 }}
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -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
|
||||
|
|
20
.patches/qt5-on-newer-gcc.patch
Normal file
20
.patches/qt5-on-newer-gcc.patch
Normal file
|
@ -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<void> : public ThreadEngineStarterBase<void>
|
||||
{
|
||||
public:
|
||||
- ThreadEngineStarter<void>(ThreadEngine<void> *_threadEngine)
|
||||
- :ThreadEngineStarterBase<void>(_threadEngine) {}
|
||||
+ ThreadEngineStarter(ThreadEngine<void> *_threadEngine)
|
||||
+ : ThreadEngineStarterBase<void>(_threadEngine) {}
|
||||
|
||||
void startBlocking()
|
||||
{
|
|
@ -8,11 +8,11 @@ Note on Qt version compatibility: If you are installing Qt from a package manage
|
|||
|
||||
_Most likely works the same for other Debian-like distros_
|
||||
|
||||
Install all of the dependencies using `sudo apt install qttools5-dev qtmultimedia5-dev qt5-image-formats-plugins libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++`
|
||||
Install all of the dependencies using `sudo apt install qttools5-dev qt5-image-formats-plugins libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++`
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Install all of the dependencies using `sudo pacman -S --needed qt5-base qt5-multimedia qt5-imageformats qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake`
|
||||
Install all of the dependencies using `sudo pacman -S --needed qt5-base qt5-imageformats qt5-svg qt5-tools boost rapidjson pkgconf openssl cmake`
|
||||
|
||||
Alternatively you can use the [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) package to build and install Chatterino for you.
|
||||
|
||||
|
@ -20,7 +20,7 @@ Alternatively you can use the [chatterino2-git](https://aur.archlinux.org/packag
|
|||
|
||||
_Most likely works the same for other Red Hat-like distros. Substitute `dnf` with `yum`._
|
||||
|
||||
Install all of the dependencies using `sudo dnf install qt5-qtbase-devel qt5-qtmultimedia-devel qt5-imageformats qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake`
|
||||
Install all of the dependencies using `sudo dnf install qt5-qtbase-devel qt5-imageformats qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake`
|
||||
|
||||
### NixOS 18.09+
|
||||
|
||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -2,15 +2,21 @@
|
|||
|
||||
## Unversioned
|
||||
|
||||
- Major: Added live emote updates for BTTV (#4147)
|
||||
- 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)
|
||||
- Minor: Cleared up highlight sound settings (#4194)
|
||||
- Minor: Tables in settings window will now scroll to newly added rows. (#4216)
|
||||
- Minor: Added link to streamlink docs for easier user setup. (#4217)
|
||||
- Major: Added live emote updates for BTTV. (#4147)
|
||||
- Minor: Added setting to turn off rendering of reply context. (#4224)
|
||||
- Minor: Changed the highlight order to prioritize Message highlights over User highlights. (#4303)
|
||||
- Minor: Added a setting to highlight your own messages in `Highlights -> Users`. (#3833)
|
||||
- Minor: Added the 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)
|
||||
- Minor: Grouped highlight sound columns together and improved wording for the default sound setting. (#4194)
|
||||
- Minor: Tables in settings window will now scroll to newly added rows. (#4216)
|
||||
- Minor: Added setting to select which channels to log. (#4302)
|
||||
- Minor: Added channel name to /mentions log entries. (#4371)
|
||||
- Minor: Added link to streamlink docs for easier user setup. (#4217)
|
||||
- Minor: Added support for HTTP and Socks5 proxies through environment variables. (#4321)
|
||||
- Minor: Removed 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 User Card moderation actions not working after Twitch IRC chat command deprecation. (#4378)
|
||||
- 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,8 +33,9 @@
|
|||
- 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: Removed 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)
|
||||
- Dev: Migrated to C++ 20 (#4252, #4257)
|
||||
- Dev: Enable LTO for main branch builds. (#4258, #4260)
|
||||
|
@ -44,6 +51,11 @@
|
|||
- 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)
|
||||
- Dev: Use `qintptr` in `QWidget::nativeEvent` on Qt 6. (#4376)
|
||||
|
||||
## 2.4.0
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit a6748f4f51273d86312e3d27ebe5277c9b1ff870
|
||||
Subproject commit c3dc841af4dbf44669e65b82cb68a575864326bd
|
|
@ -14,8 +14,11 @@
|
|||
"description": "Plugin description shown to the user."
|
||||
},
|
||||
"authors": {
|
||||
"type": "string",
|
||||
"description": "A string describing authors of the Plugin. This could be a list of names, an organisation or something else, as long as it's clear who made the Plugin."
|
||||
"type": "array",
|
||||
"description": "An array of authors of this Plugin.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
|
|
|
@ -17,7 +17,7 @@ Chatterino Plugins dir/
|
|||
└── info.json
|
||||
```
|
||||
|
||||
`init.lua` will be the file loaded when the plugin is enabled. You may load other files using `loadfile` Lua global function.
|
||||
`init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`import` global function](#importfilename=).
|
||||
|
||||
`info.json` contains metadata about the plugin, like its name, description,
|
||||
authors, homepage link, tags, version, license name. The version field **must**
|
||||
|
@ -145,12 +145,13 @@ end
|
|||
|
||||
#### `load(chunk [, chunkname [, mode [, env]]])`
|
||||
|
||||
This function is only available if Chatterino is compiled in debug mode. It is meant for debugging with little exception.
|
||||
This function behaves really similarity to Lua's `load`, however it does not allow for bytecode to be executed.
|
||||
It achieves this by forcing all inputs to be encoded with `UTF-8`.
|
||||
|
||||
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-load)
|
||||
|
||||
#### `execfile(filename)`
|
||||
#### `import(filename)`
|
||||
|
||||
This function mimics Lua's `dofile` however relative paths are relative to your plugin's directory.
|
||||
You are restricted to loading files in your plugin's directory. You cannot load files with bytecode inside.
|
||||
|
@ -158,10 +159,10 @@ You are restricted to loading files in your plugin's directory. You cannot load
|
|||
Example:
|
||||
|
||||
```lua
|
||||
execfile("stuff.lua") -- executes Plugins/name/stuff.lua
|
||||
execfile("./stuff.lua") -- executes Plugins/name/stuff.lua
|
||||
execfile("../stuff.lua") -- tries to load Plugins/stuff.lua and errors
|
||||
execfile("luac.out") -- tried to load Plugins/name/luac.out and errors because it contains non-utf8 data
|
||||
import("stuff.lua") -- executes Plugins/name/stuff.lua
|
||||
import("./stuff.lua") -- executes Plugins/name/stuff.lua
|
||||
import("../stuff.lua") -- tries to load Plugins/stuff.lua and errors
|
||||
import("luac.out") -- tried to load Plugins/name/luac.out and errors because it contains non-utf8 data
|
||||
```
|
||||
|
||||
#### `print(Args...)`
|
||||
|
|
1
lib/crashpad
Submodule
1
lib/crashpad
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit ec992578688b4c51c1856d08731cf7dcf10e446a
|
|
@ -1 +1 @@
|
|||
Subproject commit d69789da1ccfa4db7c241de6b471d6b729f1561e
|
||||
Subproject commit 5d708c3f9cae12820e415d4f89c9eacbe2ab964b
|
|
@ -1 +0,0 @@
|
|||
Subproject commit a4626c12e9ae6f02fc1ca7a4e399bd8307424103
|
202
resources/licenses/crashpad.txt
Normal file
202
resources/licenses/crashpad.txt
Normal file
|
@ -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.
|
|
@ -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"
|
||||
|
|
|
@ -136,8 +136,8 @@ set(SOURCE_FILES
|
|||
controllers/pings/MutedChannelModel.cpp
|
||||
controllers/pings/MutedChannelModel.hpp
|
||||
|
||||
controllers/plugins/LuaApi.cpp
|
||||
controllers/plugins/LuaApi.hpp
|
||||
controllers/plugins/LuaAPI.cpp
|
||||
controllers/plugins/LuaAPI.hpp
|
||||
controllers/plugins/Plugin.cpp
|
||||
controllers/plugins/Plugin.hpp
|
||||
controllers/plugins/PluginController.hpp
|
||||
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -44,6 +44,17 @@ namespace {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
boost::optional<QString> 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"))
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
#include <QString>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -16,6 +17,7 @@ public:
|
|||
const QString twitchServerHost;
|
||||
const uint16_t twitchServerPort;
|
||||
const bool twitchServerSecure;
|
||||
const boost::optional<QString> proxyUrl;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -962,7 +956,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
QString message) {
|
||||
using Error = HelixGetChattersError;
|
||||
|
||||
QString errorMessage = QString("Failed to get chatter count: ");
|
||||
QString errorMessage = QString("Failed to get chatter count - ");
|
||||
|
||||
switch (error)
|
||||
{
|
||||
|
@ -1070,7 +1064,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
auto formatModsError = [](HelixGetModeratorsError error, QString message) {
|
||||
using Error = HelixGetModeratorsError;
|
||||
|
||||
QString errorMessage = QString("Failed to get moderators: ");
|
||||
QString errorMessage = QString("Failed to get moderators - ");
|
||||
|
||||
switch (error)
|
||||
{
|
||||
|
|
|
@ -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<HighlightResult> {
|
||||
(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);
|
||||
|
|
|
@ -36,6 +36,10 @@ public:
|
|||
ThreadMessageRow = 6,
|
||||
};
|
||||
|
||||
enum UserHighlightRowIndexes {
|
||||
SelfMessageRow = 0,
|
||||
};
|
||||
|
||||
protected:
|
||||
// turn a vector item into a model row
|
||||
virtual HighlightPhrase getItemFromRow(
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<QStandardItem *> 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<QStandardItem *> &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<QColor>().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 &>(ColorProvider::instance())
|
||||
.updateColor(ColorType::SelfMessageHighlight,
|
||||
QColor(colorName));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
getApp()->windows->forceLayoutChannelViews();
|
||||
}
|
||||
|
||||
// row into vector item
|
||||
void UserHighlightModel::getRowFromItem(const HighlightPhrase &item,
|
||||
std::vector<QStandardItem *> &row)
|
||||
|
|
|
@ -22,6 +22,12 @@ protected:
|
|||
|
||||
virtual void getRowFromItem(const HighlightPhrase &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
|
||||
virtual void afterInit() override;
|
||||
|
||||
virtual void customRowSetData(const std::vector<QStandardItem *> &row,
|
||||
int column, const QVariant &value, int role,
|
||||
int rowIndex) override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "LuaApi.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
|
@ -9,15 +9,29 @@
|
|||
# include "messages/MessageBuilder.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
// lua stuff
|
||||
# include "lauxlib.h"
|
||||
# include "lua.h"
|
||||
# include "lualib.h"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <QFileInfo>
|
||||
# include <QLoggingCategory>
|
||||
# include <QTextCodec>
|
||||
|
||||
namespace {
|
||||
using namespace chatterino;
|
||||
|
||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc)
|
||||
{
|
||||
stream.noquote();
|
||||
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
||||
for (int i = 1; i <= argc; i++)
|
||||
{
|
||||
stream << lua::toString(L, i);
|
||||
}
|
||||
lua_pop(L, argc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// NOLINTBEGIN(*vararg)
|
||||
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
||||
namespace chatterino::lua::api {
|
||||
|
@ -103,20 +117,6 @@ int c2_system_msg(lua_State *L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc)
|
||||
{
|
||||
stream.noquote();
|
||||
stream << "[" + pl->codename + ":" + pl->meta.name + "]";
|
||||
for (int i = 1; i <= argc; i++)
|
||||
{
|
||||
stream << lua::toString(L, i);
|
||||
}
|
||||
lua_pop(L, argc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int c2_log(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
|
@ -162,6 +162,10 @@ int c2_log(lua_State *L)
|
|||
|
||||
int g_load(lua_State *L)
|
||||
{
|
||||
# ifdef NDEBUG
|
||||
luaL_error(L, "load() is only usable in debug mode");
|
||||
return 0;
|
||||
# else
|
||||
auto countArgs = lua_gettop(L);
|
||||
QByteArray data;
|
||||
if (lua::peek(L, &data, 1))
|
||||
|
@ -199,9 +203,10 @@ int g_load(lua_State *L)
|
|||
lua_call(L, countArgs, LUA_MULTRET);
|
||||
|
||||
return lua_gettop(L);
|
||||
# endif
|
||||
}
|
||||
|
||||
int g_dofile(lua_State *L)
|
||||
int g_import(lua_State *L)
|
||||
{
|
||||
auto countArgs = lua_gettop(L);
|
||||
// Lua allows dofile() which loads from stdin, but this is very useless in our case
|
||||
|
@ -223,8 +228,8 @@ int g_dofile(lua_State *L)
|
|||
auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/");
|
||||
auto file = dir.resolved(fname);
|
||||
|
||||
qCDebug(chatterinoLua) << "plugin" << pl->codename << "is trying to load"
|
||||
<< file << "(its dir is" << dir << ")";
|
||||
qCDebug(chatterinoLua) << "plugin" << pl->id << "is trying to load" << file
|
||||
<< "(its dir is" << dir << ")";
|
||||
if (!dir.isParentOf(file))
|
||||
{
|
||||
lua_pushnil(L);
|
|
@ -1,4 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
struct lua_State;
|
||||
|
@ -15,14 +16,12 @@ int c2_log(lua_State *L);
|
|||
// These ones are global
|
||||
int g_load(lua_State *L);
|
||||
int g_print(lua_State *L);
|
||||
|
||||
// this one is exposed as execfile
|
||||
int g_dofile(lua_State *L);
|
||||
int g_import(lua_State *L);
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
// Exposed as c2.LogLevel
|
||||
// Represents "calls" to qCDebug, qCInfo ...
|
||||
enum LogLevel { Debug, Info, Warning, Critical };
|
||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
#include "LuaUtilities.hpp"
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include "common/Channel.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "lauxlib.h"
|
||||
# include "lua.h"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
|
||||
# include <climits>
|
||||
# include <cstdlib>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "lua.h"
|
||||
# include "lualib.h"
|
||||
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <magic_enum.hpp>
|
||||
# include <QList>
|
||||
|
||||
|
@ -155,7 +155,7 @@ bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
|||
{
|
||||
if (idx < 0)
|
||||
{
|
||||
idx = lua_gettop(L) + idx;
|
||||
idx = lua_gettop(L) + idx + 1;
|
||||
}
|
||||
lua_remove(L, idx);
|
||||
}
|
||||
|
@ -187,4 +187,5 @@ StackIdx pushEnumTable(lua_State *L)
|
|||
}
|
||||
|
||||
} // namespace chatterino::lua
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#include "Plugin.hpp"
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
|
||||
# include "lua.h"
|
||||
# include <lua.h>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
||||
{
|
||||
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "Application.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
|
@ -18,65 +19,143 @@ struct lua_State;
|
|||
namespace chatterino {
|
||||
|
||||
struct PluginMeta {
|
||||
// required fields
|
||||
// for more info on these fields see docs/plugin-info.schema.json
|
||||
|
||||
// display name of the plugin
|
||||
QString name;
|
||||
|
||||
// description shown to the user
|
||||
QString description;
|
||||
QString authors;
|
||||
|
||||
// plugin authors shown to the user
|
||||
std::vector<QString> authors;
|
||||
|
||||
// license name
|
||||
QString license;
|
||||
|
||||
// version of the plugin
|
||||
semver::version version;
|
||||
|
||||
// optional
|
||||
// optionally a homepage link
|
||||
QString homepage;
|
||||
|
||||
// optionally tags that might help in searching for the plugin
|
||||
std::vector<QString> tags;
|
||||
|
||||
bool valid{};
|
||||
std::vector<QString> invalidWhy;
|
||||
// errors that occurred while parsing info.json
|
||||
std::vector<QString> errors;
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
return this->errors.empty();
|
||||
}
|
||||
|
||||
explicit PluginMeta(const QJsonObject &obj)
|
||||
: homepage(obj.value("homepage").toString(""))
|
||||
{
|
||||
auto homepageObj = obj.value("homepage");
|
||||
if (homepageObj.isString())
|
||||
{
|
||||
this->homepage = homepageObj.toString();
|
||||
}
|
||||
else if (!homepageObj.isUndefined())
|
||||
{
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(homepageObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString(
|
||||
"homepage is defined but is not a string (its type is %1)")
|
||||
.arg(type));
|
||||
}
|
||||
auto nameObj = obj.value("name");
|
||||
if (!nameObj.isString())
|
||||
if (nameObj.isString())
|
||||
{
|
||||
this->invalidWhy.emplace_back("name is not a string");
|
||||
this->valid = false;
|
||||
}
|
||||
this->name = nameObj.toString();
|
||||
|
||||
auto descrObj = obj.value("description");
|
||||
if (!descrObj.isString())
|
||||
{
|
||||
this->invalidWhy.emplace_back("description is not a string");
|
||||
this->valid = false;
|
||||
}
|
||||
this->description = descrObj.toString();
|
||||
|
||||
auto authorsObj = obj.value("authors");
|
||||
if (!authorsObj.isString())
|
||||
{
|
||||
this->invalidWhy.emplace_back("description is not a string");
|
||||
this->valid = false;
|
||||
}
|
||||
this->authors = authorsObj.toString();
|
||||
|
||||
auto licenseObj = obj.value("license");
|
||||
if (!licenseObj.isString())
|
||||
{
|
||||
this->invalidWhy.emplace_back("license is not a string");
|
||||
this->valid = false;
|
||||
}
|
||||
this->license = licenseObj.toString();
|
||||
|
||||
auto v = semver::from_string_noexcept(
|
||||
obj.value("version").toString().toStdString());
|
||||
if (v.has_value())
|
||||
{
|
||||
this->version = v.value();
|
||||
this->name = nameObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->invalidWhy.emplace_back("unable to parse version");
|
||||
this->valid = false;
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(nameObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString("name is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto descrObj = obj.value("description");
|
||||
if (descrObj.isString())
|
||||
{
|
||||
this->description = descrObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(descrObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString("description is not a string (its type is %1)")
|
||||
.arg(type));
|
||||
}
|
||||
|
||||
auto authorsObj = obj.value("authors");
|
||||
if (authorsObj.isArray())
|
||||
{
|
||||
auto authorsArr = authorsObj.toArray();
|
||||
for (int i = 0; i < authorsArr.size(); i++)
|
||||
{
|
||||
const auto &t = authorsArr.at(i);
|
||||
if (!t.isString())
|
||||
{
|
||||
this->errors.push_back(
|
||||
QString(
|
||||
"authors element #%1 is not a string (it is a %2)")
|
||||
.arg(i)
|
||||
.arg(QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(t.type())))));
|
||||
break;
|
||||
}
|
||||
this->authors.push_back(t.toString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(authorsObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString("authors is not an array (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto licenseObj = obj.value("license");
|
||||
if (licenseObj.isString())
|
||||
{
|
||||
this->license = licenseObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(licenseObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString("license is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto verObj = obj.value("version");
|
||||
if (verObj.isString())
|
||||
{
|
||||
auto v =
|
||||
semver::from_string_noexcept(verObj.toString().toStdString());
|
||||
if (v.has_value())
|
||||
{
|
||||
this->version = v.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->errors.emplace_back(
|
||||
"unable to parse version (use semver)");
|
||||
this->version = semver::version(0, 0, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(verObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString("version is not a string (its type is %1)").arg(type));
|
||||
this->version = semver::version(0, 0, 0);
|
||||
}
|
||||
auto tagsObj = obj.value("tags");
|
||||
|
@ -84,8 +163,10 @@ struct PluginMeta {
|
|||
{
|
||||
if (!tagsObj.isArray())
|
||||
{
|
||||
this->invalidWhy.emplace_back("tags is not an array");
|
||||
this->valid = false;
|
||||
auto type = QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(licenseObj.type())));
|
||||
this->errors.emplace_back(
|
||||
QString("tags is not an array (its type is %1)").arg(type));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -95,12 +176,12 @@ struct PluginMeta {
|
|||
const auto &t = tagsArr.at(i);
|
||||
if (!t.isString())
|
||||
{
|
||||
this->invalidWhy.push_back(
|
||||
QString("tags element #%1 is not a string (it is a %2)")
|
||||
this->errors.push_back(
|
||||
QString(
|
||||
"tags element #%1 is not a string (its type is %2)")
|
||||
.arg(i)
|
||||
.arg(QString::fromStdString(
|
||||
std::string(magic_enum::enum_name(t.type())))));
|
||||
this->valid = false;
|
||||
return;
|
||||
}
|
||||
this->tags.push_back(t.toString());
|
||||
|
@ -112,13 +193,12 @@ struct PluginMeta {
|
|||
class Plugin
|
||||
{
|
||||
public:
|
||||
QString codename;
|
||||
QString id;
|
||||
PluginMeta meta;
|
||||
bool isDupeName{};
|
||||
|
||||
Plugin(QString codename, lua_State *state, PluginMeta meta,
|
||||
Plugin(QString id, lua_State *state, PluginMeta meta,
|
||||
const QDir &loadDirectory)
|
||||
: codename(std::move(codename))
|
||||
: id(std::move(id))
|
||||
, meta(std::move(meta))
|
||||
, loadDirectory_(loadDirectory)
|
||||
, state_(state)
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
#include "PluginController.hpp"
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/plugins/LuaApi.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
|
||||
// lua stuff
|
||||
# include "lauxlib.h"
|
||||
# include "lua.h"
|
||||
# include "lualib.h"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <QJsonDocument>
|
||||
|
||||
# include <memory>
|
||||
|
@ -27,10 +25,10 @@ void PluginController::initialize(Settings &settings, Paths &paths)
|
|||
(void)paths;
|
||||
|
||||
// actuallyInitialize will be called by this connection
|
||||
settings.enableAnyPlugins.connect([this](bool enabled) {
|
||||
settings.pluginSupportEnabled.connect([this](bool enabled) {
|
||||
if (enabled)
|
||||
{
|
||||
this->actuallyInitialize();
|
||||
this->loadPlugins();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -40,15 +38,8 @@ void PluginController::initialize(Settings &settings, Paths &paths)
|
|||
});
|
||||
}
|
||||
|
||||
// this function exists to allow for connecting to enableAnyPlugins option
|
||||
void PluginController::actuallyInitialize()
|
||||
void PluginController::loadPlugins()
|
||||
{
|
||||
if (!getSettings()->enableAnyPlugins)
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Loading plugins disabled via Setting, skipping";
|
||||
return;
|
||||
}
|
||||
this->plugins_.clear();
|
||||
auto dir = QDir(getPaths()->pluginsDirectory);
|
||||
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
||||
|
@ -94,14 +85,17 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
|||
}
|
||||
|
||||
auto meta = PluginMeta(doc.object());
|
||||
if (!meta.invalidWhy.empty())
|
||||
if (!meta.isValid())
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Plugin from" << pluginDir << "is invalid because:";
|
||||
for (const auto &why : meta.invalidWhy)
|
||||
for (const auto &why : meta.errors)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "- " << why;
|
||||
}
|
||||
auto plugin = std::make_unique<Plugin>(pluginDir.dirName(), nullptr,
|
||||
meta, pluginDir);
|
||||
this->plugins_.insert({pluginDir.dirName(), std::move(plugin)});
|
||||
return false;
|
||||
}
|
||||
this->load(index, pluginDir, meta);
|
||||
|
@ -165,10 +159,13 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
|
||||
lua_pushglobaltable(L);
|
||||
auto gtable = lua_gettop(L);
|
||||
lua_getfield(L, gtable, "load");
|
||||
|
||||
// possibly randomize this name at runtime to prevent some attacks?
|
||||
|
||||
# ifndef NDEBUG
|
||||
lua_getfield(L, gtable, "load");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
# endif
|
||||
|
||||
lua_getfield(L, gtable, "dofile");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
|
@ -176,11 +173,10 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg replacementFuncs[] = {
|
||||
{"load", lua::api::g_load},
|
||||
|
||||
// chatterino dofile is way more similar to require() than dofile()
|
||||
{"execfile", lua::api::g_dofile},
|
||||
|
||||
{"print", lua::api::g_print},
|
||||
|
||||
// This function replaces both `dofile` and `require`, see docs/wip-plugins.md for more info
|
||||
{"import", lua::api::g_import},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
luaL_setfuncs(L, replacementFuncs, 0);
|
||||
|
@ -202,15 +198,6 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||
|
||||
auto pluginName = pluginDir.dirName();
|
||||
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
||||
|
||||
for (const auto &[codename, other] : this->plugins_)
|
||||
{
|
||||
if (other->meta.name == meta.name)
|
||||
{
|
||||
plugin->isDupeName = true;
|
||||
other->isDupeName = true;
|
||||
}
|
||||
}
|
||||
this->plugins_.insert({pluginName, std::move(plugin)});
|
||||
if (!PluginController::isEnabled(pluginName))
|
||||
{
|
||||
|
@ -230,9 +217,9 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
||||
}
|
||||
|
||||
bool PluginController::reload(const QString &codename)
|
||||
bool PluginController::reload(const QString &id)
|
||||
{
|
||||
auto it = this->plugins_.find(codename);
|
||||
auto it = this->plugins_.find(id);
|
||||
if (it == this->plugins_.end())
|
||||
{
|
||||
return false;
|
||||
|
@ -247,12 +234,9 @@ bool PluginController::reload(const QString &codename)
|
|||
getApp()->commands->unregisterPluginCommand(cmd);
|
||||
}
|
||||
it->second->ownedCommands.clear();
|
||||
if (PluginController::isEnabled(codename))
|
||||
{
|
||||
QDir loadDir = it->second->loadDirectory_;
|
||||
this->plugins_.erase(codename);
|
||||
this->tryLoadFromDir(loadDir);
|
||||
}
|
||||
QDir loadDir = it->second->loadDirectory_;
|
||||
this->plugins_.erase(id);
|
||||
this->tryLoadFromDir(loadDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -287,14 +271,14 @@ QString PluginController::tryExecPluginCommand(const QString &commandName,
|
|||
return "";
|
||||
}
|
||||
|
||||
bool PluginController::isEnabled(const QString &codename)
|
||||
bool PluginController::isEnabled(const QString &id)
|
||||
{
|
||||
if (!getSettings()->enableAnyPlugins)
|
||||
if (!getSettings()->pluginSupportEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto vec = getSettings()->enabledPlugins.getValue();
|
||||
auto it = std::find(vec.begin(), vec.end(), codename);
|
||||
auto it = std::find(vec.begin(), vec.end(), id);
|
||||
return it != vec.end();
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ class PluginController : public Singleton
|
|||
{
|
||||
public:
|
||||
void initialize(Settings &settings, Paths &paths) override;
|
||||
void save() override{};
|
||||
|
||||
QString tryExecPluginCommand(const QString &commandName,
|
||||
const CommandContext &ctx);
|
||||
|
@ -52,21 +51,21 @@ public:
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Reload plugin given by codename
|
||||
* @brief Reload plugin given by id
|
||||
*
|
||||
* @param codename This is the 'codename' of the plugin, the name of the directory it is in
|
||||
* @param id This is the unique identifier of the plugin, the name of the directory it is in
|
||||
*/
|
||||
bool reload(const QString &codename);
|
||||
bool reload(const QString &id);
|
||||
|
||||
/**
|
||||
* @brief Checks settings to tell if a plugin named by codename.
|
||||
* @brief Checks settings to tell if a plugin named by id.
|
||||
*
|
||||
* It accounts for plugins being enabled/disabled globally.
|
||||
*/
|
||||
static bool isEnabled(const QString &codename);
|
||||
static bool isEnabled(const QString &id);
|
||||
|
||||
private:
|
||||
void actuallyInitialize();
|
||||
void loadPlugins();
|
||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta);
|
||||
|
||||
|
|
10
src/main.cpp
10
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 <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<Image *>(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<std::mutex> 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
|
||||
|
|
|
@ -19,6 +19,14 @@
|
|||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#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 <typename Image>
|
||||
|
@ -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
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
|
95
src/providers/Crashpad.cpp
Normal file
95
src/providers/Crashpad.cpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include "providers/Crashpad.hpp"
|
||||
|
||||
# include "common/QLogging.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
|
||||
# include <QApplication>
|
||||
# include <QDir>
|
||||
# include <QString>
|
||||
|
||||
# include <memory>
|
||||
# include <string>
|
||||
|
||||
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<crashpad::CrashpadClient> 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<crashpad::CrashpadClient>();
|
||||
|
||||
// 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
|
14
src/providers/Crashpad.hpp
Normal file
14
src/providers/Crashpad.hpp
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include <client/crashpad_client.h>
|
||||
|
||||
# include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler();
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
76
src/providers/NetworkConfigurationProvider.cpp
Normal file
76
src/providers/NetworkConfigurationProvider.cpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
#include "providers/NetworkConfigurationProvider.hpp"
|
||||
|
||||
#include "common/Env.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
|
||||
#include <QNetworkProxy>
|
||||
#include <QSslConfiguration>
|
||||
#include <QUrl>
|
||||
|
||||
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
|
59
src/providers/NetworkConfigurationProvider.hpp
Normal file
59
src/providers/NetworkConfigurationProvider.hpp
Normal file
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
|
||||
#include <QNetworkProxy>
|
||||
#include <websocketpp/error.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
|
@ -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 =
|
||||
|
|
|
@ -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<QColor>(customColor)});
|
||||
}
|
||||
else
|
||||
{
|
||||
this->typeColorMap_.insert(
|
||||
{ColorType::SelfMessageHighlight,
|
||||
std::make_shared<QColor>(
|
||||
HighlightPhrase::FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR)});
|
||||
}
|
||||
|
||||
customColor = getSettings()->subHighlightColor;
|
||||
if (QColor(customColor).isValid())
|
||||
{
|
||||
|
|
|
@ -16,6 +16,8 @@ enum class ColorType {
|
|||
FirstMessageHighlight,
|
||||
ElevatedMessageHighlight,
|
||||
ThreadMessageHighlight,
|
||||
// Used in automatic highlights of your own messages
|
||||
SelfMessageHighlight,
|
||||
};
|
||||
|
||||
class ColorProvider
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#include <rapidjson/rapidjson.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
class QAbstractTableModel;
|
||||
|
||||
namespace chatterino {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<EmoteMap>())
|
||||
{
|
||||
|
@ -401,7 +404,7 @@ void SeventvEmotes::loadChannelEmotes(
|
|||
|
||||
boost::optional<EmotePtr> SeventvEmotes::addEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &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<EmotePtr> SeventvEmotes::addEmote(
|
|||
|
||||
boost::optional<EmotePtr> SeventvEmotes::updateEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &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<EmotePtr> SeventvEmotes::updateEmote(
|
|||
|
||||
boost::optional<EmotePtr> SeventvEmotes::removeEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||
const SeventvEventAPIEmoteRemoveDispatch &dispatch)
|
||||
const EmoteRemoveDispatch &dispatch)
|
||||
{
|
||||
// This copies the map.
|
||||
EmoteMap updatedMap = *map.get();
|
||||
|
|
|
@ -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<EmotePtr> addEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||
const SeventvEventAPIEmoteAddDispatch &dispatch);
|
||||
const seventv::eventapi::EmoteAddDispatch &dispatch);
|
||||
|
||||
/**
|
||||
* Updates an emote in this `map`.
|
||||
|
@ -97,7 +100,7 @@ public:
|
|||
*/
|
||||
static boost::optional<EmotePtr> updateEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||
const SeventvEventAPIEmoteUpdateDispatch &dispatch);
|
||||
const seventv::eventapi::EmoteUpdateDispatch &dispatch);
|
||||
|
||||
/**
|
||||
* Removes an emote from this `map`.
|
||||
|
@ -108,7 +111,7 @@ public:
|
|||
*/
|
||||
static boost::optional<EmotePtr> removeEmote(
|
||||
Atomic<std::shared_ptr<const EmoteMap>> &map,
|
||||
const SeventvEventAPIEmoteRemoveDispatch &dispatch);
|
||||
const seventv::eventapi::EmoteRemoveDispatch &dispatch);
|
||||
|
||||
/** Fetches an emote-set by its id */
|
||||
static void getEmoteSet(
|
||||
|
|
|
@ -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 <QJsonArray>
|
||||
|
||||
|
@ -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<BasicPubSubClient<SeventvEventAPISubscription>>
|
||||
SeventvEventAPI::createClient(liveupdates::WebsocketClient &client,
|
||||
websocketpp::connection_hdl hdl)
|
||||
std::shared_ptr<BasicPubSubClient<Subscription>> SeventvEventAPI::createClient(
|
||||
liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl)
|
||||
{
|
||||
auto shared = std::make_shared<SeventvEventAPIClient>(
|
||||
client, hdl, this->heartbeatInterval_);
|
||||
return std::static_pointer_cast<
|
||||
BasicPubSubClient<SeventvEventAPISubscription>>(std::move(shared));
|
||||
auto shared =
|
||||
std::make_shared<Client>(client, hdl, this->heartbeatInterval_);
|
||||
return std::static_pointer_cast<BasicPubSubClient<Subscription>>(
|
||||
std::move(shared));
|
||||
}
|
||||
|
||||
void SeventvEventAPI::onMessage(
|
||||
websocketpp::connection_hdl hdl,
|
||||
BasicPubSubManager<SeventvEventAPISubscription>::WebsocketMessagePtr msg)
|
||||
BasicPubSubManager<Subscription>::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<SeventvEventAPIClient *>(client.get()))
|
||||
if (auto *stvClient = dynamic_cast<Client *>(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<SeventvEventAPIClient *>(client.get()))
|
||||
if (auto *stvClient = dynamic_cast<Client *>(client.get()))
|
||||
{
|
||||
stvClient->handleHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SeventvEventAPIOpcode::Dispatch: {
|
||||
auto dispatch = message.toInner<SeventvEventAPIDispatch>();
|
||||
case Opcode::Dispatch: {
|
||||
auto dispatch = message.toInner<Dispatch>();
|
||||
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<SeventvEventAPIClient *>(client.get()))
|
||||
if (auto *stvClient = dynamic_cast<Client *>(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
|
||||
|
|
|
@ -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<SeventvEventAPISubscription>
|
||||
class SeventvEventAPI
|
||||
: public BasicPubSubManager<seventv::eventapi::Subscription>
|
||||
{
|
||||
template <typename T>
|
||||
using Signal =
|
||||
|
@ -27,10 +30,10 @@ public:
|
|||
std::chrono::milliseconds(25000));
|
||||
|
||||
struct {
|
||||
Signal<SeventvEventAPIEmoteAddDispatch> emoteAdded;
|
||||
Signal<SeventvEventAPIEmoteUpdateDispatch> emoteUpdated;
|
||||
Signal<SeventvEventAPIEmoteRemoveDispatch> emoteRemoved;
|
||||
Signal<SeventvEventAPIUserConnectionUpdateDispatch> userUpdated;
|
||||
Signal<seventv::eventapi::EmoteAddDispatch> emoteAdded;
|
||||
Signal<seventv::eventapi::EmoteUpdateDispatch> emoteUpdated;
|
||||
Signal<seventv::eventapi::EmoteRemoveDispatch> emoteRemoved;
|
||||
Signal<seventv::eventapi::UserConnectionUpdateDispatch> userUpdated;
|
||||
} signals_; // NOLINT(readability-identifier-naming)
|
||||
|
||||
/**
|
||||
|
@ -48,18 +51,23 @@ public:
|
|||
void unsubscribeEmoteSet(const QString &id);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<BasicPubSubClient<SeventvEventAPISubscription>>
|
||||
std::shared_ptr<BasicPubSubClient<seventv::eventapi::Subscription>>
|
||||
createClient(liveupdates::WebsocketClient &client,
|
||||
websocketpp::connection_hdl hdl) override;
|
||||
void onMessage(
|
||||
websocketpp::connection_hdl hdl,
|
||||
BasicPubSubManager<SeventvEventAPISubscription>::WebsocketMessagePtr
|
||||
BasicPubSubManager<seventv::eventapi::Subscription>::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<QString> subscribedEmoteSets_;
|
||||
/** user ids */
|
||||
std::unordered_set<QString> subscribedUsers_;
|
||||
std::chrono::milliseconds heartbeatInterval_;
|
||||
};
|
||||
|
|
|
@ -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 <utility>
|
||||
|
||||
namespace chatterino {
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
||||
SeventvEventAPIClient::SeventvEventAPIClient(
|
||||
liveupdates::WebsocketClient &websocketClient,
|
||||
liveupdates::WebsocketHandle handle,
|
||||
std::chrono::milliseconds heartbeatInterval)
|
||||
: BasicPubSubClient<SeventvEventAPISubscription>(websocketClient,
|
||||
std::move(handle))
|
||||
Client::Client(liveupdates::WebsocketClient &websocketClient,
|
||||
liveupdates::WebsocketHandle handle,
|
||||
std::chrono::milliseconds heartbeatInterval)
|
||||
: BasicPubSubClient<Subscription>(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<SeventvEventAPIClient>(
|
||||
this->shared_from_this());
|
||||
auto self = std::dynamic_pointer_cast<Client>(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
|
|
@ -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<SeventvEventAPISubscription>
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
||||
class Client : public BasicPubSubClient<Subscription>
|
||||
{
|
||||
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
|
|
@ -1,21 +1,20 @@
|
|||
#include "providers/seventv/eventapi/SeventvEventAPIDispatch.hpp"
|
||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
||||
SeventvEventAPIDispatch::SeventvEventAPIDispatch(QJsonObject obj)
|
||||
: type(magic_enum::enum_cast<SeventvEventAPISubscriptionType>(
|
||||
Dispatch::Dispatch(QJsonObject obj)
|
||||
: type(magic_enum::enum_cast<SubscriptionType>(
|
||||
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
|
70
src/providers/seventv/eventapi/Dispatch.hpp
Normal file
70
src/providers/seventv/eventapi/Dispatch.hpp
Normal file
|
@ -0,0 +1,70 @@
|
|||
#pragma once
|
||||
|
||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
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
|
11
src/providers/seventv/eventapi/Message.cpp
Normal file
11
src/providers/seventv/eventapi/Message.cpp
Normal file
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
#include <magic_enum.hpp>
|
||||
|
@ -8,27 +8,26 @@
|
|||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace chatterino {
|
||||
namespace chatterino::seventv::eventapi {
|
||||
|
||||
struct SeventvEventAPIMessage {
|
||||
struct Message {
|
||||
QJsonObject data;
|
||||
|
||||
SeventvEventAPIOpcode op;
|
||||
Opcode op;
|
||||
|
||||
SeventvEventAPIMessage(QJsonObject _json);
|
||||
Message(QJsonObject _json);
|
||||
|
||||
template <class InnerClass>
|
||||
boost::optional<InnerClass> toInner();
|
||||
};
|
||||
|
||||
template <class InnerClass>
|
||||
boost::optional<InnerClass> SeventvEventAPIMessage::toInner()
|
||||
boost::optional<InnerClass> Message::toInner()
|
||||
{
|
||||
return InnerClass{this->data};
|
||||
}
|
||||
|
||||
static boost::optional<SeventvEventAPIMessage> parseSeventvEventAPIBaseMessage(
|
||||
const QString &blob)
|
||||
static boost::optional<Message> parseBaseMessage(const QString &blob)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||
|
||||
|
@ -37,7 +36,7 @@ static boost::optional<SeventvEventAPIMessage> parseSeventvEventAPIBaseMessage(
|
|||
return boost::none;
|
||||
}
|
||||
|
||||
return SeventvEventAPIMessage(jsonDoc.object());
|
||||
return Message(jsonDoc.object());
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino::seventv::eventapi
|
|
@ -1,72 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
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
|
|
@ -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
|
|
@ -1,80 +0,0 @@
|
|||
#include "providers/seventv/eventapi/SeventvEventAPISubscription.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <tuple>
|
||||
|
||||
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
|
|
@ -1,76 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <magic_enum.hpp>
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
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<chatterino::SeventvEventAPISubscription> {
|
||||
size_t operator()(const chatterino::SeventvEventAPISubscription &sub) const
|
||||
{
|
||||
return (size_t)qHash(sub.condition, qHash((int)sub.type));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
105
src/providers/seventv/eventapi/Subscription.cpp
Normal file
105
src/providers/seventv/eventapi/Subscription.cpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
#include "providers/seventv/eventapi/Subscription.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
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
|
106
src/providers/seventv/eventapi/Subscription.hpp
Normal file
106
src/providers/seventv/eventapi/Subscription.hpp
Normal file
|
@ -0,0 +1,106 @@
|
|||
#pragma once
|
||||
|
||||
#include <magic_enum.hpp>
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <variant>
|
||||
|
||||
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<ObjectIDCondition>;
|
||||
|
||||
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<chatterino::seventv::eventapi::ObjectIDCondition> {
|
||||
size_t operator()(
|
||||
const chatterino::seventv::eventapi::ObjectIDCondition &c) const
|
||||
{
|
||||
return (size_t)qHash(c.objectID);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<chatterino::seventv::eventapi::Subscription> {
|
||||
size_t operator()(
|
||||
const chatterino::seventv::eventapi::Subscription &sub) const
|
||||
{
|
||||
const size_t conditionHash =
|
||||
std::hash<chatterino::seventv::eventapi::Condition>{}(
|
||||
sub.condition);
|
||||
return (size_t)qHash(conditionHash, qHash((int)sub.type));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
|
@ -311,10 +311,11 @@ std::vector<MessagePtr> 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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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_)
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct Emote;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,9 @@ public:
|
|||
// Directory for miscellaneous files. Same as <appDataDirectory>/Misc
|
||||
QString miscDirectory;
|
||||
|
||||
// Directory for crashdumps. Same as <appDataDirectory>/Crashes
|
||||
QString crashdumpDirectory;
|
||||
|
||||
// Hash of QCoreApplication::applicationFilePath()
|
||||
QString applicationFilePathHash;
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -520,9 +527,9 @@ public:
|
|||
{"d", 1},
|
||||
{"w", 1}}};
|
||||
|
||||
BoolSetting enableAnyPlugins = {"/plugins/load", false};
|
||||
BoolSetting pluginSupportEnabled = {"/plugins/supportEnabled", false};
|
||||
ChatterinoSetting<std::vector<QString>> enabledPlugins = {
|
||||
"/plugins/enabled", {}};
|
||||
"/plugins/enabledIds", {}};
|
||||
|
||||
private:
|
||||
void updateModerationActions();
|
||||
|
|
|
@ -95,6 +95,11 @@ void LoggingChannel::addMessage(MessagePtr message)
|
|||
}
|
||||
|
||||
QString str;
|
||||
if (channelName.startsWith("/mentions"))
|
||||
{
|
||||
str.append("#" + message->channelName + " ");
|
||||
}
|
||||
|
||||
str.append('[');
|
||||
str.append(now.toString("HH:mm:ss"));
|
||||
str.append("] ");
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
#include "util/IncognitoBrowser.hpp"
|
||||
#ifdef USEWINSDK
|
||||
# include "util/WindowsHelper.hpp"
|
||||
#endif
|
||||
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
|
||||
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
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
#ifdef USEWINSDK
|
||||
|
||||
# include <Shlwapi.h>
|
||||
# include <VersionHelpers.h>
|
||||
|
||||
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<UINT> 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
|
||||
|
|
|
@ -7,12 +7,16 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
enum class AssociationQueryType { Protocol, FileExtension };
|
||||
|
||||
boost::optional<UINT> getWindowDpi(HWND hwnd);
|
||||
void flushClipboard();
|
||||
|
||||
bool isRegisteredForStartup();
|
||||
void setRegisteredForStartup(bool isRegistered);
|
||||
|
||||
QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query);
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
||||
|
|
|
@ -631,8 +631,13 @@ void BaseWindow::moveIntoDesktopRect(QPoint point)
|
|||
this->move(point);
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
|
||||
qintptr *result)
|
||||
#else
|
||||
bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
|
||||
long *result)
|
||||
#endif
|
||||
{
|
||||
#ifdef USEWINSDK
|
||||
MSG *msg = reinterpret_cast<MSG *>(message);
|
||||
|
@ -830,7 +835,11 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
|
|||
#endif
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool BaseWindow::handleNCCALCSIZE(MSG *msg, qintptr *result)
|
||||
#else
|
||||
bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result)
|
||||
#endif
|
||||
{
|
||||
#ifdef USEWINSDK
|
||||
if (this->hasCustomWindowFrame())
|
||||
|
@ -914,7 +923,11 @@ bool BaseWindow::handleMOVE(MSG *msg)
|
|||
return false;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool BaseWindow::handleNCHITTEST(MSG *msg, qintptr *result)
|
||||
#else
|
||||
bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
|
||||
#endif
|
||||
{
|
||||
#ifdef USEWINSDK
|
||||
const LONG border_width = 8; // in pixels
|
||||
|
|
|
@ -67,8 +67,13 @@ public:
|
|||
static bool supportsCustomWindowFrame();
|
||||
|
||||
protected:
|
||||
virtual bool nativeEvent(const QByteArray &eventType, void *message,
|
||||
long *result) override;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool nativeEvent(const QByteArray &eventType, void *message,
|
||||
qintptr *result) override;
|
||||
#else
|
||||
bool nativeEvent(const QByteArray &eventType, void *message,
|
||||
long *result) override;
|
||||
#endif
|
||||
virtual void scaleChangedEvent(float) override;
|
||||
|
||||
virtual void paintEvent(QPaintEvent *) override;
|
||||
|
@ -103,10 +108,15 @@ private:
|
|||
|
||||
bool handleDPICHANGED(MSG *msg);
|
||||
bool handleSHOWWINDOW(MSG *msg);
|
||||
bool handleNCCALCSIZE(MSG *msg, long *result);
|
||||
bool handleSIZE(MSG *msg);
|
||||
bool handleMOVE(MSG *msg);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool handleNCCALCSIZE(MSG *msg, qintptr *result);
|
||||
bool handleNCHITTEST(MSG *msg, qintptr *result);
|
||||
#else
|
||||
bool handleNCCALCSIZE(MSG *msg, long *result);
|
||||
bool handleNCHITTEST(MSG *msg, long *result);
|
||||
#endif
|
||||
|
||||
bool enableCustomFrame_;
|
||||
ActionOnFocusLoss actionOnFocusLoss_ = Nothing;
|
||||
|
|
|
@ -27,8 +27,13 @@ FramelessEmbedWindow::FramelessEmbedWindow()
|
|||
}
|
||||
|
||||
#ifdef USEWINSDK
|
||||
# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType,
|
||||
void *message, qintptr *result)
|
||||
# else
|
||||
bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType,
|
||||
void *message, long *result)
|
||||
# endif
|
||||
{
|
||||
MSG *msg = reinterpret_cast<MSG *>(message);
|
||||
|
||||
|
|
|
@ -13,8 +13,15 @@ public:
|
|||
|
||||
protected:
|
||||
#ifdef USEWINSDK
|
||||
|
||||
# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool nativeEvent(const QByteArray &eventType, void *message,
|
||||
qintptr *result) override;
|
||||
# else
|
||||
bool nativeEvent(const QByteArray &eventType, void *message,
|
||||
long *result) override;
|
||||
# endif
|
||||
|
||||
void showEvent(QShowEvent *event) override;
|
||||
#endif
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/CommandController.hpp"
|
||||
#include "controllers/highlights/HighlightBlacklistUser.hpp"
|
||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
|
@ -223,6 +224,10 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
|
|||
.arg(this->userName_)
|
||||
.arg(calculateTimeoutDuration(button));
|
||||
}
|
||||
|
||||
msg = getApp()->commands->execCommand(
|
||||
msg, this->underlyingChannel_, false);
|
||||
|
||||
this->underlyingChannel_->sendMessage(msg);
|
||||
return "";
|
||||
}},
|
||||
|
@ -478,25 +483,35 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
|
|||
case TimeoutWidget::Ban: {
|
||||
if (this->underlyingChannel_)
|
||||
{
|
||||
this->underlyingChannel_->sendMessage("/ban " +
|
||||
this->userName_);
|
||||
QString value = "/ban " + this->userName_;
|
||||
value = getApp()->commands->execCommand(
|
||||
value, this->underlyingChannel_, false);
|
||||
|
||||
this->underlyingChannel_->sendMessage(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TimeoutWidget::Unban: {
|
||||
if (this->underlyingChannel_)
|
||||
{
|
||||
this->underlyingChannel_->sendMessage("/unban " +
|
||||
this->userName_);
|
||||
QString value = "/unban " + this->userName_;
|
||||
value = getApp()->commands->execCommand(
|
||||
value, this->underlyingChannel_, false);
|
||||
|
||||
this->underlyingChannel_->sendMessage(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TimeoutWidget::Timeout: {
|
||||
if (this->underlyingChannel_)
|
||||
{
|
||||
this->underlyingChannel_->sendMessage(
|
||||
"/timeout " + this->userName_ + " " +
|
||||
QString::number(arg));
|
||||
QString value = "/timeout " + this->userName_ + " " +
|
||||
QString::number(arg);
|
||||
|
||||
value = getApp()->commands->execCommand(
|
||||
value, this->underlyingChannel_, false);
|
||||
|
||||
this->underlyingChannel_->sendMessage(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#include "PluginsPage.hpp"
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "widgets/settingspages/PluginsPage.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
@ -47,7 +47,7 @@ PluginsPage::PluginsPage()
|
|||
groupLayout->addRow(description);
|
||||
|
||||
auto *box = this->createCheckBox("Enable plugins",
|
||||
getSettings()->enableAnyPlugins);
|
||||
getSettings()->pluginSupportEnabled);
|
||||
QObject::connect(box, &QCheckBox::released, [this]() {
|
||||
this->rebuildContent();
|
||||
});
|
||||
|
@ -68,83 +68,118 @@ void PluginsPage::rebuildContent()
|
|||
this->dataFrame_ = frame.getElement();
|
||||
this->scrollAreaWidget_.append(this->dataFrame_);
|
||||
auto layout = frame.setLayoutType<QVBoxLayout>();
|
||||
for (const auto &[codename, plugin] : getApp()->plugins->plugins())
|
||||
layout->setParent(this->dataFrame_);
|
||||
for (const auto &[id, plugin] : getApp()->plugins->plugins())
|
||||
{
|
||||
QString headerText;
|
||||
if (plugin->isDupeName)
|
||||
{
|
||||
headerText = QString("%1 (%2, from %3)")
|
||||
.arg(plugin->meta.name,
|
||||
QString::fromStdString(
|
||||
plugin->meta.version.to_string()),
|
||||
codename);
|
||||
}
|
||||
else
|
||||
{
|
||||
headerText = QString("%1 (%2)").arg(
|
||||
plugin->meta.name,
|
||||
QString::fromStdString(plugin->meta.version.to_string()));
|
||||
}
|
||||
auto plgroup = layout.emplace<QGroupBox>(headerText);
|
||||
auto pl = plgroup.setLayoutType<QFormLayout>();
|
||||
auto *descrText = new QLabel(plugin->meta.description);
|
||||
descrText->setWordWrap(true);
|
||||
descrText->setStyleSheet("color: #bbb");
|
||||
pl->addRow(descrText);
|
||||
pl->addRow("Authors", new QLabel(plugin->meta.authors));
|
||||
auto *homepage = new QLabel(formatRichLink(plugin->meta.homepage));
|
||||
homepage->setOpenExternalLinks(true);
|
||||
auto groupHeaderText =
|
||||
QString("%1 (%2, from %3)")
|
||||
.arg(plugin->meta.name,
|
||||
QString::fromStdString(plugin->meta.version.to_string()),
|
||||
id);
|
||||
auto groupBox = layout.emplace<QGroupBox>(groupHeaderText);
|
||||
groupBox->setParent(this->dataFrame_);
|
||||
auto pluginEntry = groupBox.setLayoutType<QFormLayout>();
|
||||
pluginEntry->setParent(groupBox.getElement());
|
||||
|
||||
pl->addRow("Homepage", homepage);
|
||||
pl->addRow("License", new QLabel(plugin->meta.license));
|
||||
|
||||
QString cmds;
|
||||
for (const auto &cmdName : plugin->listRegisteredCommands())
|
||||
if (!plugin->meta.isValid())
|
||||
{
|
||||
if (!cmds.isEmpty())
|
||||
QString errors = "<ul>";
|
||||
for (const auto &err : plugin->meta.errors)
|
||||
{
|
||||
cmds += ", ";
|
||||
errors += "<li>" + err.toHtmlEscaped() + "</li>";
|
||||
}
|
||||
errors += "</ul>";
|
||||
|
||||
auto *warningLabel = new QLabel(
|
||||
"There were errors while loading metadata for this plugin:" +
|
||||
errors,
|
||||
this->dataFrame_);
|
||||
warningLabel->setTextFormat(Qt::RichText);
|
||||
warningLabel->setStyleSheet("color: #f00");
|
||||
pluginEntry->addRow(warningLabel);
|
||||
}
|
||||
|
||||
auto *description =
|
||||
new QLabel(plugin->meta.description, this->dataFrame_);
|
||||
description->setWordWrap(true);
|
||||
description->setStyleSheet("color: #bbb");
|
||||
pluginEntry->addRow(description);
|
||||
|
||||
QString authorsTxt;
|
||||
for (const auto &author : plugin->meta.authors)
|
||||
{
|
||||
if (!authorsTxt.isEmpty())
|
||||
{
|
||||
authorsTxt += ", ";
|
||||
}
|
||||
|
||||
cmds += cmdName;
|
||||
authorsTxt += author;
|
||||
}
|
||||
pl->addRow("Commands", new QLabel(cmds));
|
||||
pluginEntry->addRow("Authors",
|
||||
new QLabel(authorsTxt, this->dataFrame_));
|
||||
|
||||
QString enableOrDisableStr = "Enable";
|
||||
if (PluginController::isEnabled(codename))
|
||||
if (!plugin->meta.homepage.isEmpty())
|
||||
{
|
||||
enableOrDisableStr = "Disable";
|
||||
auto *homepage = new QLabel(formatRichLink(plugin->meta.homepage),
|
||||
this->dataFrame_);
|
||||
homepage->setOpenExternalLinks(true);
|
||||
pluginEntry->addRow("Homepage", homepage);
|
||||
}
|
||||
pluginEntry->addRow("License",
|
||||
new QLabel(plugin->meta.license, this->dataFrame_));
|
||||
|
||||
QString commandsTxt;
|
||||
for (const auto &cmdName : plugin->listRegisteredCommands())
|
||||
{
|
||||
if (!commandsTxt.isEmpty())
|
||||
{
|
||||
commandsTxt += ", ";
|
||||
}
|
||||
|
||||
commandsTxt += cmdName;
|
||||
}
|
||||
pluginEntry->addRow("Commands",
|
||||
new QLabel(commandsTxt, this->dataFrame_));
|
||||
|
||||
if (plugin->meta.isValid())
|
||||
{
|
||||
QString toggleTxt = "Enable";
|
||||
if (PluginController::isEnabled(id))
|
||||
{
|
||||
toggleTxt = "Disable";
|
||||
}
|
||||
|
||||
auto *toggleButton = new QPushButton(toggleTxt, this->dataFrame_);
|
||||
QObject::connect(
|
||||
toggleButton, &QPushButton::pressed, [name = id, this]() {
|
||||
std::vector<QString> val =
|
||||
getSettings()->enabledPlugins.getValue();
|
||||
if (PluginController::isEnabled(name))
|
||||
{
|
||||
val.erase(std::remove(val.begin(), val.end(), name),
|
||||
val.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
val.push_back(name);
|
||||
}
|
||||
getSettings()->enabledPlugins.setValue(val);
|
||||
getApp()->plugins->reload(name);
|
||||
this->rebuildContent();
|
||||
});
|
||||
pluginEntry->addRow(toggleButton);
|
||||
}
|
||||
|
||||
auto *enableDisable = new QPushButton(enableOrDisableStr);
|
||||
QObject::connect(
|
||||
enableDisable, &QPushButton::pressed, [name = codename, this]() {
|
||||
std::vector<QString> val =
|
||||
getSettings()->enabledPlugins.getValue();
|
||||
if (PluginController::isEnabled(name))
|
||||
{
|
||||
val.erase(std::remove(val.begin(), val.end(), name),
|
||||
val.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
val.push_back(name);
|
||||
}
|
||||
getSettings()->enabledPlugins.setValue(val);
|
||||
getApp()->plugins->reload(name);
|
||||
this->rebuildContent();
|
||||
});
|
||||
pl->addRow(enableDisable);
|
||||
|
||||
auto *reload = new QPushButton("Reload");
|
||||
QObject::connect(reload, &QPushButton::pressed,
|
||||
[name = codename, this]() {
|
||||
auto *reloadButton = new QPushButton("Reload", this->dataFrame_);
|
||||
QObject::connect(reloadButton, &QPushButton::pressed,
|
||||
[name = id, this]() {
|
||||
getApp()->plugins->reload(name);
|
||||
this->rebuildContent();
|
||||
});
|
||||
pl->addRow(reload);
|
||||
pluginEntry->addRow(reloadButton);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue