mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Merge branch 'master' into fix/stop_windows_code_from_abort()ing_our_process
This commit is contained in:
commit
48ca328199
162 changed files with 4024 additions and 1352 deletions
|
@ -1,9 +1,9 @@
|
|||
[Flatpak Ref]
|
||||
Name=com.chatterino.chatterino
|
||||
Branch=nightly
|
||||
Branch=beta
|
||||
Title=com.chatterino.chatterino from flathub
|
||||
IsRuntime=false
|
||||
Url=https://dl.flathub.org/repo/
|
||||
SuggestRemoteName=flathub
|
||||
Url=https://dl.flathub.org/beta-repo/
|
||||
SuggestRemoteName=flathub-beta
|
||||
GPGKey=mQINBFlD2sABEADsiUZUOYBg1UdDaWkEdJYkTSZD68214m8Q1fbrP5AptaUfCl8KYKFMNoAJRBXn9FbE6q6VBzghHXj/rSnA8WPnkbaEWR7xltOqzB1yHpCQ1l8xSfH5N02DMUBSRtD/rOYsBKbaJcOgW0K21sX+BecMY/AI2yADvCJEjhVKrjR9yfRX+NQEhDcbXUFRGt9ZT+TI5yT4xcwbvvTu7aFUR/dH7+wjrQ7lzoGlZGFFrQXSs2WI0WaYHWDeCwymtohXryF8lcWQkhH8UhfNJVBJFgCY8Q6UHkZG0FxMu8xnIDBMjBmSZKwKQn0nwzwM2afskZEnmNPYDI8nuNsSZBZSAw+ThhkdCZHZZRwzmjzyRuLLVFpOj3XryXwZcSefNMPDkZAuWWzPYjxS80cm2hG1WfqrG0Gl8+iX69cbQchb7gbEb0RtqNskTo9DDmO0bNKNnMbzmIJ3/rTbSahKSwtewklqSP/01o0WKZiy+n/RAkUKOFBprjJtWOZkc8SPXV/rnoS2dWsJWQZhuPPtv3tefdDiEyp7ePrfgfKxuHpZES0IZRiFI4J/nAUP5bix+srcIxOVqAam68CbAlPvWTivRUMRVbKjJiGXIOJ78wAMjqPg3QIC0GQ0EPAWwAOzzpdgbnG7TCQetaVV8rSYCuirlPYN+bJIwBtkOC9SWLoPMVZTwQARAQABtC5GbGF0aHViIFJlcG8gU2lnbmluZyBLZXkgPGZsYXRodWJAZmxhdGh1Yi5vcmc+iQJUBBMBCAA+FiEEblwF2XnHba+TwIE1QYTdTZB6fK4FAllD2sACGwMFCRLMAwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQQYTdTZB6fK5RJQ/+Ptd4sWxaiAW91FFk7+wmYOkEe1NY2UDNJjEEz34PNP/1RoxveHDt43kYJQ23OWaPJuZAbu+fWtjRYcMBzOsMCaFcRSHFiDIC9aTp4ux/mo+IEeyarYt/oyKb5t5lta6xaAqg7rwt65jW5/aQjnS4h7eFZ+dAKta7Y/fljNrOznUp81/SMcx4QA5G2Pw0hs4Xrxg59oONOTFGBgA6FF8WQghrpR7SnEe0FSEOVsAjwQ13Cfkfa7b70omXSWp7GWfUzgBKyoWxKTqzMN3RQHjjhPJcsQnrqH5enUu4Pcb2LcMFpzimHnUgb9ft72DP5wxfzHGAWOUiUXHbAekfq5iFks8cha/RST6wkxG3Rf44Zn09aOxh1btMcGL+5xb1G0BuCQnA0fP/kDYIPwh9z22EqwRQOspIcvGeLVkFeIfubxpcMdOfQqQnZtHMCabV5Q/Rk9K1ZGc8M2hlg8gHbXMFch2xJ0Wu72eXbA/UY5MskEeBgawTQnQOK/vNm7t0AJMpWK26Qg6178UmRghmeZDj9uNRc3EI1nSbgvmGlpDmCxaAGqaGL1zW4KPW5yN25/qeqXcgCvUjZLI9PNq3Kvizp1lUrbx7heRiSoazCucvHQ1VHUzcPVLUKKTkoTP8okThnRRRsBcZ1+jI4yMWIDLOCT7IW3FePr+3xyuy5eEo9a25Ag0EWUPa7AEQALT/CmSyZ8LWlRYQZKYw417p7Z2hxqd6TjwkwM3IQ1irumkWcTZBZIbBgrSOg6CcXD2oWydCQHWi9qaxhuhEl2bJL5LskmBcMxVdQeD0LLHd8QUnbnnIby8ocvWN1alPfvJFjCUTrmD22U1ycOzRw2lIe4kiQONbOZtdWrVImQQSndjFlisitbmlWHvHm2lOOYy8+GJB7YffVV193hmnBSJffCy4bvkuLxsI+n1DhOzc7MPV3z6HGk4HiEcF0yyt9tCYhpsxHFdBoq2h771HfAcS0s98EVAqYMFnf9em+4cnYpdI6mhIfS1FQiKl6DBAYA8tT3ggla00DurPo0JwX/zN+PaO5h/6O9aCZwV7G6rbkgMuqMergXaf8oP38gr0z+MqWnkfM63Bodq68GP4l4hd02BoFBbDf38TMuGQB14+twJMdfbAxo2MbgluvQgfwHfZ2ca6gyEY+9s/YD1gugLjV+S6CB51WkFNe1z4tAPgJZNxUcKCbeaHNbthl8Hks/pY9RCEseX/EdfzF18epbSjJMPh4DPQXbUoFwmyuYcoBOPmvZHNl9hK7B/1RP8w1ZrXk8qdupC0SNbafX7270B7lMMVImzZetGsM9ypXJ6llhp3FwW09iseNyGJGPsr/dvTMGDXqOPfU/9SAS1LSTY4K9PbRtdrBE318YX8mIk5ABEBAAGJBHIEGAEIACYWIQRuXAXZecdtr5PAgTVBhN1NkHp8rgUCWUPa7AIbAgUJEswDAAJACRBBhN1NkHp8rsF0IAQZAQgAHRYhBFSmzd2JGfsgQgDYrFYnAunj7X7oBQJZQ9rsAAoJEFYnAunj7X7oR6AP/0KYmiAFeqx14Z43/6s2gt3VhxlSd8bmcVV7oJFbMhdHBIeWBp2BvsUf00I0Zl14ZkwCKfLwbbORC2eIxvzJ+QWjGfPhDmS4XUSmhlXxWnYEveSek5Tde+fmu6lqKM8CHg5BNx4GWIX/vdLi1wWJZyhrUwwICAxkuhKxuP2Z1An48930eslTD2GGcjByc27+9cIZjHKa07I/aLffo04V+oMT9/tgzoquzgpVV4jwekADo2MJjhkkPveSNI420bgT+Q7Fi1l0X1aFUniBvQMsaBa27PngWm6xE2ZYvh7nWCdd5g0c0eLIHxWwzV1lZ4Ryx4ITO/VL25ItECcjhTRdYa64sA62MYSaB0x3eR+SihpgP3wSNPFu3MJo6FKTFdi4CBAEmpWHFW7FcRmd+cQXeFrHLN3iNVWryy0HK/CUEJmiZEmpNiXecl4vPIIuyF0zgSCztQtKoMr+injpmQGC/rF/ELBVZTUSLNB350S0Ztvw0FKWDAJSxFmoxt3xycqvvt47rxTrhi78nkk6jATKGyvP55sO+K7Q7Wh0DXA69hvPrYW2eu8jGCdVGxi6HX7L1qcfEd0378S71dZ3g9o6KKl1OsDWWQ6MJ6FGBZedl/ibRfs8p5+sbCX3lQSjEFy3rx6n0rUrXx8U2qb+RCLzJlmC5MNBOTDJwHPcX6gKsUcXZrEQALmRHoo3SrewO41RCr+5nUlqiqV3AohBMhnQbGzyHf2+drutIaoh7Rj80XRh2bkkuPLwlNPf+bTXwNVGse4bej7B3oV6Ae1N7lTNVF4Qh+1OowtGjmfJPWo0z1s6HFJVxoIof9z58Msvgao0zrKGqaMWaNQ6LUeC9g9Aj/9Uqjbo8X54aLiYs8Z1WNc06jKP+gv8AWLtv6CR+l2kLez1YMDucjm7v6iuCMVAmZdmxhg5I/X2+OM3vBsqPDdQpr2TPDLX3rCrSBiS0gOQ6DwN5N5QeTkxmY/7QO8bgLo/Wzu1iilH4vMKW6LBKCaRx5UEJxKpL4wkgITsYKneIt3NTHo5EOuaYk+y2+Dvt6EQFiuMsdbfUjs3seIHsghX/cbPJa4YUqZAL8C4OtVHaijwGo0ymt9MWvS9yNKMyT0JhN2/BdeOVWrHk7wXXJn/ZjpXilicXKPx4udCF76meE+6N2u/T+RYZ7fP1QMEtNZNmYDOfA6sViuPDfQSHLNbauJBo/n1sRYAsL5mcG22UDchJrlKvmK3EOADCQg+myrm8006LltubNB4wWNzHDJ0Ls2JGzQZCd/xGyVmUiidCBUrD537WdknOYE4FD7P0cHaM9brKJ/M8LkEH0zUlo73bY4XagbnCqve6PvQb5G2Z55qhWphd6f4B6DGed86zJEa/RhS
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
|
|
@ -29,6 +29,7 @@ Checks: "-*,
|
|||
-readability-function-cognitive-complexity,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cert-err58-cpp,
|
||||
-modernize-avoid-c-arrays
|
||||
"
|
||||
CheckOptions:
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
|
|
|
@ -36,7 +36,7 @@ NOTE: The AppImage from Ubuntu 22.04 is broken. Approach with caution
|
|||
|
||||
#### Testing
|
||||
|
||||
1. Build a docker image builds the chatterino tests
|
||||
1. Build a docker image builds the Chatterino tests
|
||||
`docker buildx build -t chatterino-ubuntu-22.04-test -f .docker/Dockerfile-ubuntu-22.04-test .`
|
||||
1. Run the tests
|
||||
`docker run --rm --network=host chatterino-ubuntu-22.04-test`
|
||||
|
|
8
.github/pull_request_template.md
vendored
8
.github/pull_request_template.md
vendored
|
@ -1,3 +1,5 @@
|
|||
# Description
|
||||
|
||||
<!-- If applicable, please include a summary of what you've changed and what issue is fixed. In the case of a bug fix, please include steps to reproduce the bug so the pull request can be tested -->
|
||||
<!--
|
||||
Please include a summary of what you've changed and what issue is fixed.
|
||||
In the case of a bug fix, please include steps to reproduce the bug so the pull request can be tested.
|
||||
If this PR fixes an issue on GitHub, mention this here to automatically close it: "Fixes #1234.".
|
||||
-->
|
||||
|
|
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
|
@ -230,9 +230,9 @@ jobs:
|
|||
run: |
|
||||
cd build
|
||||
set cl=/MP
|
||||
nmake /S /NOLOGO crashpad_handler
|
||||
nmake /S /NOLOGO chatterino-crash-handler
|
||||
mkdir Chatterino2/crashpad
|
||||
cp bin/crashpad/crashpad_handler.exe Chatterino2/crashpad/crashpad_handler.exe
|
||||
cp bin/crashpad/crashpad-handler.exe Chatterino2/crashpad/crashpad-handler.exe
|
||||
7z a bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z bin/chatterino.pdb
|
||||
|
||||
- name: Prepare build dir (windows)
|
||||
|
@ -257,14 +257,14 @@ jobs:
|
|||
|
||||
- name: Upload artifact (Windows - binary)
|
||||
if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
||||
path: build/chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
||||
|
||||
- name: Upload artifact (Windows - symbols)
|
||||
if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}-symbols.pdb.7z
|
||||
path: build/bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z
|
||||
|
@ -339,14 +339,14 @@ jobs:
|
|||
|
||||
- name: Upload artifact - AppImage (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Chatterino-x86_64-${{ matrix.qt-version }}.AppImage
|
||||
path: build/Chatterino-x86_64.AppImage
|
||||
|
||||
- name: Upload artifact - .deb (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu') && !matrix.skip-artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}.deb
|
||||
path: build/Chatterino-${{ matrix.os }}-x86_64.deb
|
||||
|
@ -390,7 +390,7 @@ jobs:
|
|||
|
||||
- name: Upload artifact (MacOS)
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
||||
path: build/chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
||||
|
@ -404,49 +404,49 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Ubuntu 22.04 Qt6.2.4 deb
|
||||
with:
|
||||
name: Chatterino-ubuntu-22.04-Qt-6.2.4.deb
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Windows Qt6.5.0
|
||||
with:
|
||||
name: chatterino-windows-x86-64-Qt-6.5.0.zip
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Windows Qt6.5.0 symbols
|
||||
with:
|
||||
name: chatterino-windows-x86-64-Qt-6.5.0-symbols.pdb.7z
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Windows Qt5.15.2
|
||||
with:
|
||||
name: chatterino-windows-x86-64-Qt-5.15.2.zip
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Linux Qt5.12.12 AppImage
|
||||
with:
|
||||
name: Chatterino-x86_64-5.12.12.AppImage
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Ubuntu 20.04 Qt5.12.12 deb
|
||||
with:
|
||||
name: Chatterino-ubuntu-20.04-Qt-5.12.12.deb
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Ubuntu 22.04 Qt5.15.2 deb
|
||||
with:
|
||||
name: Chatterino-ubuntu-22.04-Qt-5.15.2.deb
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
name: macOS x86_64 Qt5.15.2 dmg
|
||||
with:
|
||||
name: chatterino-macos-Qt-5.15.2.dmg
|
||||
|
|
2
.github/workflows/check-formatting.yml
vendored
2
.github/workflows/check-formatting.yml
vendored
|
@ -33,4 +33,4 @@ jobs:
|
|||
clangFormatVersion: 16
|
||||
|
||||
- name: Check line-endings
|
||||
run: ./tools/check-line-endings.sh
|
||||
run: ./scripts/check-line-endings.sh
|
||||
|
|
2
.github/workflows/clang-tidy.yml
vendored
2
.github/workflows/clang-tidy.yml
vendored
|
@ -124,7 +124,7 @@ jobs:
|
|||
build_dir: build-clang-tidy
|
||||
config_file: ".clang-tidy"
|
||||
split_workflow: true
|
||||
exclude: "lib/*"
|
||||
exclude: "lib/*,tools/crash-handler/*"
|
||||
cmake_command: >-
|
||||
cmake -S. -Bbuild-clang-tidy
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
|
|
4
.github/workflows/create-installer.yml
vendored
4
.github/workflows/create-installer.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- name: Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: build.yml
|
||||
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
||||
|
@ -52,7 +52,7 @@ jobs:
|
|||
shell: powershell
|
||||
|
||||
- name: Upload installer
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: build/${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}.exe
|
||||
name: ${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}.exe
|
||||
|
|
37
.github/workflows/test-macos.yml
vendored
37
.github/workflows/test-macos.yml
vendored
|
@ -7,7 +7,7 @@ on:
|
|||
merge_group:
|
||||
|
||||
env:
|
||||
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.6
|
||||
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
|
||||
QT_QPA_PLATFORM: minimal
|
||||
|
||||
concurrency:
|
||||
|
@ -54,11 +54,7 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install boost openssl rapidjson p7zip create-dmg cmake tree docker colima
|
||||
|
||||
- name: Setup Colima
|
||||
run: |
|
||||
colima start
|
||||
brew install boost openssl rapidjson p7zip create-dmg cmake
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
@ -74,18 +70,27 @@ jobs:
|
|||
..
|
||||
make -j"$(sysctl -n hw.logicalcpu)"
|
||||
|
||||
- name: Download and extract Twitch PubSub Server Test
|
||||
run: |
|
||||
mkdir pubsub-server-test
|
||||
curl -L -o pubsub-server.tar.gz "https://github.com/Chatterino/twitch-pubsub-server-test/releases/download/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/server-${{ env.TWITCH_PUBSUB_SERVER_TAG }}-darwin-amd64.tar.gz"
|
||||
tar -xzf pubsub-server.tar.gz -C pubsub-server-test
|
||||
rm pubsub-server.tar.gz
|
||||
cd pubsub-server-test
|
||||
curl -L -o server.crt "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.crt"
|
||||
curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key"
|
||||
cd ..
|
||||
|
||||
- name: Cargo Install httpbox
|
||||
run: |
|
||||
cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
docker pull kennethreitz/httpbin
|
||||
docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
||||
docker run --network=host --detach ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
||||
docker run -p 9051:80 --detach kennethreitz/httpbin
|
||||
httpbox --port 9051 &
|
||||
cd ../pubsub-server-test
|
||||
./server 127.0.0.1:9050 &
|
||||
cd ../build-test
|
||||
ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering
|
||||
working-directory: build-test
|
||||
|
||||
- name: Post Setup Colima
|
||||
if: always()
|
||||
run: |
|
||||
colima stop
|
||||
working-directory: build-test
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -38,6 +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
|
||||
[submodule "tools/crash-handler"]
|
||||
path = tools/crash-handler
|
||||
url = https://github.com/Chatterino/crash-handler
|
||||
|
|
|
@ -6,13 +6,13 @@ Note on Qt version compatibility: If you are installing Qt from a package manage
|
|||
|
||||
### Ubuntu 20.04
|
||||
|
||||
_Most likely works the same for other Debian-like distros_
|
||||
_Most likely works the same for other Debian-like distros._
|
||||
|
||||
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++ libsecret-1-dev`
|
||||
Install all 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++ libsecret-1-dev`
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Install all of the dependencies using `sudo pacman -S --needed qt5-base qt5-imageformats qt5-svg qt5-tools boost rapidjson pkgconf openssl cmake`
|
||||
Install all 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,11 +20,11 @@ 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-qtimageformats qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake`
|
||||
Install all the dependencies using `sudo dnf install qt5-qtbase-devel qt5-qtimageformats qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake`
|
||||
|
||||
### NixOS 18.09+
|
||||
|
||||
Enter the development environment with all of the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake`
|
||||
Enter the development environment with all the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake`
|
||||
|
||||
## Compile
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ Local dev machines for testing are available on Apple Silicon on macOS 13.
|
|||
1. Go to the project directory where you cloned Chatterino2 & its submodules
|
||||
1. Create a build directory and go into it:
|
||||
`mkdir build && cd build`
|
||||
1. Run cmake:
|
||||
1. Run CMake:
|
||||
`cmake -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@5 -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@1.1 ..`
|
||||
1. Build:
|
||||
`make`
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
# Building on Windows
|
||||
|
||||
**Note that installing all of the development prerequisites and libraries will require about 40 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.**
|
||||
**Note that installing all the development prerequisites and libraries will require about 12 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.**
|
||||
|
||||
This guide assumes you are on a 64-bit system. You might need to manually search out alternate download links should you desire to build Chatterino on a 32-bit system.
|
||||
|
||||
## Installing prerequisites
|
||||
## Prerequisites
|
||||
|
||||
### Visual Studio
|
||||
|
||||
Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/). In the installer, select "Desktop development with C++" and "Universal Windows Platform development".
|
||||
Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/). In the installer, select "Desktop development with C++".
|
||||
|
||||
Notes:
|
||||
|
||||
- This installation will take about 21 GB of disk space
|
||||
- This installation will take about 8 GB of disk space
|
||||
- You do not need to sign in with a Microsoft account after setup completes. You may simply exit the login dialog.
|
||||
|
||||
### Qt
|
||||
|
@ -26,7 +26,9 @@ Notes:
|
|||
|
||||
- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.12 or later**.
|
||||
|
||||
#### When prompted which components to install:
|
||||
#### Components
|
||||
|
||||
When prompted which components to install, do the following:
|
||||
|
||||
1. Unfold the tree element that says "Qt"
|
||||
2. Unfold the top most tree element (latest stable Qt version, e.g. `Qt 6.5.3`)
|
||||
|
@ -43,7 +45,7 @@ Once Qt is done installing, make sure you add its bin directory to your `PATH` (
|
|||
|
||||
<details>
|
||||
<summary>How to add Qt to PATH</summary>
|
||||
|
||||
|
||||
1. Type "path" in the Windows start menu and click `Edit the system environment variables`.
|
||||
2. Click the `Environment Variables...` button bottom right.
|
||||
3. In the `User variables` (scoped to the current user) or `System variables` (system-wide) section, scroll down until you find `Path` and double click it.
|
||||
|
@ -78,7 +80,7 @@ Note: This installation will take about 2.1 GB of disk space.
|
|||
<details>
|
||||
<summary>OpenSSL</summary>
|
||||
|
||||
### For our websocket library, we need OpenSSL 1.1
|
||||
For our websocket library, we need OpenSSL 1.1.
|
||||
|
||||
1. Download OpenSSL for windows, version `1.1.1s`: **[Download](https://web.archive.org/web/20221101204129/https://slproweb.com/download/Win64OpenSSL-1_1_1s.exe)**
|
||||
2. When prompted, install OpenSSL to `C:\local\openssl`
|
||||
|
@ -120,57 +122,69 @@ Then in a terminal, configure conan to use `NMake Makefiles` as its generator:
|
|||
|
||||
Open up your terminal with the Visual Studio environment variables (e.g. `x64 Native Tools Command Prompt for VS 2022`), cd to the cloned chatterino2 directory and run the following commands:
|
||||
|
||||
1. `mkdir build`
|
||||
1. `cd build`
|
||||
1. `conan install .. -s build_type=Release -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" --build=missing --output-folder=.`
|
||||
1. `cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_PREFIX_PATH="C:\Qt\6.5.3\msvc2019_64" ..`
|
||||
1. `nmake`
|
||||
```cmd
|
||||
mkdir build
|
||||
cd build
|
||||
conan install .. -s build_type=Release -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" --build=missing --output-folder=.
|
||||
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_PREFIX_PATH="C:\Qt\6.5.3\msvc2019_64" ..
|
||||
nmake
|
||||
```
|
||||
|
||||
To build a debug build, you'll also need to add the `-s compiler.runtime_type=Debug` flag to the `conan install` invocation. See [this StackOverflow post](https://stackoverflow.com/questions/59828611/windeployqt-doesnt-deploy-qwindowsd-dll-for-a-debug-application/75607313#75607313)
|
||||
|
||||
#### Ensure DLLs are available
|
||||
#### Deploying Qt libraries
|
||||
|
||||
Once Chatterino has finished building, to ensure all .dll's are available you can run this from the build directory:
|
||||
`windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir bin/`
|
||||
|
||||
Can't find windeployqt? You forgot to add your Qt bin directory (e.g. `C:\Qt\6.5.3\msvc2019_64\bin`) to your `PATH`
|
||||
|
||||
### Run the build in Qt Creator
|
||||
### Developing in Qt Creator
|
||||
|
||||
1. Open the `CMakeLists.txt` file by double-clicking it, or by opening it via Qt Creator.
|
||||
2. You will be presented with a screen that is titled "Configure Project". In this screen, you should have at least one option present ready to be configured, like this:
|
||||
![Qt Create Configure Project screenshot](https://user-images.githubusercontent.com/69117321/169887645-2ae0871a-fe8a-4eb9-98db-7b996dea3a54.png)
|
||||
3. Select the profile(s) you want to build with and click "Configure Project".
|
||||
|
||||
#### How to run and produce builds
|
||||
#### Building and running
|
||||
|
||||
- In the main screen, click the green "play symbol" on the bottom left to run the project directly.
|
||||
- Click the hammer on the bottom left to generate a build (does not run the build though).
|
||||
|
||||
Build results will be placed in a folder at the same level as the "chatterino2" project folder (e.g. if your sources are at `C:\Users\example\src\chatterino2`, then the build will be placed in an automatically generated folder under `C:\Users\example\src`, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release`.)
|
||||
|
||||
- Note that if you are building chatterino purely for usage, not for development, it is recommended that you click the "PC" icon above the play icon and select "Release" instead of "Debug".
|
||||
- Note that if you are building Chatterino purely for usage, not for development, it is recommended that you click the "PC" icon above the play icon and select "Release" instead of "Debug".
|
||||
- Output and error messages produced by the compiler can be seen under the "4 Compile Output" tab in Qt Creator.
|
||||
|
||||
#### Producing standalone builds
|
||||
|
||||
If you build chatterino, the result directories will contain a `chatterino.exe` file in the `$OUTPUTDIR\release\` directory. This `.exe` file will not directly run on any given target system, because it will be lacking various Qt runtimes.
|
||||
If you build Chatterino, the result directories will contain a `chatterino.exe` file in the `$OUTPUTDIR\release\` directory. This `.exe` file will not directly run on any given target system, because it will be lacking various Qt runtimes.
|
||||
|
||||
To produce a standalone package, you need to generate all required files using the tool `windeployqt`. This tool can be found in the `bin` directory of your Qt installation, e.g. at `C:\Qt\6.5.3\msvc2019_64\bin\windeployqt.exe`.
|
||||
|
||||
To produce all supplement files for a standalone build, follow these steps (adjust paths as required):
|
||||
|
||||
1. Navigate to your build output directory with Windows Explorer, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release`
|
||||
2. Enter the `release` directory
|
||||
3. Delete all files except the `chatterino.exe` file. You should be left with a directory only containing `chatterino.exe`.
|
||||
4. Open a command prompt and execute:
|
||||
1. Navigate to your build output directory with Windows Explorer, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release`
|
||||
2. Enter the `release` directory
|
||||
3. Delete all files except the `chatterino.exe` file. You should be left with a directory only containing `chatterino.exe`.
|
||||
4. Open a command prompt and execute:
|
||||
```cmd
|
||||
cd C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release\release
|
||||
windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir bin/
|
||||
```
|
||||
5. The `releases` directory will now be populated with all the required files to make the Chatterino build standalone.
|
||||
|
||||
cd C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release\release
|
||||
C:\Qt\6.5.3\msvc2019_64\bin\windeployqt.exe chatterino.exe
|
||||
You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the CRT must be present, as usual - see the [README](README.md)).
|
||||
|
||||
5. The `releases` directory will now be populated with all the required files to make the chatterino build standalone.
|
||||
#### Formatting
|
||||
|
||||
You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the vcredist package must be present, as usual - see the [README](README.md)).
|
||||
To automatically format your code, do the following:
|
||||
|
||||
1. Download [LLVM 16.0.6](https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.6/LLVM-16.0.6-win64.exe)
|
||||
2. During the installation, make sure to add it to your path
|
||||
3. In Qt Creator, Select `Tools` > `Options` > `Beautifier`
|
||||
4. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save`
|
||||
5. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
|
||||
|
||||
### Building on MSVC with AddressSanitizer
|
||||
|
||||
|
@ -183,7 +197,7 @@ copy the file found in `<VisualStudio-installation-path>\VC\Tools\MSVC\<version>
|
|||
|
||||
To learn more about AddressSanitizer and MSVC, visit the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan).
|
||||
|
||||
### Building/Running in CLion
|
||||
### Developing in CLion
|
||||
|
||||
_Note:_ We're using `build` instead of the CLion default `cmake-build-debug` folder.
|
||||
|
||||
|
@ -196,7 +210,7 @@ Clone the repository as described in the readme. Open a terminal in the cloned f
|
|||
|
||||
Now open the project in CLion. You will be greeted with the _Open Project Wizard_. Set the _CMake Options_ to
|
||||
|
||||
```
|
||||
```text
|
||||
-DCMAKE_PREFIX_PATH=C:\Qt\6.5.3\msvc2019_64\lib\cmake\Qt6
|
||||
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
|
||||
```
|
||||
|
@ -227,9 +241,9 @@ Select the `CMake Applications > chatterino` configuration and add a new _Run Ex
|
|||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Screenshot of chatterino configuration</summary>
|
||||
<summary>Screenshot of Chatterino configuration</summary>
|
||||
|
||||
![Screenshot of chatterino configuration](https://user-images.githubusercontent.com/41973452/160240843-dc0c603c-227f-4f56-98ca-57f03989dfb4.png)
|
||||
![Screenshot of Chatterino configuration](https://user-images.githubusercontent.com/41973452/160240843-dc0c603c-227f-4f56-98ca-57f03989dfb4.png)
|
||||
|
||||
</details>
|
||||
|
||||
|
@ -240,26 +254,24 @@ write `portable` into it.
|
|||
|
||||
#### Debugging
|
||||
|
||||
To visualize QT types like `QString`, you need to inform CLion and LLDB
|
||||
To visualize Qt types like `QString`, you need to inform CLion and LLDB
|
||||
about these types.
|
||||
|
||||
1. Set `Enable NatVis renderers for LLDB option`
|
||||
in `Settings | Build, Execution, Deployment | Debugger | Data Views | C/C++` (should be enabled by default).
|
||||
2. Use the official NatVis file for QT from [`qt-labs/vstools`](https://github.com/qt-labs/vstools) by saving them to
|
||||
2. Use the official NatVis file for Qt from [`qt-labs/vstools`](https://github.com/qt-labs/vstools) by saving them to
|
||||
the project root using PowerShell:
|
||||
|
||||
<!--
|
||||
When switching to QT6 these need to be updated to qt6.natvis.xml.
|
||||
We need to do the replacement as the QT tools:
|
||||
https://github.com/qt-labs/vstools/blob/0769d945f8d0040917d654d9731e6b65951e102c/QtVsTools.Package/QtVsToolsPackage.cs#L390-L393
|
||||
We can't use Invoke-RestMethod here, because it will automatically convert the body to an xml document.
|
||||
-->
|
||||
|
||||
```powershell
|
||||
(irm "https://github.com/qt-labs/vstools/raw/dev/QtVsTools.Package/qt5.natvis.xml").Replace('##NAMESPACE##::', '') | Out-File qt5.natvis
|
||||
(iwr "https://github.com/qt-labs/vstools/raw/dev/QtVsTools.Package/qt6.natvis.xml").Content.Replace('##NAMESPACE##::', '') | Out-File qt6.natvis
|
||||
# [OR] using the permalink
|
||||
(irm "https://github.com/qt-labs/vstools/raw/0769d945f8d0040917d654d9731e6b65951e102c/QtVsTools.Package/qt5.natvis.xml").Replace('##NAMESPACE##::', '') | Out-File qt5.natvis
|
||||
(iwr "https://github.com/qt-labs/vstools/raw/1c8ba533bd88d935be3724667e0087fd0796102c/QtVsTools.Package/qt6.natvis.xml").Content.Replace('##NAMESPACE##::', '') | Out-File qt6.natvis
|
||||
```
|
||||
|
||||
Now you can debug the application and see QT types rendered correctly.
|
||||
Now you can debug the application and see Qt types rendered correctly.
|
||||
If this didn't work for you, try following
|
||||
the [tutorial from JetBrains](https://www.jetbrains.com/help/clion/qt-tutorial.html#debug-renderers).
|
||||
|
|
|
@ -8,12 +8,16 @@ This will require more than 30GB of free space on your hard drive.
|
|||
1. Install [CMake](https://cmake.org/)
|
||||
1. Install [git](https://git-scm.com/)
|
||||
1. Install [vcpkg](https://vcpkg.io/)
|
||||
- `git clone https://github.com/Microsoft/vcpkg.git`
|
||||
- `cd .\vcpkg\`
|
||||
- `.\bootstrap-vcpkg.bat`
|
||||
- `.\vcpkg integrate install`
|
||||
- `.\vcpkg integrate powershell`
|
||||
- `cd ..`
|
||||
|
||||
```shell
|
||||
git clone https://github.com/Microsoft/vcpkg.git
|
||||
cd vcpkg
|
||||
.\bootstrap-vcpkg.bat
|
||||
.\vcpkg integrate install
|
||||
.\vcpkg integrate powershell
|
||||
cd ..
|
||||
```
|
||||
|
||||
1. Configure the environment variables for vcpkg.
|
||||
Check [this document](https://gist.github.com/mitchmindtree/92c8e37fa80c8dddee5b94fc88d1288b#setting-an-environment-variable-on-windows) for more information for how to set environment variables on Windows.
|
||||
- Ensure your dependencies are built as 64-bit
|
||||
|
@ -31,15 +35,19 @@ This will require more than 30GB of free space on your hard drive.
|
|||
## Building
|
||||
|
||||
1. Clone
|
||||
- `git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git`
|
||||
```shell
|
||||
git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git
|
||||
```
|
||||
1. Install dependencies
|
||||
- `cd .\chatterino2\`
|
||||
- `vcpkg install`
|
||||
```powershell
|
||||
cd .\chatterino2\
|
||||
vcpkg install
|
||||
```
|
||||
1. Build
|
||||
- `mkdir .\build\`
|
||||
- `cd .\build\`
|
||||
- (cmd) `cmake .. -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake`
|
||||
- (ps1) `cmake .. -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"`
|
||||
- `cmake --build . --parallel <threads> --config Release`
|
||||
1. Run
|
||||
- `.\bin\chatterino2.exe`
|
||||
```powershell
|
||||
cmake -B build -DCMAKE_TOOLCHAIN_FILE="$Env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||
cd build
|
||||
cmake --build . --parallel <threads> --config Release
|
||||
```
|
||||
When using CMD, use `-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` to specify the toolchain.
|
||||
1. Run `.\bin\chatterino2.exe`
|
||||
|
|
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -3,6 +3,8 @@
|
|||
## Unversioned
|
||||
|
||||
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
|
||||
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
|
||||
- Major: Show restricted chat messages and suspicious treatment updates. (#5056)
|
||||
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
|
||||
- Minor: The account switcher is now styled to match your theme. (#4817)
|
||||
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
|
||||
|
@ -13,6 +15,11 @@
|
|||
- Minor: The `/reply` command now replies to the latest message of the user. (#4919)
|
||||
- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978)
|
||||
- Minor: Add an option to use new experimental smarter emote completion. (#4987)
|
||||
- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
|
||||
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
|
||||
- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047)
|
||||
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)
|
||||
- Minor: The whisper highlight color can now be configured through the settings. (#5053)
|
||||
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
|
||||
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
|
||||
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
|
||||
|
@ -43,13 +50,24 @@
|
|||
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965)
|
||||
- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971)
|
||||
- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971)
|
||||
- Bugfix: Fixed an issue on macOS where the image uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011)
|
||||
- Bugfix: Hide the Usercard button in the User Info Popup when in special channels. (#4972)
|
||||
- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994)
|
||||
- Bugfix: Fixed some windows appearing between screens. (#4797)
|
||||
- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051)
|
||||
- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051)
|
||||
- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040)
|
||||
- Bugfix: Fixes to section deletion in text input fields. (#5013)
|
||||
- Bugfix: Show user text input within watch streak notices. (#5029)
|
||||
- Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052)
|
||||
- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056)
|
||||
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
|
||||
- Dev: Change clang-format from v14 to v16. (#4929)
|
||||
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
|
||||
- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767)
|
||||
- Dev: Tests now run on Ubuntu 22.04 instead of 20.04 to loosen C++ restrictions in tests. (#4774)
|
||||
- Dev: Do a pretty major refactor of the Settings classes. List settings (e.g. highlights) are most heavily modified, and should have an extra eye kept on them. (#4775)
|
||||
- Dev: conan: Update Boost to 1.83 & OpenSSL to 3.2.0. (#5007)
|
||||
- Dev: Remove `boost::noncopyable` use & `boost_random` dependency. (#4776)
|
||||
- Dev: Fix clang-tidy `cppcoreguidelines-pro-type-member-init` warnings. (#4426)
|
||||
- Dev: Immediate layout for invisible `ChannelView`s is skipped. (#4811)
|
||||
|
@ -69,16 +87,27 @@
|
|||
- Dev: Refactor `Emoji`'s EmojiMap into a vector. (#4980)
|
||||
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
|
||||
- Dev: Refactor `common/Credentials`. (#4979)
|
||||
- Dev: Refactor chat logger. (#5058)
|
||||
- Dev: Changed lifetime of context menus. (#4924)
|
||||
- Dev: Renamed `tools` directory to `scripts`. (#5035)
|
||||
- Dev: Refactor `ChannelView`, removing a bunch of clang-tidy warnings. (#4926)
|
||||
- Dev: Refactor `IrcMessageHandler`, removing a bunch of clang-tidy warnings & changing its public API. (#4927)
|
||||
- Dev: `Details` file properties tab is now populated on Windows. (#4912)
|
||||
- Dev: Removed `Outcome` from network requests. (#4959)
|
||||
- Dev: Added Tests for Windows and MacOS in CI. (#4970)
|
||||
- Dev: Added Tests for Windows and MacOS in CI. (#4970, #5032)
|
||||
- Dev: Move `clang-tidy` checker to its own CI job. (#4996)
|
||||
- Dev: Refactored the Image Uploader feature. (#4971)
|
||||
- Dev: Fixed deadlock and use-after-free in tests. (#4981)
|
||||
- Dev: Cleanly exit Chatterino instead of force exiting. (#4993)
|
||||
- Dev: Moved all `.clang-format` files to the root directory. (#5037)
|
||||
- Dev: Load less message history upon reconnects. (#5001, #5018)
|
||||
- Dev: Load less message history upon reconnects. (#5001)
|
||||
- Dev: BREAKING: Replace custom `import()` with normal Lua `require()`. (#5014)
|
||||
- Dev: Fixed most compiler warnings. (#5028)
|
||||
- Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747)
|
||||
- Dev: Refactor Args to be less of a singleton. (#5041)
|
||||
- Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045)
|
||||
- Dev: Autogenerate docs/plugin-meta.lua. (#5055)
|
||||
|
||||
## 2.4.6
|
||||
|
||||
|
|
|
@ -210,7 +210,7 @@ if (CHATTERINO_PLUGINS)
|
|||
endif()
|
||||
|
||||
if (BUILD_WITH_CRASHPAD)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler")
|
||||
endif()
|
||||
|
||||
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
|
||||
|
|
|
@ -35,7 +35,7 @@ int compare(const QString &a, const QString &b);
|
|||
|
||||
```cpp
|
||||
/*
|
||||
* Matches a link and returns boost::none if it failed and a
|
||||
* Matches a link and returns std::nullopt if it failed and a
|
||||
* QRegularExpressionMatch on success.
|
||||
* ^^^ This comment just repeats the function signature!!!
|
||||
*
|
||||
|
|
37
README.md
37
README.md
|
@ -22,34 +22,29 @@ If you still receive an error about `MSVCR120.dll missing`, then you should inst
|
|||
|
||||
To get source code with required submodules run:
|
||||
|
||||
```
|
||||
```shell
|
||||
git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
```shell
|
||||
git clone https://github.com/Chatterino/chatterino2.git
|
||||
cd chatterino2
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
[Building on Windows](../master/BUILDING_ON_WINDOWS.md)
|
||||
|
||||
[Building on Windows with vcpkg](../master/BUILDING_ON_WINDOWS_WITH_VCPKG.md)
|
||||
|
||||
[Building on Linux](../master/BUILDING_ON_LINUX.md)
|
||||
|
||||
[Building on Mac](../master/BUILDING_ON_MAC.md)
|
||||
|
||||
[Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
|
||||
- [Building on Windows](../master/BUILDING_ON_WINDOWS.md)
|
||||
- [Building on Windows with vcpkg](../master/BUILDING_ON_WINDOWS_WITH_VCPKG.md)
|
||||
- [Building on Linux](../master/BUILDING_ON_LINUX.md)
|
||||
- [Building on macOS](../master/BUILDING_ON_MAC.md)
|
||||
- [Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
|
||||
|
||||
## Git blame
|
||||
|
||||
This project has big commits in the history which for example update all line
|
||||
endings. To improve the output of git-blame, consider setting:
|
||||
This project has big commits in the history which touch most files while only doing stylistic changes. To improve the output of git-blame, consider setting:
|
||||
|
||||
```
|
||||
```shell
|
||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
```
|
||||
|
||||
|
@ -58,19 +53,9 @@ file](./.git-blame-ignore-revs). GitHub does this by default.
|
|||
|
||||
## Code style
|
||||
|
||||
The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format.
|
||||
The code is formatted using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). Our configuration is found in the [.clang-format](.clang-format) file in the repository root directory.
|
||||
|
||||
### Get it automated with QT Creator + Beautifier + Clang Format
|
||||
|
||||
1. Download LLVM: https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.6/LLVM-16.0.6-win64.exe
|
||||
2. During the installation, make sure to add it to your path
|
||||
3. In QT Creator, select `Help` > `About Plugins` > `C++` > `Beautifier` to enable the plugin
|
||||
4. Restart QT Creator
|
||||
5. Select `Tools` > `Options` > `Beautifier`
|
||||
6. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save`
|
||||
7. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
|
||||
|
||||
Qt creator should now format the documents when saving it.
|
||||
For more contribution guidelines, take a look at [the wiki](https://wiki.chatterino.com/Contributing%20for%20Developers/).
|
||||
|
||||
## Doxygen
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from os import path
|
|||
|
||||
class Chatterino(ConanFile):
|
||||
name = "Chatterino"
|
||||
requires = "boost/1.81.0"
|
||||
requires = "boost/1.83.0"
|
||||
settings = "os", "compiler", "build_type", "arch"
|
||||
default_options = {
|
||||
"with_benchmark": False,
|
||||
|
@ -24,7 +24,7 @@ class Chatterino(ConanFile):
|
|||
self.requires("benchmark/1.7.1")
|
||||
|
||||
if self.options.get_safe("with_openssl3", False):
|
||||
self.requires("openssl/3.1.0")
|
||||
self.requires("openssl/3.2.0")
|
||||
else:
|
||||
self.requires("openssl/1.1.1t")
|
||||
|
||||
|
|
21
docs/chatterino.d.ts
vendored
21
docs/chatterino.d.ts
vendored
|
@ -19,4 +19,25 @@ declare module c2 {
|
|||
): boolean;
|
||||
function send_msg(channel: String, text: String): boolean;
|
||||
function system_msg(channel: String, text: String): boolean;
|
||||
|
||||
class CompletionList {
|
||||
values: String[];
|
||||
hide_others: boolean;
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
CompletionRequested = "CompletionRequested",
|
||||
}
|
||||
|
||||
type CbFuncCompletionsRequested = (
|
||||
query: string,
|
||||
full_text_content: string,
|
||||
cursor_position: number,
|
||||
is_first_word: boolean
|
||||
) => CompletionList;
|
||||
type CbFunc<T> = T extends EventType.CompletionRequested
|
||||
? CbFuncCompletionsRequested
|
||||
: never;
|
||||
|
||||
function register_callback<T>(type: T, func: CbFunc<T>): void;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- [ ] Updated version code in `src/common/Version.hpp`
|
||||
- [ ] Updated version code in `CMakeLists.txt`
|
||||
This can only be "whole versions", so if you're releasing `2.4.0-beta` you'll need to condense it to `2.4.0`
|
||||
- [ ] Add a new release at the top of of the `releases` key in `resources/com.chatterino.chatterino.appdata.xml`
|
||||
- [ ] Add a new release at the top of the `releases` key in `resources/com.chatterino.chatterino.appdata.xml`
|
||||
This cannot use dash to denote a pre-release identifier, you have to use a tilde instead.
|
||||
|
||||
- [ ] Updated version code in `.CI/chatterino-installer.iss`
|
||||
|
|
|
@ -43,7 +43,8 @@
|
|||
"type": "string",
|
||||
"description": "A small description of your license.",
|
||||
"examples": ["MIT", "GPL-2.0-or-later"]
|
||||
}
|
||||
},
|
||||
"$schema": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "description", "authors", "version", "license"]
|
||||
}
|
||||
|
|
58
docs/plugin-meta.lua
Normal file
58
docs/plugin-meta.lua
Normal file
|
@ -0,0 +1,58 @@
|
|||
---@meta Chatterino2
|
||||
|
||||
-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script
|
||||
-- This file is intended to be used with LuaLS (https://luals.github.io/).
|
||||
-- Add the folder this file is in to "Lua.workspace.library".
|
||||
|
||||
c2 = {}
|
||||
|
||||
---@alias LogLevel integer
|
||||
---@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel }
|
||||
c2.LogLevel = {}
|
||||
|
||||
---@alias EventType integer
|
||||
---@type { CompletionRequested: EventType }
|
||||
c2.EventType = {}
|
||||
---@class CommandContext
|
||||
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
||||
---@field channel_name string The name of the channel the command was executed in.
|
||||
|
||||
---@class CompletionList
|
||||
---@field values string[] The completions
|
||||
---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
||||
|
||||
--- Registers a new command called `name` which when executed will call `handler`.
|
||||
---
|
||||
---@param name string The name of the command.
|
||||
---@param handler fun(ctx: CommandContext) The handler to be invoked when the command gets executed.
|
||||
---@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
|
||||
function c2.register_command(name, handler) end
|
||||
|
||||
--- Registers a callback to be invoked when completions for a term are requested.
|
||||
---
|
||||
---@param type "CompletionRequested"
|
||||
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
|
||||
function c2.register_callback(type, func) end
|
||||
|
||||
--- Sends a message to `channel` with the specified text. Also executes commands.
|
||||
---
|
||||
--- **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
||||
---
|
||||
---@param channel string The name of the Twitch channel
|
||||
---@param text string The text to be sent
|
||||
---@return boolean ok
|
||||
function c2.send_msg(channel, text) end
|
||||
|
||||
--- Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
|
||||
---
|
||||
---@param channel string
|
||||
---@param text string
|
||||
---@return boolean ok
|
||||
function c2.system_msg(channel, text) end
|
||||
|
||||
--- Writes a message to the Chatterino log.
|
||||
---
|
||||
---@param level LogLevel The desired level.
|
||||
---@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
||||
function c2.log(level, ...) end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Test and Benchmark
|
||||
|
||||
Chatterino includes a set of unit tests and benchmarks. These can be built using cmake by adding the `-DBUILD_TESTS=On` and `-DBUILD_BENCHMARKS=On` flags respectively.
|
||||
Chatterino includes a set of unit tests and benchmarks. These can be built using CMake by adding the `-DBUILD_TESTS=On` and `-DBUILD_BENCHMARKS=On` flags respectively.
|
||||
|
||||
## Adding your own test
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plug
|
|||
|
||||
If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io)
|
||||
to typecheck your plugins. There is a `chatterino.d.ts` file describing the API
|
||||
in this directory. However this has several drawbacks like harder debugging at
|
||||
in this directory. However, this has several drawbacks like harder debugging at
|
||||
runtime.
|
||||
|
||||
## API
|
||||
|
@ -113,6 +113,42 @@ Limitations/known issues:
|
|||
rebuilding the window content caused by reloading another plugin will solve this.
|
||||
- Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517).
|
||||
|
||||
#### `register_callback("CompletionRequested", handler)`
|
||||
|
||||
Registers a callback (`handler`) to process completions. The callback gets the following parameters:
|
||||
|
||||
- `query`: The queried word.
|
||||
- `full_text_content`: The whole input.
|
||||
- `cursor_position`: The position of the cursor in the input.
|
||||
- `is_first_word`: Flag whether `query` is the first word in the input.
|
||||
|
||||
Example:
|
||||
|
||||
| Input | `query` | `full_text_content` | `cursor_position` | `is_first_word` |
|
||||
| ---------- | ------- | ------------------- | ----------------- | --------------- |
|
||||
| `foo│` | `foo` | `foo` | 3 | `true` |
|
||||
| `fo│o` | `fo` | `foo` | 2 | `true` |
|
||||
| `foo bar│` | `bar` | `foo bar` | 7 | `false` |
|
||||
| `foo │bar` | `foo` | `foo bar` | 4 | `false` |
|
||||
|
||||
```lua
|
||||
function string.startswith(s, other)
|
||||
return string.sub(s, 1, string.len(other)) == other
|
||||
end
|
||||
|
||||
c2.register_callback(
|
||||
"CompletionRequested",
|
||||
function(query, full_text_content, cursor_position, is_first_word)
|
||||
if ("!join"):startswith(query) then
|
||||
---@type CompletionList
|
||||
return { hide_others = true, values = { "!join" } }
|
||||
end
|
||||
---@type CompletionList
|
||||
return { hide_others = false, values = {} }
|
||||
end
|
||||
)
|
||||
```
|
||||
|
||||
#### `send_msg(channel, text)`
|
||||
|
||||
Sends a message to `channel` with the specified text. Also executes commands.
|
||||
|
@ -158,18 +194,25 @@ 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)
|
||||
|
||||
#### `import(filename)`
|
||||
#### `require(modname)`
|
||||
|
||||
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.
|
||||
This is Lua's [`require()`](https://www.lua.org/manual/5.3/manual.html#pdf-require) function.
|
||||
However, the searcher and load configuration is notably different from the default:
|
||||
|
||||
- Lua's built-in dynamic library searcher is removed,
|
||||
- `package.path` is not used, in its place are two searchers,
|
||||
- when `require()` is used, first a file relative to the currently executing
|
||||
file will be checked, then a file relative to the plugin directory,
|
||||
- binary chunks are never loaded
|
||||
|
||||
As in normal Lua, dots are converted to the path separators (`'/'` on Linux and Mac, `'\'` on Windows).
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
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
|
||||
require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua
|
||||
require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua
|
||||
require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file
|
||||
```
|
||||
|
||||
#### `print(Args...)`
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Third party libraries are stored here
|
||||
Third party libraries are stored here.
|
||||
|
||||
Fetched via `git submodule update --init --recursive`
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3182e3be21a8a753f9f269f0a590370d49c8f3cf
|
|
@ -1,52 +0,0 @@
|
|||
Language: Cpp
|
||||
|
||||
AccessModifierOffset: -4
|
||||
AlignEscapedNewlinesLeft: true
|
||||
AllowShortFunctionsOnASingleLine: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: false
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
BasedOnStyle: Google
|
||||
BraceWrapping:
|
||||
AfterClass: "true"
|
||||
AfterControlStatement: "true"
|
||||
AfterFunction: "true"
|
||||
AfterNamespace: "false"
|
||||
BeforeCatch: "true"
|
||||
BeforeElse: "true"
|
||||
BreakBeforeBraces: Custom
|
||||
BreakConstructorInitializersBeforeComma: true
|
||||
ColumnLimit: 80
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
DerivePointerBinding: false
|
||||
FixNamespaceComments: true
|
||||
IndentCaseLabels: true
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: true
|
||||
IndentPPDirectives: AfterHash
|
||||
SortIncludes: CaseInsensitive
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
# Project includes
|
||||
- Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$'
|
||||
Priority: 1
|
||||
# Qt includes
|
||||
- Regex: '^<Q[a-zA-Z0-9\._\/-]+>$'
|
||||
Priority: 3
|
||||
CaseSensitive: true
|
||||
# LibCommuni includes
|
||||
- Regex: "^<Irc[a-zA-Z]+>$"
|
||||
Priority: 3
|
||||
# Standard library includes
|
||||
- Regex: "^<[a-zA-Z_]+>$"
|
||||
Priority: 4
|
||||
# Third party library includes
|
||||
- Regex: "^<([a-zA-Z_0-9-]+/)*[a-zA-Z_0-9-]+.h(pp)?>$"
|
||||
Priority: 3
|
||||
NamespaceIndentation: Inner
|
||||
PointerBindsToType: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
Standard: Auto
|
||||
ReflowComments: false
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Args.hpp"
|
||||
|
||||
namespace chatterino::mock {
|
||||
|
||||
|
@ -9,6 +10,11 @@ class EmptyApplication : public IApplication
|
|||
public:
|
||||
virtual ~EmptyApplication() = default;
|
||||
|
||||
const Args &getArgs() override
|
||||
{
|
||||
return this->args_;
|
||||
}
|
||||
|
||||
Theme *getThemes() override
|
||||
{
|
||||
return nullptr;
|
||||
|
@ -44,6 +50,11 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
CrashHandler *getCrashHandler() override
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CommandController *getCommands() override
|
||||
{
|
||||
return nullptr;
|
||||
|
@ -64,6 +75,12 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
Logging *getChatLogger() override
|
||||
{
|
||||
assert(!"getChatLogger was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ChatterinoBadges *getChatterinoBadges() override
|
||||
{
|
||||
return nullptr;
|
||||
|
@ -105,6 +122,9 @@ public:
|
|||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Args args_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::mock
|
||||
|
|
BIN
resources/avatars/crazysmc.png
Normal file
BIN
resources/avatars/crazysmc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -65,6 +65,8 @@ ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Co
|
|||
olafyang | https://github.com/olafyang | | Contributor
|
||||
chrrs | https://github.com/chrrs | | Contributor
|
||||
4rneee | https://github.com/4rneee | | Contributor
|
||||
crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor
|
||||
SputNikPlop | https://github.com/SputNikPlop | | Contributor
|
||||
|
||||
# If you are a contributor add yourself above this line
|
||||
|
||||
|
|
3
scripts/README.md
Normal file
3
scripts/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# scripts
|
||||
|
||||
This directory contains scripts that may be useful for a contributor to run while working on Chatterino
|
|
@ -11,7 +11,7 @@ while read -r file; do
|
|||
echo "$file differs!!!!!!!"
|
||||
fail="1"
|
||||
fi
|
||||
done < <(find src/ -type f \( -iname "*.hpp" -o -iname "*.cpp" \))
|
||||
done < <(find src/ tests/src benchmarks/src mocks/include -type f \( -iname "*.hpp" -o -iname "*.cpp" \))
|
||||
|
||||
if [ "$fail" = "1" ]; then
|
||||
echo "At least one file is poorly formatted - check the output above"
|
0
tools/get-tlds-update.sh → scripts/get-tlds-update.sh
Normal file → Executable file
0
tools/get-tlds-update.sh → scripts/get-tlds-update.sh
Normal file → Executable file
142
scripts/make_luals_meta.py
Normal file
142
scripts/make_luals_meta.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
"""
|
||||
This script generates docs/plugin-meta.lua. It accepts no arguments
|
||||
|
||||
It assumes comments look like:
|
||||
/**
|
||||
* Thing
|
||||
*
|
||||
* @lua@param thing boolean
|
||||
* @lua@returns boolean
|
||||
* @exposed name
|
||||
*/
|
||||
- Do not have any useful info on '/**' and '*/' lines.
|
||||
- Class members are not allowed to have non-@command lines and commands different from @lua@field
|
||||
|
||||
Valid commands are:
|
||||
1. @exposeenum [dotted.name.in_lua.last_part]
|
||||
Define a table with keys of the enum. Values behind those keys aren't
|
||||
written on purpose.
|
||||
This generates three lines:
|
||||
- An type alias of [last_part] to integer,
|
||||
- A type description that describes available values of the enum,
|
||||
- A global table definition for the num
|
||||
2. @lua[@command]
|
||||
Writes [@command] to the file as a comment, usually this is @class, @param, @return, ...
|
||||
@lua@class and @lua@field have special treatment when it comes to generation of spacing new lines
|
||||
3. @exposed [c2.name]
|
||||
Generates a function definition line from the last `@lua@param`s.
|
||||
|
||||
Non-command lines of comments are written with a space after '---'
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
BOILERPLATE = """
|
||||
---@meta Chatterino2
|
||||
|
||||
-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script
|
||||
-- This file is intended to be used with LuaLS (https://luals.github.io/).
|
||||
-- Add the folder this file is in to "Lua.workspace.library".
|
||||
|
||||
c2 = {}
|
||||
"""
|
||||
|
||||
repo_root = Path(__file__).parent.parent
|
||||
lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp"
|
||||
lua_meta = repo_root / "docs" / "plugin-meta.lua"
|
||||
|
||||
print("Reading from", lua_api_file.relative_to(repo_root))
|
||||
print("Writing to", lua_meta.relative_to(repo_root))
|
||||
with lua_api_file.open("r") as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
# Are we in a doc comment?
|
||||
comment: bool = False
|
||||
|
||||
# Last `@lua@param`s seen - for @exposed generation
|
||||
last_params_names: list[str] = []
|
||||
# Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier
|
||||
is_class = False
|
||||
|
||||
# The name of the next enum in lua world
|
||||
expose_next_enum_as: str | None = None
|
||||
# Name of the current enum in c++ world, used to generate internal typenames for
|
||||
current_enum_name: str | None = None
|
||||
|
||||
with lua_meta.open("w") as out:
|
||||
out.write(BOILERPLATE[1:]) # skip the newline after triple quote
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("enum class "):
|
||||
line = line.removeprefix("enum class ")
|
||||
temp = line.split(" ", 2)
|
||||
current_enum_name = temp[0]
|
||||
if not expose_next_enum_as:
|
||||
print(
|
||||
f"Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
|
||||
)
|
||||
current_enum_name = None
|
||||
continue
|
||||
current_enum_name = expose_next_enum_as.split(".", 1)[-1]
|
||||
out.write("---@alias " + current_enum_name + " integer\n")
|
||||
out.write("---@type { ")
|
||||
# temp[1] is '{'
|
||||
if len(temp) == 2: # no values on this line
|
||||
continue
|
||||
line = temp[2]
|
||||
|
||||
if current_enum_name is not None:
|
||||
for i, tok in enumerate(line.split(" ")):
|
||||
if tok == "};":
|
||||
break
|
||||
entry = tok.removesuffix(",")
|
||||
if i != 0:
|
||||
out.write(", ")
|
||||
out.write(entry + ": " + current_enum_name)
|
||||
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
|
||||
print(f"Wrote enum {expose_next_enum_as} => {current_enum_name}")
|
||||
current_enum_name = None
|
||||
expose_next_enum_as = None
|
||||
continue
|
||||
|
||||
if line.startswith("/**"):
|
||||
comment = True
|
||||
continue
|
||||
elif "*/" in line:
|
||||
comment = False
|
||||
if not is_class:
|
||||
out.write("\n")
|
||||
continue
|
||||
if not comment:
|
||||
continue
|
||||
line = line.replace("*", "", 1).lstrip()
|
||||
if line == "":
|
||||
out.write("---\n")
|
||||
elif line.startswith("@exposeenum "):
|
||||
expose_next_enum_as = line.split(" ", 1)[1]
|
||||
elif line.startswith("@exposed "):
|
||||
exp = line.replace("@exposed ", "", 1)
|
||||
params = ", ".join(last_params_names)
|
||||
out.write(f"function {exp}({params}) end\n")
|
||||
print(f"Wrote function {exp}(...)")
|
||||
last_params_names = []
|
||||
elif line.startswith("@lua"):
|
||||
command = line.replace("@lua", "", 1)
|
||||
if command.startswith("@param"):
|
||||
last_params_names.append(command.split(" ", 2)[1])
|
||||
elif command.startswith("@class"):
|
||||
print(f"Writing {command}")
|
||||
if is_class:
|
||||
out.write("\n")
|
||||
is_class = True
|
||||
elif not command.startswith("@field"):
|
||||
is_class = False
|
||||
|
||||
out.write("---" + command + "\n")
|
||||
else:
|
||||
if is_class:
|
||||
is_class = False
|
||||
out.write("\n")
|
||||
|
||||
# note the space difference from the branch above
|
||||
out.write("--- " + line + "\n")
|
0
tools/windows-fix-directory-case-sensitivity.sh → scripts/windows-fix-directory-case-sensitivity.sh
Normal file → Executable file
0
tools/windows-fix-directory-case-sensitivity.sh → scripts/windows-fix-directory-case-sensitivity.sh
Normal file → Executable file
|
@ -1,52 +0,0 @@
|
|||
Language: Cpp
|
||||
|
||||
AccessModifierOffset: -4
|
||||
AlignEscapedNewlinesLeft: true
|
||||
AllowShortFunctionsOnASingleLine: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: false
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
BasedOnStyle: Google
|
||||
BraceWrapping:
|
||||
AfterClass: "true"
|
||||
AfterControlStatement: "true"
|
||||
AfterFunction: "true"
|
||||
AfterNamespace: "false"
|
||||
BeforeCatch: "true"
|
||||
BeforeElse: "true"
|
||||
BreakBeforeBraces: Custom
|
||||
BreakConstructorInitializersBeforeComma: true
|
||||
ColumnLimit: 80
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
DerivePointerBinding: false
|
||||
FixNamespaceComments: true
|
||||
IndentCaseLabels: true
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: true
|
||||
IndentPPDirectives: AfterHash
|
||||
SortIncludes: CaseInsensitive
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
# Project includes
|
||||
- Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$'
|
||||
Priority: 1
|
||||
# Qt includes
|
||||
- Regex: '^<Q[a-zA-Z0-9\._\/-]+>$'
|
||||
Priority: 3
|
||||
CaseSensitive: true
|
||||
# LibCommuni includes
|
||||
- Regex: "^<Irc[a-zA-Z]+>$"
|
||||
Priority: 3
|
||||
# Standard library includes
|
||||
- Regex: "^<[a-zA-Z_]+>$"
|
||||
Priority: 4
|
||||
# Third party library includes
|
||||
- Regex: "^<([a-zA-Z_0-9-]+/)*[a-zA-Z_0-9-]+.h(pp)?>$"
|
||||
Priority: 3
|
||||
NamespaceIndentation: Inner
|
||||
PointerBindsToType: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
Standard: Auto
|
||||
ReflowComments: false
|
|
@ -35,9 +35,11 @@
|
|||
#include "providers/twitch/PubSubActions.hpp"
|
||||
#include "providers/twitch/PubSubManager.hpp"
|
||||
#include "providers/twitch/PubSubMessages.hpp"
|
||||
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
#include "singletons/helper/LoggingChannel.hpp"
|
||||
|
@ -103,8 +105,9 @@ IApplication::IApplication()
|
|||
// It will create the instances of the major classes, and connect their signals
|
||||
// to each other
|
||||
|
||||
Application::Application(Settings &_settings, Paths &_paths)
|
||||
: themes(&this->emplace<Theme>())
|
||||
Application::Application(Settings &_settings, Paths &_paths, const Args &_args)
|
||||
: args_(_args)
|
||||
, themes(&this->emplace<Theme>())
|
||||
, fonts(&this->emplace<Fonts>())
|
||||
, emotes(&this->emplace<Emotes>())
|
||||
, accounts(&this->emplace<AccountController>())
|
||||
|
@ -114,6 +117,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
, toasts(&this->emplace<Toasts>())
|
||||
, imageUploader(&this->emplace<ImageUploader>())
|
||||
, seventvAPI(&this->emplace<SeventvAPI>())
|
||||
, crashHandler(&this->emplace<CrashHandler>())
|
||||
|
||||
, commands(&this->emplace<CommandController>())
|
||||
, notifications(&this->emplace<NotificationController>())
|
||||
|
@ -124,12 +128,12 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
, userData(&this->emplace<UserDataController>())
|
||||
, sound(&this->emplace<ISoundController>(makeSoundController(_settings)))
|
||||
, twitchLiveController(&this->emplace<TwitchLiveController>())
|
||||
, logging(new Logging(_settings))
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
, plugins(&this->emplace<PluginController>())
|
||||
#endif
|
||||
, logging(&this->emplace<Logging>())
|
||||
{
|
||||
this->instance = this;
|
||||
Application::instance = this;
|
||||
|
||||
// We can safely ignore this signal's connection since the Application will always
|
||||
// be destroyed after fonts
|
||||
|
@ -138,13 +142,15 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
});
|
||||
}
|
||||
|
||||
Application::~Application() = default;
|
||||
|
||||
void Application::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
assert(isAppInitialized == false);
|
||||
isAppInitialized = true;
|
||||
|
||||
// Show changelog
|
||||
if (!getArgs().isFramelessEmbed &&
|
||||
if (!this->args_.isFramelessEmbed &&
|
||||
getSettings()->currentVersion.getValue() != "" &&
|
||||
getSettings()->currentVersion.getValue() != CHATTERINO_VERSION)
|
||||
{
|
||||
|
@ -159,7 +165,7 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
}
|
||||
}
|
||||
|
||||
if (!getArgs().isFramelessEmbed)
|
||||
if (!this->args_.isFramelessEmbed)
|
||||
{
|
||||
getSettings()->currentVersion.setValue(CHATTERINO_VERSION);
|
||||
|
||||
|
@ -174,8 +180,10 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
singleton->initialize(settings, paths);
|
||||
}
|
||||
|
||||
// add crash message
|
||||
if (!getArgs().isFramelessEmbed && getArgs().crashRecovery)
|
||||
// Show crash message.
|
||||
// On Windows, the crash message was already shown.
|
||||
#ifndef Q_OS_WIN
|
||||
if (!this->args_.isFramelessEmbed && this->args_.crashRecovery)
|
||||
{
|
||||
if (auto selected =
|
||||
this->windows->getMainWindow().getNotebook().getSelectedPage())
|
||||
|
@ -195,10 +203,11 @@ void Application::initialize(Settings &settings, Paths &paths)
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->windows->updateWordTypeMask();
|
||||
|
||||
if (!getArgs().isFramelessEmbed)
|
||||
if (!this->args_.isFramelessEmbed)
|
||||
{
|
||||
this->initNm(paths);
|
||||
}
|
||||
|
@ -214,7 +223,7 @@ int Application::run(QApplication &qtApp)
|
|||
|
||||
this->twitch->connect();
|
||||
|
||||
if (!getArgs().isFramelessEmbed)
|
||||
if (!this->args_.isFramelessEmbed)
|
||||
{
|
||||
this->windows->getMainWindow().show();
|
||||
}
|
||||
|
@ -305,6 +314,11 @@ ITwitchIrcServer *Application::getTwitch()
|
|||
return this->twitch;
|
||||
}
|
||||
|
||||
Logging *Application::getChatLogger()
|
||||
{
|
||||
return this->logging.get();
|
||||
}
|
||||
|
||||
void Application::save()
|
||||
{
|
||||
for (auto &singleton : this->singletons_)
|
||||
|
@ -467,6 +481,87 @@ void Application::initPubSub()
|
|||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.suspiciousMessageReceived
|
||||
.connect([&](const auto &action) {
|
||||
if (action.treatment ==
|
||||
PubSubLowTrustUsersMessage::Treatment::INVALID)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Received suspicious message with unknown "
|
||||
"treatment:"
|
||||
<< action.treatmentString;
|
||||
return;
|
||||
}
|
||||
|
||||
// monitored chats are received over irc; in the future, we will use pubsub instead
|
||||
if (action.treatment !=
|
||||
PubSubLowTrustUsersMessage::Treatment::Restricted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSettings()->streamerModeHideModActions &&
|
||||
isInStreamerMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto chan =
|
||||
this->twitch->getChannelOrEmptyByID(action.channelID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeLowTrustUserMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.suspiciousTreatmentUpdated
|
||||
.connect([&](const auto &action) {
|
||||
if (action.treatment ==
|
||||
PubSubLowTrustUsersMessage::Treatment::INVALID)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Received suspicious user update with unknown "
|
||||
"treatment:"
|
||||
<< action.treatmentString;
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.updatedByUserLogin.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSettings()->streamerModeHideModActions &&
|
||||
isInStreamerMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto chan =
|
||||
this->twitch->getChannelOrEmptyByID(action.channelID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
auto msg =
|
||||
TwitchMessageBuilder::makeLowTrustUpdateMessage(action);
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
|
||||
[&](const auto &msg, const QString &channelID) {
|
||||
|
@ -546,9 +641,16 @@ void Application::initPubSub()
|
|||
msg.senderUserID, msg.senderUserLogin,
|
||||
senderDisplayName, senderColor};
|
||||
postToThread([chan, action] {
|
||||
const auto p = makeAutomodMessage(action);
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeAutomodMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
|
||||
getApp()->twitch->automodChannel->addMessage(
|
||||
p.first);
|
||||
getApp()->twitch->automodChannel->addMessage(
|
||||
p.second);
|
||||
});
|
||||
}
|
||||
// "ALLOWED" and "DENIED" statuses remain unimplemented
|
||||
|
@ -573,7 +675,8 @@ void Application::initPubSub()
|
|||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p = makeAutomodMessage(action);
|
||||
const auto p = TwitchMessageBuilder::makeAutomodMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
});
|
||||
|
@ -615,7 +718,8 @@ void Application::initPubSub()
|
|||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p = makeAutomodInfoMessage(action);
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeAutomodInfoMessage(action);
|
||||
chan->addMessage(p);
|
||||
});
|
||||
});
|
||||
|
@ -657,6 +761,7 @@ void Application::initPubSub()
|
|||
[this] {
|
||||
this->twitch->pubsub->unlistenAllModerationActions();
|
||||
this->twitch->pubsub->unlistenAutomod();
|
||||
this->twitch->pubsub->unlistenLowTrustUsers();
|
||||
this->twitch->pubsub->unlistenWhispers();
|
||||
},
|
||||
boost::signals2::at_front);
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
class Args;
|
||||
class TwitchIrcServer;
|
||||
class ITwitchIrcServer;
|
||||
class PubSub;
|
||||
|
@ -44,6 +45,7 @@ class FfzBadges;
|
|||
class SeventvBadges;
|
||||
class ImageUploader;
|
||||
class SeventvAPI;
|
||||
class CrashHandler;
|
||||
|
||||
class IApplication
|
||||
{
|
||||
|
@ -53,6 +55,7 @@ public:
|
|||
|
||||
static IApplication *instance;
|
||||
|
||||
virtual const Args &getArgs() = 0;
|
||||
virtual Theme *getThemes() = 0;
|
||||
virtual Fonts *getFonts() = 0;
|
||||
virtual IEmotes *getEmotes() = 0;
|
||||
|
@ -60,10 +63,12 @@ public:
|
|||
virtual HotkeyController *getHotkeys() = 0;
|
||||
virtual WindowManager *getWindows() = 0;
|
||||
virtual Toasts *getToasts() = 0;
|
||||
virtual CrashHandler *getCrashHandler() = 0;
|
||||
virtual CommandController *getCommands() = 0;
|
||||
virtual HighlightController *getHighlights() = 0;
|
||||
virtual NotificationController *getNotifications() = 0;
|
||||
virtual ITwitchIrcServer *getTwitch() = 0;
|
||||
virtual Logging *getChatLogger() = 0;
|
||||
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
||||
virtual FfzBadges *getFfzBadges() = 0;
|
||||
virtual SeventvBadges *getSeventvBadges() = 0;
|
||||
|
@ -76,6 +81,7 @@ public:
|
|||
|
||||
class Application : public IApplication
|
||||
{
|
||||
const Args &args_;
|
||||
std::vector<std::unique_ptr<Singleton>> singletons_;
|
||||
int argc_{};
|
||||
char **argv_{};
|
||||
|
@ -83,7 +89,13 @@ class Application : public IApplication
|
|||
public:
|
||||
static Application *instance;
|
||||
|
||||
Application(Settings &settings, Paths &paths);
|
||||
Application(Settings &_settings, Paths &_paths, const Args &_args);
|
||||
~Application() override;
|
||||
|
||||
Application(const Application &) = delete;
|
||||
Application(Application &&) = delete;
|
||||
Application &operator=(const Application &) = delete;
|
||||
Application &operator=(Application &&) = delete;
|
||||
|
||||
void initialize(Settings &settings, Paths &paths);
|
||||
void load();
|
||||
|
@ -103,6 +115,7 @@ public:
|
|||
Toasts *const toasts{};
|
||||
ImageUploader *const imageUploader{};
|
||||
SeventvAPI *const seventvAPI{};
|
||||
CrashHandler *const crashHandler{};
|
||||
|
||||
CommandController *const commands{};
|
||||
NotificationController *const notifications{};
|
||||
|
@ -115,14 +128,17 @@ public:
|
|||
|
||||
private:
|
||||
TwitchLiveController *const twitchLiveController{};
|
||||
const std::unique_ptr<Logging> logging;
|
||||
|
||||
public:
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginController *const plugins{};
|
||||
#endif
|
||||
|
||||
/*[[deprecated]]*/ Logging *const logging{};
|
||||
|
||||
const Args &getArgs() override
|
||||
{
|
||||
return this->args_;
|
||||
}
|
||||
Theme *getThemes() override
|
||||
{
|
||||
return this->themes;
|
||||
|
@ -148,6 +164,10 @@ public:
|
|||
{
|
||||
return this->toasts;
|
||||
}
|
||||
CrashHandler *getCrashHandler() override
|
||||
{
|
||||
return this->crashHandler;
|
||||
}
|
||||
CommandController *getCommands() override
|
||||
{
|
||||
return this->commands;
|
||||
|
@ -161,6 +181,7 @@ public:
|
|||
return this->highlights;
|
||||
}
|
||||
ITwitchIrcServer *getTwitch() override;
|
||||
Logging *getChatLogger() override;
|
||||
ChatterinoBadges *getChatterinoBadges() override
|
||||
{
|
||||
return this->chatterinoBadges;
|
||||
|
|
|
@ -289,8 +289,6 @@ 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
|
||||
|
@ -414,6 +412,8 @@ set(SOURCE_FILES
|
|||
providers/twitch/pubsubmessages/ChatModeratorAction.hpp
|
||||
providers/twitch/pubsubmessages/Listen.cpp
|
||||
providers/twitch/pubsubmessages/Listen.hpp
|
||||
providers/twitch/pubsubmessages/LowTrustUsers.cpp
|
||||
providers/twitch/pubsubmessages/LowTrustUsers.hpp
|
||||
providers/twitch/pubsubmessages/Message.hpp
|
||||
providers/twitch/pubsubmessages/Unlisten.cpp
|
||||
providers/twitch/pubsubmessages/Unlisten.hpp
|
||||
|
@ -425,6 +425,8 @@ set(SOURCE_FILES
|
|||
|
||||
singletons/Badges.cpp
|
||||
singletons/Badges.hpp
|
||||
singletons/CrashHandler.cpp
|
||||
singletons/CrashHandler.hpp
|
||||
singletons/Emotes.cpp
|
||||
singletons/Emotes.hpp
|
||||
singletons/Fonts.cpp
|
||||
|
@ -627,6 +629,8 @@ set(SOURCE_FILES
|
|||
widgets/helper/SignalLabel.hpp
|
||||
widgets/helper/TitlebarButton.cpp
|
||||
widgets/helper/TitlebarButton.hpp
|
||||
widgets/helper/TitlebarButtons.cpp
|
||||
widgets/helper/TitlebarButtons.hpp
|
||||
|
||||
widgets/listview/GenericItemDelegate.cpp
|
||||
widgets/listview/GenericItemDelegate.hpp
|
||||
|
@ -1006,7 +1010,6 @@ 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
|
||||
|
@ -1066,7 +1069,6 @@ else ()
|
|||
-Wno-switch
|
||||
-Wno-deprecated-declarations
|
||||
-Wno-sign-compare
|
||||
-Wno-unused-variable
|
||||
|
||||
# Disabling strict-aliasing warnings for now, although we probably want to re-enable this in the future
|
||||
-Wno-strict-aliasing
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "common/Modes.hpp"
|
||||
#include "common/NetworkManager.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "singletons/CrashHandler.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
@ -78,7 +79,7 @@ namespace {
|
|||
{
|
||||
// set up the QApplication flags
|
||||
QApplication::setAttribute(Qt::AA_Use96Dpi, true);
|
||||
#ifdef Q_OS_WIN32
|
||||
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true);
|
||||
#endif
|
||||
|
||||
|
@ -98,23 +99,12 @@ namespace {
|
|||
installCustomPalette();
|
||||
}
|
||||
|
||||
void showLastCrashDialog()
|
||||
void showLastCrashDialog(const Args &args)
|
||||
{
|
||||
//#ifndef C_DISABLE_CRASH_DIALOG
|
||||
// LastRunCrashDialog dialog;
|
||||
|
||||
// switch (dialog.exec())
|
||||
// {
|
||||
// case QDialog::Accepted:
|
||||
// {
|
||||
// };
|
||||
// break;
|
||||
// default:
|
||||
// {
|
||||
// _exit(0);
|
||||
// }
|
||||
// }
|
||||
//#endif
|
||||
auto *dialog = new LastRunCrashDialog(args);
|
||||
// Use exec() over open() to block the app from being loaded
|
||||
// and to be able to set the safe mode.
|
||||
dialog->exec();
|
||||
}
|
||||
|
||||
void createRunningFile(const QString &path)
|
||||
|
@ -132,14 +122,13 @@ namespace {
|
|||
}
|
||||
|
||||
std::chrono::steady_clock::time_point signalsInitTime;
|
||||
bool restartOnSignal = false;
|
||||
|
||||
[[noreturn]] void handleSignal(int signum)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
if (restartOnSignal &&
|
||||
std::chrono::steady_clock::now() - signalsInitTime > 30s)
|
||||
if (std::chrono::steady_clock::now() - signalsInitTime > 30s &&
|
||||
getIApp()->getCrashHandler()->shouldRecover())
|
||||
{
|
||||
QProcess proc;
|
||||
|
||||
|
@ -235,15 +224,18 @@ namespace {
|
|||
}
|
||||
} // namespace
|
||||
|
||||
void runGui(QApplication &a, Paths &paths, Settings &settings)
|
||||
void runGui(QApplication &a, Paths &paths, Settings &settings, const Args &args)
|
||||
{
|
||||
initQt();
|
||||
initResources();
|
||||
initSignalHandler();
|
||||
|
||||
settings.restartOnCrash.connect([](const bool &value) {
|
||||
restartOnSignal = value;
|
||||
});
|
||||
#ifdef Q_OS_WIN
|
||||
if (args.crashRecovery)
|
||||
{
|
||||
showLastCrashDialog(args);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto thread = std::thread([dir = paths.miscDirectory] {
|
||||
#ifdef Q_OS_WIN32
|
||||
|
@ -282,31 +274,12 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
|
|||
chatterino::NetworkManager::init();
|
||||
chatterino::Updates::instance().checkForUpdates();
|
||||
|
||||
#ifdef C_USE_BREAKPAD
|
||||
QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes");
|
||||
#endif
|
||||
|
||||
// Running file
|
||||
auto runningPath =
|
||||
paths.miscDirectory + "/running_" + paths.applicationFilePathHash;
|
||||
|
||||
if (QFile::exists(runningPath))
|
||||
{
|
||||
showLastCrashDialog();
|
||||
}
|
||||
else
|
||||
{
|
||||
createRunningFile(runningPath);
|
||||
}
|
||||
|
||||
Application app(settings, paths);
|
||||
Application app(settings, paths, args);
|
||||
app.initialize(settings, paths);
|
||||
app.run(a);
|
||||
app.save();
|
||||
|
||||
removeRunningFile(runningPath);
|
||||
|
||||
if (!getArgs().dontSaveSettings)
|
||||
if (!args.dontSaveSettings)
|
||||
{
|
||||
pajlada::Settings::SettingManager::gSave();
|
||||
}
|
||||
|
|
|
@ -3,8 +3,12 @@
|
|||
class QApplication;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Args;
|
||||
class Paths;
|
||||
class Settings;
|
||||
|
||||
void runGui(QApplication &a, Paths &paths, Settings &settings);
|
||||
void runGui(QApplication &a, Paths &paths, Settings &settings,
|
||||
const Args &args);
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "Args.hpp"
|
||||
#include "common/Args.hpp"
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/AttachToConsole.hpp"
|
||||
|
@ -14,6 +15,55 @@
|
|||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
namespace {
|
||||
|
||||
template <class... Args>
|
||||
QCommandLineOption hiddenOption(Args... args)
|
||||
{
|
||||
QCommandLineOption opt(args...);
|
||||
opt.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
return opt;
|
||||
}
|
||||
|
||||
QStringList extractCommandLine(
|
||||
const QCommandLineParser &parser,
|
||||
std::initializer_list<QCommandLineOption> options)
|
||||
{
|
||||
QStringList args;
|
||||
for (const auto &option : options)
|
||||
{
|
||||
if (parser.isSet(option))
|
||||
{
|
||||
auto optionName = option.names().first();
|
||||
if (optionName.length() == 1)
|
||||
{
|
||||
optionName.prepend(u'-');
|
||||
}
|
||||
else
|
||||
{
|
||||
optionName.prepend("--");
|
||||
}
|
||||
|
||||
auto values = parser.values(option);
|
||||
if (values.empty())
|
||||
{
|
||||
args += optionName;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto &value : values)
|
||||
{
|
||||
args += optionName;
|
||||
args += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
Args::Args(const QApplication &app)
|
||||
|
@ -23,35 +73,44 @@ Args::Args(const QApplication &app)
|
|||
parser.addHelpOption();
|
||||
|
||||
// Used internally by app to restart after unexpected crashes
|
||||
QCommandLineOption crashRecoveryOption("crash-recovery");
|
||||
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
auto crashRecoveryOption = hiddenOption("crash-recovery");
|
||||
auto exceptionCodeOption = hiddenOption("cr-exception-code", "", "code");
|
||||
auto exceptionMessageOption =
|
||||
hiddenOption("cr-exception-message", "", "message");
|
||||
|
||||
// Added to ignore the parent-window option passed during native messaging
|
||||
QCommandLineOption parentWindowOption("parent-window");
|
||||
parentWindowOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
QCommandLineOption parentWindowIdOption("x-attach-split-to-window", "",
|
||||
"window-id");
|
||||
parentWindowIdOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
auto parentWindowOption = hiddenOption("parent-window");
|
||||
auto parentWindowIdOption =
|
||||
hiddenOption("x-attach-split-to-window", "", "window-id");
|
||||
|
||||
// Verbose
|
||||
QCommandLineOption verboseOption({{"v", "verbose"},
|
||||
"Attaches to the Console on windows, "
|
||||
"allowing you to see debug output."});
|
||||
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
auto verboseOption = QCommandLineOption(
|
||||
QStringList{"v", "verbose"}, "Attaches to the Console on windows, "
|
||||
"allowing you to see debug output.");
|
||||
// Safe mode
|
||||
QCommandLineOption safeModeOption(
|
||||
"safe-mode", "Starts Chatterino without loading Plugins and always "
|
||||
"show the settings button.");
|
||||
|
||||
parser.addOptions({
|
||||
{{"V", "version"}, "Displays version information."},
|
||||
crashRecoveryOption,
|
||||
parentWindowOption,
|
||||
parentWindowIdOption,
|
||||
verboseOption,
|
||||
});
|
||||
parser.addOption(QCommandLineOption(
|
||||
// Channel layout
|
||||
auto channelLayout = QCommandLineOption(
|
||||
{"c", "channels"},
|
||||
"Joins only supplied channels on startup. Use letters with colons to "
|
||||
"specify platform. Only Twitch channels are supported at the moment.\n"
|
||||
"If platform isn't specified, default is Twitch.",
|
||||
"t:channel1;t:channel2;..."));
|
||||
"t:channel1;t:channel2;...");
|
||||
|
||||
parser.addOptions({
|
||||
{{"V", "version"}, "Displays version information."},
|
||||
crashRecoveryOption,
|
||||
exceptionCodeOption,
|
||||
exceptionMessageOption,
|
||||
parentWindowOption,
|
||||
parentWindowIdOption,
|
||||
verboseOption,
|
||||
safeModeOption,
|
||||
channelLayout,
|
||||
});
|
||||
|
||||
if (!parser.parse(app.arguments()))
|
||||
{
|
||||
|
@ -71,15 +130,25 @@ Args::Args(const QApplication &app)
|
|||
(args.size() > 0 && (args[0].startsWith("chrome-extension://") ||
|
||||
args[0].endsWith(".json")));
|
||||
|
||||
if (parser.isSet("c"))
|
||||
if (parser.isSet(channelLayout))
|
||||
{
|
||||
this->applyCustomChannelLayout(parser.value("c"));
|
||||
this->applyCustomChannelLayout(parser.value(channelLayout));
|
||||
}
|
||||
|
||||
this->verbose = parser.isSet(verboseOption);
|
||||
|
||||
this->printVersion = parser.isSet("V");
|
||||
this->crashRecovery = parser.isSet("crash-recovery");
|
||||
|
||||
this->crashRecovery = parser.isSet(crashRecoveryOption);
|
||||
if (parser.isSet(exceptionCodeOption))
|
||||
{
|
||||
this->exceptionCode =
|
||||
static_cast<uint32_t>(parser.value(exceptionCodeOption).toULong());
|
||||
}
|
||||
if (parser.isSet(exceptionMessageOption))
|
||||
{
|
||||
this->exceptionMessage = parser.value(exceptionMessageOption);
|
||||
}
|
||||
|
||||
if (parser.isSet(parentWindowIdOption))
|
||||
{
|
||||
|
@ -89,6 +158,21 @@ Args::Args(const QApplication &app)
|
|||
|
||||
this->parentWindowId = parser.value(parentWindowIdOption).toULongLong();
|
||||
}
|
||||
if (parser.isSet(safeModeOption))
|
||||
{
|
||||
this->safeMode = true;
|
||||
}
|
||||
|
||||
this->currentArguments_ = extractCommandLine(parser, {
|
||||
verboseOption,
|
||||
safeModeOption,
|
||||
channelLayout,
|
||||
});
|
||||
}
|
||||
|
||||
QStringList Args::currentArguments() const
|
||||
{
|
||||
return this->currentArguments_;
|
||||
}
|
||||
|
||||
void Args::applyCustomChannelLayout(const QString &argValue)
|
||||
|
@ -164,18 +248,4 @@ void Args::applyCustomChannelLayout(const QString &argValue)
|
|||
}
|
||||
}
|
||||
|
||||
static Args *instance = nullptr;
|
||||
|
||||
void initArgs(const QApplication &app)
|
||||
{
|
||||
instance = new Args(app);
|
||||
}
|
||||
|
||||
const Args &getArgs()
|
||||
{
|
||||
assert(instance);
|
||||
|
||||
return *instance;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -9,13 +9,38 @@
|
|||
namespace chatterino {
|
||||
|
||||
/// Command line arguments passed to Chatterino.
|
||||
///
|
||||
/// All accepted arguments:
|
||||
///
|
||||
/// Crash recovery:
|
||||
/// --crash-recovery
|
||||
/// --cr-exception-code code
|
||||
/// --cr-exception-message message
|
||||
///
|
||||
/// Native messaging:
|
||||
/// --parent-window
|
||||
/// --x-attach-split-to-window=window-id
|
||||
///
|
||||
/// -v, --verbose
|
||||
/// -V, --version
|
||||
/// -c, --channels=t:channel1;t:channel2;...
|
||||
/// --safe-mode
|
||||
///
|
||||
/// See documentation on `QGuiApplication` for documentation on Qt arguments like -platform.
|
||||
class Args
|
||||
{
|
||||
public:
|
||||
Args() = default;
|
||||
Args(const QApplication &app);
|
||||
|
||||
bool printVersion{};
|
||||
|
||||
bool crashRecovery{};
|
||||
/// Native, platform-specific exception code from crashpad
|
||||
std::optional<uint32_t> exceptionCode{};
|
||||
/// Text version of the exception code. Potentially contains more context.
|
||||
std::optional<QString> exceptionMessage{};
|
||||
|
||||
bool shouldRunBrowserExtensionHost{};
|
||||
// Shows a single chat. Used on windows to embed in another application.
|
||||
bool isFramelessEmbed{};
|
||||
|
@ -26,12 +51,14 @@ public:
|
|||
bool dontLoadMainWindow{};
|
||||
std::optional<WindowLayout> customChannelLayout;
|
||||
bool verbose{};
|
||||
bool safeMode{};
|
||||
|
||||
QStringList currentArguments() const;
|
||||
|
||||
private:
|
||||
void applyCustomChannelLayout(const QString &argValue);
|
||||
|
||||
QStringList currentArguments_;
|
||||
};
|
||||
|
||||
void initArgs(const QApplication &app);
|
||||
const Args &getArgs();
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -101,7 +101,8 @@ void Channel::addMessage(MessagePtr message,
|
|||
{
|
||||
channelPlatform = "twitch";
|
||||
}
|
||||
app->logging->addMessage(this->name_, message, channelPlatform);
|
||||
getIApp()->getChatLogger()->addMessage(this->name_, message,
|
||||
channelPlatform);
|
||||
}
|
||||
|
||||
if (this->messages_.pushBack(message, deleted))
|
||||
|
@ -295,7 +296,8 @@ bool Channel::isWritable() const
|
|||
{
|
||||
using Type = Channel::Type;
|
||||
auto type = this->getType();
|
||||
return type != Type::TwitchMentions && type != Type::TwitchLive;
|
||||
return type != Type::TwitchMentions && type != Type::TwitchLive &&
|
||||
type != Type::TwitchAutomod;
|
||||
}
|
||||
|
||||
void Channel::sendMessage(const QString &message)
|
||||
|
@ -314,7 +316,6 @@ bool Channel::isBroadcaster() const
|
|||
|
||||
bool Channel::hasModRights() const
|
||||
{
|
||||
// fourtf: check if staff
|
||||
return this->isMod() || this->isBroadcaster();
|
||||
}
|
||||
|
||||
|
@ -330,7 +331,8 @@ bool Channel::isLive() const
|
|||
|
||||
bool Channel::shouldIgnoreHighlights() const
|
||||
{
|
||||
return this->type_ == Type::TwitchMentions ||
|
||||
return this->type_ == Type::TwitchAutomod ||
|
||||
this->type_ == Type::TwitchMentions ||
|
||||
this->type_ == Type::TwitchWhispers;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ public:
|
|||
TwitchWatching,
|
||||
TwitchMentions,
|
||||
TwitchLive,
|
||||
TwitchAutomod,
|
||||
TwitchEnd,
|
||||
Irc,
|
||||
Misc
|
||||
|
@ -60,8 +61,6 @@ public:
|
|||
pajlada::Signals::Signal<const std::vector<MessagePtr> &> filledInMessages;
|
||||
pajlada::Signals::NoArgSignal destroyed;
|
||||
pajlada::Signals::NoArgSignal displayNameChanged;
|
||||
/// Invoked when AbstractIrcServer::onReadConnected occurs
|
||||
pajlada::Signals::NoArgSignal connected;
|
||||
|
||||
Type getType() const;
|
||||
const QString &getName() const;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/QLogging.hpp"
|
||||
#include "util/TypeName.hpp"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QVariant>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -10,16 +11,8 @@ namespace chatterino {
|
|||
namespace {
|
||||
|
||||
template <typename T>
|
||||
void warn(const char *envName, T defaultValue)
|
||||
void warn(const char *envName, const QString &envString, T defaultValue)
|
||||
{
|
||||
auto *envString = std::getenv(envName);
|
||||
if (!envString)
|
||||
{
|
||||
// This function is not supposed to be used for non-existant
|
||||
// environment variables.
|
||||
return;
|
||||
}
|
||||
|
||||
const auto typeName = QString::fromStdString(
|
||||
std::string(type_name<decltype(defaultValue)>()));
|
||||
|
||||
|
@ -33,23 +26,12 @@ namespace {
|
|||
.arg(defaultValue);
|
||||
}
|
||||
|
||||
QString readStringEnv(const char *envName, QString defaultValue)
|
||||
{
|
||||
auto envString = std::getenv(envName);
|
||||
if (envString != nullptr)
|
||||
{
|
||||
return QString(envString);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
std::optional<QString> readOptionalStringEnv(const char *envName)
|
||||
{
|
||||
auto envString = std::getenv(envName);
|
||||
if (envString != nullptr)
|
||||
auto envString = qEnvironmentVariable(envName);
|
||||
if (!envString.isEmpty())
|
||||
{
|
||||
return QString(envString);
|
||||
return envString;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
|
@ -57,30 +39,28 @@ namespace {
|
|||
|
||||
uint16_t readPortEnv(const char *envName, uint16_t defaultValue)
|
||||
{
|
||||
auto envString = std::getenv(envName);
|
||||
if (envString != nullptr)
|
||||
auto envString = qEnvironmentVariable(envName);
|
||||
if (!envString.isEmpty())
|
||||
{
|
||||
bool ok;
|
||||
auto val = QString(envString).toUShort(&ok);
|
||||
bool ok = false;
|
||||
auto val = envString.toUShort(&ok);
|
||||
if (ok)
|
||||
{
|
||||
return val;
|
||||
}
|
||||
else
|
||||
{
|
||||
warn(envName, defaultValue);
|
||||
}
|
||||
|
||||
warn(envName, envString, defaultValue);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
uint16_t readBoolEnv(const char *envName, bool defaultValue)
|
||||
bool readBoolEnv(const char *envName, bool defaultValue)
|
||||
{
|
||||
auto envString = std::getenv(envName);
|
||||
if (envString != nullptr)
|
||||
auto envString = qEnvironmentVariable(envName);
|
||||
if (!envString.isEmpty())
|
||||
{
|
||||
return QVariant(QString(envString)).toBool();
|
||||
return QVariant(envString).toBool();
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
|
@ -90,14 +70,14 @@ namespace {
|
|||
|
||||
Env::Env()
|
||||
: recentMessagesApiUrl(
|
||||
readStringEnv("CHATTERINO2_RECENT_MESSAGES_URL",
|
||||
"https://recent-messages.robotty.de/api/v2/"
|
||||
"recent-messages/%1"))
|
||||
, linkResolverUrl(readStringEnv(
|
||||
qEnvironmentVariable("CHATTERINO2_RECENT_MESSAGES_URL",
|
||||
"https://recent-messages.robotty.de/api/v2/"
|
||||
"recent-messages/%1"))
|
||||
, linkResolverUrl(qEnvironmentVariable(
|
||||
"CHATTERINO2_LINK_RESOLVER_URL",
|
||||
"https://braize.pajlada.com/chatterino/link_resolver/%1"))
|
||||
, twitchServerHost(
|
||||
readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv"))
|
||||
, twitchServerHost(qEnvironmentVariable("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"))
|
||||
|
|
|
@ -97,6 +97,11 @@ public:
|
|||
return !this->hasAny(flags);
|
||||
}
|
||||
|
||||
T value() const
|
||||
{
|
||||
return this->value_;
|
||||
}
|
||||
|
||||
private:
|
||||
T value_{};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,8 @@ Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold);
|
|||
Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoCrashhandler, "chatterino.crashhandler",
|
||||
logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoEmoji, "chatterino.emoji", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold);
|
||||
|
|
|
@ -8,6 +8,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoBenchmark);
|
|||
Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCache);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes);
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace chatterino {
|
|||
enum class WindowType;
|
||||
|
||||
struct SplitDescriptor {
|
||||
// Twitch or mentions or watching or whispers or IRC
|
||||
// Twitch or mentions or watching or live or automod or whispers or IRC
|
||||
QString type_;
|
||||
|
||||
// Twitch Channel name or IRC channel name
|
||||
|
|
|
@ -616,10 +616,8 @@ QString openUsercard(const CommandContext &ctx)
|
|||
"should be open.");
|
||||
}
|
||||
|
||||
auto *userPopup = new UserInfoPopup(
|
||||
getSettings()->autoCloseUserPopup,
|
||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())),
|
||||
currentSplit);
|
||||
auto *userPopup =
|
||||
new UserInfoPopup(getSettings()->autoCloseUserPopup, currentSplit);
|
||||
userPopup->setData(userName, channel);
|
||||
userPopup->moveTo(QCursor::pos(), widgets::BoundsChecking::CursorPosition);
|
||||
userPopup->show();
|
||||
|
|
|
@ -47,7 +47,7 @@ QString sendShoutout(const CommandContext &ctx)
|
|||
|
||||
getHelix()->getUserByName(
|
||||
target,
|
||||
[twitchChannel, channel, currentUser, &target](const auto targetUser) {
|
||||
[twitchChannel, channel, currentUser](const auto targetUser) {
|
||||
getHelix()->sendShoutout(
|
||||
twitchChannel->roomId(), targetUser.id,
|
||||
currentUser->getUserId(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "controllers/completion/TabCompletionModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "controllers/completion/sources/CommandSource.hpp"
|
||||
#include "controllers/completion/sources/EmoteSource.hpp"
|
||||
|
@ -9,6 +10,9 @@
|
|||
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
||||
#include "controllers/completion/strategies/CommandStrategy.hpp"
|
||||
#include "controllers/completion/strategies/SmartEmoteStrategy.hpp"
|
||||
#include "controllers/plugins/LuaUtilities.hpp"
|
||||
#include "controllers/plugins/Plugin.hpp"
|
||||
#include "controllers/plugins/PluginController.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -19,7 +23,9 @@ TabCompletionModel::TabCompletionModel(Channel &channel, QObject *parent)
|
|||
{
|
||||
}
|
||||
|
||||
void TabCompletionModel::updateResults(const QString &query, bool isFirstWord)
|
||||
void TabCompletionModel::updateResults(const QString &query,
|
||||
const QString &fullTextContent,
|
||||
int cursorPosition, bool isFirstWord)
|
||||
{
|
||||
this->updateSourceFromQuery(query);
|
||||
|
||||
|
@ -29,6 +35,17 @@ void TabCompletionModel::updateResults(const QString &query, bool isFirstWord)
|
|||
|
||||
// Copy results to this model
|
||||
QStringList results;
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
// Try plugins first
|
||||
bool done{};
|
||||
std::tie(done, results) = getApp()->plugins->updateCustomCompletions(
|
||||
query, fullTextContent, cursorPosition, isFirstWord);
|
||||
if (done)
|
||||
{
|
||||
this->setStringList(results);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->source_->addToStringList(results, 0, isFirstWord);
|
||||
this->setStringList(results);
|
||||
}
|
||||
|
|
|
@ -26,8 +26,12 @@ public:
|
|||
|
||||
/// @brief Updates the model based on the completion query
|
||||
/// @param query Completion query
|
||||
/// @param fullTextContent Full text of the input, used by plugins for contextual completion
|
||||
/// @param cursorPosition Number of characters behind the cursor from the
|
||||
/// beginning of fullTextContent, also used by plugins
|
||||
/// @param isFirstWord Whether the completion is the first word in the input
|
||||
void updateResults(const QString &query, bool isFirstWord = false);
|
||||
void updateResults(const QString &query, const QString &fullTextContent,
|
||||
int cursorPosition, bool isFirstWord = false);
|
||||
|
||||
private:
|
||||
enum class SourceKind {
|
||||
|
|
|
@ -204,6 +204,41 @@ void rebuildMessageHighlights(Settings &settings,
|
|||
{
|
||||
checks.emplace_back(highlightPhraseCheck(highlight));
|
||||
}
|
||||
|
||||
if (settings.enableAutomodHighlight)
|
||||
{
|
||||
const auto highlightSound =
|
||||
settings.enableAutomodHighlightSound.getValue();
|
||||
const auto highlightAlert =
|
||||
settings.enableAutomodHighlightTaskbar.getValue();
|
||||
const auto highlightSoundUrlValue =
|
||||
settings.automodHighlightSoundUrl.getValue();
|
||||
|
||||
checks.emplace_back(HighlightCheck{
|
||||
[=](const auto & /*args*/, const auto & /*badges*/,
|
||||
const auto & /*senderName*/, const auto & /*originalMessage*/,
|
||||
const auto &flags,
|
||||
const auto /*self*/) -> std::optional<HighlightResult> {
|
||||
if (!flags.has(MessageFlag::AutoModOffendingMessage))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<QUrl> highlightSoundUrl;
|
||||
if (!highlightSoundUrlValue.isEmpty())
|
||||
{
|
||||
highlightSoundUrl = highlightSoundUrlValue;
|
||||
}
|
||||
|
||||
return HighlightResult{
|
||||
highlightAlert, // alert
|
||||
highlightSound, // playSound
|
||||
highlightSoundUrl, // customSoundUrl
|
||||
nullptr, // color
|
||||
false, // showInMentions
|
||||
};
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
void rebuildUserHighlights(Settings &settings,
|
||||
|
@ -434,6 +469,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
|
|||
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
|
||||
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
|
||||
|
||||
this->rebuildListener_.addSetting(settings.enableAutomodHighlight);
|
||||
this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound);
|
||||
this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar);
|
||||
this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl);
|
||||
|
||||
this->rebuildListener_.setCB([this, &settings] {
|
||||
qCDebug(chatterinoHighlights)
|
||||
<< "Rebuild checks because a setting changed";
|
||||
|
|
|
@ -98,9 +98,8 @@ void HighlightModel::afterInit()
|
|||
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
||||
setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
|
||||
|
||||
// auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
|
||||
// setColorItem(whisperRow[Column::Color], *whisperColor, false);
|
||||
whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
|
||||
auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
|
||||
setColorItem(whisperRow[Column::Color], *whisperColor, false);
|
||||
|
||||
this->insertCustomRow(whisperRow, HighlightRowIndexes::WhisperRow);
|
||||
|
||||
|
@ -234,6 +233,30 @@ void HighlightModel::afterInit()
|
|||
|
||||
this->insertCustomRow(threadMessageRow,
|
||||
HighlightRowIndexes::ThreadMessageRow);
|
||||
|
||||
// Highlight settings for automod caught messages
|
||||
const std::vector<QStandardItem *> automodRow = this->createRow();
|
||||
setBoolItem(automodRow[Column::Pattern],
|
||||
getSettings()->enableAutomodHighlight.getValue(), true, false);
|
||||
automodRow[Column::Pattern]->setData("AutoMod Caught Messages",
|
||||
Qt::DisplayRole);
|
||||
automodRow[Column::ShowInMentions]->setFlags({});
|
||||
setBoolItem(automodRow[Column::FlashTaskbar],
|
||||
getSettings()->enableAutomodHighlightTaskbar.getValue(), true,
|
||||
false);
|
||||
setBoolItem(automodRow[Column::PlaySound],
|
||||
getSettings()->enableAutomodHighlightSound.getValue(), true,
|
||||
false);
|
||||
automodRow[Column::UseRegex]->setFlags({});
|
||||
automodRow[Column::CaseSensitive]->setFlags({});
|
||||
|
||||
const auto automodSound =
|
||||
QUrl(getSettings()->automodHighlightSoundUrl.getValue());
|
||||
setFilePathItem(automodRow[Column::SoundPath], automodSound, false);
|
||||
|
||||
automodRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
|
||||
|
||||
this->insertCustomRow(automodRow, HighlightRowIndexes::AutomodRow);
|
||||
}
|
||||
|
||||
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||
|
@ -278,6 +301,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableThreadHighlight.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||
{
|
||||
getSettings()->enableAutomodHighlight.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -336,6 +364,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableThreadHighlightTaskbar.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||
{
|
||||
getSettings()->enableAutomodHighlightTaskbar.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -377,6 +410,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->enableThreadHighlightSound.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||
{
|
||||
getSettings()->enableAutomodHighlightSound.setValue(
|
||||
value.toBool());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -412,6 +450,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
getSettings()->threadHighlightSoundUrl.setValue(
|
||||
value.toString());
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||
{
|
||||
getSettings()->automodHighlightSoundUrl.setValue(
|
||||
value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -419,48 +462,47 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
|||
// Custom color
|
||||
if (role == Qt::DecorationRole)
|
||||
{
|
||||
auto colorName = value.value<QColor>().name(QColor::HexArgb);
|
||||
const auto setColor = [&](auto &setting, ColorType ty) {
|
||||
auto color = value.value<QColor>();
|
||||
setting.setValue(color.name(QColor::HexArgb));
|
||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
||||
.updateColor(ty, color);
|
||||
};
|
||||
|
||||
if (rowIndex == HighlightRowIndexes::SelfHighlightRow)
|
||||
{
|
||||
getSettings()->selfHighlightColor.setValue(colorName);
|
||||
setColor(getSettings()->selfHighlightColor,
|
||||
ColorType::SelfHighlight);
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::WhisperRow)
|
||||
{
|
||||
setColor(getSettings()->whisperHighlightColor,
|
||||
ColorType::Whisper);
|
||||
}
|
||||
// else if (rowIndex == HighlightRowIndexes::WhisperRow)
|
||||
// {
|
||||
// getSettings()->whisperHighlightColor.setValue(colorName);
|
||||
// }
|
||||
else if (rowIndex == HighlightRowIndexes::SubRow)
|
||||
{
|
||||
getSettings()->subHighlightColor.setValue(colorName);
|
||||
setColor(getSettings()->subHighlightColor,
|
||||
ColorType::Subscription);
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::RedeemedRow)
|
||||
{
|
||||
getSettings()->redeemedHighlightColor.setValue(colorName);
|
||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
||||
.updateColor(ColorType::RedeemedHighlight,
|
||||
QColor(colorName));
|
||||
setColor(getSettings()->redeemedHighlightColor,
|
||||
ColorType::RedeemedHighlight);
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::FirstMessageRow)
|
||||
{
|
||||
getSettings()->firstMessageHighlightColor.setValue(
|
||||
colorName);
|
||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
||||
.updateColor(ColorType::FirstMessageHighlight,
|
||||
QColor(colorName));
|
||||
setColor(getSettings()->firstMessageHighlightColor,
|
||||
ColorType::FirstMessageHighlight);
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow)
|
||||
{
|
||||
getSettings()->elevatedMessageHighlightColor.setValue(
|
||||
colorName);
|
||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
||||
.updateColor(ColorType::ElevatedMessageHighlight,
|
||||
QColor(colorName));
|
||||
setColor(getSettings()->elevatedMessageHighlightColor,
|
||||
ColorType::ElevatedMessageHighlight);
|
||||
}
|
||||
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
|
||||
{
|
||||
getSettings()->threadHighlightColor.setValue(colorName);
|
||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
||||
.updateColor(ColorType::ThreadMessageHighlight,
|
||||
QColor(colorName));
|
||||
setColor(getSettings()->threadHighlightColor,
|
||||
ColorType::ThreadMessageHighlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ public:
|
|||
FirstMessageRow = 4,
|
||||
ElevatedMessageRow = 5,
|
||||
ThreadMessageRow = 6,
|
||||
AutomodRow = 7,
|
||||
};
|
||||
|
||||
enum UserHighlightRowIndexes {
|
||||
|
|
|
@ -208,11 +208,11 @@ void NotificationController::checkStream(bool live, QString channelName)
|
|||
|
||||
void NotificationController::removeFakeChannel(const QString channelName)
|
||||
{
|
||||
auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
||||
channelName);
|
||||
if (i != fakeTwitchChannels.end())
|
||||
auto it = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
||||
channelName);
|
||||
if (it != fakeTwitchChannels.end())
|
||||
{
|
||||
fakeTwitchChannels.erase(i);
|
||||
fakeTwitchChannels.erase(it);
|
||||
// "delete" old 'CHANNEL is live' message
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
getApp()->twitch->liveChannel->getMessageSnapshot();
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# include <QFileInfo>
|
||||
# include <QLoggingCategory>
|
||||
# include <QTextCodec>
|
||||
# include <QUrl>
|
||||
|
||||
namespace {
|
||||
using namespace chatterino;
|
||||
|
@ -94,6 +95,37 @@ int c2_register_command(lua_State *L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
int c2_register_callback(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
EventType evtType{};
|
||||
if (!lua::peek(L, &evtType, 1))
|
||||
{
|
||||
luaL_error(L, "cannot get event name (1st arg of register_callback, "
|
||||
"expected a string)");
|
||||
return 0;
|
||||
}
|
||||
if (lua_isnoneornil(L, 2))
|
||||
{
|
||||
luaL_error(L, "missing argument for register_callback: function "
|
||||
"\"pointer\"");
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto callbackSavedName = QString("c2cb-%1").arg(
|
||||
magic_enum::enum_name<EventType>(evtType).data());
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str());
|
||||
|
||||
lua_pop(L, 2);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int c2_send_msg(lua_State *L)
|
||||
{
|
||||
QString text;
|
||||
|
@ -167,6 +199,7 @@ int c2_system_msg(lua_State *L)
|
|||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
|
@ -250,69 +283,87 @@ int g_load(lua_State *L)
|
|||
# endif
|
||||
}
|
||||
|
||||
int g_import(lua_State *L)
|
||||
int loadfile(lua_State *L, const QString &str)
|
||||
{
|
||||
auto countArgs = lua_gettop(L);
|
||||
// Lua allows dofile() which loads from stdin, but this is very useless in our case
|
||||
if (countArgs == 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "it is not allowed to call import() without arguments");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
QString fname;
|
||||
if (!lua::pop(L, &fname))
|
||||
if (pl == nullptr)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: expected a string for a filename");
|
||||
return 1;
|
||||
return luaL_error(L, "loadfile: internal error: no plugin?");
|
||||
}
|
||||
auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/");
|
||||
auto file = dir.resolved(fname);
|
||||
|
||||
qCDebug(chatterinoLua) << "plugin" << pl->id << "is trying to load" << file
|
||||
<< "(its dir is" << dir << ")";
|
||||
if (!dir.isParentOf(file))
|
||||
if (!dir.isParentOf(str))
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: filename must be inside of the "
|
||||
"plugin directory");
|
||||
// XXX: This intentionally hides the resolved path to not leak it
|
||||
lua::push(
|
||||
L, QString("requested module is outside of the plugin directory"));
|
||||
return 1;
|
||||
}
|
||||
QFileInfo info(str);
|
||||
if (!info.exists())
|
||||
{
|
||||
lua::push(L, QString("no file '%1'").arg(str));
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto path = file.path(QUrl::FullyDecoded);
|
||||
QFile qf(path);
|
||||
qf.open(QIODevice::ReadOnly);
|
||||
if (qf.size() > 10'000'000)
|
||||
auto temp = str.toStdString();
|
||||
const auto *filename = temp.c_str();
|
||||
|
||||
auto res = luaL_loadfilex(L, filename, "t");
|
||||
// Yoinked from checkload lib/lua/src/loadlib.c
|
||||
if (res == LUA_OK)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: size limit of 10MB exceeded, what "
|
||||
"the hell are you doing");
|
||||
lua_pushstring(L, filename);
|
||||
return 2;
|
||||
}
|
||||
|
||||
return luaL_error(L, "error loading module '%s' from file '%s':\n\t%s",
|
||||
lua_tostring(L, 1), filename, lua_tostring(L, -1));
|
||||
}
|
||||
|
||||
int searcherAbsolute(lua_State *L)
|
||||
{
|
||||
auto name = QString::fromUtf8(luaL_checkstring(L, 1));
|
||||
name = name.replace('.', QDir::separator());
|
||||
|
||||
QString filename;
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
return luaL_error(L, "searcherAbsolute: internal error: no plugin?");
|
||||
}
|
||||
|
||||
QFileInfo file(pl->loadDirectory().filePath(name + ".lua"));
|
||||
return loadfile(L, file.canonicalFilePath());
|
||||
}
|
||||
|
||||
int searcherRelative(lua_State *L)
|
||||
{
|
||||
lua_Debug dbg;
|
||||
lua_getstack(L, 1, &dbg);
|
||||
lua_getinfo(L, "S", &dbg);
|
||||
auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen);
|
||||
if (currentFile.startsWith("@"))
|
||||
{
|
||||
currentFile = currentFile.mid(1);
|
||||
}
|
||||
if (currentFile == "=[C]" || currentFile == "")
|
||||
{
|
||||
lua::push(
|
||||
L,
|
||||
QString(
|
||||
"Unable to load relative to file:caller has no source file"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// validate utf-8 to block bytecode exploits
|
||||
auto data = qf.readAll();
|
||||
auto *utf8 = QTextCodec::codecForName("UTF-8");
|
||||
QTextCodec::ConverterState state;
|
||||
utf8->toUnicode(data.constData(), data.size(), &state);
|
||||
if (state.invalidChars != 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "invalid utf-8 in import() target (%s) is not allowed",
|
||||
fname.toStdString().c_str());
|
||||
return 1;
|
||||
}
|
||||
auto parent = QFileInfo(currentFile).dir();
|
||||
|
||||
// fetch dofile and call it
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
// maybe data race here if symlink was swapped?
|
||||
lua::push(L, path);
|
||||
lua_call(L, 1, LUA_MULTRET);
|
||||
auto name = QString::fromUtf8(luaL_checkstring(L, 1));
|
||||
name = name.replace('.', QDir::separator());
|
||||
QString filename =
|
||||
parent.canonicalPath() + QDir::separator() + name + ".lua";
|
||||
|
||||
return lua_gettop(L);
|
||||
return loadfile(L, filename);
|
||||
}
|
||||
|
||||
int g_print(lua_State *L)
|
||||
|
|
|
@ -1,27 +1,111 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include <QString>
|
||||
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
namespace chatterino::lua::api {
|
||||
// names in this namespace reflect what's visible inside Lua and follow the lua naming scheme
|
||||
// function names in this namespace reflect what's visible inside Lua and follow the lua naming scheme
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
// Following functions are exposed in c2 table.
|
||||
|
||||
// Comments in this file are special, the docs/plugin-meta.lua file is generated from them
|
||||
// All multiline comments will be added into that file. See scripts/make_luals_meta.py script for more info.
|
||||
|
||||
/**
|
||||
* @exposeenum c2.LogLevel
|
||||
*/
|
||||
// Represents "calls" to qCDebug, qCInfo ...
|
||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||
|
||||
/**
|
||||
* @exposeenum c2.EventType
|
||||
*/
|
||||
enum class EventType {
|
||||
CompletionRequested,
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class CommandContext
|
||||
* @lua@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
||||
* @lua@field channel_name string The name of the channel the command was executed in.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @lua@class CompletionList
|
||||
*/
|
||||
struct CompletionList {
|
||||
/**
|
||||
* @lua@field values string[] The completions
|
||||
*/
|
||||
std::vector<QString> values{};
|
||||
|
||||
/**
|
||||
* @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
||||
*/
|
||||
bool hideOthers{};
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a new command called `name` which when executed will call `handler`.
|
||||
*
|
||||
* @lua@param name string The name of the command.
|
||||
* @lua@param handler fun(ctx: CommandContext) The handler to be invoked when the command gets executed.
|
||||
* @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
|
||||
* @exposed c2.register_command
|
||||
*/
|
||||
int c2_register_command(lua_State *L);
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when completions for a term are requested.
|
||||
*
|
||||
* @lua@param type "CompletionRequested"
|
||||
* @lua@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
|
||||
* @exposed c2.register_callback
|
||||
*/
|
||||
int c2_register_callback(lua_State *L);
|
||||
|
||||
/**
|
||||
* Sends a message to `channel` with the specified text. Also executes commands.
|
||||
*
|
||||
* **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
||||
*
|
||||
* @lua@param channel string The name of the Twitch channel
|
||||
* @lua@param text string The text to be sent
|
||||
* @lua@return boolean ok
|
||||
* @exposed c2.send_msg
|
||||
*/
|
||||
int c2_send_msg(lua_State *L);
|
||||
/**
|
||||
* Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
|
||||
*
|
||||
* @lua@param channel string
|
||||
* @lua@param text string
|
||||
* @lua@return boolean ok
|
||||
* @exposed c2.system_msg
|
||||
*/
|
||||
int c2_system_msg(lua_State *L);
|
||||
|
||||
/**
|
||||
* Writes a message to the Chatterino log.
|
||||
*
|
||||
* @lua@param level LogLevel The desired level.
|
||||
* @lua@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
||||
* @exposed c2.log
|
||||
*/
|
||||
int c2_log(lua_State *L);
|
||||
|
||||
// These ones are global
|
||||
int g_load(lua_State *L);
|
||||
int g_print(lua_State *L);
|
||||
int g_import(lua_State *L);
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
// Exposed as c2.LogLevel
|
||||
// Represents "calls" to qCDebug, qCInfo ...
|
||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||
// This is for require() exposed as an element of package.searchers
|
||||
int searcherAbsolute(lua_State *L);
|
||||
int searcherRelative(lua_State *L);
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
# include "common/Channel.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
|
@ -75,6 +76,9 @@ QString humanErrorText(lua_State *L, int errCode)
|
|||
case LUA_ERRFILE:
|
||||
errName = "(file error)";
|
||||
break;
|
||||
case ERROR_BAD_PEEK:
|
||||
errName = "(unable to convert value to c++)";
|
||||
break;
|
||||
default:
|
||||
errName = "(unknown error type)";
|
||||
}
|
||||
|
@ -111,6 +115,7 @@ StackIdx push(lua_State *L, const std::string &str)
|
|||
|
||||
StackIdx push(lua_State *L, const CommandContext &ctx)
|
||||
{
|
||||
StackGuard guard(L, 1);
|
||||
auto outIdx = pushEmptyTable(L, 2);
|
||||
|
||||
push(L, ctx.words);
|
||||
|
@ -127,8 +132,27 @@ StackIdx push(lua_State *L, const bool &b)
|
|||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const int &b)
|
||||
{
|
||||
lua_pushinteger(L, b);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, bool *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
if (!lua_isboolean(L, idx))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = bool(lua_toboolean(L, idx));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, double *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
int ok{0};
|
||||
auto v = lua_tonumberx(L, idx, &ok);
|
||||
if (ok != 0)
|
||||
|
@ -140,6 +164,7 @@ bool peek(lua_State *L, double *out, StackIdx idx)
|
|||
|
||||
bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
size_t len{0};
|
||||
const char *str = lua_tolstring(L, idx, &len);
|
||||
if (str == nullptr)
|
||||
|
@ -156,6 +181,7 @@ bool peek(lua_State *L, QString *out, StackIdx idx)
|
|||
|
||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
size_t len{0};
|
||||
const char *str = lua_tolstring(L, idx, &len);
|
||||
if (str == nullptr)
|
||||
|
@ -172,6 +198,7 @@ bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
|||
|
||||
bool peek(lua_State *L, std::string *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
size_t len{0};
|
||||
const char *str = lua_tolstring(L, idx, &len);
|
||||
if (str == nullptr)
|
||||
|
@ -186,6 +213,23 @@ bool peek(lua_State *L, std::string *out, StackIdx idx)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, api::CompletionList *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
int typ = lua_getfield(L, idx, "values");
|
||||
if (typ != LUA_TTABLE)
|
||||
{
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
if (!lua::pop(L, &out->values, -1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lua_getfield(L, idx, "hide_others");
|
||||
return lua::pop(L, &out->hideOthers);
|
||||
}
|
||||
|
||||
QString toString(lua_State *L, StackIdx idx)
|
||||
{
|
||||
size_t len{};
|
||||
|
|
|
@ -2,14 +2,19 @@
|
|||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "common/QLogging.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <magic_enum/magic_enum.hpp>
|
||||
# include <QList>
|
||||
|
||||
# include <cassert>
|
||||
# include <optional>
|
||||
# include <string>
|
||||
# include <string_view>
|
||||
# include <type_traits>
|
||||
# include <variant>
|
||||
# include <vector>
|
||||
struct lua_State;
|
||||
class QJsonObject;
|
||||
|
@ -19,6 +24,12 @@ struct CommandContext;
|
|||
|
||||
namespace chatterino::lua {
|
||||
|
||||
namespace api {
|
||||
struct CompletionList;
|
||||
} // namespace api
|
||||
|
||||
constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
|
||||
|
||||
/**
|
||||
* @brief Dumps the Lua stack into qCDebug(chatterinoLua)
|
||||
*
|
||||
|
@ -52,20 +63,136 @@ StackIdx push(lua_State *L, const CommandContext &ctx);
|
|||
StackIdx push(lua_State *L, const QString &str);
|
||||
StackIdx push(lua_State *L, const std::string &str);
|
||||
StackIdx push(lua_State *L, const bool &b);
|
||||
StackIdx push(lua_State *L, const int &b);
|
||||
|
||||
// returns OK?
|
||||
bool peek(lua_State *L, bool *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, double *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, std::string *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, api::CompletionList *out, StackIdx idx = -1);
|
||||
|
||||
/**
|
||||
* @brief Converts Lua object at stack index idx to a string.
|
||||
*/
|
||||
QString toString(lua_State *L, StackIdx idx = -1);
|
||||
|
||||
// This object ensures that the stack is of expected size when it is destroyed
|
||||
class StackGuard
|
||||
{
|
||||
int expected;
|
||||
lua_State *L;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Use this constructor if you expect the stack size to be the same on the
|
||||
* destruction of the object as its creation
|
||||
*/
|
||||
StackGuard(lua_State *L)
|
||||
: expected(lua_gettop(L))
|
||||
, L(L)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you expect the stack size changing, diff is the expected difference
|
||||
* Ex: diff=3 means three elements added to the stack
|
||||
*/
|
||||
StackGuard(lua_State *L, int diff)
|
||||
: expected(lua_gettop(L) + diff)
|
||||
, L(L)
|
||||
{
|
||||
}
|
||||
|
||||
~StackGuard()
|
||||
{
|
||||
if (expected < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int after = lua_gettop(this->L);
|
||||
if (this->expected != after)
|
||||
{
|
||||
stackDump(this->L, "StackGuard check tripped");
|
||||
// clang-format off
|
||||
// clang format likes to insert a new line which means that some builds won't show this message fully
|
||||
assert(false && "internal error: lua stack was not in an expected state");
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
|
||||
// This object isn't meant to be passed around
|
||||
StackGuard operator=(StackGuard &) = delete;
|
||||
StackGuard &operator=(StackGuard &&) = delete;
|
||||
StackGuard(StackGuard &) = delete;
|
||||
StackGuard(StackGuard &&) = delete;
|
||||
|
||||
// This function tells the StackGuard that the stack isn't in an expected state but it was handled
|
||||
void handled()
|
||||
{
|
||||
this->expected = -1;
|
||||
}
|
||||
};
|
||||
|
||||
/// TEMPLATES
|
||||
|
||||
template <typename T>
|
||||
bool peek(lua_State *L, std::optional<T> *out, StackIdx idx = -1)
|
||||
{
|
||||
if (lua_isnil(L, idx))
|
||||
{
|
||||
*out = std::nullopt;
|
||||
return true;
|
||||
}
|
||||
|
||||
*out = T();
|
||||
return peek(L, out->operator->(), idx);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool peek(lua_State *L, std::vector<T> *vec, StackIdx idx = -1)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
|
||||
if (!lua_istable(L, idx))
|
||||
{
|
||||
lua::stackDump(L, "!table");
|
||||
qCDebug(chatterinoLua)
|
||||
<< "value is not a table, type is" << lua_type(L, idx);
|
||||
return false;
|
||||
}
|
||||
auto len = lua_rawlen(L, idx);
|
||||
if (len == 0)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "value has 0 length";
|
||||
return true;
|
||||
}
|
||||
if (len > 1'000'000)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "value is too long";
|
||||
return false;
|
||||
}
|
||||
// count like lua
|
||||
for (int i = 1; i <= len; i++)
|
||||
{
|
||||
lua_geti(L, idx, i);
|
||||
std::optional<T> obj;
|
||||
if (!lua::peek(L, &obj))
|
||||
{
|
||||
//lua_seti(L, LUA_REGISTRYINDEX, 1); // lazy
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Failed to convert lua object into c++: at array index " << i
|
||||
<< ":";
|
||||
stackDump(L, "bad conversion into string");
|
||||
return false;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
vec->push_back(obj.value());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts object at stack index idx to enum given by template parameter T
|
||||
*/
|
||||
|
@ -150,6 +277,7 @@ StackIdx push(lua_State *L, T inp)
|
|||
template <typename T>
|
||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
||||
{
|
||||
StackGuard guard(L, -1);
|
||||
auto ok = peek(L, out, idx);
|
||||
if (ok)
|
||||
{
|
||||
|
@ -186,6 +314,58 @@ StackIdx pushEnumTable(lua_State *L)
|
|||
return out;
|
||||
}
|
||||
|
||||
// Represents a Lua function on the stack
|
||||
template <typename ReturnType, typename... Args>
|
||||
class CallbackFunction
|
||||
{
|
||||
StackIdx stackIdx_;
|
||||
lua_State *L;
|
||||
|
||||
public:
|
||||
CallbackFunction(lua_State *L, StackIdx stackIdx)
|
||||
: stackIdx_(stackIdx)
|
||||
, L(L)
|
||||
{
|
||||
}
|
||||
|
||||
// this type owns the stackidx, it must not be trivially copiable
|
||||
CallbackFunction operator=(CallbackFunction &) = delete;
|
||||
CallbackFunction(CallbackFunction &) = delete;
|
||||
|
||||
// Permit only move
|
||||
CallbackFunction &operator=(CallbackFunction &&) = default;
|
||||
CallbackFunction(CallbackFunction &&) = default;
|
||||
|
||||
~CallbackFunction()
|
||||
{
|
||||
lua_remove(L, this->stackIdx_);
|
||||
}
|
||||
|
||||
std::variant<int, ReturnType> operator()(Args... arguments)
|
||||
{
|
||||
lua_pushvalue(this->L, this->stackIdx_);
|
||||
( // apparently this calls lua::push() for every Arg
|
||||
[this, &arguments] {
|
||||
lua::push(this->L, arguments);
|
||||
}(),
|
||||
...);
|
||||
|
||||
int res = lua_pcall(L, sizeof...(Args), 1, 0);
|
||||
if (res != LUA_OK)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "error is: " << res;
|
||||
return {res};
|
||||
}
|
||||
|
||||
ReturnType val;
|
||||
if (!lua::pop(L, &val))
|
||||
{
|
||||
return {ERROR_BAD_PEEK};
|
||||
}
|
||||
return {val};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace chatterino::lua
|
||||
|
||||
#endif
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include <QDir>
|
||||
# include <QString>
|
||||
|
@ -85,10 +87,51 @@ public:
|
|||
return this->loadDirectory_;
|
||||
}
|
||||
|
||||
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
||||
using LuaCompletionCallback =
|
||||
lua::CallbackFunction<lua::api::CompletionList, QString, QString, int,
|
||||
bool>;
|
||||
std::optional<LuaCompletionCallback> getCompletionCallback()
|
||||
{
|
||||
if (this->state_ == nullptr || !this->error_.isNull())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// this uses magic enum to help automatic tooling find usages
|
||||
auto typ =
|
||||
lua_getfield(this->state_, LUA_REGISTRYINDEX,
|
||||
QString("c2cb-%1")
|
||||
.arg(magic_enum::enum_name<lua::api::EventType>(
|
||||
lua::api::EventType::CompletionRequested)
|
||||
.data())
|
||||
.toStdString()
|
||||
.c_str());
|
||||
if (typ != LUA_TFUNCTION)
|
||||
{
|
||||
lua_pop(this->state_, 1);
|
||||
return {};
|
||||
}
|
||||
|
||||
// move
|
||||
return std::make_optional<lua::CallbackFunction<
|
||||
lua::api::CompletionList, QString, QString, int, bool>>(
|
||||
this->state_, lua_gettop(this->state_));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the plugin crashes while evaluating the main file, this function will return the error
|
||||
*/
|
||||
QString error()
|
||||
{
|
||||
return this->error_;
|
||||
}
|
||||
|
||||
private:
|
||||
QDir loadDirectory_;
|
||||
lua_State *state_;
|
||||
|
||||
QString error_;
|
||||
|
||||
// maps command name -> function name
|
||||
std::unordered_map<QString, QString> ownedCommands;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/Args.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
|
@ -18,6 +19,7 @@
|
|||
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
# include <variant>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -45,15 +47,13 @@ void PluginController::loadPlugins()
|
|||
auto dir = QDir(getPaths()->pluginsDirectory);
|
||||
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
||||
for (const auto &info :
|
||||
dir.entryInfoList(QDir::NoFilter | QDir::NoDotAndDotDot))
|
||||
dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
|
||||
{
|
||||
if (info.isDir())
|
||||
{
|
||||
auto pluginDir = QDir(info.absoluteFilePath());
|
||||
this->tryLoadFromDir(pluginDir);
|
||||
}
|
||||
auto pluginDir = QDir(info.absoluteFilePath());
|
||||
this->tryLoadFromDir(pluginDir);
|
||||
}
|
||||
}
|
||||
|
||||
bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||
{
|
||||
// look for init.lua
|
||||
|
@ -103,9 +103,10 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
|||
return true;
|
||||
}
|
||||
|
||||
void PluginController::openLibrariesFor(lua_State *L,
|
||||
const PluginMeta & /*meta*/)
|
||||
void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||
const QDir &pluginDir)
|
||||
{
|
||||
lua::StackGuard guard(L);
|
||||
// Stuff to change, remove or hide behind a permission system:
|
||||
static const std::vector<luaL_Reg> loadedlibs = {
|
||||
luaL_Reg{LUA_GNAME, luaopen_base},
|
||||
|
@ -123,6 +124,7 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
luaL_Reg{LUA_STRLIBNAME, luaopen_string},
|
||||
luaL_Reg{LUA_MATHLIBNAME, luaopen_math},
|
||||
luaL_Reg{LUA_UTF8LIBNAME, luaopen_utf8},
|
||||
luaL_Reg{LUA_LOADLIBNAME, luaopen_package},
|
||||
};
|
||||
// Warning: Do not add debug library to this, it would make the security of
|
||||
// this a living nightmare due to stuff like registry access
|
||||
|
@ -138,29 +140,30 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
static const luaL_Reg c2Lib[] = {
|
||||
{"system_msg", lua::api::c2_system_msg},
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"register_callback", lua::api::c2_register_callback},
|
||||
{"send_msg", lua::api::c2_send_msg},
|
||||
{"log", lua::api::c2_log},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
lua_pushglobaltable(L);
|
||||
auto global = lua_gettop(L);
|
||||
auto gtable = lua_gettop(L);
|
||||
|
||||
// count of elements in C2LIB + LogLevel
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 5);
|
||||
// count of elements in C2LIB + LogLevel + EventType
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 8);
|
||||
|
||||
luaL_setfuncs(L, c2Lib, 0);
|
||||
|
||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
||||
lua_setfield(L, c2libIdx, "LogLevel");
|
||||
|
||||
lua_setfield(L, global, "c2");
|
||||
lua::pushEnumTable<lua::api::EventType>(L);
|
||||
lua_setfield(L, c2libIdx, "EventType");
|
||||
|
||||
lua_setfield(L, gtable, "c2");
|
||||
|
||||
// ban functions
|
||||
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
||||
|
||||
lua_pushglobaltable(L);
|
||||
auto gtable = lua_gettop(L);
|
||||
|
||||
// possibly randomize this name at runtime to prevent some attacks?
|
||||
|
||||
# ifndef NDEBUG
|
||||
|
@ -168,16 +171,10 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
# endif
|
||||
|
||||
lua_getfield(L, gtable, "dofile");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg replacementFuncs[] = {
|
||||
{"load", lua::api::g_load},
|
||||
{"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);
|
||||
|
@ -188,18 +185,63 @@ void PluginController::openLibrariesFor(lua_State *L,
|
|||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "dofile");
|
||||
|
||||
lua_pop(L, 1);
|
||||
// set up package lib
|
||||
lua_getfield(L, gtable, "package");
|
||||
|
||||
auto package = lua_gettop(L);
|
||||
lua_pushstring(L, "");
|
||||
lua_setfield(L, package, "cpath");
|
||||
|
||||
// we don't use path
|
||||
lua_pushstring(L, "");
|
||||
lua_setfield(L, package, "path");
|
||||
|
||||
{
|
||||
lua_getfield(L, gtable, "table");
|
||||
auto table = lua_gettop(L);
|
||||
lua_getfield(L, -1, "remove");
|
||||
lua_remove(L, table);
|
||||
}
|
||||
auto remove = lua_gettop(L);
|
||||
|
||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
lua_pushvalue(L, remove);
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pcall(L, 1, 0, 0);
|
||||
}
|
||||
lua_pop(L, 1); // get rid of remove
|
||||
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pushcclosure(L, lua::api::searcherRelative, 0);
|
||||
lua_seti(L, -2, 2);
|
||||
|
||||
lua::push(L, QString(pluginDir.absolutePath()));
|
||||
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
||||
lua_seti(L, -2, 3);
|
||||
|
||||
lua_pop(L, 3); // remove gtable, package, package.searchers
|
||||
}
|
||||
|
||||
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta)
|
||||
{
|
||||
lua_State *l = luaL_newstate();
|
||||
PluginController::openLibrariesFor(l, meta);
|
||||
|
||||
auto pluginName = pluginDir.dirName();
|
||||
lua_State *l = luaL_newstate();
|
||||
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
||||
auto *temp = plugin.get();
|
||||
this->plugins_.insert({pluginName, std::move(plugin)});
|
||||
|
||||
if (getApp()->getArgs().safeMode)
|
||||
{
|
||||
// This isn't done earlier to ensure the user can disable a misbehaving plugin
|
||||
qCWarning(chatterinoLua) << "Skipping loading plugin " << meta.name
|
||||
<< " because safe mode is enabled.";
|
||||
return;
|
||||
}
|
||||
PluginController::openLibrariesFor(l, meta, pluginDir);
|
||||
|
||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||
!getSettings()->pluginsEnabled)
|
||||
{
|
||||
|
@ -211,9 +253,10 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||
if (err != 0)
|
||||
{
|
||||
temp->error_ = lua::humanErrorText(l, err);
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Failed to load" << pluginName << "plugin from" << index << ": "
|
||||
<< lua::humanErrorText(l, err);
|
||||
<< temp->error_;
|
||||
return;
|
||||
}
|
||||
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
||||
|
@ -298,5 +341,52 @@ const std::map<QString, std::unique_ptr<Plugin>> &PluginController::plugins()
|
|||
return this->plugins_;
|
||||
}
|
||||
|
||||
}; // namespace chatterino
|
||||
std::pair<bool, QStringList> PluginController::updateCustomCompletions(
|
||||
const QString &query, const QString &fullTextContent, int cursorPosition,
|
||||
bool isFirstWord) const
|
||||
{
|
||||
QStringList results;
|
||||
|
||||
for (const auto &[name, pl] : this->plugins())
|
||||
{
|
||||
if (!pl->error().isNull())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lua::StackGuard guard(pl->state_);
|
||||
|
||||
auto opt = pl->getCompletionCallback();
|
||||
if (opt)
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Processing custom completions from plugin" << name;
|
||||
auto &cb = *opt;
|
||||
auto errOrList =
|
||||
cb(query, fullTextContent, cursorPosition, isFirstWord);
|
||||
if (std::holds_alternative<int>(errOrList))
|
||||
{
|
||||
guard.handled();
|
||||
int err = std::get<int>(errOrList);
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Got error from plugin " << pl->meta.name
|
||||
<< " while refreshing tab completion: "
|
||||
<< lua::humanErrorText(pl->state_, err);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto list = std::get<lua::api::CompletionList>(errOrList);
|
||||
if (list.hideOthers)
|
||||
{
|
||||
results = QStringList(list.values.begin(), list.values.end());
|
||||
return {true, results};
|
||||
}
|
||||
results += QStringList(list.values.begin(), list.values.end());
|
||||
}
|
||||
}
|
||||
|
||||
return {false, results};
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
|
|
|
@ -36,6 +36,7 @@ public:
|
|||
// This is required to be public because of c functions
|
||||
Plugin *getPluginByStatePtr(lua_State *L);
|
||||
|
||||
// TODO: make a function that iterates plugins that aren't errored/enabled
|
||||
const std::map<QString, std::unique_ptr<Plugin>> &plugins() const;
|
||||
|
||||
/**
|
||||
|
@ -52,17 +53,22 @@ public:
|
|||
*/
|
||||
static bool isPluginEnabled(const QString &id);
|
||||
|
||||
std::pair<bool, QStringList> updateCustomCompletions(
|
||||
const QString &query, const QString &fullTextContent,
|
||||
int cursorPosition, bool isFirstWord) const;
|
||||
|
||||
private:
|
||||
void loadPlugins();
|
||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta);
|
||||
|
||||
// This function adds lua standard libraries into the state
|
||||
static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/);
|
||||
static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/,
|
||||
const QDir &pluginDir);
|
||||
static void loadChatterinoLib(lua_State *l);
|
||||
bool tryLoadFromDir(const QDir &pluginDir);
|
||||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||
};
|
||||
|
||||
}; // namespace chatterino
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
|
|
|
@ -256,8 +256,8 @@ void MiniaudioBackend::play(const QUrl &sound)
|
|||
if (sound.isLocalFile())
|
||||
{
|
||||
auto soundPath = sound.toLocalFile();
|
||||
auto result = ma_engine_play_sound(this->engine.get(),
|
||||
qPrintable(soundPath), nullptr);
|
||||
result = ma_engine_play_sound(this->engine.get(),
|
||||
qPrintable(soundPath), nullptr);
|
||||
if (result != MA_SUCCESS)
|
||||
{
|
||||
qCWarning(chatterinoSound) << "Failed to play sound" << sound
|
||||
|
|
14
src/main.cpp
14
src/main.cpp
|
@ -4,11 +4,11 @@
|
|||
#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/CrashHandler.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/AttachToConsole.hpp"
|
||||
|
@ -62,18 +62,18 @@ int main(int argc, char **argv)
|
|||
return 1;
|
||||
}
|
||||
|
||||
initArgs(a);
|
||||
const Args args(a);
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
const auto crashpadHandler = installCrashHandler();
|
||||
const auto crashpadHandler = installCrashHandler(args);
|
||||
#endif
|
||||
|
||||
// run in gui mode or browser extension host mode
|
||||
if (getArgs().shouldRunBrowserExtensionHost)
|
||||
if (args.shouldRunBrowserExtensionHost)
|
||||
{
|
||||
runBrowserExtensionHost();
|
||||
}
|
||||
else if (getArgs().printVersion)
|
||||
else if (args.printVersion)
|
||||
{
|
||||
attachToConsole();
|
||||
|
||||
|
@ -87,7 +87,7 @@ int main(int argc, char **argv)
|
|||
}
|
||||
else
|
||||
{
|
||||
if (getArgs().verbose)
|
||||
if (args.verbose)
|
||||
{
|
||||
attachToConsole();
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ int main(int argc, char **argv)
|
|||
|
||||
Settings settings(paths->settingsDirectory);
|
||||
|
||||
runGui(a, *paths, settings);
|
||||
runGui(a, *paths, settings, args);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,9 @@ enum class MessageFlag : int64_t {
|
|||
LiveUpdatesAdd = (1LL << 28),
|
||||
LiveUpdatesRemove = (1LL << 29),
|
||||
LiveUpdatesUpdate = (1LL << 30),
|
||||
/// The message caught by AutoMod containing the user who sent the message & its contents
|
||||
AutoModOffendingMessage = (1LL << 31),
|
||||
LowTrustUsers = (1LL << 32),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
|
|
|
@ -78,152 +78,6 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time)
|
|||
return MessageBuilder(systemMessage, text, time).release();
|
||||
}
|
||||
|
||||
EmotePtr makeAutoModBadge()
|
||||
{
|
||||
return std::make_shared<Emote>(Emote{
|
||||
EmoteName{},
|
||||
ImageSet{Image::fromResourcePixmap(getResources().twitch.automod)},
|
||||
Tooltip{"AutoMod"},
|
||||
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
|
||||
}
|
||||
|
||||
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action)
|
||||
{
|
||||
auto builder = MessageBuilder();
|
||||
QString text("AutoMod: ");
|
||||
|
||||
builder.emplace<TimestampElement>();
|
||||
builder.message().flags.set(MessageFlag::PubSub);
|
||||
|
||||
// AutoMod shield badge
|
||||
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
||||
MessageElementFlag::BadgeChannelAuthority);
|
||||
// AutoMod "username"
|
||||
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
||||
MessageColor(QColor("blue")),
|
||||
FontStyle::ChatMediumBold);
|
||||
builder.emplace<TextElement>(
|
||||
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
||||
MessageColor(QColor("blue")));
|
||||
switch (action.type)
|
||||
{
|
||||
case AutomodInfoAction::OnHold: {
|
||||
QString info("Hey! Your message is being checked "
|
||||
"by mods and has not been sent.");
|
||||
text += info;
|
||||
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
||||
MessageColor::Text);
|
||||
}
|
||||
break;
|
||||
case AutomodInfoAction::Denied: {
|
||||
QString info("Mods have removed your message.");
|
||||
text += info;
|
||||
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
||||
MessageColor::Text);
|
||||
}
|
||||
break;
|
||||
case AutomodInfoAction::Approved: {
|
||||
QString info("Mods have accepted your message.");
|
||||
text += info;
|
||||
builder.emplace<TextElement>(info, MessageElementFlag::Text,
|
||||
MessageColor::Text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
builder.message().flags.set(MessageFlag::AutoMod);
|
||||
builder.message().messageText = text;
|
||||
builder.message().searchText = text;
|
||||
|
||||
auto message = builder.release();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
||||
const AutomodAction &action)
|
||||
{
|
||||
MessageBuilder builder, builder2;
|
||||
|
||||
//
|
||||
// Builder for AutoMod message with explanation
|
||||
builder.message().loginName = "automod";
|
||||
builder.message().flags.set(MessageFlag::PubSub);
|
||||
builder.message().flags.set(MessageFlag::Timeout);
|
||||
builder.message().flags.set(MessageFlag::AutoMod);
|
||||
|
||||
// AutoMod shield badge
|
||||
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
||||
MessageElementFlag::BadgeChannelAuthority);
|
||||
// AutoMod "username"
|
||||
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
||||
MessageColor(QColor("blue")),
|
||||
FontStyle::ChatMediumBold);
|
||||
builder.emplace<TextElement>(
|
||||
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
||||
MessageColor(QColor("blue")));
|
||||
// AutoMod header message
|
||||
builder.emplace<TextElement>(
|
||||
("Held a message for reason: " + action.reason +
|
||||
". Allow will post it in chat. "),
|
||||
MessageElementFlag::Text, MessageColor::Text);
|
||||
// Allow link button
|
||||
builder
|
||||
.emplace<TextElement>("Allow", MessageElementFlag::Text,
|
||||
MessageColor(QColor("green")),
|
||||
FontStyle::ChatMediumBold)
|
||||
->setLink({Link::AutoModAllow, action.msgID});
|
||||
// Deny link button
|
||||
builder
|
||||
.emplace<TextElement>(" Deny", MessageElementFlag::Text,
|
||||
MessageColor(QColor("red")),
|
||||
FontStyle::ChatMediumBold)
|
||||
->setLink({Link::AutoModDeny, action.msgID});
|
||||
// ID of message caught by AutoMod
|
||||
// builder.emplace<TextElement>(action.msgID, MessageElementFlag::Text,
|
||||
// MessageColor::Text);
|
||||
auto text1 =
|
||||
QString("AutoMod: Held a message for reason: %1. Allow will post "
|
||||
"it in chat. Allow Deny")
|
||||
.arg(action.reason);
|
||||
builder.message().messageText = text1;
|
||||
builder.message().searchText = text1;
|
||||
|
||||
auto message1 = builder.release();
|
||||
|
||||
//
|
||||
// Builder for offender's message
|
||||
builder2.emplace<TimestampElement>();
|
||||
builder2.emplace<TwitchModerationElement>();
|
||||
builder2.message().loginName = action.target.login;
|
||||
builder2.message().flags.set(MessageFlag::PubSub);
|
||||
builder2.message().flags.set(MessageFlag::Timeout);
|
||||
builder2.message().flags.set(MessageFlag::AutoMod);
|
||||
|
||||
// sender username
|
||||
builder2
|
||||
.emplace<TextElement>(
|
||||
action.target.displayName + ":", MessageElementFlag::BoldUsername,
|
||||
MessageColor(action.target.color), FontStyle::ChatMediumBold)
|
||||
->setLink({Link::UserInfo, action.target.login});
|
||||
builder2
|
||||
.emplace<TextElement>(action.target.displayName + ":",
|
||||
MessageElementFlag::NonBoldUsername,
|
||||
MessageColor(action.target.color))
|
||||
->setLink({Link::UserInfo, action.target.login});
|
||||
// sender's message caught by AutoMod
|
||||
builder2.emplace<TextElement>(action.message, MessageElementFlag::Text,
|
||||
MessageColor::Text);
|
||||
auto text2 =
|
||||
QString("%1: %2").arg(action.target.displayName, action.message);
|
||||
builder2.message().messageText = text2;
|
||||
builder2.message().searchText = text2;
|
||||
|
||||
auto message2 = builder2.release();
|
||||
|
||||
return std::make_pair(message1, message2);
|
||||
}
|
||||
|
||||
MessageBuilder::MessageBuilder()
|
||||
: message_(std::make_shared<Message>())
|
||||
{
|
||||
|
|
|
@ -53,9 +53,6 @@ const ImageUploaderResultTag imageUploaderResultMessage{};
|
|||
|
||||
MessagePtr makeSystemMessage(const QString &text);
|
||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
||||
const AutomodAction &action);
|
||||
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
|
||||
|
||||
struct MessageParseArgs {
|
||||
bool disablePingSounds = false;
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
@ -170,18 +172,10 @@ void SharedMessageBuilder::parseHighlights()
|
|||
this->highlightAlert_ = highlightResult.alert;
|
||||
|
||||
this->highlightSound_ = highlightResult.playSound;
|
||||
this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl;
|
||||
|
||||
this->message().highlightColor = highlightResult.color;
|
||||
|
||||
if (highlightResult.customSoundUrl)
|
||||
{
|
||||
this->highlightSoundUrl_ = *highlightResult.customSoundUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
||||
}
|
||||
|
||||
if (highlightResult.showInMentions)
|
||||
{
|
||||
this->message().flags.set(MessageFlag::ShowInMentions);
|
||||
|
@ -199,6 +193,15 @@ void SharedMessageBuilder::appendChannelName()
|
|||
}
|
||||
|
||||
void SharedMessageBuilder::triggerHighlights()
|
||||
{
|
||||
SharedMessageBuilder::triggerHighlights(
|
||||
this->channel->getName(), this->highlightSound_,
|
||||
this->highlightSoundCustomUrl_, this->highlightAlert_);
|
||||
}
|
||||
|
||||
void SharedMessageBuilder::triggerHighlights(
|
||||
const QString &channelName, bool playSound,
|
||||
const std::optional<QUrl> &customSoundUrl, bool windowAlert)
|
||||
{
|
||||
if (isInStreamerMode() && getSettings()->streamerModeMuteMentions)
|
||||
{
|
||||
|
@ -206,21 +209,32 @@ void SharedMessageBuilder::triggerHighlights()
|
|||
return;
|
||||
}
|
||||
|
||||
if (getSettings()->isMutedChannel(this->channel->getName()))
|
||||
if (getSettings()->isMutedChannel(channelName))
|
||||
{
|
||||
// Do nothing. Pings are muted in this channel.
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||
const bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||
const bool resolveFocus =
|
||||
!hasFocus || getSettings()->highlightAlwaysPlaySound;
|
||||
|
||||
if (this->highlightSound_ && resolveFocus)
|
||||
if (playSound && resolveFocus)
|
||||
{
|
||||
getIApp()->getSound()->play(this->highlightSoundUrl_);
|
||||
// TODO(C++23): optional or_else
|
||||
QUrl soundUrl;
|
||||
if (customSoundUrl)
|
||||
{
|
||||
soundUrl = *customSoundUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
soundUrl = getFallbackHighlightSound();
|
||||
}
|
||||
getIApp()->getSound()->play(soundUrl);
|
||||
}
|
||||
|
||||
if (this->highlightAlert_)
|
||||
if (windowAlert)
|
||||
{
|
||||
getApp()->windows->sendAlert();
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#include <QColor>
|
||||
#include <QUrl>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Badge;
|
||||
|
@ -57,6 +59,9 @@ protected:
|
|||
|
||||
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
|
||||
virtual void parseHighlights();
|
||||
static void triggerHighlights(const QString &channelName, bool playSound,
|
||||
const std::optional<QUrl> &customSoundUrl,
|
||||
bool windowAlert);
|
||||
|
||||
void appendChannelName();
|
||||
|
||||
|
@ -72,8 +77,7 @@ protected:
|
|||
|
||||
bool highlightAlert_ = false;
|
||||
bool highlightSound_ = false;
|
||||
|
||||
QUrl highlightSoundUrl_;
|
||||
std::optional<QUrl> highlightSoundCustomUrl_{};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -196,8 +196,10 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
|||
}
|
||||
|
||||
// Painting
|
||||
void MessageLayout::paint(const MessagePaintContext &ctx)
|
||||
MessagePaintResult MessageLayout::paint(const MessagePaintContext &ctx)
|
||||
{
|
||||
MessagePaintResult result;
|
||||
|
||||
QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth);
|
||||
|
||||
if (!this->bufferValid_)
|
||||
|
@ -209,7 +211,8 @@ void MessageLayout::paint(const MessagePaintContext &ctx)
|
|||
ctx.painter.drawPixmap(0, ctx.y, *pixmap);
|
||||
|
||||
// draw gif emotes
|
||||
this->container_.paintAnimatedElements(ctx.painter, ctx.y);
|
||||
result.hasAnimatedElements =
|
||||
this->container_.paintAnimatedElements(ctx.painter, ctx.y);
|
||||
|
||||
// draw disabled
|
||||
if (this->message_->flags.has(MessageFlag::Disabled))
|
||||
|
@ -270,6 +273,8 @@ void MessageLayout::paint(const MessagePaintContext &ctx)
|
|||
}
|
||||
|
||||
this->bufferValid_ = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width)
|
||||
|
@ -337,9 +342,13 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
|
|||
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
|
||||
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
|
||||
{
|
||||
// Blend highlight color with usual background color
|
||||
backgroundColor =
|
||||
blendColors(backgroundColor, *this->message_->highlightColor);
|
||||
assert(this->message_->highlightColor);
|
||||
if (this->message_->highlightColor)
|
||||
{
|
||||
// Blend highlight color with usual background color
|
||||
backgroundColor =
|
||||
blendColors(backgroundColor, *this->message_->highlightColor);
|
||||
}
|
||||
}
|
||||
else if (this->message_->flags.has(MessageFlag::Subscription) &&
|
||||
ctx.preferences.enableSubHighlight)
|
||||
|
@ -358,7 +367,8 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
|
|||
blendColors(backgroundColor,
|
||||
*ctx.colorProvider.color(ColorType::RedeemedHighlight));
|
||||
}
|
||||
else if (this->message_->flags.has(MessageFlag::AutoMod))
|
||||
else if (this->message_->flags.has(MessageFlag::AutoMod) ||
|
||||
this->message_->flags.has(MessageFlag::LowTrustUsers))
|
||||
{
|
||||
backgroundColor = QColor("#404040");
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ enum class MessageLayoutFlag : uint8_t {
|
|||
};
|
||||
using MessageLayoutFlags = FlagsEnum<MessageLayoutFlag>;
|
||||
|
||||
struct MessagePaintResult {
|
||||
bool hasAnimatedElements = false;
|
||||
};
|
||||
|
||||
class MessageLayout
|
||||
{
|
||||
public:
|
||||
|
@ -55,7 +59,7 @@ public:
|
|||
bool layout(int width, float scale_, MessageElementFlags flags);
|
||||
|
||||
// Painting
|
||||
void paint(const MessagePaintContext &ctx);
|
||||
MessagePaintResult paint(const MessagePaintContext &ctx);
|
||||
void invalidateBuffer();
|
||||
void deleteBuffer();
|
||||
void deleteCache();
|
||||
|
|
|
@ -235,13 +235,15 @@ void MessageLayoutContainer::paintElements(QPainter &painter,
|
|||
}
|
||||
}
|
||||
|
||||
void MessageLayoutContainer::paintAnimatedElements(QPainter &painter,
|
||||
bool MessageLayoutContainer::paintAnimatedElements(QPainter &painter,
|
||||
int yOffset) const
|
||||
{
|
||||
bool anyAnimatedElement = false;
|
||||
for (const auto &element : this->elements_)
|
||||
{
|
||||
element->paintAnimated(painter, yOffset);
|
||||
anyAnimatedElement |= element->paintAnimated(painter, yOffset);
|
||||
}
|
||||
return anyAnimatedElement;
|
||||
}
|
||||
|
||||
void MessageLayoutContainer::paintSelection(QPainter &painter,
|
||||
|
|
|
@ -64,8 +64,9 @@ struct MessageLayoutContainer {
|
|||
|
||||
/**
|
||||
* Paint the animated elements in this message
|
||||
* @returns true if this container contains at least one animated element
|
||||
*/
|
||||
void paintAnimatedElements(QPainter &painter, int yOffset) const;
|
||||
bool paintAnimatedElements(QPainter &painter, int yOffset) const;
|
||||
|
||||
/**
|
||||
* Paint the selection for this container
|
||||
|
|
|
@ -47,6 +47,12 @@ void MessagePreferences::connectSettings(Settings *settings,
|
|||
},
|
||||
holder);
|
||||
|
||||
settings->enableAutomodHighlight.connect(
|
||||
[this](const auto &newValue) {
|
||||
this->enableAutomodHighlight = newValue;
|
||||
},
|
||||
holder);
|
||||
|
||||
settings->alternateMessages.connect(
|
||||
[this](const auto &newValue) {
|
||||
this->alternateMessages = newValue;
|
||||
|
|
|
@ -39,6 +39,7 @@ struct MessagePreferences {
|
|||
bool enableElevatedMessageHighlight{};
|
||||
bool enableFirstMessageHighlight{};
|
||||
bool enableSubHighlight{};
|
||||
bool enableAutomodHighlight{};
|
||||
|
||||
bool alternateMessages{};
|
||||
bool separateMessages{};
|
||||
|
|
|
@ -153,11 +153,11 @@ void ImageLayoutElement::paint(QPainter &painter,
|
|||
}
|
||||
}
|
||||
|
||||
void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
bool ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
{
|
||||
if (this->image_ == nullptr)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->image_->animated())
|
||||
|
@ -167,8 +167,10 @@ void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
|||
auto rect = this->getRect();
|
||||
rect.moveTop(rect.y() + yOffset);
|
||||
painter.drawPixmap(QRectF(rect), *pixmap, QRectF());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int ImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
|
@ -265,7 +267,7 @@ void LayeredImageLayoutElement::paint(QPainter &painter,
|
|||
}
|
||||
}
|
||||
|
||||
void LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
bool LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
{
|
||||
auto fullRect = QRectF(this->getRect());
|
||||
fullRect.moveTop(fullRect.y() + yOffset);
|
||||
|
@ -297,6 +299,7 @@ void LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
|||
}
|
||||
}
|
||||
}
|
||||
return animatedFlag;
|
||||
}
|
||||
|
||||
int LayeredImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
|
@ -446,8 +449,9 @@ void TextLayoutElement::paint(QPainter &painter,
|
|||
QTextOption(Qt::AlignLeft | Qt::AlignTop));
|
||||
}
|
||||
|
||||
void TextLayoutElement::paintAnimated(QPainter &, int)
|
||||
bool TextLayoutElement::paintAnimated(QPainter & /*painter*/, int /*yOffset*/)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int TextLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
|
@ -567,8 +571,10 @@ void TextIconLayoutElement::paint(QPainter &painter,
|
|||
}
|
||||
}
|
||||
|
||||
void TextIconLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
bool TextIconLayoutElement::paintAnimated(QPainter & /*painter*/,
|
||||
int /*yOffset*/)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int TextIconLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
|
@ -640,8 +646,10 @@ void ReplyCurveLayoutElement::paint(QPainter &painter,
|
|||
painter.drawPath(path);
|
||||
}
|
||||
|
||||
void ReplyCurveLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
bool ReplyCurveLayoutElement::paintAnimated(QPainter & /*painter*/,
|
||||
int /*yOffset*/)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int ReplyCurveLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
|
|
|
@ -52,7 +52,8 @@ public:
|
|||
virtual size_t getSelectionIndexCount() const = 0;
|
||||
virtual void paint(QPainter &painter,
|
||||
const MessageColors &messageColors) = 0;
|
||||
virtual void paintAnimated(QPainter &painter, int yOffset) = 0;
|
||||
/// @returns true if anything was painted
|
||||
virtual bool paintAnimated(QPainter &painter, int yOffset) = 0;
|
||||
virtual int getMouseOverIndex(const QPoint &abs) const = 0;
|
||||
virtual int getXFromIndex(size_t index) = 0;
|
||||
|
||||
|
@ -86,7 +87,7 @@ protected:
|
|||
uint32_t to = UINT32_MAX) const override;
|
||||
size_t getSelectionIndexCount() const override;
|
||||
void paint(QPainter &painter, const MessageColors &messageColors) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
bool paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(size_t index) override;
|
||||
|
||||
|
@ -105,7 +106,7 @@ protected:
|
|||
uint32_t to = UINT32_MAX) const override;
|
||||
size_t getSelectionIndexCount() const override;
|
||||
void paint(QPainter &painter, const MessageColors &messageColors) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
bool paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(size_t index) override;
|
||||
|
||||
|
@ -158,7 +159,7 @@ protected:
|
|||
uint32_t to = UINT32_MAX) const override;
|
||||
size_t getSelectionIndexCount() const override;
|
||||
void paint(QPainter &painter, const MessageColors &messageColors) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
bool paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(size_t index) override;
|
||||
|
||||
|
@ -182,7 +183,7 @@ protected:
|
|||
uint32_t to = UINT32_MAX) const override;
|
||||
size_t getSelectionIndexCount() const override;
|
||||
void paint(QPainter &painter, const MessageColors &messageColors) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
bool paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(size_t index) override;
|
||||
|
||||
|
@ -200,7 +201,7 @@ public:
|
|||
|
||||
protected:
|
||||
void paint(QPainter &painter, const MessageColors &messageColors) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
bool paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(size_t index) override;
|
||||
void addCopyTextToString(QString &str, uint32_t from = 0,
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
#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
|
|
@ -1,14 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
# include <client/crashpad_client.h>
|
||||
|
||||
# include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::unique_ptr<crashpad::CrashpadClient> installCrashHandler();
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
|
@ -5,15 +5,12 @@
|
|||
#include "messages/LimitedQueueSnapshot.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
const int RECONNECT_BASE_INTERVAL = 2000;
|
||||
// 60 falloff counter means it will try to reconnect at most every 60*2 seconds
|
||||
const int MAX_FALLOFF_COUNTER = 60;
|
||||
|
||||
// Ratelimits for joinBucket_
|
||||
const int JOIN_RATELIMIT_BUDGET = 18;
|
||||
const int JOIN_RATELIMIT_COOLDOWN = 12500;
|
||||
|
@ -88,6 +85,9 @@ AbstractIrcServer::AbstractIrcServer()
|
|||
}
|
||||
this->readConnection_->smartReconnect();
|
||||
});
|
||||
this->connections_.managedConnect(this->readConnection_->heartbeat, [this] {
|
||||
this->markChannelsConnected();
|
||||
});
|
||||
}
|
||||
|
||||
void AbstractIrcServer::initializeIrc()
|
||||
|
@ -331,8 +331,6 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection)
|
|||
{
|
||||
chan->addMessage(connectedMsg);
|
||||
}
|
||||
|
||||
chan->connected.invoke();
|
||||
}
|
||||
|
||||
this->falloffCounter_ = 1;
|
||||
|
@ -360,9 +358,24 @@ void AbstractIrcServer::onDisconnected()
|
|||
}
|
||||
|
||||
chan->addMessage(disconnectedMsg);
|
||||
|
||||
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
|
||||
{
|
||||
channel->markDisconnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractIrcServer::markChannelsConnected()
|
||||
{
|
||||
this->forEachChannel([](const ChannelPtr &chan) {
|
||||
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
|
||||
{
|
||||
channel->markConnected();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> AbstractIrcServer::getCustomChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
|
|
|
@ -73,6 +73,7 @@ protected:
|
|||
virtual void onReadConnected(IrcConnection *connection);
|
||||
virtual void onWriteConnected(IrcConnection *connection);
|
||||
virtual void onDisconnected();
|
||||
void markChannelsConnected();
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelName);
|
||||
|
|
|
@ -16,7 +16,7 @@ IrcConnection::IrcConnection(QObject *parent)
|
|||
{
|
||||
// Log connection errors for ease-of-debugging
|
||||
QObject::connect(this, &Communi::IrcConnection::socketError, this,
|
||||
[this](QAbstractSocket::SocketError error) {
|
||||
[](QAbstractSocket::SocketError error) {
|
||||
qCDebug(chatterinoIrc) << "Connection error:" << error;
|
||||
});
|
||||
|
||||
|
@ -64,6 +64,7 @@ IrcConnection::IrcConnection(QObject *parent)
|
|||
// If we're still receiving messages, all is well
|
||||
this->recentlyReceivedMessage_ = false;
|
||||
this->waitingForPong_ = false;
|
||||
this->heartbeat.invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ public:
|
|||
// receiver to trigger a reconnect, if desired
|
||||
pajlada::Signals::Signal<bool> connectionLost;
|
||||
|
||||
// Signal to indicate the connection is still healthy
|
||||
pajlada::Signals::NoArgSignal heartbeat;
|
||||
|
||||
// Request a reconnect with a minimum interval between attempts.
|
||||
// This won't violate RECONNECT_MIN_INTERVAL
|
||||
void smartReconnect();
|
||||
|
|
|
@ -18,72 +18,84 @@ namespace chatterino::recentmessages {
|
|||
|
||||
using namespace recentmessages::detail;
|
||||
|
||||
void load(const QString &channelName, std::weak_ptr<Channel> channelPtr,
|
||||
ResultCallback onLoaded, ErrorCallback onError)
|
||||
void load(
|
||||
const QString &channelName, std::weak_ptr<Channel> channelPtr,
|
||||
ResultCallback onLoaded, ErrorCallback onError, const int limit,
|
||||
const std::optional<std::chrono::time_point<std::chrono::system_clock>>
|
||||
after,
|
||||
const std::optional<std::chrono::time_point<std::chrono::system_clock>>
|
||||
before,
|
||||
const bool jitter)
|
||||
{
|
||||
qCDebug(LOG) << "Loading recent messages for" << channelName;
|
||||
|
||||
const auto url = constructRecentMessagesUrl(channelName);
|
||||
const auto url =
|
||||
constructRecentMessagesUrl(channelName, limit, after, before);
|
||||
|
||||
NetworkRequest(url)
|
||||
.onSuccess([channelPtr, onLoaded](const auto &result) {
|
||||
auto shared = channelPtr.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(LOG) << "Successfully loaded recent messages for"
|
||||
<< shared->getName();
|
||||
|
||||
auto root = result.parseJson();
|
||||
auto parsedMessages = parseRecentMessages(root);
|
||||
|
||||
// build the Communi messages into chatterino messages
|
||||
auto builtMessages =
|
||||
buildRecentMessages(parsedMessages, shared.get());
|
||||
|
||||
postToThread([shared = std::move(shared), root = std::move(root),
|
||||
messages = std::move(builtMessages),
|
||||
onLoaded]() mutable {
|
||||
// Notify user about a possible gap in logs if it returned some messages
|
||||
// but isn't currently joined to a channel
|
||||
const auto errorCode = root.value("error_code").toString();
|
||||
if (!errorCode.isEmpty())
|
||||
const long delayMs = jitter ? std::rand() % 100 : 0;
|
||||
QTimer::singleShot(delayMs, [=] {
|
||||
NetworkRequest(url)
|
||||
.onSuccess([channelPtr, onLoaded](const auto &result) {
|
||||
auto shared = channelPtr.lock();
|
||||
if (!shared)
|
||||
{
|
||||
qCDebug(LOG)
|
||||
<< QString("Got error from API: error_code=%1, "
|
||||
"channel=%2")
|
||||
.arg(errorCode, shared->getName());
|
||||
if (errorCode == "channel_not_joined" && !messages.empty())
|
||||
{
|
||||
shared->addMessage(makeSystemMessage(
|
||||
"Message history service recovering, there may "
|
||||
"be gaps in the message history."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onLoaded(messages);
|
||||
});
|
||||
})
|
||||
.onError([channelPtr, onError](const NetworkResult &result) {
|
||||
auto shared = channelPtr.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
qCDebug(LOG) << "Successfully loaded recent messages for"
|
||||
<< shared->getName();
|
||||
|
||||
qCDebug(LOG) << "Failed to load recent messages for"
|
||||
<< shared->getName();
|
||||
auto root = result.parseJson();
|
||||
auto parsedMessages = parseRecentMessages(root);
|
||||
|
||||
shared->addMessage(makeSystemMessage(
|
||||
QStringLiteral(
|
||||
"Message history service unavailable (Error: %1)")
|
||||
.arg(result.formatError())));
|
||||
// build the Communi messages into chatterino messages
|
||||
auto builtMessages =
|
||||
buildRecentMessages(parsedMessages, shared.get());
|
||||
|
||||
onError();
|
||||
})
|
||||
.execute();
|
||||
postToThread([shared = std::move(shared),
|
||||
root = std::move(root),
|
||||
messages = std::move(builtMessages),
|
||||
onLoaded]() mutable {
|
||||
// Notify user about a possible gap in logs if it returned some messages
|
||||
// but isn't currently joined to a channel
|
||||
const auto errorCode = root.value("error_code").toString();
|
||||
if (!errorCode.isEmpty())
|
||||
{
|
||||
qCDebug(LOG)
|
||||
<< QString("Got error from API: error_code=%1, "
|
||||
"channel=%2")
|
||||
.arg(errorCode, shared->getName());
|
||||
if (errorCode == "channel_not_joined" &&
|
||||
!messages.empty())
|
||||
{
|
||||
shared->addMessage(makeSystemMessage(
|
||||
"Message history service recovering, there may "
|
||||
"be gaps in the message history."));
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded(messages);
|
||||
});
|
||||
})
|
||||
.onError([channelPtr, onError](const NetworkResult &result) {
|
||||
auto shared = channelPtr.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(LOG) << "Failed to load recent messages for"
|
||||
<< shared->getName();
|
||||
|
||||
shared->addMessage(makeSystemMessage(
|
||||
QStringLiteral(
|
||||
"Message history service unavailable (Error: %1)")
|
||||
.arg(result.formatError())));
|
||||
|
||||
onError();
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino::recentmessages
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
#include <QString>
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -28,8 +30,16 @@ using ErrorCallback = std::function<void()>;
|
|||
* @param channelPtr Weak pointer to Channel to use to build messages
|
||||
* @param onLoaded Callback taking the built messages as a const std::vector<MessagePtr> &
|
||||
* @param onError Callback called when the network request fails
|
||||
* @param limit Maximum number of messages to query
|
||||
* @param after Only return messages that were received after this timestamp; ignored if `std::nullopt`
|
||||
* @param before Only return messages that were received before this timestamp; ignored if `std::nullopt`
|
||||
* @param jitter Whether to delay the request by a small random duration
|
||||
*/
|
||||
void load(const QString &channelName, std::weak_ptr<Channel> channelPtr,
|
||||
ResultCallback onLoaded, ErrorCallback onError);
|
||||
void load(
|
||||
const QString &channelName, std::weak_ptr<Channel> channelPtr,
|
||||
ResultCallback onLoaded, ErrorCallback onError, int limit,
|
||||
std::optional<std::chrono::time_point<std::chrono::system_clock>> after,
|
||||
std::optional<std::chrono::time_point<std::chrono::system_clock>> before,
|
||||
bool jitter);
|
||||
|
||||
} // namespace chatterino::recentmessages
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/FormatTime.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
@ -94,14 +93,34 @@ std::vector<MessagePtr> buildRecentMessages(
|
|||
|
||||
// Returns the URL to be used for querying the Recent Messages API for the
|
||||
// given channel.
|
||||
QUrl constructRecentMessagesUrl(const QString &name)
|
||||
QUrl constructRecentMessagesUrl(
|
||||
const QString &name, const int limit,
|
||||
const std::optional<std::chrono::time_point<std::chrono::system_clock>>
|
||||
after,
|
||||
const std::optional<std::chrono::time_point<std::chrono::system_clock>>
|
||||
before)
|
||||
{
|
||||
QUrl url(Env::get().recentMessagesApiUrl.arg(name));
|
||||
QUrlQuery urlQuery(url);
|
||||
if (!urlQuery.hasQueryItem("limit"))
|
||||
{
|
||||
urlQuery.addQueryItem("limit", QString::number(limit));
|
||||
}
|
||||
if (after.has_value())
|
||||
{
|
||||
urlQuery.addQueryItem(
|
||||
"limit", QString::number(getSettings()->twitchMessageHistoryLimit));
|
||||
"after", QString::number(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
after->time_since_epoch())
|
||||
.count()));
|
||||
}
|
||||
if (before.has_value())
|
||||
{
|
||||
urlQuery.addQueryItem(
|
||||
"before", QString::number(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
before->time_since_epoch())
|
||||
.count()));
|
||||
}
|
||||
url.setQuery(urlQuery);
|
||||
return url;
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino::recentmessages::detail {
|
||||
|
@ -24,6 +26,9 @@ std::vector<MessagePtr> buildRecentMessages(
|
|||
|
||||
// Returns the URL to be used for querying the Recent Messages API for the
|
||||
// given channel.
|
||||
QUrl constructRecentMessagesUrl(const QString &name);
|
||||
QUrl constructRecentMessagesUrl(
|
||||
const QString &name, int limit,
|
||||
std::optional<std::chrono::time_point<std::chrono::system_clock>> after,
|
||||
std::optional<std::chrono::time_point<std::chrono::system_clock>> before);
|
||||
|
||||
} // namespace chatterino::recentmessages::detail
|
||||
|
|
|
@ -44,12 +44,13 @@ using namespace chatterino;
|
|||
|
||||
// Message types below are the ones that might contain special user's message on USERNOTICE
|
||||
const QSet<QString> SPECIAL_MESSAGE_TYPES{
|
||||
"sub", //
|
||||
"subgift", //
|
||||
"resub", // resub messages
|
||||
"bitsbadgetier", // bits badge upgrade
|
||||
"ritual", // new viewer ritual
|
||||
"announcement", // new mod announcement thing
|
||||
"sub", //
|
||||
"subgift", //
|
||||
"resub", // resub messages
|
||||
"bitsbadgetier", // bits badge upgrade
|
||||
"ritual", // new viewer ritual
|
||||
"announcement", // new mod announcement thing
|
||||
"viewermilestone", // watch streak, but other categories possible in future
|
||||
};
|
||||
|
||||
MessagePtr generateBannedMessage(bool confirmedBan)
|
||||
|
@ -1136,6 +1137,7 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
|
|||
getApp()->accounts->twitch.getCurrent()->getUserName())
|
||||
{
|
||||
twitchChannel->addMessage(makeSystemMessage("joined channel"));
|
||||
twitchChannel->joined.invoke();
|
||||
}
|
||||
else if (getSettings()->showJoins.getValue())
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "providers/twitch/PubSubHelpers.hpp"
|
||||
#include "providers/twitch/PubSubMessages.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "pubsubmessages/LowTrustUsers.hpp"
|
||||
#include "util/DebugCount.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
@ -210,7 +211,6 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
}
|
||||
|
||||
action.target.login = args[0].toString();
|
||||
bool ok;
|
||||
action.messageText = args[1].toString();
|
||||
action.messageId = args[2].toString();
|
||||
|
||||
|
@ -586,6 +586,25 @@ void PubSub::unlistenAutomod()
|
|||
}
|
||||
}
|
||||
|
||||
void PubSub::unlistenLowTrustUsers()
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
{
|
||||
const auto &client = p.second;
|
||||
if (const auto &[topics, nonce] =
|
||||
client->unlistenPrefix("low-trust-users.");
|
||||
!topics.empty())
|
||||
{
|
||||
this->registerNonce(nonce, {
|
||||
client,
|
||||
"UNLISTEN",
|
||||
topics,
|
||||
topics.size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PubSub::unlistenWhispers()
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
|
@ -671,6 +690,30 @@ void PubSub::listenToAutomod(const QString &channelID)
|
|||
this->listenToTopic(topic);
|
||||
}
|
||||
|
||||
void PubSub::listenToLowTrustUsers(const QString &channelID)
|
||||
{
|
||||
if (this->userID_.isEmpty())
|
||||
{
|
||||
qCDebug(chatterinoPubSub)
|
||||
<< "Unable to listen to low trust users topic, no user logged in";
|
||||
return;
|
||||
}
|
||||
|
||||
static const QString topicFormat("low-trust-users.%1.%2");
|
||||
assert(!channelID.isEmpty());
|
||||
|
||||
auto topic = topicFormat.arg(this->userID_, channelID);
|
||||
|
||||
if (this->isListeningToTopic(topic))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(chatterinoPubSub) << "Listen to topic" << topic;
|
||||
|
||||
this->listenToTopic(topic);
|
||||
}
|
||||
|
||||
void PubSub::listenToChannelPointRewards(const QString &channelID)
|
||||
{
|
||||
static const QString topicFormat("community-points-channel-v1.%1");
|
||||
|
@ -1170,6 +1213,38 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
|
|||
this->signals_.moderation.autoModMessageCaught.invoke(innerMessage,
|
||||
channelID);
|
||||
}
|
||||
else if (topic.startsWith("low-trust-users."))
|
||||
{
|
||||
auto oInnerMessage = message.toInner<PubSubLowTrustUsersMessage>();
|
||||
if (!oInnerMessage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto innerMessage = *oInnerMessage;
|
||||
|
||||
switch (innerMessage.type)
|
||||
{
|
||||
case PubSubLowTrustUsersMessage::Type::UserMessage: {
|
||||
this->signals_.moderation.suspiciousMessageReceived.invoke(
|
||||
innerMessage);
|
||||
}
|
||||
break;
|
||||
|
||||
case PubSubLowTrustUsersMessage::Type::TreatmentUpdate: {
|
||||
this->signals_.moderation.suspiciousTreatmentUpdated.invoke(
|
||||
innerMessage);
|
||||
}
|
||||
break;
|
||||
|
||||
case PubSubLowTrustUsersMessage::Type::INVALID: {
|
||||
qCWarning(chatterinoPubSub)
|
||||
<< "Invalid low trust users event type:"
|
||||
<< innerMessage.typeString;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoPubSub) << "Unknown topic:" << topic;
|
||||
|
|
|
@ -34,6 +34,7 @@ struct PubSubAutoModQueueMessage;
|
|||
struct AutomodAction;
|
||||
struct AutomodUserAction;
|
||||
struct AutomodInfoAction;
|
||||
struct PubSubLowTrustUsersMessage;
|
||||
struct PubSubWhisperMessage;
|
||||
|
||||
struct PubSubListenMessage;
|
||||
|
@ -67,9 +68,6 @@ class PubSub
|
|||
QString userID_;
|
||||
|
||||
public:
|
||||
// The max amount of connections we may open
|
||||
static constexpr int maxConnections = 10;
|
||||
|
||||
PubSub(const QString &host,
|
||||
std::chrono::seconds pingInterval = std::chrono::seconds(15));
|
||||
|
||||
|
@ -100,6 +98,9 @@ public:
|
|||
Signal<BanAction> userBanned;
|
||||
Signal<UnbanAction> userUnbanned;
|
||||
|
||||
Signal<PubSubLowTrustUsersMessage> suspiciousMessageReceived;
|
||||
Signal<PubSubLowTrustUsersMessage> suspiciousTreatmentUpdated;
|
||||
|
||||
// Message caught by automod
|
||||
// channelID
|
||||
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
|
||||
|
@ -126,12 +127,56 @@ public:
|
|||
|
||||
void unlistenAllModerationActions();
|
||||
void unlistenAutomod();
|
||||
void unlistenLowTrustUsers();
|
||||
void unlistenWhispers();
|
||||
|
||||
/**
|
||||
* Listen to incoming whispers for the currently logged in user.
|
||||
* This topic is relevant for everyone.
|
||||
*
|
||||
* PubSub topic: whispers.{currentUserID}
|
||||
*/
|
||||
bool listenToWhispers();
|
||||
|
||||
/**
|
||||
* Listen to moderation actions in the given channel.
|
||||
* This topic is relevant for everyone.
|
||||
* For moderators, this topic includes blocked/permitted terms updates,
|
||||
* roomstate changes, general mod/vip updates, all bans/timeouts/deletions.
|
||||
* For normal users, this topic includes moderation actions that are targetted at the local user:
|
||||
* automod catching a user's sent message, a moderator approving or denying their caught messages,
|
||||
* the user gaining/losing mod/vip, the user receiving a ban/timeout/deletion.
|
||||
*
|
||||
* PubSub topic: chat_moderator_actions.{currentUserID}.{channelID}
|
||||
*/
|
||||
void listenToChannelModerationActions(const QString &channelID);
|
||||
|
||||
/**
|
||||
* Listen to Automod events in the given channel.
|
||||
* This topic is only relevant for moderators.
|
||||
* This will send events about incoming messages that
|
||||
* are caught by Automod.
|
||||
*
|
||||
* PubSub topic: automod-queue.{currentUserID}.{channelID}
|
||||
*/
|
||||
void listenToAutomod(const QString &channelID);
|
||||
|
||||
/**
|
||||
* Listen to Low Trust events in the given channel.
|
||||
* This topic is only relevant for moderators.
|
||||
* This will fire events about suspicious treatment updates
|
||||
* and messages sent by restricted/monitored users.
|
||||
*
|
||||
* PubSub topic: low-trust-users.{currentUserID}.{channelID}
|
||||
*/
|
||||
void listenToLowTrustUsers(const QString &channelID);
|
||||
|
||||
/**
|
||||
* Listen to incoming channel point redemptions in the given channel.
|
||||
* This topic is relevant for everyone.
|
||||
*
|
||||
* PubSub topic: community-points-channel-v1.{channelID}
|
||||
*/
|
||||
void listenToChannelPointRewards(const QString &channelID);
|
||||
|
||||
std::vector<QString> requests;
|
||||
|
|
|
@ -56,7 +56,6 @@ namespace {
|
|||
#else
|
||||
const QString MAGIC_MESSAGE_SUFFIX = QString::fromUtf8(u8" \U000E0000");
|
||||
#endif
|
||||
constexpr int TITLE_REFRESH_PERIOD = 10000;
|
||||
constexpr int CLIP_CREATION_COOLDOWN = 5000;
|
||||
const QString CLIPS_LINK("https://clips.twitch.tv/%1");
|
||||
const QString CLIPS_FAILURE_CLIPS_DISABLED_TEXT(
|
||||
|
@ -103,17 +102,13 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
|
||||
// We can safely ignore this signal connection this has no external dependencies - once the signal
|
||||
// is destroyed, it will no longer be able to fire
|
||||
std::ignore = this->connected.connect([this]() {
|
||||
if (this->roomId().isEmpty())
|
||||
std::ignore = this->joined.connect([this]() {
|
||||
if (this->disconnected_)
|
||||
{
|
||||
// If we get a reconnected event when the room id is not set, we
|
||||
// just connected for the first time. After receiving the first
|
||||
// message from a channel, setRoomId is called and further
|
||||
// invocations of this event will load recent messages.
|
||||
return;
|
||||
this->loadRecentMessagesReconnect();
|
||||
this->lastConnectedAt_ = std::chrono::system_clock::now();
|
||||
this->disconnected_ = false;
|
||||
}
|
||||
|
||||
this->loadRecentMessagesReconnect();
|
||||
});
|
||||
|
||||
// timers
|
||||
|
@ -737,6 +732,8 @@ void TwitchChannel::setRoomId(const QString &id)
|
|||
*this->roomID_.access() = id;
|
||||
this->roomIdChanged();
|
||||
this->loadRecentMessages();
|
||||
this->disconnected_ = false;
|
||||
this->lastConnectedAt_ = std::chrono::system_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1111,6 +1108,25 @@ bool TwitchChannel::setLive(bool newLiveStatus)
|
|||
return true;
|
||||
}
|
||||
|
||||
void TwitchChannel::markConnected()
|
||||
{
|
||||
if (this->lastConnectedAt_.has_value() && !this->disconnected_)
|
||||
{
|
||||
this->lastConnectedAt_ = std::chrono::system_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchChannel::markDisconnected()
|
||||
{
|
||||
if (this->roomId().isEmpty())
|
||||
{
|
||||
// we were never joined in the first place
|
||||
return;
|
||||
}
|
||||
|
||||
this->disconnected_ = true;
|
||||
}
|
||||
|
||||
void TwitchChannel::loadRecentMessages()
|
||||
{
|
||||
if (!getSettings()->loadTwitchMessageHistoryOnConnect)
|
||||
|
@ -1163,7 +1179,9 @@ void TwitchChannel::loadRecentMessages()
|
|||
return;
|
||||
|
||||
tc->loadingRecentMessages_.clear();
|
||||
});
|
||||
},
|
||||
getSettings()->twitchMessageHistoryLimit.getValue(), std::nullopt,
|
||||
std::nullopt, false);
|
||||
}
|
||||
|
||||
void TwitchChannel::loadRecentMessagesReconnect()
|
||||
|
@ -1178,6 +1196,21 @@ void TwitchChannel::loadRecentMessagesReconnect()
|
|||
return; // already loading
|
||||
}
|
||||
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
int limit = getSettings()->twitchMessageHistoryLimit.getValue();
|
||||
if (this->lastConnectedAt_.has_value())
|
||||
{
|
||||
// calculate how many messages could have occured
|
||||
// while we were not connected to the channel
|
||||
// assuming a maximum of 10 messages per second
|
||||
const auto secondsSinceDisconnect =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
now - this->lastConnectedAt_.value())
|
||||
.count();
|
||||
limit =
|
||||
std::min(static_cast<int>(secondsSinceDisconnect + 1) * 10, limit);
|
||||
}
|
||||
|
||||
auto weak = weakOf<Channel>(this);
|
||||
recentmessages::load(
|
||||
this->getName(), weak,
|
||||
|
@ -1203,7 +1236,8 @@ void TwitchChannel::loadRecentMessagesReconnect()
|
|||
return;
|
||||
|
||||
tc->loadingRecentMessages_.clear();
|
||||
});
|
||||
},
|
||||
limit, this->lastConnectedAt_, now, true);
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshPubSub()
|
||||
|
@ -1219,7 +1253,11 @@ void TwitchChannel::refreshPubSub()
|
|||
getApp()->twitch->pubsub->setAccount(currentAccount);
|
||||
|
||||
getApp()->twitch->pubsub->listenToChannelModerationActions(roomId);
|
||||
getApp()->twitch->pubsub->listenToAutomod(roomId);
|
||||
if (this->hasModRights())
|
||||
{
|
||||
getApp()->twitch->pubsub->listenToAutomod(roomId);
|
||||
getApp()->twitch->pubsub->listenToLowTrustUsers(roomId);
|
||||
}
|
||||
getApp()->twitch->pubsub->listenToChannelPointRewards(roomId);
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue