Merge branch 'feature/lua_scripting' of github.com:Chatterino/chatterino2 into feature/lua_scripting

This commit is contained in:
Mm2PL 2023-02-20 14:10:56 +01:00
commit 9c8b4b3240
107 changed files with 2259 additions and 861 deletions

View file

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

View file

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

View 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

View 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

View 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

View 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
View 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
View file

@ -0,0 +1,4 @@
build*
.mypy_cache
.cache
.docker

View file

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

View file

@ -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
View file

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

View 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()
{

View file

@ -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+

View file

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

View file

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

View file

@ -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",

View file

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

@ -0,0 +1 @@
Subproject commit ec992578688b4c51c1856d08731cf7dcf10e446a

@ -1 +1 @@
Subproject commit d69789da1ccfa4db7c241de6b471d6b729f1561e
Subproject commit 5d708c3f9cae12820e415d4f89c9eacbe2ab964b

@ -1 +0,0 @@
Subproject commit a4626c12e9ae6f02fc1ca7a4e399bd8307424103

View 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.

View file

@ -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"

View file

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

View file

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

View file

@ -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"))
{
}

View file

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

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

@ -36,6 +36,10 @@ public:
ThreadMessageRow = 6,
};
enum UserHighlightRowIndexes {
SelfMessageRow = 0,
};
protected:
// turn a vector item into a model row
virtual HighlightPhrase getItemFromRow(

View file

@ -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 =

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 nameObj = obj.value("name");
if (!nameObj.isString())
auto homepageObj = obj.value("homepage");
if (homepageObj.isString())
{
this->invalidWhy.emplace_back("name is not a string");
this->valid = false;
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())
{
this->name = nameObj.toString();
}
else
{
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())
if (descrObj.isString())
{
this->invalidWhy.emplace_back("description is not a string");
this->valid = false;
}
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.isString())
if (authorsObj.isArray())
{
this->invalidWhy.emplace_back("description is not a string");
this->valid = false;
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));
}
this->authors = authorsObj.toString();
auto licenseObj = obj.value("license");
if (!licenseObj.isString())
if (licenseObj.isString())
{
this->invalidWhy.emplace_back("license is not a string");
this->valid = false;
}
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 v = semver::from_string_noexcept(
obj.value("version").toString().toStdString());
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->invalidWhy.emplace_back("unable to parse version");
this->valid = false;
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)

View file

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

View file

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

View file

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

View file

@ -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] {
@ -643,4 +649,6 @@ void ImageExpirationPool::freeOld()
# endif
}
#endif
} // namespace chatterino

View file

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

View file

@ -9,6 +9,7 @@
#include <cinttypes>
#include <memory>
#include <unordered_map>
#include <vector>
namespace chatterino {

View 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

View 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

View 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

View 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

View file

@ -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 =

View file

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

View file

@ -16,6 +16,8 @@ enum class ColorType {
FirstMessageHighlight,
ElevatedMessageHighlight,
ThreadMessageHighlight,
// Used in automatic highlights of your own messages
SelfMessageHighlight,
};
class ColorProvider

View file

@ -4,6 +4,8 @@
#include <rapidjson/rapidjson.h>
#include <unordered_map>
class QAbstractTableModel;
namespace chatterino {

View file

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

View file

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

View file

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

View file

@ -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,17 +111,20 @@ 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;
}
@ -128,11 +132,29 @@ void SeventvEventAPI::onMessage(
}
}
void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
void SeventvEventAPI::handleDispatch(const Dispatch &dispatch)
{
switch (dispatch.type)
{
case SeventvEventAPISubscriptionType::UpdateEmoteSet: {
case SubscriptionType::UpdateEmoteSet: {
this->onEmoteSetUpdate(dispatch);
}
break;
case SubscriptionType::UpdateUser: {
this->onUserUpdate(dispatch);
}
break;
default: {
qCDebug(chatterinoSeventvEventAPI)
<< "Unknown subscription type:" << (int)dispatch.type
<< "body:" << dispatch.body;
}
break;
}
}
void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch)
{
// dispatchBody: {
// pushed: Array<{ key, value }>,
// pulled: Array<{ key, old_value }>,
@ -146,8 +168,7 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
continue;
}
SeventvEventAPIEmoteAddDispatch added(
dispatch, pushed["value"].toObject());
const EmoteAddDispatch added(dispatch, pushed["value"].toObject());
if (added.validate())
{
@ -167,8 +188,8 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
continue;
}
SeventvEventAPIEmoteUpdateDispatch update(
dispatch, updated["old_value"].toObject(),
const EmoteUpdateDispatch update(dispatch,
updated["old_value"].toObject(),
updated["value"].toObject());
if (update.validate())
@ -189,8 +210,8 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
continue;
}
SeventvEventAPIEmoteRemoveDispatch removed(
dispatch, pulled["old_value"].toObject());
const EmoteRemoveDispatch removed(dispatch,
pulled["old_value"].toObject());
if (removed.validate())
{
@ -203,8 +224,9 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
}
}
}
break;
case SeventvEventAPISubscriptionType::UpdateUser: {
void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch)
{
// dispatchBody: {
// updated: Array<{ key, value: Array<{key, value}> }>
// }
@ -223,7 +245,7 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
continue;
}
SeventvEventAPIUserConnectionUpdateDispatch update(
const UserConnectionUpdateDispatch update(
dispatch, value, (size_t)updated["index"].toInt());
if (update.validate())
@ -238,14 +260,5 @@ void SeventvEventAPI::handleDispatch(const SeventvEventAPIDispatch &dispatch)
}
}
}
break;
default: {
qCDebug(chatterinoSeventvEventAPI)
<< "Unknown subscription type:" << (int)dispatch.type
<< "body:" << dispatch.body;
}
break;
}
}
} // namespace chatterino

