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]
|
[Flatpak Ref]
|
||||||
Name=com.chatterino.chatterino
|
Name=com.chatterino.chatterino
|
||||||
Branch=nightly
|
Branch=beta
|
||||||
Title=com.chatterino.chatterino from flathub
|
Title=com.chatterino.chatterino from flathub
|
||||||
IsRuntime=false
|
IsRuntime=false
|
||||||
Url=https://dl.flathub.org/repo/
|
Url=https://dl.flathub.org/beta-repo/
|
||||||
SuggestRemoteName=flathub
|
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
|
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
|
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
|
@ -29,6 +29,7 @@ Checks: "-*,
|
||||||
-readability-function-cognitive-complexity,
|
-readability-function-cognitive-complexity,
|
||||||
-bugprone-easily-swappable-parameters,
|
-bugprone-easily-swappable-parameters,
|
||||||
-cert-err58-cpp,
|
-cert-err58-cpp,
|
||||||
|
-modernize-avoid-c-arrays
|
||||||
"
|
"
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
- key: readability-identifier-naming.ClassCase
|
- key: readability-identifier-naming.ClassCase
|
||||||
|
|
|
@ -36,7 +36,7 @@ NOTE: The AppImage from Ubuntu 22.04 is broken. Approach with caution
|
||||||
|
|
||||||
#### Testing
|
#### 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 .`
|
`docker buildx build -t chatterino-ubuntu-22.04-test -f .docker/Dockerfile-ubuntu-22.04-test .`
|
||||||
1. Run the tests
|
1. Run the tests
|
||||||
`docker run --rm --network=host chatterino-ubuntu-22.04-test`
|
`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
|
<!--
|
||||||
|
Please include a summary of what you've changed and what issue is fixed.
|
||||||
<!-- 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 -->
|
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: |
|
run: |
|
||||||
cd build
|
cd build
|
||||||
set cl=/MP
|
set cl=/MP
|
||||||
nmake /S /NOLOGO crashpad_handler
|
nmake /S /NOLOGO chatterino-crash-handler
|
||||||
mkdir Chatterino2/crashpad
|
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
|
7z a bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z bin/chatterino.pdb
|
||||||
|
|
||||||
- name: Prepare build dir (windows)
|
- name: Prepare build dir (windows)
|
||||||
|
@ -257,14 +257,14 @@ jobs:
|
||||||
|
|
||||||
- name: Upload artifact (Windows - binary)
|
- name: Upload artifact (Windows - binary)
|
||||||
if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact
|
if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
||||||
path: build/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)
|
- name: Upload artifact (Windows - symbols)
|
||||||
if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact
|
if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}-symbols.pdb.7z
|
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}-symbols.pdb.7z
|
||||||
path: build/bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z
|
path: build/bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z
|
||||||
|
@ -339,14 +339,14 @@ jobs:
|
||||||
|
|
||||||
- name: Upload artifact - AppImage (Ubuntu)
|
- name: Upload artifact - AppImage (Ubuntu)
|
||||||
if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact
|
if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Chatterino-x86_64-${{ matrix.qt-version }}.AppImage
|
name: Chatterino-x86_64-${{ matrix.qt-version }}.AppImage
|
||||||
path: build/Chatterino-x86_64.AppImage
|
path: build/Chatterino-x86_64.AppImage
|
||||||
|
|
||||||
- name: Upload artifact - .deb (Ubuntu)
|
- name: Upload artifact - .deb (Ubuntu)
|
||||||
if: startsWith(matrix.os, 'ubuntu') && !matrix.skip-artifact
|
if: startsWith(matrix.os, 'ubuntu') && !matrix.skip-artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}.deb
|
name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}.deb
|
||||||
path: build/Chatterino-${{ matrix.os }}-x86_64.deb
|
path: build/Chatterino-${{ matrix.os }}-x86_64.deb
|
||||||
|
@ -390,7 +390,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload artifact (MacOS)
|
- name: Upload artifact (MacOS)
|
||||||
if: startsWith(matrix.os, 'macos')
|
if: startsWith(matrix.os, 'macos')
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
name: chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
||||||
path: build/chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
path: build/chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
||||||
|
@ -404,49 +404,49 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # allows for tags access
|
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
|
name: Ubuntu 22.04 Qt6.2.4 deb
|
||||||
with:
|
with:
|
||||||
name: Chatterino-ubuntu-22.04-Qt-6.2.4.deb
|
name: Chatterino-ubuntu-22.04-Qt-6.2.4.deb
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: Windows Qt6.5.0
|
name: Windows Qt6.5.0
|
||||||
with:
|
with:
|
||||||
name: chatterino-windows-x86-64-Qt-6.5.0.zip
|
name: chatterino-windows-x86-64-Qt-6.5.0.zip
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: Windows Qt6.5.0 symbols
|
name: Windows Qt6.5.0 symbols
|
||||||
with:
|
with:
|
||||||
name: chatterino-windows-x86-64-Qt-6.5.0-symbols.pdb.7z
|
name: chatterino-windows-x86-64-Qt-6.5.0-symbols.pdb.7z
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: Windows Qt5.15.2
|
name: Windows Qt5.15.2
|
||||||
with:
|
with:
|
||||||
name: chatterino-windows-x86-64-Qt-5.15.2.zip
|
name: chatterino-windows-x86-64-Qt-5.15.2.zip
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: Linux Qt5.12.12 AppImage
|
name: Linux Qt5.12.12 AppImage
|
||||||
with:
|
with:
|
||||||
name: Chatterino-x86_64-5.12.12.AppImage
|
name: Chatterino-x86_64-5.12.12.AppImage
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: Ubuntu 20.04 Qt5.12.12 deb
|
name: Ubuntu 20.04 Qt5.12.12 deb
|
||||||
with:
|
with:
|
||||||
name: Chatterino-ubuntu-20.04-Qt-5.12.12.deb
|
name: Chatterino-ubuntu-20.04-Qt-5.12.12.deb
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: Ubuntu 22.04 Qt5.15.2 deb
|
name: Ubuntu 22.04 Qt5.15.2 deb
|
||||||
with:
|
with:
|
||||||
name: Chatterino-ubuntu-22.04-Qt-5.15.2.deb
|
name: Chatterino-ubuntu-22.04-Qt-5.15.2.deb
|
||||||
path: release-artifacts/
|
path: release-artifacts/
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
name: macOS x86_64 Qt5.15.2 dmg
|
name: macOS x86_64 Qt5.15.2 dmg
|
||||||
with:
|
with:
|
||||||
name: chatterino-macos-Qt-5.15.2.dmg
|
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
|
clangFormatVersion: 16
|
||||||
|
|
||||||
- name: Check line-endings
|
- 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
|
build_dir: build-clang-tidy
|
||||||
config_file: ".clang-tidy"
|
config_file: ".clang-tidy"
|
||||||
split_workflow: true
|
split_workflow: true
|
||||||
exclude: "lib/*"
|
exclude: "lib/*,tools/crash-handler/*"
|
||||||
cmake_command: >-
|
cmake_command: >-
|
||||||
cmake -S. -Bbuild-clang-tidy
|
cmake -S. -Bbuild-clang-tidy
|
||||||
-DCMAKE_BUILD_TYPE=Release
|
-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
|
fetch-depth: 0 # allows for tags access
|
||||||
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: dawidd6/action-download-artifact@v2
|
uses: dawidd6/action-download-artifact@v3
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
|
||||||
|
@ -52,7 +52,7 @@ jobs:
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
|
||||||
- name: Upload installer
|
- name: Upload installer
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: build/${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}.exe
|
path: build/${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}.exe
|
||||||
name: ${{ 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:
|
merge_group:
|
||||||
|
|
||||||
env:
|
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
|
QT_QPA_PLATFORM: minimal
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
@ -54,11 +54,7 @@ jobs:
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
brew install boost openssl rapidjson p7zip create-dmg cmake tree docker colima
|
brew install boost openssl rapidjson p7zip create-dmg cmake
|
||||||
|
|
||||||
- name: Setup Colima
|
|
||||||
run: |
|
|
||||||
colima start
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
@ -74,18 +70,27 @@ jobs:
|
||||||
..
|
..
|
||||||
make -j"$(sysctl -n hw.logicalcpu)"
|
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
|
- name: Test
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
run: |
|
run: |
|
||||||
docker pull kennethreitz/httpbin
|
httpbox --port 9051 &
|
||||||
docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
cd ../pubsub-server-test
|
||||||
docker run --network=host --detach ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
./server 127.0.0.1:9050 &
|
||||||
docker run -p 9051:80 --detach kennethreitz/httpbin
|
cd ../build-test
|
||||||
ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering
|
ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering
|
||||||
working-directory: build-test
|
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"]
|
[submodule "lib/lua/src"]
|
||||||
path = lib/lua/src
|
path = lib/lua/src
|
||||||
url = https://github.com/lua/lua
|
url = https://github.com/lua/lua
|
||||||
[submodule "lib/crashpad"]
|
[submodule "tools/crash-handler"]
|
||||||
path = lib/crashpad
|
path = tools/crash-handler
|
||||||
url = https://github.com/getsentry/crashpad
|
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
|
### 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
|
### 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.
|
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`._
|
_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+
|
### 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
|
## 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. Go to the project directory where you cloned Chatterino2 & its submodules
|
||||||
1. Create a build directory and go into it:
|
1. Create a build directory and go into it:
|
||||||
`mkdir build && cd build`
|
`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 ..`
|
`cmake -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@5 -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@1.1 ..`
|
||||||
1. Build:
|
1. Build:
|
||||||
`make`
|
`make`
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
# Building on Windows
|
# 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.
|
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
|
### 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:
|
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.
|
- You do not need to sign in with a Microsoft account after setup completes. You may simply exit the login dialog.
|
||||||
|
|
||||||
### Qt
|
### 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**.
|
- 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"
|
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`)
|
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>
|
<details>
|
||||||
<summary>How to add Qt to PATH</summary>
|
<summary>How to add Qt to PATH</summary>
|
||||||
|
|
||||||
1. Type "path" in the Windows start menu and click `Edit the system environment variables`.
|
1. Type "path" in the Windows start menu and click `Edit the system environment variables`.
|
||||||
2. Click the `Environment Variables...` button bottom right.
|
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.
|
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>
|
<details>
|
||||||
<summary>OpenSSL</summary>
|
<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)**
|
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`
|
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:
|
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`
|
```cmd
|
||||||
1. `cd build`
|
mkdir build
|
||||||
1. `conan install .. -s build_type=Release -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" --build=missing --output-folder=.`
|
cd build
|
||||||
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" ..`
|
conan install .. -s build_type=Release -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" --build=missing --output-folder=.
|
||||||
1. `nmake`
|
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)
|
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:
|
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/`
|
`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`
|
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.
|
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:
|
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)
|
![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".
|
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.
|
- 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).
|
- 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`.)
|
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.
|
- Output and error messages produced by the compiler can be seen under the "4 Compile Output" tab in Qt Creator.
|
||||||
|
|
||||||
#### Producing standalone builds
|
#### 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 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):
|
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`
|
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
|
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`.
|
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:
|
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
|
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)).
|
||||||
C:\Qt\6.5.3\msvc2019_64\bin\windeployqt.exe chatterino.exe
|
|
||||||
|
|
||||||
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
|
### 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).
|
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.
|
_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
|
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_PREFIX_PATH=C:\Qt\6.5.3\msvc2019_64\lib\cmake\Qt6
|
||||||
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
|
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
|
||||||
```
|
```
|
||||||
|
@ -227,9 +241,9 @@ Select the `CMake Applications > chatterino` configuration and add a new _Run Ex
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<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>
|
</details>
|
||||||
|
|
||||||
|
@ -240,26 +254,24 @@ write `portable` into it.
|
||||||
|
|
||||||
#### Debugging
|
#### 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.
|
about these types.
|
||||||
|
|
||||||
1. Set `Enable NatVis renderers for LLDB option`
|
1. Set `Enable NatVis renderers for LLDB option`
|
||||||
in `Settings | Build, Execution, Deployment | Debugger | Data Views | C/C++` (should be enabled by default).
|
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:
|
the project root using PowerShell:
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
When switching to QT6 these need to be updated to qt6.natvis.xml.
|
We can't use Invoke-RestMethod here, because it will automatically convert the body to an xml document.
|
||||||
We need to do the replacement as the QT tools:
|
|
||||||
https://github.com/qt-labs/vstools/blob/0769d945f8d0040917d654d9731e6b65951e102c/QtVsTools.Package/QtVsToolsPackage.cs#L390-L393
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
```powershell
|
```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
|
# [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
|
If this didn't work for you, try following
|
||||||
the [tutorial from JetBrains](https://www.jetbrains.com/help/clion/qt-tutorial.html#debug-renderers).
|
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 [CMake](https://cmake.org/)
|
||||||
1. Install [git](https://git-scm.com/)
|
1. Install [git](https://git-scm.com/)
|
||||||
1. Install [vcpkg](https://vcpkg.io/)
|
1. Install [vcpkg](https://vcpkg.io/)
|
||||||
- `git clone https://github.com/Microsoft/vcpkg.git`
|
|
||||||
- `cd .\vcpkg\`
|
```shell
|
||||||
- `.\bootstrap-vcpkg.bat`
|
git clone https://github.com/Microsoft/vcpkg.git
|
||||||
- `.\vcpkg integrate install`
|
cd vcpkg
|
||||||
- `.\vcpkg integrate powershell`
|
.\bootstrap-vcpkg.bat
|
||||||
- `cd ..`
|
.\vcpkg integrate install
|
||||||
|
.\vcpkg integrate powershell
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
1. Configure the environment variables for vcpkg.
|
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.
|
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
|
- 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
|
## Building
|
||||||
|
|
||||||
1. Clone
|
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
|
1. Install dependencies
|
||||||
- `cd .\chatterino2\`
|
```powershell
|
||||||
- `vcpkg install`
|
cd .\chatterino2\
|
||||||
|
vcpkg install
|
||||||
|
```
|
||||||
1. Build
|
1. Build
|
||||||
- `mkdir .\build\`
|
```powershell
|
||||||
- `cd .\build\`
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE="$Env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||||
- (cmd) `cmake .. -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake`
|
cd build
|
||||||
- (ps1) `cmake .. -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"`
|
cmake --build . --parallel <threads> --config Release
|
||||||
- `cmake --build . --parallel <threads> --config Release`
|
```
|
||||||
1. Run
|
When using CMD, use `-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` to specify the toolchain.
|
||||||
- `.\bin\chatterino2.exe`
|
1. Run `.\bin\chatterino2.exe`
|
||||||
|
|
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -3,6 +3,8 @@
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
|
- 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: 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: 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)
|
- 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: 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: 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 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 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: 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)
|
- 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 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 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 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 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: 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: Change clang-format from v14 to v16. (#4929)
|
||||||
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
|
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
|
||||||
- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767)
|
- 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: 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: 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: Remove `boost::noncopyable` use & `boost_random` dependency. (#4776)
|
||||||
- Dev: Fix clang-tidy `cppcoreguidelines-pro-type-member-init` warnings. (#4426)
|
- Dev: Fix clang-tidy `cppcoreguidelines-pro-type-member-init` warnings. (#4426)
|
||||||
- Dev: Immediate layout for invisible `ChannelView`s is skipped. (#4811)
|
- 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 `Emoji`'s EmojiMap into a vector. (#4980)
|
||||||
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
|
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
|
||||||
- Dev: Refactor `common/Credentials`. (#4979)
|
- Dev: Refactor `common/Credentials`. (#4979)
|
||||||
|
- Dev: Refactor chat logger. (#5058)
|
||||||
- Dev: Changed lifetime of context menus. (#4924)
|
- 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 `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: 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: `Details` file properties tab is now populated on Windows. (#4912)
|
||||||
- Dev: Removed `Outcome` from network requests. (#4959)
|
- 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: Move `clang-tidy` checker to its own CI job. (#4996)
|
||||||
- Dev: Refactored the Image Uploader feature. (#4971)
|
- Dev: Refactored the Image Uploader feature. (#4971)
|
||||||
- Dev: Fixed deadlock and use-after-free in tests. (#4981)
|
- Dev: Fixed deadlock and use-after-free in tests. (#4981)
|
||||||
- Dev: Cleanly exit Chatterino instead of force exiting. (#4993)
|
- 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
|
## 2.4.6
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ if (CHATTERINO_PLUGINS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (BUILD_WITH_CRASHPAD)
|
if (BUILD_WITH_CRASHPAD)
|
||||||
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL)
|
add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
|
# 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
|
```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.
|
* QRegularExpressionMatch on success.
|
||||||
* ^^^ This comment just repeats the function signature!!!
|
* ^^^ 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:
|
To get source code with required submodules run:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git
|
git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```
|
```shell
|
||||||
git clone https://github.com/Chatterino/chatterino2.git
|
git clone https://github.com/Chatterino/chatterino2.git
|
||||||
cd chatterino2
|
cd chatterino2
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
```
|
```
|
||||||
|
|
||||||
[Building on Windows](../master/BUILDING_ON_WINDOWS.md)
|
- [Building on Windows](../master/BUILDING_ON_WINDOWS.md)
|
||||||
|
- [Building on Windows with vcpkg](../master/BUILDING_ON_WINDOWS_WITH_VCPKG.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 Linux](../master/BUILDING_ON_LINUX.md)
|
- [Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
|
||||||
|
|
||||||
[Building on Mac](../master/BUILDING_ON_MAC.md)
|
|
||||||
|
|
||||||
[Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
|
|
||||||
|
|
||||||
## Git blame
|
## Git blame
|
||||||
|
|
||||||
This project has big commits in the history which for example update all line
|
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:
|
||||||
endings. To improve the output of git-blame, consider setting:
|
|
||||||
|
|
||||||
```
|
```shell
|
||||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -58,19 +53,9 @@ file](./.git-blame-ignore-revs). GitHub does this by default.
|
||||||
|
|
||||||
## Code style
|
## 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
|
For more contribution guidelines, take a look at [the wiki](https://wiki.chatterino.com/Contributing%20for%20Developers/).
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Doxygen
|
## Doxygen
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from os import path
|
||||||
|
|
||||||
class Chatterino(ConanFile):
|
class Chatterino(ConanFile):
|
||||||
name = "Chatterino"
|
name = "Chatterino"
|
||||||
requires = "boost/1.81.0"
|
requires = "boost/1.83.0"
|
||||||
settings = "os", "compiler", "build_type", "arch"
|
settings = "os", "compiler", "build_type", "arch"
|
||||||
default_options = {
|
default_options = {
|
||||||
"with_benchmark": False,
|
"with_benchmark": False,
|
||||||
|
@ -24,7 +24,7 @@ class Chatterino(ConanFile):
|
||||||
self.requires("benchmark/1.7.1")
|
self.requires("benchmark/1.7.1")
|
||||||
|
|
||||||
if self.options.get_safe("with_openssl3", False):
|
if self.options.get_safe("with_openssl3", False):
|
||||||
self.requires("openssl/3.1.0")
|
self.requires("openssl/3.2.0")
|
||||||
else:
|
else:
|
||||||
self.requires("openssl/1.1.1t")
|
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;
|
): boolean;
|
||||||
function send_msg(channel: String, text: String): boolean;
|
function send_msg(channel: String, text: String): boolean;
|
||||||
function system_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 `src/common/Version.hpp`
|
||||||
- [ ] Updated version code in `CMakeLists.txt`
|
- [ ] 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`
|
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.
|
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`
|
- [ ] Updated version code in `.CI/chatterino-installer.iss`
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A small description of your license.",
|
"description": "A small description of your license.",
|
||||||
"examples": ["MIT", "GPL-2.0-or-later"]
|
"examples": ["MIT", "GPL-2.0-or-later"]
|
||||||
}
|
},
|
||||||
|
"$schema": { "type": "string" }
|
||||||
},
|
},
|
||||||
"required": ["name", "description", "authors", "version", "license"]
|
"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
|
# 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
|
## 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)
|
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
|
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.
|
runtime.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
@ -113,6 +113,42 @@ Limitations/known issues:
|
||||||
rebuilding the window content caused by reloading another plugin will solve this.
|
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).
|
- 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)`
|
#### `send_msg(channel, text)`
|
||||||
|
|
||||||
Sends a message to `channel` with the specified text. Also executes commands.
|
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)
|
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.
|
This is Lua's [`require()`](https://www.lua.org/manual/5.3/manual.html#pdf-require) function.
|
||||||
You are restricted to loading files in your plugin's directory. You cannot load files with bytecode inside.
|
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:
|
Example:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
import("stuff.lua") -- executes Plugins/name/stuff.lua
|
require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua
|
||||||
import("./stuff.lua") -- executes Plugins/name/stuff.lua
|
require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua
|
||||||
import("../stuff.lua") -- tries to load Plugins/stuff.lua and errors
|
require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file
|
||||||
import("luac.out") -- tried to load Plugins/name/luac.out and errors because it contains non-utf8 data
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `print(Args...)`
|
#### `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`
|
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
|
#pragma once
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
|
#include "common/Args.hpp"
|
||||||
|
|
||||||
namespace chatterino::mock {
|
namespace chatterino::mock {
|
||||||
|
|
||||||
|
@ -9,6 +10,11 @@ class EmptyApplication : public IApplication
|
||||||
public:
|
public:
|
||||||
virtual ~EmptyApplication() = default;
|
virtual ~EmptyApplication() = default;
|
||||||
|
|
||||||
|
const Args &getArgs() override
|
||||||
|
{
|
||||||
|
return this->args_;
|
||||||
|
}
|
||||||
|
|
||||||
Theme *getThemes() override
|
Theme *getThemes() override
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
@ -44,6 +50,11 @@ public:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CrashHandler *getCrashHandler() override
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
CommandController *getCommands() override
|
CommandController *getCommands() override
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
@ -64,6 +75,12 @@ public:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logging *getChatLogger() override
|
||||||
|
{
|
||||||
|
assert(!"getChatLogger was called without being initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
ChatterinoBadges *getChatterinoBadges() override
|
ChatterinoBadges *getChatterinoBadges() override
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
@ -105,6 +122,9 @@ public:
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Args args_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino::mock
|
} // 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
|
olafyang | https://github.com/olafyang | | Contributor
|
||||||
chrrs | https://github.com/chrrs | | Contributor
|
chrrs | https://github.com/chrrs | | Contributor
|
||||||
4rneee | https://github.com/4rneee | | 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
|
# 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!!!!!!!"
|
echo "$file differs!!!!!!!"
|
||||||
fail="1"
|
fail="1"
|
||||||
fi
|
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
|
if [ "$fail" = "1" ]; then
|
||||||
echo "At least one file is poorly formatted - check the output above"
|
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/PubSubActions.hpp"
|
||||||
#include "providers/twitch/PubSubManager.hpp"
|
#include "providers/twitch/PubSubManager.hpp"
|
||||||
#include "providers/twitch/PubSubMessages.hpp"
|
#include "providers/twitch/PubSubMessages.hpp"
|
||||||
|
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||||
|
#include "singletons/CrashHandler.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "singletons/Fonts.hpp"
|
#include "singletons/Fonts.hpp"
|
||||||
#include "singletons/helper/LoggingChannel.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
|
// It will create the instances of the major classes, and connect their signals
|
||||||
// to each other
|
// to each other
|
||||||
|
|
||||||
Application::Application(Settings &_settings, Paths &_paths)
|
Application::Application(Settings &_settings, Paths &_paths, const Args &_args)
|
||||||
: themes(&this->emplace<Theme>())
|
: args_(_args)
|
||||||
|
, themes(&this->emplace<Theme>())
|
||||||
, fonts(&this->emplace<Fonts>())
|
, fonts(&this->emplace<Fonts>())
|
||||||
, emotes(&this->emplace<Emotes>())
|
, emotes(&this->emplace<Emotes>())
|
||||||
, accounts(&this->emplace<AccountController>())
|
, accounts(&this->emplace<AccountController>())
|
||||||
|
@ -114,6 +117,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
||||||
, toasts(&this->emplace<Toasts>())
|
, toasts(&this->emplace<Toasts>())
|
||||||
, imageUploader(&this->emplace<ImageUploader>())
|
, imageUploader(&this->emplace<ImageUploader>())
|
||||||
, seventvAPI(&this->emplace<SeventvAPI>())
|
, seventvAPI(&this->emplace<SeventvAPI>())
|
||||||
|
, crashHandler(&this->emplace<CrashHandler>())
|
||||||
|
|
||||||
, commands(&this->emplace<CommandController>())
|
, commands(&this->emplace<CommandController>())
|
||||||
, notifications(&this->emplace<NotificationController>())
|
, notifications(&this->emplace<NotificationController>())
|
||||||
|
@ -124,12 +128,12 @@ Application::Application(Settings &_settings, Paths &_paths)
|
||||||
, userData(&this->emplace<UserDataController>())
|
, userData(&this->emplace<UserDataController>())
|
||||||
, sound(&this->emplace<ISoundController>(makeSoundController(_settings)))
|
, sound(&this->emplace<ISoundController>(makeSoundController(_settings)))
|
||||||
, twitchLiveController(&this->emplace<TwitchLiveController>())
|
, twitchLiveController(&this->emplace<TwitchLiveController>())
|
||||||
|
, logging(new Logging(_settings))
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
, plugins(&this->emplace<PluginController>())
|
, plugins(&this->emplace<PluginController>())
|
||||||
#endif
|
#endif
|
||||||
, logging(&this->emplace<Logging>())
|
|
||||||
{
|
{
|
||||||
this->instance = this;
|
Application::instance = this;
|
||||||
|
|
||||||
// We can safely ignore this signal's connection since the Application will always
|
// We can safely ignore this signal's connection since the Application will always
|
||||||
// be destroyed after fonts
|
// be destroyed after fonts
|
||||||
|
@ -138,13 +142,15 @@ Application::Application(Settings &_settings, Paths &_paths)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Application::~Application() = default;
|
||||||
|
|
||||||
void Application::initialize(Settings &settings, Paths &paths)
|
void Application::initialize(Settings &settings, Paths &paths)
|
||||||
{
|
{
|
||||||
assert(isAppInitialized == false);
|
assert(isAppInitialized == false);
|
||||||
isAppInitialized = true;
|
isAppInitialized = true;
|
||||||
|
|
||||||
// Show changelog
|
// Show changelog
|
||||||
if (!getArgs().isFramelessEmbed &&
|
if (!this->args_.isFramelessEmbed &&
|
||||||
getSettings()->currentVersion.getValue() != "" &&
|
getSettings()->currentVersion.getValue() != "" &&
|
||||||
getSettings()->currentVersion.getValue() != CHATTERINO_VERSION)
|
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);
|
getSettings()->currentVersion.setValue(CHATTERINO_VERSION);
|
||||||
|
|
||||||
|
@ -174,8 +180,10 @@ void Application::initialize(Settings &settings, Paths &paths)
|
||||||
singleton->initialize(settings, paths);
|
singleton->initialize(settings, paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add crash message
|
// Show crash message.
|
||||||
if (!getArgs().isFramelessEmbed && getArgs().crashRecovery)
|
// On Windows, the crash message was already shown.
|
||||||
|
#ifndef Q_OS_WIN
|
||||||
|
if (!this->args_.isFramelessEmbed && this->args_.crashRecovery)
|
||||||
{
|
{
|
||||||
if (auto selected =
|
if (auto selected =
|
||||||
this->windows->getMainWindow().getNotebook().getSelectedPage())
|
this->windows->getMainWindow().getNotebook().getSelectedPage())
|
||||||
|
@ -195,10 +203,11 @@ void Application::initialize(Settings &settings, Paths &paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
this->windows->updateWordTypeMask();
|
this->windows->updateWordTypeMask();
|
||||||
|
|
||||||
if (!getArgs().isFramelessEmbed)
|
if (!this->args_.isFramelessEmbed)
|
||||||
{
|
{
|
||||||
this->initNm(paths);
|
this->initNm(paths);
|
||||||
}
|
}
|
||||||
|
@ -214,7 +223,7 @@ int Application::run(QApplication &qtApp)
|
||||||
|
|
||||||
this->twitch->connect();
|
this->twitch->connect();
|
||||||
|
|
||||||
if (!getArgs().isFramelessEmbed)
|
if (!this->args_.isFramelessEmbed)
|
||||||
{
|
{
|
||||||
this->windows->getMainWindow().show();
|
this->windows->getMainWindow().show();
|
||||||
}
|
}
|
||||||
|
@ -305,6 +314,11 @@ ITwitchIrcServer *Application::getTwitch()
|
||||||
return this->twitch;
|
return this->twitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logging *Application::getChatLogger()
|
||||||
|
{
|
||||||
|
return this->logging.get();
|
||||||
|
}
|
||||||
|
|
||||||
void Application::save()
|
void Application::save()
|
||||||
{
|
{
|
||||||
for (auto &singleton : this->singletons_)
|
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 =
|
std::ignore =
|
||||||
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
|
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
|
||||||
[&](const auto &msg, const QString &channelID) {
|
[&](const auto &msg, const QString &channelID) {
|
||||||
|
@ -546,9 +641,16 @@ void Application::initPubSub()
|
||||||
msg.senderUserID, msg.senderUserLogin,
|
msg.senderUserID, msg.senderUserLogin,
|
||||||
senderDisplayName, senderColor};
|
senderDisplayName, senderColor};
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
const auto p = makeAutomodMessage(action);
|
const auto p =
|
||||||
|
TwitchMessageBuilder::makeAutomodMessage(
|
||||||
|
action, chan->getName());
|
||||||
chan->addMessage(p.first);
|
chan->addMessage(p.first);
|
||||||
chan->addMessage(p.second);
|
chan->addMessage(p.second);
|
||||||
|
|
||||||
|
getApp()->twitch->automodChannel->addMessage(
|
||||||
|
p.first);
|
||||||
|
getApp()->twitch->automodChannel->addMessage(
|
||||||
|
p.second);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// "ALLOWED" and "DENIED" statuses remain unimplemented
|
// "ALLOWED" and "DENIED" statuses remain unimplemented
|
||||||
|
@ -573,7 +675,8 @@ void Application::initPubSub()
|
||||||
}
|
}
|
||||||
|
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
const auto p = makeAutomodMessage(action);
|
const auto p = TwitchMessageBuilder::makeAutomodMessage(
|
||||||
|
action, chan->getName());
|
||||||
chan->addMessage(p.first);
|
chan->addMessage(p.first);
|
||||||
chan->addMessage(p.second);
|
chan->addMessage(p.second);
|
||||||
});
|
});
|
||||||
|
@ -615,7 +718,8 @@ void Application::initPubSub()
|
||||||
}
|
}
|
||||||
|
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
const auto p = makeAutomodInfoMessage(action);
|
const auto p =
|
||||||
|
TwitchMessageBuilder::makeAutomodInfoMessage(action);
|
||||||
chan->addMessage(p);
|
chan->addMessage(p);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -657,6 +761,7 @@ void Application::initPubSub()
|
||||||
[this] {
|
[this] {
|
||||||
this->twitch->pubsub->unlistenAllModerationActions();
|
this->twitch->pubsub->unlistenAllModerationActions();
|
||||||
this->twitch->pubsub->unlistenAutomod();
|
this->twitch->pubsub->unlistenAutomod();
|
||||||
|
this->twitch->pubsub->unlistenLowTrustUsers();
|
||||||
this->twitch->pubsub->unlistenWhispers();
|
this->twitch->pubsub->unlistenWhispers();
|
||||||
},
|
},
|
||||||
boost::signals2::at_front);
|
boost::signals2::at_front);
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Args;
|
||||||
class TwitchIrcServer;
|
class TwitchIrcServer;
|
||||||
class ITwitchIrcServer;
|
class ITwitchIrcServer;
|
||||||
class PubSub;
|
class PubSub;
|
||||||
|
@ -44,6 +45,7 @@ class FfzBadges;
|
||||||
class SeventvBadges;
|
class SeventvBadges;
|
||||||
class ImageUploader;
|
class ImageUploader;
|
||||||
class SeventvAPI;
|
class SeventvAPI;
|
||||||
|
class CrashHandler;
|
||||||
|
|
||||||
class IApplication
|
class IApplication
|
||||||
{
|
{
|
||||||
|
@ -53,6 +55,7 @@ public:
|
||||||
|
|
||||||
static IApplication *instance;
|
static IApplication *instance;
|
||||||
|
|
||||||
|
virtual const Args &getArgs() = 0;
|
||||||
virtual Theme *getThemes() = 0;
|
virtual Theme *getThemes() = 0;
|
||||||
virtual Fonts *getFonts() = 0;
|
virtual Fonts *getFonts() = 0;
|
||||||
virtual IEmotes *getEmotes() = 0;
|
virtual IEmotes *getEmotes() = 0;
|
||||||
|
@ -60,10 +63,12 @@ public:
|
||||||
virtual HotkeyController *getHotkeys() = 0;
|
virtual HotkeyController *getHotkeys() = 0;
|
||||||
virtual WindowManager *getWindows() = 0;
|
virtual WindowManager *getWindows() = 0;
|
||||||
virtual Toasts *getToasts() = 0;
|
virtual Toasts *getToasts() = 0;
|
||||||
|
virtual CrashHandler *getCrashHandler() = 0;
|
||||||
virtual CommandController *getCommands() = 0;
|
virtual CommandController *getCommands() = 0;
|
||||||
virtual HighlightController *getHighlights() = 0;
|
virtual HighlightController *getHighlights() = 0;
|
||||||
virtual NotificationController *getNotifications() = 0;
|
virtual NotificationController *getNotifications() = 0;
|
||||||
virtual ITwitchIrcServer *getTwitch() = 0;
|
virtual ITwitchIrcServer *getTwitch() = 0;
|
||||||
|
virtual Logging *getChatLogger() = 0;
|
||||||
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
||||||
virtual FfzBadges *getFfzBadges() = 0;
|
virtual FfzBadges *getFfzBadges() = 0;
|
||||||
virtual SeventvBadges *getSeventvBadges() = 0;
|
virtual SeventvBadges *getSeventvBadges() = 0;
|
||||||
|
@ -76,6 +81,7 @@ public:
|
||||||
|
|
||||||
class Application : public IApplication
|
class Application : public IApplication
|
||||||
{
|
{
|
||||||
|
const Args &args_;
|
||||||
std::vector<std::unique_ptr<Singleton>> singletons_;
|
std::vector<std::unique_ptr<Singleton>> singletons_;
|
||||||
int argc_{};
|
int argc_{};
|
||||||
char **argv_{};
|
char **argv_{};
|
||||||
|
@ -83,7 +89,13 @@ class Application : public IApplication
|
||||||
public:
|
public:
|
||||||
static Application *instance;
|
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 initialize(Settings &settings, Paths &paths);
|
||||||
void load();
|
void load();
|
||||||
|
@ -103,6 +115,7 @@ public:
|
||||||
Toasts *const toasts{};
|
Toasts *const toasts{};
|
||||||
ImageUploader *const imageUploader{};
|
ImageUploader *const imageUploader{};
|
||||||
SeventvAPI *const seventvAPI{};
|
SeventvAPI *const seventvAPI{};
|
||||||
|
CrashHandler *const crashHandler{};
|
||||||
|
|
||||||
CommandController *const commands{};
|
CommandController *const commands{};
|
||||||
NotificationController *const notifications{};
|
NotificationController *const notifications{};
|
||||||
|
@ -115,14 +128,17 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TwitchLiveController *const twitchLiveController{};
|
TwitchLiveController *const twitchLiveController{};
|
||||||
|
const std::unique_ptr<Logging> logging;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
PluginController *const plugins{};
|
PluginController *const plugins{};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/*[[deprecated]]*/ Logging *const logging{};
|
const Args &getArgs() override
|
||||||
|
{
|
||||||
|
return this->args_;
|
||||||
|
}
|
||||||
Theme *getThemes() override
|
Theme *getThemes() override
|
||||||
{
|
{
|
||||||
return this->themes;
|
return this->themes;
|
||||||
|
@ -148,6 +164,10 @@ public:
|
||||||
{
|
{
|
||||||
return this->toasts;
|
return this->toasts;
|
||||||
}
|
}
|
||||||
|
CrashHandler *getCrashHandler() override
|
||||||
|
{
|
||||||
|
return this->crashHandler;
|
||||||
|
}
|
||||||
CommandController *getCommands() override
|
CommandController *getCommands() override
|
||||||
{
|
{
|
||||||
return this->commands;
|
return this->commands;
|
||||||
|
@ -161,6 +181,7 @@ public:
|
||||||
return this->highlights;
|
return this->highlights;
|
||||||
}
|
}
|
||||||
ITwitchIrcServer *getTwitch() override;
|
ITwitchIrcServer *getTwitch() override;
|
||||||
|
Logging *getChatLogger() override;
|
||||||
ChatterinoBadges *getChatterinoBadges() override
|
ChatterinoBadges *getChatterinoBadges() override
|
||||||
{
|
{
|
||||||
return this->chatterinoBadges;
|
return this->chatterinoBadges;
|
||||||
|
|
|
@ -289,8 +289,6 @@ set(SOURCE_FILES
|
||||||
messages/search/SubtierPredicate.cpp
|
messages/search/SubtierPredicate.cpp
|
||||||
messages/search/SubtierPredicate.hpp
|
messages/search/SubtierPredicate.hpp
|
||||||
|
|
||||||
providers/Crashpad.cpp
|
|
||||||
providers/Crashpad.hpp
|
|
||||||
providers/IvrApi.cpp
|
providers/IvrApi.cpp
|
||||||
providers/IvrApi.hpp
|
providers/IvrApi.hpp
|
||||||
providers/LinkResolver.cpp
|
providers/LinkResolver.cpp
|
||||||
|
@ -414,6 +412,8 @@ set(SOURCE_FILES
|
||||||
providers/twitch/pubsubmessages/ChatModeratorAction.hpp
|
providers/twitch/pubsubmessages/ChatModeratorAction.hpp
|
||||||
providers/twitch/pubsubmessages/Listen.cpp
|
providers/twitch/pubsubmessages/Listen.cpp
|
||||||
providers/twitch/pubsubmessages/Listen.hpp
|
providers/twitch/pubsubmessages/Listen.hpp
|
||||||
|
providers/twitch/pubsubmessages/LowTrustUsers.cpp
|
||||||
|
providers/twitch/pubsubmessages/LowTrustUsers.hpp
|
||||||
providers/twitch/pubsubmessages/Message.hpp
|
providers/twitch/pubsubmessages/Message.hpp
|
||||||
providers/twitch/pubsubmessages/Unlisten.cpp
|
providers/twitch/pubsubmessages/Unlisten.cpp
|
||||||
providers/twitch/pubsubmessages/Unlisten.hpp
|
providers/twitch/pubsubmessages/Unlisten.hpp
|
||||||
|
@ -425,6 +425,8 @@ set(SOURCE_FILES
|
||||||
|
|
||||||
singletons/Badges.cpp
|
singletons/Badges.cpp
|
||||||
singletons/Badges.hpp
|
singletons/Badges.hpp
|
||||||
|
singletons/CrashHandler.cpp
|
||||||
|
singletons/CrashHandler.hpp
|
||||||
singletons/Emotes.cpp
|
singletons/Emotes.cpp
|
||||||
singletons/Emotes.hpp
|
singletons/Emotes.hpp
|
||||||
singletons/Fonts.cpp
|
singletons/Fonts.cpp
|
||||||
|
@ -627,6 +629,8 @@ set(SOURCE_FILES
|
||||||
widgets/helper/SignalLabel.hpp
|
widgets/helper/SignalLabel.hpp
|
||||||
widgets/helper/TitlebarButton.cpp
|
widgets/helper/TitlebarButton.cpp
|
||||||
widgets/helper/TitlebarButton.hpp
|
widgets/helper/TitlebarButton.hpp
|
||||||
|
widgets/helper/TitlebarButtons.cpp
|
||||||
|
widgets/helper/TitlebarButtons.hpp
|
||||||
|
|
||||||
widgets/listview/GenericItemDelegate.cpp
|
widgets/listview/GenericItemDelegate.cpp
|
||||||
widgets/listview/GenericItemDelegate.hpp
|
widgets/listview/GenericItemDelegate.hpp
|
||||||
|
@ -1006,7 +1010,6 @@ endif ()
|
||||||
if (BUILD_WITH_CRASHPAD)
|
if (BUILD_WITH_CRASHPAD)
|
||||||
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD)
|
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD)
|
||||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client)
|
target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client)
|
||||||
set_target_directory_hierarchy(crashpad_handler crashpad)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Configure compiler warnings
|
# Configure compiler warnings
|
||||||
|
@ -1066,7 +1069,6 @@ else ()
|
||||||
-Wno-switch
|
-Wno-switch
|
||||||
-Wno-deprecated-declarations
|
-Wno-deprecated-declarations
|
||||||
-Wno-sign-compare
|
-Wno-sign-compare
|
||||||
-Wno-unused-variable
|
|
||||||
|
|
||||||
# Disabling strict-aliasing warnings for now, although we probably want to re-enable this in the future
|
# Disabling strict-aliasing warnings for now, although we probably want to re-enable this in the future
|
||||||
-Wno-strict-aliasing
|
-Wno-strict-aliasing
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "common/Modes.hpp"
|
#include "common/Modes.hpp"
|
||||||
#include "common/NetworkManager.hpp"
|
#include "common/NetworkManager.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
#include "singletons/CrashHandler.hpp"
|
||||||
#include "singletons/Paths.hpp"
|
#include "singletons/Paths.hpp"
|
||||||
#include "singletons/Resources.hpp"
|
#include "singletons/Resources.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
@ -78,7 +79,7 @@ namespace {
|
||||||
{
|
{
|
||||||
// set up the QApplication flags
|
// set up the QApplication flags
|
||||||
QApplication::setAttribute(Qt::AA_Use96Dpi, true);
|
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);
|
QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -98,23 +99,12 @@ namespace {
|
||||||
installCustomPalette();
|
installCustomPalette();
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLastCrashDialog()
|
void showLastCrashDialog(const Args &args)
|
||||||
{
|
{
|
||||||
//#ifndef C_DISABLE_CRASH_DIALOG
|
auto *dialog = new LastRunCrashDialog(args);
|
||||||
// LastRunCrashDialog dialog;
|
// Use exec() over open() to block the app from being loaded
|
||||||
|
// and to be able to set the safe mode.
|
||||||
// switch (dialog.exec())
|
dialog->exec();
|
||||||
// {
|
|
||||||
// case QDialog::Accepted:
|
|
||||||
// {
|
|
||||||
// };
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// {
|
|
||||||
// _exit(0);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void createRunningFile(const QString &path)
|
void createRunningFile(const QString &path)
|
||||||
|
@ -132,14 +122,13 @@ namespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::chrono::steady_clock::time_point signalsInitTime;
|
std::chrono::steady_clock::time_point signalsInitTime;
|
||||||
bool restartOnSignal = false;
|
|
||||||
|
|
||||||
[[noreturn]] void handleSignal(int signum)
|
[[noreturn]] void handleSignal(int signum)
|
||||||
{
|
{
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
if (restartOnSignal &&
|
if (std::chrono::steady_clock::now() - signalsInitTime > 30s &&
|
||||||
std::chrono::steady_clock::now() - signalsInitTime > 30s)
|
getIApp()->getCrashHandler()->shouldRecover())
|
||||||
{
|
{
|
||||||
QProcess proc;
|
QProcess proc;
|
||||||
|
|
||||||
|
@ -235,15 +224,18 @@ namespace {
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void runGui(QApplication &a, Paths &paths, Settings &settings)
|
void runGui(QApplication &a, Paths &paths, Settings &settings, const Args &args)
|
||||||
{
|
{
|
||||||
initQt();
|
initQt();
|
||||||
initResources();
|
initResources();
|
||||||
initSignalHandler();
|
initSignalHandler();
|
||||||
|
|
||||||
settings.restartOnCrash.connect([](const bool &value) {
|
#ifdef Q_OS_WIN
|
||||||
restartOnSignal = value;
|
if (args.crashRecovery)
|
||||||
});
|
{
|
||||||
|
showLastCrashDialog(args);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
auto thread = std::thread([dir = paths.miscDirectory] {
|
auto thread = std::thread([dir = paths.miscDirectory] {
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
|
@ -282,31 +274,12 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
|
||||||
chatterino::NetworkManager::init();
|
chatterino::NetworkManager::init();
|
||||||
chatterino::Updates::instance().checkForUpdates();
|
chatterino::Updates::instance().checkForUpdates();
|
||||||
|
|
||||||
#ifdef C_USE_BREAKPAD
|
Application app(settings, paths, args);
|
||||||
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);
|
|
||||||
app.initialize(settings, paths);
|
app.initialize(settings, paths);
|
||||||
app.run(a);
|
app.run(a);
|
||||||
app.save();
|
app.save();
|
||||||
|
|
||||||
removeRunningFile(runningPath);
|
if (!args.dontSaveSettings)
|
||||||
|
|
||||||
if (!getArgs().dontSaveSettings)
|
|
||||||
{
|
{
|
||||||
pajlada::Settings::SettingManager::gSave();
|
pajlada::Settings::SettingManager::gSave();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,12 @@
|
||||||
class QApplication;
|
class QApplication;
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Args;
|
||||||
class Paths;
|
class Paths;
|
||||||
class Settings;
|
class Settings;
|
||||||
|
|
||||||
void runGui(QApplication &a, Paths &paths, Settings &settings);
|
void runGui(QApplication &a, Paths &paths, Settings &settings,
|
||||||
|
const Args &args);
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include "Args.hpp"
|
#include "common/Args.hpp"
|
||||||
|
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
#include "singletons/Paths.hpp"
|
#include "singletons/Paths.hpp"
|
||||||
#include "singletons/WindowManager.hpp"
|
#include "singletons/WindowManager.hpp"
|
||||||
#include "util/AttachToConsole.hpp"
|
#include "util/AttachToConsole.hpp"
|
||||||
|
@ -14,6 +15,55 @@
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QUuid>
|
#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 {
|
namespace chatterino {
|
||||||
|
|
||||||
Args::Args(const QApplication &app)
|
Args::Args(const QApplication &app)
|
||||||
|
@ -23,35 +73,44 @@ Args::Args(const QApplication &app)
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
|
||||||
// Used internally by app to restart after unexpected crashes
|
// Used internally by app to restart after unexpected crashes
|
||||||
QCommandLineOption crashRecoveryOption("crash-recovery");
|
auto crashRecoveryOption = hiddenOption("crash-recovery");
|
||||||
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
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
|
// Added to ignore the parent-window option passed during native messaging
|
||||||
QCommandLineOption parentWindowOption("parent-window");
|
auto parentWindowOption = hiddenOption("parent-window");
|
||||||
parentWindowOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
auto parentWindowIdOption =
|
||||||
QCommandLineOption parentWindowIdOption("x-attach-split-to-window", "",
|
hiddenOption("x-attach-split-to-window", "", "window-id");
|
||||||
"window-id");
|
|
||||||
parentWindowIdOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
|
||||||
|
|
||||||
// Verbose
|
// Verbose
|
||||||
QCommandLineOption verboseOption({{"v", "verbose"},
|
auto verboseOption = QCommandLineOption(
|
||||||
"Attaches to the Console on windows, "
|
QStringList{"v", "verbose"}, "Attaches to the Console on windows, "
|
||||||
"allowing you to see debug output."});
|
"allowing you to see debug output.");
|
||||||
crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
// Safe mode
|
||||||
|
QCommandLineOption safeModeOption(
|
||||||
|
"safe-mode", "Starts Chatterino without loading Plugins and always "
|
||||||
|
"show the settings button.");
|
||||||
|
|
||||||
parser.addOptions({
|
// Channel layout
|
||||||
{{"V", "version"}, "Displays version information."},
|
auto channelLayout = QCommandLineOption(
|
||||||
crashRecoveryOption,
|
|
||||||
parentWindowOption,
|
|
||||||
parentWindowIdOption,
|
|
||||||
verboseOption,
|
|
||||||
});
|
|
||||||
parser.addOption(QCommandLineOption(
|
|
||||||
{"c", "channels"},
|
{"c", "channels"},
|
||||||
"Joins only supplied channels on startup. Use letters with colons to "
|
"Joins only supplied channels on startup. Use letters with colons to "
|
||||||
"specify platform. Only Twitch channels are supported at the moment.\n"
|
"specify platform. Only Twitch channels are supported at the moment.\n"
|
||||||
"If platform isn't specified, default is Twitch.",
|
"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()))
|
if (!parser.parse(app.arguments()))
|
||||||
{
|
{
|
||||||
|
@ -71,15 +130,25 @@ Args::Args(const QApplication &app)
|
||||||
(args.size() > 0 && (args[0].startsWith("chrome-extension://") ||
|
(args.size() > 0 && (args[0].startsWith("chrome-extension://") ||
|
||||||
args[0].endsWith(".json")));
|
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->verbose = parser.isSet(verboseOption);
|
||||||
|
|
||||||
this->printVersion = parser.isSet("V");
|
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))
|
if (parser.isSet(parentWindowIdOption))
|
||||||
{
|
{
|
||||||
|
@ -89,6 +158,21 @@ Args::Args(const QApplication &app)
|
||||||
|
|
||||||
this->parentWindowId = parser.value(parentWindowIdOption).toULongLong();
|
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)
|
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
|
} // namespace chatterino
|
||||||
|
|
|
@ -9,13 +9,38 @@
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
/// Command line arguments passed to 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
|
class Args
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
Args() = default;
|
||||||
Args(const QApplication &app);
|
Args(const QApplication &app);
|
||||||
|
|
||||||
bool printVersion{};
|
bool printVersion{};
|
||||||
|
|
||||||
bool crashRecovery{};
|
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{};
|
bool shouldRunBrowserExtensionHost{};
|
||||||
// Shows a single chat. Used on windows to embed in another application.
|
// Shows a single chat. Used on windows to embed in another application.
|
||||||
bool isFramelessEmbed{};
|
bool isFramelessEmbed{};
|
||||||
|
@ -26,12 +51,14 @@ public:
|
||||||
bool dontLoadMainWindow{};
|
bool dontLoadMainWindow{};
|
||||||
std::optional<WindowLayout> customChannelLayout;
|
std::optional<WindowLayout> customChannelLayout;
|
||||||
bool verbose{};
|
bool verbose{};
|
||||||
|
bool safeMode{};
|
||||||
|
|
||||||
|
QStringList currentArguments() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void applyCustomChannelLayout(const QString &argValue);
|
void applyCustomChannelLayout(const QString &argValue);
|
||||||
|
|
||||||
|
QStringList currentArguments_;
|
||||||
};
|
};
|
||||||
|
|
||||||
void initArgs(const QApplication &app);
|
|
||||||
const Args &getArgs();
|
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -101,7 +101,8 @@ void Channel::addMessage(MessagePtr message,
|
||||||
{
|
{
|
||||||
channelPlatform = "twitch";
|
channelPlatform = "twitch";
|
||||||
}
|
}
|
||||||
app->logging->addMessage(this->name_, message, channelPlatform);
|
getIApp()->getChatLogger()->addMessage(this->name_, message,
|
||||||
|
channelPlatform);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->messages_.pushBack(message, deleted))
|
if (this->messages_.pushBack(message, deleted))
|
||||||
|
@ -295,7 +296,8 @@ bool Channel::isWritable() const
|
||||||
{
|
{
|
||||||
using Type = Channel::Type;
|
using Type = Channel::Type;
|
||||||
auto type = this->getType();
|
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)
|
void Channel::sendMessage(const QString &message)
|
||||||
|
@ -314,7 +316,6 @@ bool Channel::isBroadcaster() const
|
||||||
|
|
||||||
bool Channel::hasModRights() const
|
bool Channel::hasModRights() const
|
||||||
{
|
{
|
||||||
// fourtf: check if staff
|
|
||||||
return this->isMod() || this->isBroadcaster();
|
return this->isMod() || this->isBroadcaster();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +331,8 @@ bool Channel::isLive() const
|
||||||
|
|
||||||
bool Channel::shouldIgnoreHighlights() const
|
bool Channel::shouldIgnoreHighlights() const
|
||||||
{
|
{
|
||||||
return this->type_ == Type::TwitchMentions ||
|
return this->type_ == Type::TwitchAutomod ||
|
||||||
|
this->type_ == Type::TwitchMentions ||
|
||||||
this->type_ == Type::TwitchWhispers;
|
this->type_ == Type::TwitchWhispers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ public:
|
||||||
TwitchWatching,
|
TwitchWatching,
|
||||||
TwitchMentions,
|
TwitchMentions,
|
||||||
TwitchLive,
|
TwitchLive,
|
||||||
|
TwitchAutomod,
|
||||||
TwitchEnd,
|
TwitchEnd,
|
||||||
Irc,
|
Irc,
|
||||||
Misc
|
Misc
|
||||||
|
@ -60,8 +61,6 @@ public:
|
||||||
pajlada::Signals::Signal<const std::vector<MessagePtr> &> filledInMessages;
|
pajlada::Signals::Signal<const std::vector<MessagePtr> &> filledInMessages;
|
||||||
pajlada::Signals::NoArgSignal destroyed;
|
pajlada::Signals::NoArgSignal destroyed;
|
||||||
pajlada::Signals::NoArgSignal displayNameChanged;
|
pajlada::Signals::NoArgSignal displayNameChanged;
|
||||||
/// Invoked when AbstractIrcServer::onReadConnected occurs
|
|
||||||
pajlada::Signals::NoArgSignal connected;
|
|
||||||
|
|
||||||
Type getType() const;
|
Type getType() const;
|
||||||
const QString &getName() const;
|
const QString &getName() const;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "util/TypeName.hpp"
|
#include "util/TypeName.hpp"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -10,16 +11,8 @@ namespace chatterino {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
template <typename T>
|
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(
|
const auto typeName = QString::fromStdString(
|
||||||
std::string(type_name<decltype(defaultValue)>()));
|
std::string(type_name<decltype(defaultValue)>()));
|
||||||
|
|
||||||
|
@ -33,23 +26,12 @@ namespace {
|
||||||
.arg(defaultValue);
|
.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)
|
std::optional<QString> readOptionalStringEnv(const char *envName)
|
||||||
{
|
{
|
||||||
auto envString = std::getenv(envName);
|
auto envString = qEnvironmentVariable(envName);
|
||||||
if (envString != nullptr)
|
if (!envString.isEmpty())
|
||||||
{
|
{
|
||||||
return QString(envString);
|
return envString;
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
@ -57,30 +39,28 @@ namespace {
|
||||||
|
|
||||||
uint16_t readPortEnv(const char *envName, uint16_t defaultValue)
|
uint16_t readPortEnv(const char *envName, uint16_t defaultValue)
|
||||||
{
|
{
|
||||||
auto envString = std::getenv(envName);
|
auto envString = qEnvironmentVariable(envName);
|
||||||
if (envString != nullptr)
|
if (!envString.isEmpty())
|
||||||
{
|
{
|
||||||
bool ok;
|
bool ok = false;
|
||||||
auto val = QString(envString).toUShort(&ok);
|
auto val = envString.toUShort(&ok);
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
warn(envName, envString, defaultValue);
|
||||||
warn(envName, defaultValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t readBoolEnv(const char *envName, bool defaultValue)
|
bool readBoolEnv(const char *envName, bool defaultValue)
|
||||||
{
|
{
|
||||||
auto envString = std::getenv(envName);
|
auto envString = qEnvironmentVariable(envName);
|
||||||
if (envString != nullptr)
|
if (!envString.isEmpty())
|
||||||
{
|
{
|
||||||
return QVariant(QString(envString)).toBool();
|
return QVariant(envString).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
@ -90,14 +70,14 @@ namespace {
|
||||||
|
|
||||||
Env::Env()
|
Env::Env()
|
||||||
: recentMessagesApiUrl(
|
: recentMessagesApiUrl(
|
||||||
readStringEnv("CHATTERINO2_RECENT_MESSAGES_URL",
|
qEnvironmentVariable("CHATTERINO2_RECENT_MESSAGES_URL",
|
||||||
"https://recent-messages.robotty.de/api/v2/"
|
"https://recent-messages.robotty.de/api/v2/"
|
||||||
"recent-messages/%1"))
|
"recent-messages/%1"))
|
||||||
, linkResolverUrl(readStringEnv(
|
, linkResolverUrl(qEnvironmentVariable(
|
||||||
"CHATTERINO2_LINK_RESOLVER_URL",
|
"CHATTERINO2_LINK_RESOLVER_URL",
|
||||||
"https://braize.pajlada.com/chatterino/link_resolver/%1"))
|
"https://braize.pajlada.com/chatterino/link_resolver/%1"))
|
||||||
, twitchServerHost(
|
, twitchServerHost(qEnvironmentVariable("CHATTERINO2_TWITCH_SERVER_HOST",
|
||||||
readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv"))
|
"irc.chat.twitch.tv"))
|
||||||
, twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443))
|
, twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443))
|
||||||
, twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true))
|
, twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true))
|
||||||
, proxyUrl(readOptionalStringEnv("CHATTERINO2_PROXY_URL"))
|
, proxyUrl(readOptionalStringEnv("CHATTERINO2_PROXY_URL"))
|
||||||
|
|
|
@ -97,6 +97,11 @@ public:
|
||||||
return !this->hasAny(flags);
|
return !this->hasAny(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T value() const
|
||||||
|
{
|
||||||
|
return this->value_;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
T value_{};
|
T value_{};
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,8 @@ Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", 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(chatterinoEmoji, "chatterino.emoji", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", 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(chatterinoBttv);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCache);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoCache);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon);
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes);
|
||||||
|
|
|
@ -30,7 +30,7 @@ namespace chatterino {
|
||||||
enum class WindowType;
|
enum class WindowType;
|
||||||
|
|
||||||
struct SplitDescriptor {
|
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_;
|
QString type_;
|
||||||
|
|
||||||
// Twitch Channel name or IRC channel name
|
// Twitch Channel name or IRC channel name
|
||||||
|
|
|
@ -616,10 +616,8 @@ QString openUsercard(const CommandContext &ctx)
|
||||||
"should be open.");
|
"should be open.");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *userPopup = new UserInfoPopup(
|
auto *userPopup =
|
||||||
getSettings()->autoCloseUserPopup,
|
new UserInfoPopup(getSettings()->autoCloseUserPopup, currentSplit);
|
||||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())),
|
|
||||||
currentSplit);
|
|
||||||
userPopup->setData(userName, channel);
|
userPopup->setData(userName, channel);
|
||||||
userPopup->moveTo(QCursor::pos(), widgets::BoundsChecking::CursorPosition);
|
userPopup->moveTo(QCursor::pos(), widgets::BoundsChecking::CursorPosition);
|
||||||
userPopup->show();
|
userPopup->show();
|
||||||
|
|
|
@ -47,7 +47,7 @@ QString sendShoutout(const CommandContext &ctx)
|
||||||
|
|
||||||
getHelix()->getUserByName(
|
getHelix()->getUserByName(
|
||||||
target,
|
target,
|
||||||
[twitchChannel, channel, currentUser, &target](const auto targetUser) {
|
[twitchChannel, channel, currentUser](const auto targetUser) {
|
||||||
getHelix()->sendShoutout(
|
getHelix()->sendShoutout(
|
||||||
twitchChannel->roomId(), targetUser.id,
|
twitchChannel->roomId(), targetUser.id,
|
||||||
currentUser->getUserId(),
|
currentUser->getUserId(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "controllers/completion/TabCompletionModel.hpp"
|
#include "controllers/completion/TabCompletionModel.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
#include "common/Channel.hpp"
|
#include "common/Channel.hpp"
|
||||||
#include "controllers/completion/sources/CommandSource.hpp"
|
#include "controllers/completion/sources/CommandSource.hpp"
|
||||||
#include "controllers/completion/sources/EmoteSource.hpp"
|
#include "controllers/completion/sources/EmoteSource.hpp"
|
||||||
|
@ -9,6 +10,9 @@
|
||||||
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
#include "controllers/completion/strategies/ClassicUserStrategy.hpp"
|
||||||
#include "controllers/completion/strategies/CommandStrategy.hpp"
|
#include "controllers/completion/strategies/CommandStrategy.hpp"
|
||||||
#include "controllers/completion/strategies/SmartEmoteStrategy.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"
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
namespace chatterino {
|
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);
|
this->updateSourceFromQuery(query);
|
||||||
|
|
||||||
|
@ -29,6 +35,17 @@ void TabCompletionModel::updateResults(const QString &query, bool isFirstWord)
|
||||||
|
|
||||||
// Copy results to this model
|
// Copy results to this model
|
||||||
QStringList results;
|
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->source_->addToStringList(results, 0, isFirstWord);
|
||||||
this->setStringList(results);
|
this->setStringList(results);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,12 @@ public:
|
||||||
|
|
||||||
/// @brief Updates the model based on the completion query
|
/// @brief Updates the model based on the completion query
|
||||||
/// @param query 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
|
/// @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:
|
private:
|
||||||
enum class SourceKind {
|
enum class SourceKind {
|
||||||
|
|
|
@ -204,6 +204,41 @@ void rebuildMessageHighlights(Settings &settings,
|
||||||
{
|
{
|
||||||
checks.emplace_back(highlightPhraseCheck(highlight));
|
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,
|
void rebuildUserHighlights(Settings &settings,
|
||||||
|
@ -434,6 +469,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
|
||||||
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
|
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
|
||||||
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
|
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] {
|
this->rebuildListener_.setCB([this, &settings] {
|
||||||
qCDebug(chatterinoHighlights)
|
qCDebug(chatterinoHighlights)
|
||||||
<< "Rebuild checks because a setting changed";
|
<< "Rebuild checks because a setting changed";
|
||||||
|
|
|
@ -98,9 +98,8 @@ void HighlightModel::afterInit()
|
||||||
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
|
||||||
setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
|
setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
|
||||||
|
|
||||||
// auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
|
auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
|
||||||
// setColorItem(whisperRow[Column::Color], *whisperColor, false);
|
setColorItem(whisperRow[Column::Color], *whisperColor, false);
|
||||||
whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
|
|
||||||
|
|
||||||
this->insertCustomRow(whisperRow, HighlightRowIndexes::WhisperRow);
|
this->insertCustomRow(whisperRow, HighlightRowIndexes::WhisperRow);
|
||||||
|
|
||||||
|
@ -234,6 +233,30 @@ void HighlightModel::afterInit()
|
||||||
|
|
||||||
this->insertCustomRow(threadMessageRow,
|
this->insertCustomRow(threadMessageRow,
|
||||||
HighlightRowIndexes::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,
|
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
|
@ -278,6 +301,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
getSettings()->enableThreadHighlight.setValue(
|
getSettings()->enableThreadHighlight.setValue(
|
||||||
value.toBool());
|
value.toBool());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->enableAutomodHighlight.setValue(
|
||||||
|
value.toBool());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -336,6 +364,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
getSettings()->enableThreadHighlightTaskbar.setValue(
|
getSettings()->enableThreadHighlightTaskbar.setValue(
|
||||||
value.toBool());
|
value.toBool());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->enableAutomodHighlightTaskbar.setValue(
|
||||||
|
value.toBool());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -377,6 +410,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
getSettings()->enableThreadHighlightSound.setValue(
|
getSettings()->enableThreadHighlightSound.setValue(
|
||||||
value.toBool());
|
value.toBool());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->enableAutomodHighlightSound.setValue(
|
||||||
|
value.toBool());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -412,6 +450,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
getSettings()->threadHighlightSoundUrl.setValue(
|
getSettings()->threadHighlightSoundUrl.setValue(
|
||||||
value.toString());
|
value.toString());
|
||||||
}
|
}
|
||||||
|
else if (rowIndex == HighlightRowIndexes::AutomodRow)
|
||||||
|
{
|
||||||
|
getSettings()->automodHighlightSoundUrl.setValue(
|
||||||
|
value.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -419,48 +462,47 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
|
||||||
// Custom color
|
// Custom color
|
||||||
if (role == Qt::DecorationRole)
|
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)
|
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)
|
else if (rowIndex == HighlightRowIndexes::SubRow)
|
||||||
{
|
{
|
||||||
getSettings()->subHighlightColor.setValue(colorName);
|
setColor(getSettings()->subHighlightColor,
|
||||||
|
ColorType::Subscription);
|
||||||
}
|
}
|
||||||
else if (rowIndex == HighlightRowIndexes::RedeemedRow)
|
else if (rowIndex == HighlightRowIndexes::RedeemedRow)
|
||||||
{
|
{
|
||||||
getSettings()->redeemedHighlightColor.setValue(colorName);
|
setColor(getSettings()->redeemedHighlightColor,
|
||||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
ColorType::RedeemedHighlight);
|
||||||
.updateColor(ColorType::RedeemedHighlight,
|
|
||||||
QColor(colorName));
|
|
||||||
}
|
}
|
||||||
else if (rowIndex == HighlightRowIndexes::FirstMessageRow)
|
else if (rowIndex == HighlightRowIndexes::FirstMessageRow)
|
||||||
{
|
{
|
||||||
getSettings()->firstMessageHighlightColor.setValue(
|
setColor(getSettings()->firstMessageHighlightColor,
|
||||||
colorName);
|
ColorType::FirstMessageHighlight);
|
||||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
|
||||||
.updateColor(ColorType::FirstMessageHighlight,
|
|
||||||
QColor(colorName));
|
|
||||||
}
|
}
|
||||||
else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow)
|
else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow)
|
||||||
{
|
{
|
||||||
getSettings()->elevatedMessageHighlightColor.setValue(
|
setColor(getSettings()->elevatedMessageHighlightColor,
|
||||||
colorName);
|
ColorType::ElevatedMessageHighlight);
|
||||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
|
||||||
.updateColor(ColorType::ElevatedMessageHighlight,
|
|
||||||
QColor(colorName));
|
|
||||||
}
|
}
|
||||||
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
|
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
|
||||||
{
|
{
|
||||||
getSettings()->threadHighlightColor.setValue(colorName);
|
setColor(getSettings()->threadHighlightColor,
|
||||||
const_cast<ColorProvider &>(ColorProvider::instance())
|
ColorType::ThreadMessageHighlight);
|
||||||
.updateColor(ColorType::ThreadMessageHighlight,
|
|
||||||
QColor(colorName));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ public:
|
||||||
FirstMessageRow = 4,
|
FirstMessageRow = 4,
|
||||||
ElevatedMessageRow = 5,
|
ElevatedMessageRow = 5,
|
||||||
ThreadMessageRow = 6,
|
ThreadMessageRow = 6,
|
||||||
|
AutomodRow = 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum UserHighlightRowIndexes {
|
enum UserHighlightRowIndexes {
|
||||||
|
|
|
@ -208,11 +208,11 @@ void NotificationController::checkStream(bool live, QString channelName)
|
||||||
|
|
||||||
void NotificationController::removeFakeChannel(const QString channelName)
|
void NotificationController::removeFakeChannel(const QString channelName)
|
||||||
{
|
{
|
||||||
auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
auto it = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(),
|
||||||
channelName);
|
channelName);
|
||||||
if (i != fakeTwitchChannels.end())
|
if (it != fakeTwitchChannels.end())
|
||||||
{
|
{
|
||||||
fakeTwitchChannels.erase(i);
|
fakeTwitchChannels.erase(it);
|
||||||
// "delete" old 'CHANNEL is live' message
|
// "delete" old 'CHANNEL is live' message
|
||||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||||
getApp()->twitch->liveChannel->getMessageSnapshot();
|
getApp()->twitch->liveChannel->getMessageSnapshot();
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
# include <QFileInfo>
|
# include <QFileInfo>
|
||||||
# include <QLoggingCategory>
|
# include <QLoggingCategory>
|
||||||
# include <QTextCodec>
|
# include <QTextCodec>
|
||||||
|
# include <QUrl>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
@ -94,6 +95,37 @@ int c2_register_command(lua_State *L)
|
||||||
return 1;
|
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)
|
int c2_send_msg(lua_State *L)
|
||||||
{
|
{
|
||||||
QString text;
|
QString text;
|
||||||
|
@ -167,6 +199,7 @@ int c2_system_msg(lua_State *L)
|
||||||
lua::push(L, false);
|
lua::push(L, false);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||||
if (chn->isEmpty())
|
if (chn->isEmpty())
|
||||||
{
|
{
|
||||||
|
@ -250,69 +283,87 @@ int g_load(lua_State *L)
|
||||||
# endif
|
# 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);
|
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||||
QString fname;
|
if (pl == nullptr)
|
||||||
if (!lua::pop(L, &fname))
|
|
||||||
{
|
{
|
||||||
lua_pushnil(L);
|
return luaL_error(L, "loadfile: internal error: no plugin?");
|
||||||
luaL_error(L, "chatterino g_import: expected a string for a filename");
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/");
|
auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/");
|
||||||
auto file = dir.resolved(fname);
|
|
||||||
|
|
||||||
qCDebug(chatterinoLua) << "plugin" << pl->id << "is trying to load" << file
|
if (!dir.isParentOf(str))
|
||||||
<< "(its dir is" << dir << ")";
|
|
||||||
if (!dir.isParentOf(file))
|
|
||||||
{
|
{
|
||||||
lua_pushnil(L);
|
// XXX: This intentionally hides the resolved path to not leak it
|
||||||
luaL_error(L, "chatterino g_import: filename must be inside of the "
|
lua::push(
|
||||||
"plugin directory");
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto path = file.path(QUrl::FullyDecoded);
|
auto temp = str.toStdString();
|
||||||
QFile qf(path);
|
const auto *filename = temp.c_str();
|
||||||
qf.open(QIODevice::ReadOnly);
|
|
||||||
if (qf.size() > 10'000'000)
|
auto res = luaL_loadfilex(L, filename, "t");
|
||||||
|
// Yoinked from checkload lib/lua/src/loadlib.c
|
||||||
|
if (res == LUA_OK)
|
||||||
{
|
{
|
||||||
lua_pushnil(L);
|
lua_pushstring(L, filename);
|
||||||
luaL_error(L, "chatterino g_import: size limit of 10MB exceeded, what "
|
return 2;
|
||||||
"the hell are you doing");
|
}
|
||||||
|
|
||||||
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate utf-8 to block bytecode exploits
|
auto parent = QFileInfo(currentFile).dir();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch dofile and call it
|
auto name = QString::fromUtf8(luaL_checkstring(L, 1));
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
name = name.replace('.', QDir::separator());
|
||||||
// maybe data race here if symlink was swapped?
|
QString filename =
|
||||||
lua::push(L, path);
|
parent.canonicalPath() + QDir::separator() + name + ".lua";
|
||||||
lua_call(L, 1, LUA_MULTRET);
|
|
||||||
|
|
||||||
return lua_gettop(L);
|
return loadfile(L, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
int g_print(lua_State *L)
|
int g_print(lua_State *L)
|
||||||
|
|
|
@ -1,27 +1,111 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include <QString>
|
||||||
|
|
||||||
|
# include <vector>
|
||||||
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
namespace chatterino::lua::api {
|
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)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
// Following functions are exposed in c2 table.
|
// 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);
|
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);
|
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);
|
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);
|
int c2_log(lua_State *L);
|
||||||
|
|
||||||
// These ones are global
|
// These ones are global
|
||||||
int g_load(lua_State *L);
|
int g_load(lua_State *L);
|
||||||
int g_print(lua_State *L);
|
int g_print(lua_State *L);
|
||||||
int g_import(lua_State *L);
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
// Exposed as c2.LogLevel
|
// This is for require() exposed as an element of package.searchers
|
||||||
// Represents "calls" to qCDebug, qCInfo ...
|
int searcherAbsolute(lua_State *L);
|
||||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
int searcherRelative(lua_State *L);
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
# include "common/Channel.hpp"
|
# include "common/Channel.hpp"
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandContext.hpp"
|
# include "controllers/commands/CommandContext.hpp"
|
||||||
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
|
@ -75,6 +76,9 @@ QString humanErrorText(lua_State *L, int errCode)
|
||||||
case LUA_ERRFILE:
|
case LUA_ERRFILE:
|
||||||
errName = "(file error)";
|
errName = "(file error)";
|
||||||
break;
|
break;
|
||||||
|
case ERROR_BAD_PEEK:
|
||||||
|
errName = "(unable to convert value to c++)";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
errName = "(unknown error type)";
|
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)
|
StackIdx push(lua_State *L, const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
|
StackGuard guard(L, 1);
|
||||||
auto outIdx = pushEmptyTable(L, 2);
|
auto outIdx = pushEmptyTable(L, 2);
|
||||||
|
|
||||||
push(L, ctx.words);
|
push(L, ctx.words);
|
||||||
|
@ -127,8 +132,27 @@ StackIdx push(lua_State *L, const bool &b)
|
||||||
return lua_gettop(L);
|
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)
|
bool peek(lua_State *L, double *out, StackIdx idx)
|
||||||
{
|
{
|
||||||
|
StackGuard guard(L);
|
||||||
int ok{0};
|
int ok{0};
|
||||||
auto v = lua_tonumberx(L, idx, &ok);
|
auto v = lua_tonumberx(L, idx, &ok);
|
||||||
if (ok != 0)
|
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)
|
bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||||
{
|
{
|
||||||
|
StackGuard guard(L);
|
||||||
size_t len{0};
|
size_t len{0};
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
const char *str = lua_tolstring(L, idx, &len);
|
||||||
if (str == nullptr)
|
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)
|
bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
||||||
{
|
{
|
||||||
|
StackGuard guard(L);
|
||||||
size_t len{0};
|
size_t len{0};
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
const char *str = lua_tolstring(L, idx, &len);
|
||||||
if (str == nullptr)
|
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)
|
bool peek(lua_State *L, std::string *out, StackIdx idx)
|
||||||
{
|
{
|
||||||
|
StackGuard guard(L);
|
||||||
size_t len{0};
|
size_t len{0};
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
const char *str = lua_tolstring(L, idx, &len);
|
||||||
if (str == nullptr)
|
if (str == nullptr)
|
||||||
|
@ -186,6 +213,23 @@ bool peek(lua_State *L, std::string *out, StackIdx idx)
|
||||||
return true;
|
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)
|
QString toString(lua_State *L, StackIdx idx)
|
||||||
{
|
{
|
||||||
size_t len{};
|
size_t len{};
|
||||||
|
|
|
@ -2,14 +2,19 @@
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
# include "common/QLogging.hpp"
|
||||||
|
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
# include <lualib.h>
|
# include <lualib.h>
|
||||||
# include <magic_enum/magic_enum.hpp>
|
# include <magic_enum/magic_enum.hpp>
|
||||||
# include <QList>
|
# include <QList>
|
||||||
|
|
||||||
|
# include <cassert>
|
||||||
|
# include <optional>
|
||||||
# include <string>
|
# include <string>
|
||||||
# include <string_view>
|
# include <string_view>
|
||||||
# include <type_traits>
|
# include <type_traits>
|
||||||
|
# include <variant>
|
||||||
# include <vector>
|
# include <vector>
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
class QJsonObject;
|
class QJsonObject;
|
||||||
|
@ -19,6 +24,12 @@ struct CommandContext;
|
||||||
|
|
||||||
namespace chatterino::lua {
|
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)
|
* @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 QString &str);
|
||||||
StackIdx push(lua_State *L, const std::string &str);
|
StackIdx push(lua_State *L, const std::string &str);
|
||||||
StackIdx push(lua_State *L, const bool &b);
|
StackIdx push(lua_State *L, const bool &b);
|
||||||
|
StackIdx push(lua_State *L, const int &b);
|
||||||
|
|
||||||
// returns OK?
|
// 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, double *out, StackIdx idx = -1);
|
||||||
bool peek(lua_State *L, QString *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, QByteArray *out, StackIdx idx = -1);
|
||||||
bool peek(lua_State *L, std::string *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.
|
* @brief Converts Lua object at stack index idx to a string.
|
||||||
*/
|
*/
|
||||||
QString toString(lua_State *L, StackIdx idx = -1);
|
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
|
/// 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
|
* @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>
|
template <typename T>
|
||||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
||||||
{
|
{
|
||||||
|
StackGuard guard(L, -1);
|
||||||
auto ok = peek(L, out, idx);
|
auto ok = peek(L, out, idx);
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
|
@ -186,6 +314,58 @@ StackIdx pushEnumTable(lua_State *L)
|
||||||
return out;
|
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
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
|
||||||
# include <QDir>
|
# include <QDir>
|
||||||
# include <QString>
|
# include <QString>
|
||||||
|
@ -85,10 +87,51 @@ public:
|
||||||
return this->loadDirectory_;
|
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:
|
private:
|
||||||
QDir loadDirectory_;
|
QDir loadDirectory_;
|
||||||
lua_State *state_;
|
lua_State *state_;
|
||||||
|
|
||||||
|
QString error_;
|
||||||
|
|
||||||
// maps command name -> function name
|
// maps command name -> function name
|
||||||
std::unordered_map<QString, QString> ownedCommands;
|
std::unordered_map<QString, QString> ownedCommands;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
|
# include "common/Args.hpp"
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandContext.hpp"
|
# include "controllers/commands/CommandContext.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
# include "controllers/commands/CommandController.hpp"
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
# include <variant>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
@ -45,15 +47,13 @@ void PluginController::loadPlugins()
|
||||||
auto dir = QDir(getPaths()->pluginsDirectory);
|
auto dir = QDir(getPaths()->pluginsDirectory);
|
||||||
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
||||||
for (const auto &info :
|
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)
|
bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||||
{
|
{
|
||||||
// look for init.lua
|
// look for init.lua
|
||||||
|
@ -103,9 +103,10 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginController::openLibrariesFor(lua_State *L,
|
void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||||
const PluginMeta & /*meta*/)
|
const QDir &pluginDir)
|
||||||
{
|
{
|
||||||
|
lua::StackGuard guard(L);
|
||||||
// Stuff to change, remove or hide behind a permission system:
|
// Stuff to change, remove or hide behind a permission system:
|
||||||
static const std::vector<luaL_Reg> loadedlibs = {
|
static const std::vector<luaL_Reg> loadedlibs = {
|
||||||
luaL_Reg{LUA_GNAME, luaopen_base},
|
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_STRLIBNAME, luaopen_string},
|
||||||
luaL_Reg{LUA_MATHLIBNAME, luaopen_math},
|
luaL_Reg{LUA_MATHLIBNAME, luaopen_math},
|
||||||
luaL_Reg{LUA_UTF8LIBNAME, luaopen_utf8},
|
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
|
// Warning: Do not add debug library to this, it would make the security of
|
||||||
// this a living nightmare due to stuff like registry access
|
// 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[] = {
|
static const luaL_Reg c2Lib[] = {
|
||||||
{"system_msg", lua::api::c2_system_msg},
|
{"system_msg", lua::api::c2_system_msg},
|
||||||
{"register_command", lua::api::c2_register_command},
|
{"register_command", lua::api::c2_register_command},
|
||||||
|
{"register_callback", lua::api::c2_register_callback},
|
||||||
{"send_msg", lua::api::c2_send_msg},
|
{"send_msg", lua::api::c2_send_msg},
|
||||||
{"log", lua::api::c2_log},
|
{"log", lua::api::c2_log},
|
||||||
{nullptr, nullptr},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
lua_pushglobaltable(L);
|
lua_pushglobaltable(L);
|
||||||
auto global = lua_gettop(L);
|
auto gtable = lua_gettop(L);
|
||||||
|
|
||||||
// count of elements in C2LIB + LogLevel
|
// count of elements in C2LIB + LogLevel + EventType
|
||||||
auto c2libIdx = lua::pushEmptyTable(L, 5);
|
auto c2libIdx = lua::pushEmptyTable(L, 8);
|
||||||
|
|
||||||
luaL_setfuncs(L, c2Lib, 0);
|
luaL_setfuncs(L, c2Lib, 0);
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
lua::pushEnumTable<lua::api::LogLevel>(L);
|
||||||
lua_setfield(L, c2libIdx, "LogLevel");
|
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
|
// ban functions
|
||||||
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
// 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?
|
// possibly randomize this name at runtime to prevent some attacks?
|
||||||
|
|
||||||
# ifndef NDEBUG
|
# ifndef NDEBUG
|
||||||
|
@ -168,16 +171,10 @@ void PluginController::openLibrariesFor(lua_State *L,
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||||
# endif
|
# endif
|
||||||
|
|
||||||
lua_getfield(L, gtable, "dofile");
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||||
static const luaL_Reg replacementFuncs[] = {
|
static const luaL_Reg replacementFuncs[] = {
|
||||||
{"load", lua::api::g_load},
|
{"load", lua::api::g_load},
|
||||||
{"print", lua::api::g_print},
|
{"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},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
luaL_setfuncs(L, replacementFuncs, 0);
|
luaL_setfuncs(L, replacementFuncs, 0);
|
||||||
|
@ -188,18 +185,63 @@ void PluginController::openLibrariesFor(lua_State *L,
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
lua_setfield(L, gtable, "dofile");
|
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,
|
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
const PluginMeta &meta)
|
const PluginMeta &meta)
|
||||||
{
|
{
|
||||||
lua_State *l = luaL_newstate();
|
|
||||||
PluginController::openLibrariesFor(l, meta);
|
|
||||||
|
|
||||||
auto pluginName = pluginDir.dirName();
|
auto pluginName = pluginDir.dirName();
|
||||||
|
lua_State *l = luaL_newstate();
|
||||||
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
||||||
|
auto *temp = plugin.get();
|
||||||
this->plugins_.insert({pluginName, std::move(plugin)});
|
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) ||
|
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||||
!getSettings()->pluginsEnabled)
|
!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());
|
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||||
if (err != 0)
|
if (err != 0)
|
||||||
{
|
{
|
||||||
|
temp->error_ = lua::humanErrorText(l, err);
|
||||||
qCWarning(chatterinoLua)
|
qCWarning(chatterinoLua)
|
||||||
<< "Failed to load" << pluginName << "plugin from" << index << ": "
|
<< "Failed to load" << pluginName << "plugin from" << index << ": "
|
||||||
<< lua::humanErrorText(l, err);
|
<< temp->error_;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
||||||
|
@ -298,5 +341,52 @@ const std::map<QString, std::unique_ptr<Plugin>> &PluginController::plugins()
|
||||||
return this->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
|
#endif
|
||||||
|
|
|
@ -36,6 +36,7 @@ public:
|
||||||
// This is required to be public because of c functions
|
// This is required to be public because of c functions
|
||||||
Plugin *getPluginByStatePtr(lua_State *L);
|
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;
|
const std::map<QString, std::unique_ptr<Plugin>> &plugins() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,17 +53,22 @@ public:
|
||||||
*/
|
*/
|
||||||
static bool isPluginEnabled(const QString &id);
|
static bool isPluginEnabled(const QString &id);
|
||||||
|
|
||||||
|
std::pair<bool, QStringList> updateCustomCompletions(
|
||||||
|
const QString &query, const QString &fullTextContent,
|
||||||
|
int cursorPosition, bool isFirstWord) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void loadPlugins();
|
void loadPlugins();
|
||||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
const PluginMeta &meta);
|
const PluginMeta &meta);
|
||||||
|
|
||||||
// This function adds lua standard libraries into the state
|
// 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);
|
static void loadChatterinoLib(lua_State *l);
|
||||||
bool tryLoadFromDir(const QDir &pluginDir);
|
bool tryLoadFromDir(const QDir &pluginDir);
|
||||||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}; // namespace chatterino
|
} // namespace chatterino
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -256,8 +256,8 @@ void MiniaudioBackend::play(const QUrl &sound)
|
||||||
if (sound.isLocalFile())
|
if (sound.isLocalFile())
|
||||||
{
|
{
|
||||||
auto soundPath = sound.toLocalFile();
|
auto soundPath = sound.toLocalFile();
|
||||||
auto result = ma_engine_play_sound(this->engine.get(),
|
result = ma_engine_play_sound(this->engine.get(),
|
||||||
qPrintable(soundPath), nullptr);
|
qPrintable(soundPath), nullptr);
|
||||||
if (result != MA_SUCCESS)
|
if (result != MA_SUCCESS)
|
||||||
{
|
{
|
||||||
qCWarning(chatterinoSound) << "Failed to play sound" << sound
|
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/Modes.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "common/Version.hpp"
|
#include "common/Version.hpp"
|
||||||
#include "providers/Crashpad.hpp"
|
|
||||||
#include "providers/IvrApi.hpp"
|
#include "providers/IvrApi.hpp"
|
||||||
#include "providers/NetworkConfigurationProvider.hpp"
|
#include "providers/NetworkConfigurationProvider.hpp"
|
||||||
#include "providers/twitch/api/Helix.hpp"
|
#include "providers/twitch/api/Helix.hpp"
|
||||||
#include "RunGui.hpp"
|
#include "RunGui.hpp"
|
||||||
|
#include "singletons/CrashHandler.hpp"
|
||||||
#include "singletons/Paths.hpp"
|
#include "singletons/Paths.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
#include "util/AttachToConsole.hpp"
|
#include "util/AttachToConsole.hpp"
|
||||||
|
@ -62,18 +62,18 @@ int main(int argc, char **argv)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
initArgs(a);
|
const Args args(a);
|
||||||
|
|
||||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||||
const auto crashpadHandler = installCrashHandler();
|
const auto crashpadHandler = installCrashHandler(args);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// run in gui mode or browser extension host mode
|
// run in gui mode or browser extension host mode
|
||||||
if (getArgs().shouldRunBrowserExtensionHost)
|
if (args.shouldRunBrowserExtensionHost)
|
||||||
{
|
{
|
||||||
runBrowserExtensionHost();
|
runBrowserExtensionHost();
|
||||||
}
|
}
|
||||||
else if (getArgs().printVersion)
|
else if (args.printVersion)
|
||||||
{
|
{
|
||||||
attachToConsole();
|
attachToConsole();
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ int main(int argc, char **argv)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (getArgs().verbose)
|
if (args.verbose)
|
||||||
{
|
{
|
||||||
attachToConsole();
|
attachToConsole();
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ int main(int argc, char **argv)
|
||||||
|
|
||||||
Settings settings(paths->settingsDirectory);
|
Settings settings(paths->settingsDirectory);
|
||||||
|
|
||||||
runGui(a, *paths, settings);
|
runGui(a, *paths, settings, args);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,9 @@ enum class MessageFlag : int64_t {
|
||||||
LiveUpdatesAdd = (1LL << 28),
|
LiveUpdatesAdd = (1LL << 28),
|
||||||
LiveUpdatesRemove = (1LL << 29),
|
LiveUpdatesRemove = (1LL << 29),
|
||||||
LiveUpdatesUpdate = (1LL << 30),
|
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>;
|
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||||
|
|
||||||
|
|
|
@ -78,152 +78,6 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time)
|
||||||
return MessageBuilder(systemMessage, text, time).release();
|
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()
|
MessageBuilder::MessageBuilder()
|
||||||
: message_(std::make_shared<Message>())
|
: message_(std::make_shared<Message>())
|
||||||
{
|
{
|
||||||
|
|
|
@ -53,9 +53,6 @@ const ImageUploaderResultTag imageUploaderResultMessage{};
|
||||||
|
|
||||||
MessagePtr makeSystemMessage(const QString &text);
|
MessagePtr makeSystemMessage(const QString &text);
|
||||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
||||||
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|
||||||
const AutomodAction &action);
|
|
||||||
MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
|
|
||||||
|
|
||||||
struct MessageParseArgs {
|
struct MessageParseArgs {
|
||||||
bool disablePingSounds = false;
|
bool disablePingSounds = false;
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
@ -170,18 +172,10 @@ void SharedMessageBuilder::parseHighlights()
|
||||||
this->highlightAlert_ = highlightResult.alert;
|
this->highlightAlert_ = highlightResult.alert;
|
||||||
|
|
||||||
this->highlightSound_ = highlightResult.playSound;
|
this->highlightSound_ = highlightResult.playSound;
|
||||||
|
this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl;
|
||||||
|
|
||||||
this->message().highlightColor = highlightResult.color;
|
this->message().highlightColor = highlightResult.color;
|
||||||
|
|
||||||
if (highlightResult.customSoundUrl)
|
|
||||||
{
|
|
||||||
this->highlightSoundUrl_ = *highlightResult.customSoundUrl;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->highlightSoundUrl_ = getFallbackHighlightSound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highlightResult.showInMentions)
|
if (highlightResult.showInMentions)
|
||||||
{
|
{
|
||||||
this->message().flags.set(MessageFlag::ShowInMentions);
|
this->message().flags.set(MessageFlag::ShowInMentions);
|
||||||
|
@ -199,6 +193,15 @@ void SharedMessageBuilder::appendChannelName()
|
||||||
}
|
}
|
||||||
|
|
||||||
void SharedMessageBuilder::triggerHighlights()
|
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)
|
if (isInStreamerMode() && getSettings()->streamerModeMuteMentions)
|
||||||
{
|
{
|
||||||
|
@ -206,21 +209,32 @@ void SharedMessageBuilder::triggerHighlights()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSettings()->isMutedChannel(this->channel->getName()))
|
if (getSettings()->isMutedChannel(channelName))
|
||||||
{
|
{
|
||||||
// Do nothing. Pings are muted in this channel.
|
// Do nothing. Pings are muted in this channel.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasFocus = (QApplication::focusWidget() != nullptr);
|
const bool hasFocus = (QApplication::focusWidget() != nullptr);
|
||||||
bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
|
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();
|
getApp()->windows->sendAlert();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
class Badge;
|
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
|
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
|
||||||
virtual void parseHighlights();
|
virtual void parseHighlights();
|
||||||
|
static void triggerHighlights(const QString &channelName, bool playSound,
|
||||||
|
const std::optional<QUrl> &customSoundUrl,
|
||||||
|
bool windowAlert);
|
||||||
|
|
||||||
void appendChannelName();
|
void appendChannelName();
|
||||||
|
|
||||||
|
@ -72,8 +77,7 @@ protected:
|
||||||
|
|
||||||
bool highlightAlert_ = false;
|
bool highlightAlert_ = false;
|
||||||
bool highlightSound_ = false;
|
bool highlightSound_ = false;
|
||||||
|
std::optional<QUrl> highlightSoundCustomUrl_{};
|
||||||
QUrl highlightSoundUrl_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -196,8 +196,10 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Painting
|
// Painting
|
||||||
void MessageLayout::paint(const MessagePaintContext &ctx)
|
MessagePaintResult MessageLayout::paint(const MessagePaintContext &ctx)
|
||||||
{
|
{
|
||||||
|
MessagePaintResult result;
|
||||||
|
|
||||||
QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth);
|
QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth);
|
||||||
|
|
||||||
if (!this->bufferValid_)
|
if (!this->bufferValid_)
|
||||||
|
@ -209,7 +211,8 @@ void MessageLayout::paint(const MessagePaintContext &ctx)
|
||||||
ctx.painter.drawPixmap(0, ctx.y, *pixmap);
|
ctx.painter.drawPixmap(0, ctx.y, *pixmap);
|
||||||
|
|
||||||
// draw gif emotes
|
// draw gif emotes
|
||||||
this->container_.paintAnimatedElements(ctx.painter, ctx.y);
|
result.hasAnimatedElements =
|
||||||
|
this->container_.paintAnimatedElements(ctx.painter, ctx.y);
|
||||||
|
|
||||||
// draw disabled
|
// draw disabled
|
||||||
if (this->message_->flags.has(MessageFlag::Disabled))
|
if (this->message_->flags.has(MessageFlag::Disabled))
|
||||||
|
@ -270,6 +273,8 @@ void MessageLayout::paint(const MessagePaintContext &ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
this->bufferValid_ = true;
|
this->bufferValid_ = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width)
|
QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width)
|
||||||
|
@ -337,9 +342,13 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
|
||||||
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
|
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
|
||||||
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
|
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
|
||||||
{
|
{
|
||||||
// Blend highlight color with usual background color
|
assert(this->message_->highlightColor);
|
||||||
backgroundColor =
|
if (this->message_->highlightColor)
|
||||||
blendColors(backgroundColor, *this->message_->highlightColor);
|
{
|
||||||
|
// Blend highlight color with usual background color
|
||||||
|
backgroundColor =
|
||||||
|
blendColors(backgroundColor, *this->message_->highlightColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (this->message_->flags.has(MessageFlag::Subscription) &&
|
else if (this->message_->flags.has(MessageFlag::Subscription) &&
|
||||||
ctx.preferences.enableSubHighlight)
|
ctx.preferences.enableSubHighlight)
|
||||||
|
@ -358,7 +367,8 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
|
||||||
blendColors(backgroundColor,
|
blendColors(backgroundColor,
|
||||||
*ctx.colorProvider.color(ColorType::RedeemedHighlight));
|
*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");
|
backgroundColor = QColor("#404040");
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ enum class MessageLayoutFlag : uint8_t {
|
||||||
};
|
};
|
||||||
using MessageLayoutFlags = FlagsEnum<MessageLayoutFlag>;
|
using MessageLayoutFlags = FlagsEnum<MessageLayoutFlag>;
|
||||||
|
|
||||||
|
struct MessagePaintResult {
|
||||||
|
bool hasAnimatedElements = false;
|
||||||
|
};
|
||||||
|
|
||||||
class MessageLayout
|
class MessageLayout
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -55,7 +59,7 @@ public:
|
||||||
bool layout(int width, float scale_, MessageElementFlags flags);
|
bool layout(int width, float scale_, MessageElementFlags flags);
|
||||||
|
|
||||||
// Painting
|
// Painting
|
||||||
void paint(const MessagePaintContext &ctx);
|
MessagePaintResult paint(const MessagePaintContext &ctx);
|
||||||
void invalidateBuffer();
|
void invalidateBuffer();
|
||||||
void deleteBuffer();
|
void deleteBuffer();
|
||||||
void deleteCache();
|
void deleteCache();
|
||||||
|
|
|
@ -235,13 +235,15 @@ void MessageLayoutContainer::paintElements(QPainter &painter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLayoutContainer::paintAnimatedElements(QPainter &painter,
|
bool MessageLayoutContainer::paintAnimatedElements(QPainter &painter,
|
||||||
int yOffset) const
|
int yOffset) const
|
||||||
{
|
{
|
||||||
|
bool anyAnimatedElement = false;
|
||||||
for (const auto &element : this->elements_)
|
for (const auto &element : this->elements_)
|
||||||
{
|
{
|
||||||
element->paintAnimated(painter, yOffset);
|
anyAnimatedElement |= element->paintAnimated(painter, yOffset);
|
||||||
}
|
}
|
||||||
|
return anyAnimatedElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLayoutContainer::paintSelection(QPainter &painter,
|
void MessageLayoutContainer::paintSelection(QPainter &painter,
|
||||||
|
|
|
@ -64,8 +64,9 @@ struct MessageLayoutContainer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paint the animated elements in this message
|
* 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
|
* Paint the selection for this container
|
||||||
|
|
|
@ -47,6 +47,12 @@ void MessagePreferences::connectSettings(Settings *settings,
|
||||||
},
|
},
|
||||||
holder);
|
holder);
|
||||||
|
|
||||||
|
settings->enableAutomodHighlight.connect(
|
||||||
|
[this](const auto &newValue) {
|
||||||
|
this->enableAutomodHighlight = newValue;
|
||||||
|
},
|
||||||
|
holder);
|
||||||
|
|
||||||
settings->alternateMessages.connect(
|
settings->alternateMessages.connect(
|
||||||
[this](const auto &newValue) {
|
[this](const auto &newValue) {
|
||||||
this->alternateMessages = newValue;
|
this->alternateMessages = newValue;
|
||||||
|
|
|
@ -39,6 +39,7 @@ struct MessagePreferences {
|
||||||
bool enableElevatedMessageHighlight{};
|
bool enableElevatedMessageHighlight{};
|
||||||
bool enableFirstMessageHighlight{};
|
bool enableFirstMessageHighlight{};
|
||||||
bool enableSubHighlight{};
|
bool enableSubHighlight{};
|
||||||
|
bool enableAutomodHighlight{};
|
||||||
|
|
||||||
bool alternateMessages{};
|
bool alternateMessages{};
|
||||||
bool separateMessages{};
|
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)
|
if (this->image_ == nullptr)
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->image_->animated())
|
if (this->image_->animated())
|
||||||
|
@ -167,8 +167,10 @@ void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||||
auto rect = this->getRect();
|
auto rect = this->getRect();
|
||||||
rect.moveTop(rect.y() + yOffset);
|
rect.moveTop(rect.y() + yOffset);
|
||||||
painter.drawPixmap(QRectF(rect), *pixmap, QRectF());
|
painter.drawPixmap(QRectF(rect), *pixmap, QRectF());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
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());
|
auto fullRect = QRectF(this->getRect());
|
||||||
fullRect.moveTop(fullRect.y() + yOffset);
|
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
|
int LayeredImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||||
|
@ -446,8 +449,9 @@ void TextLayoutElement::paint(QPainter &painter,
|
||||||
QTextOption(Qt::AlignLeft | Qt::AlignTop));
|
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
|
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
|
int TextIconLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||||
|
@ -640,8 +646,10 @@ void ReplyCurveLayoutElement::paint(QPainter &painter,
|
||||||
painter.drawPath(path);
|
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
|
int ReplyCurveLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||||
|
|
|
@ -52,7 +52,8 @@ public:
|
||||||
virtual size_t getSelectionIndexCount() const = 0;
|
virtual size_t getSelectionIndexCount() const = 0;
|
||||||
virtual void paint(QPainter &painter,
|
virtual void paint(QPainter &painter,
|
||||||
const MessageColors &messageColors) = 0;
|
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 getMouseOverIndex(const QPoint &abs) const = 0;
|
||||||
virtual int getXFromIndex(size_t index) = 0;
|
virtual int getXFromIndex(size_t index) = 0;
|
||||||
|
|
||||||
|
@ -86,7 +87,7 @@ protected:
|
||||||
uint32_t to = UINT32_MAX) const override;
|
uint32_t to = UINT32_MAX) const override;
|
||||||
size_t getSelectionIndexCount() const override;
|
size_t getSelectionIndexCount() const override;
|
||||||
void paint(QPainter &painter, const MessageColors &messageColors) 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 getMouseOverIndex(const QPoint &abs) const override;
|
||||||
int getXFromIndex(size_t index) override;
|
int getXFromIndex(size_t index) override;
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ protected:
|
||||||
uint32_t to = UINT32_MAX) const override;
|
uint32_t to = UINT32_MAX) const override;
|
||||||
size_t getSelectionIndexCount() const override;
|
size_t getSelectionIndexCount() const override;
|
||||||
void paint(QPainter &painter, const MessageColors &messageColors) 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 getMouseOverIndex(const QPoint &abs) const override;
|
||||||
int getXFromIndex(size_t index) override;
|
int getXFromIndex(size_t index) override;
|
||||||
|
|
||||||
|
@ -158,7 +159,7 @@ protected:
|
||||||
uint32_t to = UINT32_MAX) const override;
|
uint32_t to = UINT32_MAX) const override;
|
||||||
size_t getSelectionIndexCount() const override;
|
size_t getSelectionIndexCount() const override;
|
||||||
void paint(QPainter &painter, const MessageColors &messageColors) 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 getMouseOverIndex(const QPoint &abs) const override;
|
||||||
int getXFromIndex(size_t index) override;
|
int getXFromIndex(size_t index) override;
|
||||||
|
|
||||||
|
@ -182,7 +183,7 @@ protected:
|
||||||
uint32_t to = UINT32_MAX) const override;
|
uint32_t to = UINT32_MAX) const override;
|
||||||
size_t getSelectionIndexCount() const override;
|
size_t getSelectionIndexCount() const override;
|
||||||
void paint(QPainter &painter, const MessageColors &messageColors) 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 getMouseOverIndex(const QPoint &abs) const override;
|
||||||
int getXFromIndex(size_t index) override;
|
int getXFromIndex(size_t index) override;
|
||||||
|
|
||||||
|
@ -200,7 +201,7 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paint(QPainter &painter, const MessageColors &messageColors) 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 getMouseOverIndex(const QPoint &abs) const override;
|
||||||
int getXFromIndex(size_t index) override;
|
int getXFromIndex(size_t index) override;
|
||||||
void addCopyTextToString(QString &str, uint32_t from = 0,
|
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/LimitedQueueSnapshot.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
|
||||||
namespace chatterino {
|
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_
|
// Ratelimits for joinBucket_
|
||||||
const int JOIN_RATELIMIT_BUDGET = 18;
|
const int JOIN_RATELIMIT_BUDGET = 18;
|
||||||
const int JOIN_RATELIMIT_COOLDOWN = 12500;
|
const int JOIN_RATELIMIT_COOLDOWN = 12500;
|
||||||
|
@ -88,6 +85,9 @@ AbstractIrcServer::AbstractIrcServer()
|
||||||
}
|
}
|
||||||
this->readConnection_->smartReconnect();
|
this->readConnection_->smartReconnect();
|
||||||
});
|
});
|
||||||
|
this->connections_.managedConnect(this->readConnection_->heartbeat, [this] {
|
||||||
|
this->markChannelsConnected();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractIrcServer::initializeIrc()
|
void AbstractIrcServer::initializeIrc()
|
||||||
|
@ -331,8 +331,6 @@ void AbstractIrcServer::onReadConnected(IrcConnection *connection)
|
||||||
{
|
{
|
||||||
chan->addMessage(connectedMsg);
|
chan->addMessage(connectedMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
chan->connected.invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->falloffCounter_ = 1;
|
this->falloffCounter_ = 1;
|
||||||
|
@ -360,9 +358,24 @@ void AbstractIrcServer::onDisconnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
chan->addMessage(disconnectedMsg);
|
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(
|
std::shared_ptr<Channel> AbstractIrcServer::getCustomChannel(
|
||||||
const QString &channelName)
|
const QString &channelName)
|
||||||
{
|
{
|
||||||
|
|
|
@ -73,6 +73,7 @@ protected:
|
||||||
virtual void onReadConnected(IrcConnection *connection);
|
virtual void onReadConnected(IrcConnection *connection);
|
||||||
virtual void onWriteConnected(IrcConnection *connection);
|
virtual void onWriteConnected(IrcConnection *connection);
|
||||||
virtual void onDisconnected();
|
virtual void onDisconnected();
|
||||||
|
void markChannelsConnected();
|
||||||
|
|
||||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||||
const QString &channelName);
|
const QString &channelName);
|
||||||
|
|
|
@ -16,7 +16,7 @@ IrcConnection::IrcConnection(QObject *parent)
|
||||||
{
|
{
|
||||||
// Log connection errors for ease-of-debugging
|
// Log connection errors for ease-of-debugging
|
||||||
QObject::connect(this, &Communi::IrcConnection::socketError, this,
|
QObject::connect(this, &Communi::IrcConnection::socketError, this,
|
||||||
[this](QAbstractSocket::SocketError error) {
|
[](QAbstractSocket::SocketError error) {
|
||||||
qCDebug(chatterinoIrc) << "Connection error:" << error;
|
qCDebug(chatterinoIrc) << "Connection error:" << error;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ IrcConnection::IrcConnection(QObject *parent)
|
||||||
// If we're still receiving messages, all is well
|
// If we're still receiving messages, all is well
|
||||||
this->recentlyReceivedMessage_ = false;
|
this->recentlyReceivedMessage_ = false;
|
||||||
this->waitingForPong_ = false;
|
this->waitingForPong_ = false;
|
||||||
|
this->heartbeat.invoke();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ public:
|
||||||
// receiver to trigger a reconnect, if desired
|
// receiver to trigger a reconnect, if desired
|
||||||
pajlada::Signals::Signal<bool> connectionLost;
|
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.
|
// Request a reconnect with a minimum interval between attempts.
|
||||||
// This won't violate RECONNECT_MIN_INTERVAL
|
// This won't violate RECONNECT_MIN_INTERVAL
|
||||||
void smartReconnect();
|
void smartReconnect();
|
||||||
|
|
|
@ -18,72 +18,84 @@ namespace chatterino::recentmessages {
|
||||||
|
|
||||||
using namespace recentmessages::detail;
|
using namespace recentmessages::detail;
|
||||||
|
|
||||||
void load(const QString &channelName, std::weak_ptr<Channel> channelPtr,
|
void load(
|
||||||
ResultCallback onLoaded, ErrorCallback onError)
|
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;
|
qCDebug(LOG) << "Loading recent messages for" << channelName;
|
||||||
|
|
||||||
const auto url = constructRecentMessagesUrl(channelName);
|
const auto url =
|
||||||
|
constructRecentMessagesUrl(channelName, limit, after, before);
|
||||||
|
|
||||||
NetworkRequest(url)
|
const long delayMs = jitter ? std::rand() % 100 : 0;
|
||||||
.onSuccess([channelPtr, onLoaded](const auto &result) {
|
QTimer::singleShot(delayMs, [=] {
|
||||||
auto shared = channelPtr.lock();
|
NetworkRequest(url)
|
||||||
if (!shared)
|
.onSuccess([channelPtr, onLoaded](const auto &result) {
|
||||||
{
|
auto shared = channelPtr.lock();
|
||||||
return;
|
if (!shared)
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
qCDebug(LOG)
|
return;
|
||||||
<< 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);
|
qCDebug(LOG) << "Successfully loaded recent messages for"
|
||||||
});
|
<< shared->getName();
|
||||||
})
|
|
||||||
.onError([channelPtr, onError](const NetworkResult &result) {
|
|
||||||
auto shared = channelPtr.lock();
|
|
||||||
if (!shared)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(LOG) << "Failed to load recent messages for"
|
auto root = result.parseJson();
|
||||||
<< shared->getName();
|
auto parsedMessages = parseRecentMessages(root);
|
||||||
|
|
||||||
shared->addMessage(makeSystemMessage(
|
// build the Communi messages into chatterino messages
|
||||||
QStringLiteral(
|
auto builtMessages =
|
||||||
"Message history service unavailable (Error: %1)")
|
buildRecentMessages(parsedMessages, shared.get());
|
||||||
.arg(result.formatError())));
|
|
||||||
|
|
||||||
onError();
|
postToThread([shared = std::move(shared),
|
||||||
})
|
root = std::move(root),
|
||||||
.execute();
|
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
|
} // namespace chatterino::recentmessages
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -28,8 +30,16 @@ using ErrorCallback = std::function<void()>;
|
||||||
* @param channelPtr Weak pointer to Channel to use to build messages
|
* @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 onLoaded Callback taking the built messages as a const std::vector<MessagePtr> &
|
||||||
* @param onError Callback called when the network request fails
|
* @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,
|
void load(
|
||||||
ResultCallback onLoaded, ErrorCallback onError);
|
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
|
} // namespace chatterino::recentmessages
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
|
||||||
#include "util/FormatTime.hpp"
|
#include "util/FormatTime.hpp"
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
@ -94,14 +93,34 @@ std::vector<MessagePtr> buildRecentMessages(
|
||||||
|
|
||||||
// Returns the URL to be used for querying the Recent Messages API for the
|
// Returns the URL to be used for querying the Recent Messages API for the
|
||||||
// given channel.
|
// 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));
|
QUrl url(Env::get().recentMessagesApiUrl.arg(name));
|
||||||
QUrlQuery urlQuery(url);
|
QUrlQuery urlQuery(url);
|
||||||
if (!urlQuery.hasQueryItem("limit"))
|
if (!urlQuery.hasQueryItem("limit"))
|
||||||
|
{
|
||||||
|
urlQuery.addQueryItem("limit", QString::number(limit));
|
||||||
|
}
|
||||||
|
if (after.has_value())
|
||||||
{
|
{
|
||||||
urlQuery.addQueryItem(
|
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);
|
url.setQuery(urlQuery);
|
||||||
return url;
|
return url;
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace chatterino::recentmessages::detail {
|
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
|
// Returns the URL to be used for querying the Recent Messages API for the
|
||||||
// given channel.
|
// 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
|
} // 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
|
// Message types below are the ones that might contain special user's message on USERNOTICE
|
||||||
const QSet<QString> SPECIAL_MESSAGE_TYPES{
|
const QSet<QString> SPECIAL_MESSAGE_TYPES{
|
||||||
"sub", //
|
"sub", //
|
||||||
"subgift", //
|
"subgift", //
|
||||||
"resub", // resub messages
|
"resub", // resub messages
|
||||||
"bitsbadgetier", // bits badge upgrade
|
"bitsbadgetier", // bits badge upgrade
|
||||||
"ritual", // new viewer ritual
|
"ritual", // new viewer ritual
|
||||||
"announcement", // new mod announcement thing
|
"announcement", // new mod announcement thing
|
||||||
|
"viewermilestone", // watch streak, but other categories possible in future
|
||||||
};
|
};
|
||||||
|
|
||||||
MessagePtr generateBannedMessage(bool confirmedBan)
|
MessagePtr generateBannedMessage(bool confirmedBan)
|
||||||
|
@ -1136,6 +1137,7 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
|
||||||
getApp()->accounts->twitch.getCurrent()->getUserName())
|
getApp()->accounts->twitch.getCurrent()->getUserName())
|
||||||
{
|
{
|
||||||
twitchChannel->addMessage(makeSystemMessage("joined channel"));
|
twitchChannel->addMessage(makeSystemMessage("joined channel"));
|
||||||
|
twitchChannel->joined.invoke();
|
||||||
}
|
}
|
||||||
else if (getSettings()->showJoins.getValue())
|
else if (getSettings()->showJoins.getValue())
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "providers/twitch/PubSubHelpers.hpp"
|
#include "providers/twitch/PubSubHelpers.hpp"
|
||||||
#include "providers/twitch/PubSubMessages.hpp"
|
#include "providers/twitch/PubSubMessages.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
|
#include "pubsubmessages/LowTrustUsers.hpp"
|
||||||
#include "util/DebugCount.hpp"
|
#include "util/DebugCount.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
#include "util/RapidjsonHelpers.hpp"
|
#include "util/RapidjsonHelpers.hpp"
|
||||||
|
@ -210,7 +211,6 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
action.target.login = args[0].toString();
|
action.target.login = args[0].toString();
|
||||||
bool ok;
|
|
||||||
action.messageText = args[1].toString();
|
action.messageText = args[1].toString();
|
||||||
action.messageId = args[2].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()
|
void PubSub::unlistenWhispers()
|
||||||
{
|
{
|
||||||
for (const auto &p : this->clients)
|
for (const auto &p : this->clients)
|
||||||
|
@ -671,6 +690,30 @@ void PubSub::listenToAutomod(const QString &channelID)
|
||||||
this->listenToTopic(topic);
|
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)
|
void PubSub::listenToChannelPointRewards(const QString &channelID)
|
||||||
{
|
{
|
||||||
static const QString topicFormat("community-points-channel-v1.%1");
|
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,
|
this->signals_.moderation.autoModMessageCaught.invoke(innerMessage,
|
||||||
channelID);
|
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
|
else
|
||||||
{
|
{
|
||||||
qCDebug(chatterinoPubSub) << "Unknown topic:" << topic;
|
qCDebug(chatterinoPubSub) << "Unknown topic:" << topic;
|
||||||
|
|
|
@ -34,6 +34,7 @@ struct PubSubAutoModQueueMessage;
|
||||||
struct AutomodAction;
|
struct AutomodAction;
|
||||||
struct AutomodUserAction;
|
struct AutomodUserAction;
|
||||||
struct AutomodInfoAction;
|
struct AutomodInfoAction;
|
||||||
|
struct PubSubLowTrustUsersMessage;
|
||||||
struct PubSubWhisperMessage;
|
struct PubSubWhisperMessage;
|
||||||
|
|
||||||
struct PubSubListenMessage;
|
struct PubSubListenMessage;
|
||||||
|
@ -67,9 +68,6 @@ class PubSub
|
||||||
QString userID_;
|
QString userID_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// The max amount of connections we may open
|
|
||||||
static constexpr int maxConnections = 10;
|
|
||||||
|
|
||||||
PubSub(const QString &host,
|
PubSub(const QString &host,
|
||||||
std::chrono::seconds pingInterval = std::chrono::seconds(15));
|
std::chrono::seconds pingInterval = std::chrono::seconds(15));
|
||||||
|
|
||||||
|
@ -100,6 +98,9 @@ public:
|
||||||
Signal<BanAction> userBanned;
|
Signal<BanAction> userBanned;
|
||||||
Signal<UnbanAction> userUnbanned;
|
Signal<UnbanAction> userUnbanned;
|
||||||
|
|
||||||
|
Signal<PubSubLowTrustUsersMessage> suspiciousMessageReceived;
|
||||||
|
Signal<PubSubLowTrustUsersMessage> suspiciousTreatmentUpdated;
|
||||||
|
|
||||||
// Message caught by automod
|
// Message caught by automod
|
||||||
// channelID
|
// channelID
|
||||||
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
|
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
|
||||||
|
@ -126,12 +127,56 @@ public:
|
||||||
|
|
||||||
void unlistenAllModerationActions();
|
void unlistenAllModerationActions();
|
||||||
void unlistenAutomod();
|
void unlistenAutomod();
|
||||||
|
void unlistenLowTrustUsers();
|
||||||
void unlistenWhispers();
|
void unlistenWhispers();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to incoming whispers for the currently logged in user.
|
||||||
|
* This topic is relevant for everyone.
|
||||||
|
*
|
||||||
|
* PubSub topic: whispers.{currentUserID}
|
||||||
|
*/
|
||||||
bool listenToWhispers();
|
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);
|
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);
|
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);
|
void listenToChannelPointRewards(const QString &channelID);
|
||||||
|
|
||||||
std::vector<QString> requests;
|
std::vector<QString> requests;
|
||||||
|
|
|
@ -56,7 +56,6 @@ namespace {
|
||||||
#else
|
#else
|
||||||
const QString MAGIC_MESSAGE_SUFFIX = QString::fromUtf8(u8" \U000E0000");
|
const QString MAGIC_MESSAGE_SUFFIX = QString::fromUtf8(u8" \U000E0000");
|
||||||
#endif
|
#endif
|
||||||
constexpr int TITLE_REFRESH_PERIOD = 10000;
|
|
||||||
constexpr int CLIP_CREATION_COOLDOWN = 5000;
|
constexpr int CLIP_CREATION_COOLDOWN = 5000;
|
||||||
const QString CLIPS_LINK("https://clips.twitch.tv/%1");
|
const QString CLIPS_LINK("https://clips.twitch.tv/%1");
|
||||||
const QString CLIPS_FAILURE_CLIPS_DISABLED_TEXT(
|
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
|
// 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
|
// is destroyed, it will no longer be able to fire
|
||||||
std::ignore = this->connected.connect([this]() {
|
std::ignore = this->joined.connect([this]() {
|
||||||
if (this->roomId().isEmpty())
|
if (this->disconnected_)
|
||||||
{
|
{
|
||||||
// If we get a reconnected event when the room id is not set, we
|
this->loadRecentMessagesReconnect();
|
||||||
// just connected for the first time. After receiving the first
|
this->lastConnectedAt_ = std::chrono::system_clock::now();
|
||||||
// message from a channel, setRoomId is called and further
|
this->disconnected_ = false;
|
||||||
// invocations of this event will load recent messages.
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->loadRecentMessagesReconnect();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// timers
|
// timers
|
||||||
|
@ -737,6 +732,8 @@ void TwitchChannel::setRoomId(const QString &id)
|
||||||
*this->roomID_.access() = id;
|
*this->roomID_.access() = id;
|
||||||
this->roomIdChanged();
|
this->roomIdChanged();
|
||||||
this->loadRecentMessages();
|
this->loadRecentMessages();
|
||||||
|
this->disconnected_ = false;
|
||||||
|
this->lastConnectedAt_ = std::chrono::system_clock::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1111,6 +1108,25 @@ bool TwitchChannel::setLive(bool newLiveStatus)
|
||||||
return true;
|
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()
|
void TwitchChannel::loadRecentMessages()
|
||||||
{
|
{
|
||||||
if (!getSettings()->loadTwitchMessageHistoryOnConnect)
|
if (!getSettings()->loadTwitchMessageHistoryOnConnect)
|
||||||
|
@ -1163,7 +1179,9 @@ void TwitchChannel::loadRecentMessages()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
tc->loadingRecentMessages_.clear();
|
tc->loadingRecentMessages_.clear();
|
||||||
});
|
},
|
||||||
|
getSettings()->twitchMessageHistoryLimit.getValue(), std::nullopt,
|
||||||
|
std::nullopt, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchChannel::loadRecentMessagesReconnect()
|
void TwitchChannel::loadRecentMessagesReconnect()
|
||||||
|
@ -1178,6 +1196,21 @@ void TwitchChannel::loadRecentMessagesReconnect()
|
||||||
return; // already loading
|
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);
|
auto weak = weakOf<Channel>(this);
|
||||||
recentmessages::load(
|
recentmessages::load(
|
||||||
this->getName(), weak,
|
this->getName(), weak,
|
||||||
|
@ -1203,7 +1236,8 @@ void TwitchChannel::loadRecentMessagesReconnect()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
tc->loadingRecentMessages_.clear();
|
tc->loadingRecentMessages_.clear();
|
||||||
});
|
},
|
||||||
|
limit, this->lastConnectedAt_, now, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchChannel::refreshPubSub()
|
void TwitchChannel::refreshPubSub()
|
||||||
|
@ -1219,7 +1253,11 @@ void TwitchChannel::refreshPubSub()
|
||||||
getApp()->twitch->pubsub->setAccount(currentAccount);
|
getApp()->twitch->pubsub->setAccount(currentAccount);
|
||||||
|
|
||||||
getApp()->twitch->pubsub->listenToChannelModerationActions(roomId);
|
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);
|
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