View file

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

View file

@ -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,
Client::Client(liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval)
: BasicPubSubClient<SeventvEventAPISubscription>(websocketClient,
std::move(handle))
: 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

View file

@ -2,16 +2,20 @@
#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,
Client(liveupdates::WebsocketClient &websocketClient,
liveupdates::WebsocketHandle handle,
std::chrono::milliseconds heartbeatInterval);
@ -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

View file

@ -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,14 +51,14 @@ 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,
EmoteUpdateDispatch::EmoteUpdateDispatch(const Dispatch &dispatch,
QJsonObject oldValue,
QJsonObject value)
: emoteSetID(dispatch.id)
, actorName(dispatch.actorName)
@ -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

View 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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View file

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

View file

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

View file

@ -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,8 +304,22 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
<< "[TwitchChannel" << this->getName()
<< "] Channel point reward added:" << reward.id << ","
<< reward.title << "," << reward.isUserInputRequired;
// 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 << " }.";
}
}
}
bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId)
@ -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_)
{

View file

@ -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,

View file

@ -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"

View file

@ -9,6 +9,8 @@
#include <QString>
#include <QVariant>
#include <unordered_map>
namespace chatterino {
struct Emote;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,8 +67,13 @@ public:
static bool supportsCustomWindowFrame();
protected:
virtual bool nativeEvent(const QByteArray &eventType, void *message,
#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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,57 +68,90 @@ 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)")
auto groupHeaderText =
QString("%1 (%2, from %3)")
.arg(plugin->meta.name,
QString::fromStdString(
plugin->meta.version.to_string()),
codename);
}
else
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());
if (!plugin->meta.isValid())
{
headerText = QString("%1 (%2)").arg(
plugin->meta.name,
QString::fromStdString(plugin->meta.version.to_string()));
QString errors = "<ul>";
for (const auto &err : plugin->meta.errors)
{
errors += "<li>" + err.toHtmlEscaped() + "</li>";
}
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));
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 += ", ";
}
authorsTxt += author;
}
pluginEntry->addRow("Authors",
new QLabel(authorsTxt, this->dataFrame_));
if (!plugin->meta.homepage.isEmpty())
{
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_));
pl->addRow("Homepage", homepage);
pl->addRow("License", new QLabel(plugin->meta.license));
QString cmds;
QString commandsTxt;
for (const auto &cmdName : plugin->listRegisteredCommands())
{
if (!cmds.isEmpty())
if (!commandsTxt.isEmpty())
{
cmds += ", ";
commandsTxt += ", ";
}
cmds += cmdName;
commandsTxt += cmdName;
}
pl->addRow("Commands", new QLabel(cmds));
pluginEntry->addRow("Commands",
new QLabel(commandsTxt, this->dataFrame_));
QString enableOrDisableStr = "Enable";
if (PluginController::isEnabled(codename))
if (plugin->meta.isValid())
{
enableOrDisableStr = "Disable";
QString toggleTxt = "Enable";
if (PluginController::isEnabled(id))
{
toggleTxt = "Disable";
}
auto *enableDisable = new QPushButton(enableOrDisableStr);
auto *toggleButton = new QPushButton(toggleTxt, this->dataFrame_);
QObject::connect(
enableDisable, &QPushButton::pressed, [name = codename, this]() {
toggleButton, &QPushButton::pressed, [name = id, this]() {
std::vector<QString> val =
getSettings()->enabledPlugins.getValue();
if (PluginController::isEnabled(name))
@ -134,17 +167,19 @@ void PluginsPage::rebuildContent()
getApp()->plugins->reload(name);
this->rebuildContent();
});
pl->addRow(enableDisable);
pluginEntry->addRow(toggleButton);
}
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