mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Merge remote-tracking branch 'origin/master' into fix/block-followed
This commit is contained in:
commit
0730110ded
227 changed files with 24185 additions and 4789 deletions
1
.github/workflows/clang-tidy.yml
vendored
1
.github/workflows/clang-tidy.yml
vendored
|
@ -10,6 +10,7 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
review:
|
review:
|
||||||
|
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clang-tidy') }}
|
||||||
name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})"
|
name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
|
14
.github/workflows/test-macos.yml
vendored
14
.github/workflows/test-macos.yml
vendored
|
@ -8,6 +8,7 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
|
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
|
||||||
|
HTTPBOX_TAG: v0.2.1
|
||||||
QT_QPA_PLATFORM: minimal
|
QT_QPA_PLATFORM: minimal
|
||||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||||
|
@ -24,7 +25,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-13]
|
os: [macos-13]
|
||||||
qt-version: [5.15.2, 6.7.1]
|
qt-version: [5.15.2, 6.7.1]
|
||||||
plugins: [false]
|
plugins: [true]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
env:
|
env:
|
||||||
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
|
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
|
||||||
|
@ -58,6 +59,13 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
brew install boost openssl rapidjson p7zip create-dmg cmake
|
brew install boost openssl rapidjson p7zip create-dmg cmake
|
||||||
|
|
||||||
|
- name: Install httpbox
|
||||||
|
run: |
|
||||||
|
curl -L -o httpbox.tar.xz "https://github.com/Chatterino/httpbox/releases/download/${{ env.HTTPBOX_TAG }}/httpbox-x86_64-apple-darwin.tar.xz"
|
||||||
|
tar -xJf httpbox.tar.xz
|
||||||
|
mv ./httpbox-x86_64-apple-darwin/httpbox /usr/local/bin
|
||||||
|
working-directory: /tmp
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
mkdir build-test
|
mkdir build-test
|
||||||
|
@ -83,10 +91,6 @@ jobs:
|
||||||
curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key"
|
curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key"
|
||||||
cd ..
|
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: |
|
||||||
|
|
16
.github/workflows/test-windows.yml
vendored
16
.github/workflows/test-windows.yml
vendored
|
@ -8,6 +8,7 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
|
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
|
||||||
|
HTTPBOX_TAG: v0.2.1
|
||||||
QT_QPA_PLATFORM: minimal
|
QT_QPA_PLATFORM: minimal
|
||||||
# Last known good conan version
|
# Last known good conan version
|
||||||
# 2.0.3 has a bug on Windows (conan-io/conan#13606)
|
# 2.0.3 has a bug on Windows (conan-io/conan#13606)
|
||||||
|
@ -25,7 +26,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest]
|
os: [windows-latest]
|
||||||
qt-version: [5.15.2, 6.7.1]
|
qt-version: [5.15.2, 6.7.1]
|
||||||
plugins: [false]
|
plugins: [true]
|
||||||
skip-artifact: [false]
|
skip-artifact: [false]
|
||||||
skip-crashpad: [false]
|
skip-crashpad: [false]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
@ -111,6 +112,13 @@ jobs:
|
||||||
mkdir -Force build-test/bin
|
mkdir -Force build-test/bin
|
||||||
cp "$((ls $Env:VCToolsRedistDir/onecore/x64 -Filter '*.CRT')[0].FullName)/*" build-test/bin
|
cp "$((ls $Env:VCToolsRedistDir/onecore/x64 -Filter '*.CRT')[0].FullName)/*" build-test/bin
|
||||||
|
|
||||||
|
- name: Install httpbox
|
||||||
|
run: |
|
||||||
|
mkdir httpbox
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Chatterino/httpbox/releases/download/${{ env.HTTPBOX_TAG }}/httpbox-x86_64-pc-windows-msvc.zip" -outfile "httpbox.zip"
|
||||||
|
Expand-Archive httpbox.zip -DestinationPath httpbox
|
||||||
|
rm httpbox.zip
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cmake `
|
cmake `
|
||||||
|
@ -139,14 +147,10 @@ jobs:
|
||||||
Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" -outfile "server.key"
|
Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" -outfile "server.key"
|
||||||
cd ..
|
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: |
|
||||||
httpbox --port 9051 &
|
..\httpbox\httpbox.exe --port 9051 &
|
||||||
cd ..\pubsub-server-test
|
cd ..\pubsub-server-test
|
||||||
.\server.exe 127.0.0.1:9050 &
|
.\server.exe 127.0.0.1:9050 &
|
||||||
cd ..\build-test
|
cd ..\build-test
|
||||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -44,3 +44,6 @@
|
||||||
[submodule "lib/expected-lite"]
|
[submodule "lib/expected-lite"]
|
||||||
path = lib/expected-lite
|
path = lib/expected-lite
|
||||||
url = https://github.com/martinmoene/expected-lite
|
url = https://github.com/martinmoene/expected-lite
|
||||||
|
[submodule "lib/sol2"]
|
||||||
|
path = lib/sol2
|
||||||
|
url = https://github.com/ThePhD/sol2.git
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
resources/*.json
|
resources/*.json
|
||||||
benchmarks/resources/*.json
|
benchmarks/resources/*.json
|
||||||
tests/resources/*.json
|
tests/resources/*.json
|
||||||
|
tests/snapshots/**/*.json
|
||||||
# ...themes should be prettified for readability.
|
# ...themes should be prettified for readability.
|
||||||
!resources/themes/*.json
|
!resources/themes/*.json
|
||||||
|
|
||||||
|
|
|
@ -170,9 +170,10 @@ 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)
|
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
|
2. During the installation, make sure to add it to your path
|
||||||
3. In Qt Creator, Select `Tools` > `Options` > `Beautifier`
|
3. Enable Beautifier under `Extensions` on the left (check "Load on start" and restart)
|
||||||
4. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save`
|
4. In Qt Creator, Select `Tools` > `Options` > `Beautifier`
|
||||||
5. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
|
5. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save`
|
||||||
|
6. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
|
||||||
|
|
||||||
### Building on MSVC with AddressSanitizer
|
### Building on MSVC with AddressSanitizer
|
||||||
|
|
||||||
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
- Major: Add option to show pronouns in user card. (#5442, #5583)
|
- Major: Add option to show pronouns in user card. (#5442, #5583)
|
||||||
- Major: Release plugins alpha. (#5288)
|
- Major: Release plugins alpha. (#5288)
|
||||||
- Major: Improve high-DPI support on Windows. (#4868, #5391)
|
- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666)
|
||||||
- Major: Added transparent overlay window (default keybind: <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>N</kbd>). (#4746)
|
- Major: Added transparent overlay window (default keybind: <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>N</kbd>). (#4746, #5643, #5659)
|
||||||
- Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530)
|
- Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530)
|
||||||
- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625)
|
- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625, #5661)
|
||||||
- Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530)
|
- Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530)
|
||||||
- Minor: Add option to customise Moderation buttons with images. (#5369)
|
- Minor: Add option to customise Moderation buttons with images. (#5369)
|
||||||
- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300)
|
- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300)
|
||||||
|
@ -30,10 +30,16 @@
|
||||||
- Minor: Links can now have prefixes and suffixes such as parentheses. (#5486, #5515)
|
- Minor: Links can now have prefixes and suffixes such as parentheses. (#5486, #5515)
|
||||||
- Minor: Added support for scrolling in splits with touchscreen panning gestures. (#5524)
|
- Minor: Added support for scrolling in splits with touchscreen panning gestures. (#5524)
|
||||||
- Minor: Removed experimental IRC support. (#5547)
|
- Minor: Removed experimental IRC support. (#5547)
|
||||||
|
- Minor: Remember last popup size for next popup. (#5635)
|
||||||
- Minor: Moderators can now see which mods start and cancel raids. (#5563)
|
- Minor: Moderators can now see which mods start and cancel raids. (#5563)
|
||||||
- Minor: The emote popup now reloads when Twitch emotes are reloaded. (#5580)
|
- Minor: The emote popup now reloads when Twitch emotes are reloaded. (#5580)
|
||||||
- Minor: Added `--login <username>` CLI argument to specify which account to start logged in as. (#5626)
|
- Minor: Added `--login <username>` CLI argument to specify which account to start logged in as. (#5626)
|
||||||
- Minor: When blocking a channel, Chatterino will now warn you about that action. (#5615)
|
- Minor: When blocking a channel, Chatterino will now warn you about that action. (#5615)
|
||||||
|
- Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642)
|
||||||
|
- Minor: Proxy URL information is now included in the `/debug-env` command. (#5648)
|
||||||
|
- Minor: Make raid entry message usernames clickable. (#5651)
|
||||||
|
- Minor: Tabs unhighlight when their content is read in other tabs. (#5649)
|
||||||
|
- Minor: Made usernames in bits and sub messages clickable. (#5686)
|
||||||
- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612)
|
- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612)
|
||||||
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
|
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
|
||||||
- Bugfix: Fixed restricted users usernames not being clickable. (#5405)
|
- Bugfix: Fixed restricted users usernames not being clickable. (#5405)
|
||||||
|
@ -45,12 +51,14 @@
|
||||||
- Bugfix: Fixed tooltips and input completion popups not working after moving a split. (#5541, #5576)
|
- Bugfix: Fixed tooltips and input completion popups not working after moving a split. (#5541, #5576)
|
||||||
- Bugfix: Fixed rare issue on shutdown where the client would hang. (#5557)
|
- Bugfix: Fixed rare issue on shutdown where the client would hang. (#5557)
|
||||||
- Bugfix: Fixed `/clearmessages` not working with more than one window. (#5489)
|
- Bugfix: Fixed `/clearmessages` not working with more than one window. (#5489)
|
||||||
- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504)
|
- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504, #5637)
|
||||||
- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509)
|
- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509)
|
||||||
- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525)
|
- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525)
|
||||||
- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582)
|
- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582, #5632)
|
||||||
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
|
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
|
||||||
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
|
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
|
||||||
|
- Bugfix: Fixed a crash that could occur when handling the quick switcher popup really quickly. (#5687)
|
||||||
|
- Bugfix: Fixed 7TV badges being inadvertently animated. (#5674)
|
||||||
- Bugfix: Fixed some tooltips not being readable. (#5578)
|
- Bugfix: Fixed some tooltips not being readable. (#5578)
|
||||||
- Bugfix: Fixed log files being locked longer than needed. (#5592)
|
- Bugfix: Fixed log files being locked longer than needed. (#5592)
|
||||||
- Bugfix: Fixed global badges not showing in anonymous mode. (#5599)
|
- Bugfix: Fixed global badges not showing in anonymous mode. (#5599)
|
||||||
|
@ -58,6 +66,8 @@
|
||||||
- Bugfix: Fixed incorrect message being disabled in some cases upon approving or denying an automod caught message. (#5611)
|
- Bugfix: Fixed incorrect message being disabled in some cases upon approving or denying an automod caught message. (#5611)
|
||||||
- Bugfix: Fixed double-click selection not working when clicking outside a message. (#5617)
|
- Bugfix: Fixed double-click selection not working when clicking outside a message. (#5617)
|
||||||
- Bugfix: Fixed emotes starting with ":" not tab-completing. (#5603)
|
- Bugfix: Fixed emotes starting with ":" not tab-completing. (#5603)
|
||||||
|
- Bugfix: Fixed 7TV emotes messing with Qt's HTML. (#5677)
|
||||||
|
- Bugfix: Fixed incorrect messages getting replaced visually. (#5683)
|
||||||
- Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420)
|
- Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420)
|
||||||
- Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422)
|
- Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422)
|
||||||
- Dev: Unsingletonize `ISoundController`. (#5462)
|
- Dev: Unsingletonize `ISoundController`. (#5462)
|
||||||
|
@ -70,7 +80,7 @@
|
||||||
- Dev: Removed unused timegate settings. (#5361)
|
- Dev: Removed unused timegate settings. (#5361)
|
||||||
- Dev: Add `Channel::addSystemMessage` helper function, allowing us to avoid the common `channel->addMessage(makeSystemMessage(...));` pattern. (#5500)
|
- Dev: Add `Channel::addSystemMessage` helper function, allowing us to avoid the common `channel->addMessage(makeSystemMessage(...));` pattern. (#5500)
|
||||||
- Dev: Unsingletonize `Resources2`. (#5460)
|
- Dev: Unsingletonize `Resources2`. (#5460)
|
||||||
- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385)
|
- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385, #5682)
|
||||||
- Dev: Images are now loaded in worker threads. (#5431)
|
- Dev: Images are now loaded in worker threads. (#5431)
|
||||||
- Dev: Fixed broken `SignalVector::operator[]` implementation. (#5556)
|
- Dev: Fixed broken `SignalVector::operator[]` implementation. (#5556)
|
||||||
- Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305)
|
- Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305)
|
||||||
|
@ -101,9 +111,17 @@
|
||||||
- Dev: Added more tests for input completion. (#5604)
|
- Dev: Added more tests for input completion. (#5604)
|
||||||
- Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594)
|
- Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594)
|
||||||
- Dev: The JSON output when copying a message (<kbd>SHIFT</kbd> + right-click) is now more extensive. (#5600)
|
- Dev: The JSON output when copying a message (<kbd>SHIFT</kbd> + right-click) is now more extensive. (#5600)
|
||||||
|
- Dev: Added more tests for message building. (#5598, #5654, #5656, #5671)
|
||||||
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
|
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
|
||||||
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
|
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
|
||||||
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
|
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
|
||||||
|
- Dev: Move plugins to Sol2. (#5622, #5682)
|
||||||
|
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
|
||||||
|
- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668)
|
||||||
|
- Dev: Refactored IRC message building. (#5663)
|
||||||
|
- Dev: Fixed some compiler warnings. (#5672)
|
||||||
|
- Dev: Unified parsing of historic and live IRC messages. (#5678)
|
||||||
|
- Dev: 7TV's `entitlement.reset` is now explicitly ignored. (#5685)
|
||||||
|
|
||||||
## 2.5.1
|
## 2.5.1
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
cmake_minimum_required(VERSION 3.15)
|
cmake_minimum_required(VERSION 3.15)
|
||||||
cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)`
|
cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)`
|
||||||
cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY`
|
cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY`
|
||||||
|
if (POLICY CMP0167)
|
||||||
|
cmake_policy(SET CMP0167 NEW) # find Boost's own CMake config file
|
||||||
|
endif ()
|
||||||
include(FeatureSummary)
|
include(FeatureSummary)
|
||||||
|
|
||||||
list(APPEND CMAKE_MODULE_PATH
|
list(APPEND CMAKE_MODULE_PATH
|
||||||
|
@ -212,6 +215,8 @@ endif()
|
||||||
if (CHATTERINO_PLUGINS)
|
if (CHATTERINO_PLUGINS)
|
||||||
set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
|
set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
|
||||||
add_subdirectory(lib/lua)
|
add_subdirectory(lib/lua)
|
||||||
|
|
||||||
|
find_package(Sol2 REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (BUILD_WITH_CRASHPAD)
|
if (BUILD_WITH_CRASHPAD)
|
||||||
|
|
|
@ -5,7 +5,6 @@ set(benchmark_SOURCES
|
||||||
resources/bench.qrc
|
resources/bench.qrc
|
||||||
|
|
||||||
src/Emojis.cpp
|
src/Emojis.cpp
|
||||||
src/Highlights.cpp
|
|
||||||
src/FormatTime.cpp
|
src/FormatTime.cpp
|
||||||
src/Helpers.cpp
|
src/Helpers.cpp
|
||||||
src/LimitedQueue.cpp
|
src/LimitedQueue.cpp
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
#include "Application.hpp"
|
|
||||||
#include "common/Channel.hpp"
|
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
|
||||||
#include "controllers/highlights/HighlightController.hpp"
|
|
||||||
#include "controllers/highlights/HighlightPhrase.hpp"
|
|
||||||
#include "messages/Message.hpp"
|
|
||||||
#include "messages/MessageBuilder.hpp"
|
|
||||||
#include "mocks/BaseApplication.hpp"
|
|
||||||
#include "mocks/UserData.hpp"
|
|
||||||
#include "util/Helpers.hpp"
|
|
||||||
|
|
||||||
#include <benchmark/benchmark.h>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QString>
|
|
||||||
#include <QTemporaryDir>
|
|
||||||
|
|
||||||
using namespace chatterino;
|
|
||||||
|
|
||||||
class BenchmarkMessageBuilder : public MessageBuilder
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit BenchmarkMessageBuilder(
|
|
||||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
|
||||||
const MessageParseArgs &_args)
|
|
||||||
: MessageBuilder(_channel, _ircMessage, _args)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual MessagePtr build()
|
|
||||||
{
|
|
||||||
// PARSE
|
|
||||||
this->parse();
|
|
||||||
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
|
|
||||||
|
|
||||||
// words
|
|
||||||
// this->addWords(this->originalMessage_.split(' '));
|
|
||||||
|
|
||||||
this->message().messageText = this->originalMessage_;
|
|
||||||
this->message().searchText = this->message().localizedName + " " +
|
|
||||||
this->userName + ": " +
|
|
||||||
this->originalMessage_;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void bench()
|
|
||||||
{
|
|
||||||
this->parseHighlights();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class MockApplication : public mock::BaseApplication
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
MockApplication()
|
|
||||||
: highlights(this->settings, &this->accounts)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountController *getAccounts() override
|
|
||||||
{
|
|
||||||
return &this->accounts;
|
|
||||||
}
|
|
||||||
HighlightController *getHighlights() override
|
|
||||||
{
|
|
||||||
return &this->highlights;
|
|
||||||
}
|
|
||||||
|
|
||||||
IUserDataController *getUserData() override
|
|
||||||
{
|
|
||||||
return &this->userData;
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountController accounts;
|
|
||||||
HighlightController highlights;
|
|
||||||
mock::UserDataController userData;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void BM_HighlightTest(benchmark::State &state)
|
|
||||||
{
|
|
||||||
MockApplication mockApplication;
|
|
||||||
|
|
||||||
std::string message =
|
|
||||||
R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))";
|
|
||||||
auto ircMessage = Communi::IrcMessage::fromData(message.c_str(), nullptr);
|
|
||||||
auto privMsg = dynamic_cast<Communi::IrcPrivateMessage *>(ircMessage);
|
|
||||||
assert(privMsg != nullptr);
|
|
||||||
MessageParseArgs args;
|
|
||||||
auto emptyChannel = Channel::getEmpty();
|
|
||||||
|
|
||||||
for (auto _ : state)
|
|
||||||
{
|
|
||||||
state.PauseTiming();
|
|
||||||
BenchmarkMessageBuilder b(emptyChannel.get(), privMsg, args);
|
|
||||||
|
|
||||||
b.build();
|
|
||||||
state.ResumeTiming();
|
|
||||||
|
|
||||||
b.bench();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BENCHMARK(BM_HighlightTest);
|
|
21
cmake/FindSol2.cmake
Normal file
21
cmake/FindSol2.cmake
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
|
||||||
|
find_path(Sol2_INCLUDE_DIR sol/sol.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/sol2/include)
|
||||||
|
|
||||||
|
find_package_handle_standard_args(Sol2 DEFAULT_MSG Sol2_INCLUDE_DIR)
|
||||||
|
|
||||||
|
if (Sol2_FOUND)
|
||||||
|
add_library(Sol2 INTERFACE IMPORTED)
|
||||||
|
set_target_properties(Sol2 PROPERTIES
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${Sol2_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
target_compile_definitions(Sol2 INTERFACE
|
||||||
|
SOL_ALL_SAFETIES_ON=1
|
||||||
|
SOL_USING_CXX_LUA=1
|
||||||
|
SOL_NO_NIL=0
|
||||||
|
)
|
||||||
|
target_link_libraries(Sol2 INTERFACE lua)
|
||||||
|
add_library(sol2::sol2 ALIAS Sol2)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
mark_as_advanced(Sol2_INCLUDE_DIR)
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3f0542e4e034aab417c51b2b22c94f83355dee15
|
Subproject commit 0573e2ea8651b9bb3083f193c41eb086497cc80a
|
|
@ -5,13 +5,22 @@
|
||||||
-- Add the folder this file is in to "Lua.workspace.library".
|
-- Add the folder this file is in to "Lua.workspace.library".
|
||||||
|
|
||||||
c2 = {}
|
c2 = {}
|
||||||
---@alias c2.LogLevel integer
|
---@enum c2.LogLevel
|
||||||
---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel }
|
c2.LogLevel = {
|
||||||
c2.LogLevel = {}
|
Debug = {}, ---@type c2.LogLevel.Debug
|
||||||
|
Info = {}, ---@type c2.LogLevel.Info
|
||||||
|
Warning = {}, ---@type c2.LogLevel.Warning
|
||||||
|
Critical = {}, ---@type c2.LogLevel.Critical
|
||||||
|
}
|
||||||
|
|
||||||
---@alias c2.EventType integer
|
-- Begin src/controllers/plugins/api/EventType.hpp
|
||||||
---@type { CompletionRequested: c2.EventType }
|
|
||||||
c2.EventType = {}
|
---@enum c2.EventType
|
||||||
|
c2.EventType = {
|
||||||
|
CompletionRequested = {}, ---@type c2.EventType.CompletionRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
-- End src/controllers/plugins/api/EventType.hpp
|
||||||
|
|
||||||
---@class CommandContext
|
---@class CommandContext
|
||||||
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
||||||
|
@ -29,19 +38,40 @@ c2.EventType = {}
|
||||||
|
|
||||||
-- Begin src/common/Channel.hpp
|
-- Begin src/common/Channel.hpp
|
||||||
|
|
||||||
---@alias c2.ChannelType integer
|
---@enum c2.ChannelType
|
||||||
---@type { None: c2.ChannelType, Direct: c2.ChannelType, Twitch: c2.ChannelType, TwitchWhispers: c2.ChannelType, TwitchWatching: c2.ChannelType, TwitchMentions: c2.ChannelType, TwitchLive: c2.ChannelType, TwitchAutomod: c2.ChannelType, TwitchEnd: c2.ChannelType, Irc: c2.ChannelType, Misc: c2.ChannelType }
|
c2.ChannelType = {
|
||||||
c2.ChannelType = {}
|
None = {}, ---@type c2.ChannelType.None
|
||||||
|
Direct = {}, ---@type c2.ChannelType.Direct
|
||||||
|
Twitch = {}, ---@type c2.ChannelType.Twitch
|
||||||
|
TwitchWhispers = {}, ---@type c2.ChannelType.TwitchWhispers
|
||||||
|
TwitchWatching = {}, ---@type c2.ChannelType.TwitchWatching
|
||||||
|
TwitchMentions = {}, ---@type c2.ChannelType.TwitchMentions
|
||||||
|
TwitchLive = {}, ---@type c2.ChannelType.TwitchLive
|
||||||
|
TwitchAutomod = {}, ---@type c2.ChannelType.TwitchAutomod
|
||||||
|
TwitchEnd = {}, ---@type c2.ChannelType.TwitchEnd
|
||||||
|
Misc = {}, ---@type c2.ChannelType.Misc
|
||||||
|
}
|
||||||
|
|
||||||
-- End src/common/Channel.hpp
|
-- End src/common/Channel.hpp
|
||||||
|
|
||||||
-- Begin src/controllers/plugins/api/ChannelRef.hpp
|
-- Begin src/controllers/plugins/api/ChannelRef.hpp
|
||||||
|
|
||||||
---@alias c2.Platform integer
|
-- Begin src/providers/twitch/TwitchChannel.hpp
|
||||||
--- This enum describes a platform for the purpose of searching for a channel.
|
|
||||||
--- Currently only Twitch is supported because identifying IRC channels is tricky.
|
---@class StreamStatus
|
||||||
---@type { Twitch: c2.Platform }
|
---@field live boolean
|
||||||
c2.Platform = {}
|
---@field viewer_count number
|
||||||
|
---@field title string Stream title or last stream title
|
||||||
|
---@field game_name string
|
||||||
|
---@field game_id string
|
||||||
|
---@field uptime number Seconds since the stream started.
|
||||||
|
|
||||||
|
---@class RoomModes
|
||||||
|
---@field subscriber_only boolean
|
||||||
|
---@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||||
|
---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||||
|
|
||||||
|
-- End src/providers/twitch/TwitchChannel.hpp
|
||||||
|
|
||||||
---@class c2.Channel
|
---@class c2.Channel
|
||||||
c2.Channel = {}
|
c2.Channel = {}
|
||||||
|
@ -72,7 +102,7 @@ function c2.Channel:get_display_name() end
|
||||||
--- Note that this does not execute client-commands.
|
--- Note that this does not execute client-commands.
|
||||||
---
|
---
|
||||||
---@param message string
|
---@param message string
|
||||||
---@param execute_commands boolean Should commands be run on the text?
|
---@param execute_commands? boolean Should commands be run on the text?
|
||||||
function c2.Channel:send_message(message, execute_commands) end
|
function c2.Channel:send_message(message, execute_commands) end
|
||||||
|
|
||||||
--- Adds a system message client-side
|
--- Adds a system message client-side
|
||||||
|
@ -131,9 +161,8 @@ function c2.Channel:__tostring() end
|
||||||
--- - /automod
|
--- - /automod
|
||||||
---
|
---
|
||||||
---@param name string Which channel are you looking for?
|
---@param name string Which channel are you looking for?
|
||||||
---@param platform c2.Platform Where to search for the channel?
|
|
||||||
---@return c2.Channel?
|
---@return c2.Channel?
|
||||||
function c2.Channel.by_name(name, platform) end
|
function c2.Channel.by_name(name) end
|
||||||
|
|
||||||
--- Finds a channel by the Twitch user ID of its owner.
|
--- Finds a channel by the Twitch user ID of its owner.
|
||||||
---
|
---
|
||||||
|
@ -141,98 +170,101 @@ function c2.Channel.by_name(name, platform) end
|
||||||
---@return c2.Channel?
|
---@return c2.Channel?
|
||||||
function c2.Channel.by_twitch_id(id) end
|
function c2.Channel.by_twitch_id(id) end
|
||||||
|
|
||||||
---@class RoomModes
|
|
||||||
---@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
|
||||||
---@field subscriber_only boolean
|
|
||||||
---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
|
||||||
---@field follower_only number? Time in minutes you need to follow to chat or nil.
|
|
||||||
---@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
|
||||||
|
|
||||||
---@class StreamStatus
|
|
||||||
---@field live boolean
|
|
||||||
---@field viewer_count number
|
|
||||||
---@field uptime number Seconds since the stream started.
|
|
||||||
---@field title string Stream title or last stream title
|
|
||||||
---@field game_name string
|
|
||||||
---@field game_id string
|
|
||||||
|
|
||||||
-- End src/controllers/plugins/api/ChannelRef.hpp
|
-- End src/controllers/plugins/api/ChannelRef.hpp
|
||||||
|
|
||||||
-- Begin src/controllers/plugins/api/HTTPResponse.hpp
|
-- Begin src/controllers/plugins/api/HTTPResponse.hpp
|
||||||
|
|
||||||
---@class HTTPResponse
|
---@class c2.HTTPResponse
|
||||||
HTTPResponse = {}
|
c2.HTTPResponse = {}
|
||||||
|
|
||||||
--- Returns the data. This is not guaranteed to be encoded using any
|
--- Returns the data. This is not guaranteed to be encoded using any
|
||||||
--- particular encoding scheme. It's just the bytes the server returned.
|
--- particular encoding scheme. It's just the bytes the server returned.
|
||||||
---
|
---
|
||||||
function HTTPResponse:data() end
|
---@return string
|
||||||
|
---@nodiscard
|
||||||
|
function c2.HTTPResponse:data() end
|
||||||
|
|
||||||
--- Returns the status code.
|
--- Returns the status code.
|
||||||
---
|
---
|
||||||
function HTTPResponse:status() end
|
---@return number|nil
|
||||||
|
---@nodiscard
|
||||||
|
function c2.HTTPResponse:status() end
|
||||||
|
|
||||||
--- A somewhat human readable description of an error if such happened
|
--- A somewhat human readable description of an error if such happened
|
||||||
---
|
---
|
||||||
function HTTPResponse:error() end
|
---@return string
|
||||||
|
---@nodiscard
|
||||||
|
function c2.HTTPResponse:error() end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
---@nodiscard
|
||||||
|
function c2.HTTPResponse:__tostring() end
|
||||||
|
|
||||||
-- End src/controllers/plugins/api/HTTPResponse.hpp
|
-- End src/controllers/plugins/api/HTTPResponse.hpp
|
||||||
|
|
||||||
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
|
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
|
||||||
|
|
||||||
---@alias HTTPCallback fun(result: HTTPResponse): nil
|
---@alias c2.HTTPCallback fun(result: c2.HTTPResponse): nil
|
||||||
---@class HTTPRequest
|
---@class c2.HTTPRequest
|
||||||
HTTPRequest = {}
|
c2.HTTPRequest = {}
|
||||||
|
|
||||||
--- Sets the success callback
|
--- Sets the success callback
|
||||||
---
|
---
|
||||||
---@param callback HTTPCallback Function to call when the HTTP request succeeds
|
---@param callback c2.HTTPCallback Function to call when the HTTP request succeeds
|
||||||
function HTTPRequest:on_success(callback) end
|
function c2.HTTPRequest:on_success(callback) end
|
||||||
|
|
||||||
--- Sets the failure callback
|
--- Sets the failure callback
|
||||||
---
|
---
|
||||||
---@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
|
---@param callback c2.HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
|
||||||
function HTTPRequest:on_error(callback) end
|
function c2.HTTPRequest:on_error(callback) end
|
||||||
|
|
||||||
--- Sets the finally callback
|
--- Sets the finally callback
|
||||||
---
|
---
|
||||||
---@param callback fun(): nil Function to call when the HTTP request finishes
|
---@param callback fun(): nil Function to call when the HTTP request finishes
|
||||||
function HTTPRequest:finally(callback) end
|
function c2.HTTPRequest:finally(callback) end
|
||||||
|
|
||||||
--- Sets the timeout
|
--- Sets the timeout
|
||||||
---
|
---
|
||||||
---@param timeout integer How long in milliseconds until the times out
|
---@param timeout integer How long in milliseconds until the times out
|
||||||
function HTTPRequest:set_timeout(timeout) end
|
function c2.HTTPRequest:set_timeout(timeout) end
|
||||||
|
|
||||||
--- Sets the request payload
|
--- Sets the request payload
|
||||||
---
|
---
|
||||||
---@param data string
|
---@param data string
|
||||||
function HTTPRequest:set_payload(data) end
|
function c2.HTTPRequest:set_payload(data) end
|
||||||
|
|
||||||
--- Sets a header in the request
|
--- Sets a header in the request
|
||||||
---
|
---
|
||||||
---@param name string
|
---@param name string
|
||||||
---@param value string
|
---@param value string
|
||||||
function HTTPRequest:set_header(name, value) end
|
function c2.HTTPRequest:set_header(name, value) end
|
||||||
|
|
||||||
--- Executes the HTTP request
|
--- Executes the HTTP request
|
||||||
---
|
---
|
||||||
function HTTPRequest:execute() end
|
function c2.HTTPRequest:execute() end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function c2.HTTPRequest:__tostring() end
|
||||||
|
|
||||||
--- Creates a new HTTPRequest
|
--- Creates a new HTTPRequest
|
||||||
---
|
---
|
||||||
---@param method HTTPMethod Method to use
|
---@param method c2.HTTPMethod Method to use
|
||||||
---@param url string Where to send the request to
|
---@param url string Where to send the request to
|
||||||
---@return HTTPRequest
|
---@return c2.HTTPRequest
|
||||||
function HTTPRequest.create(method, url) end
|
function c2.HTTPRequest.create(method, url) end
|
||||||
|
|
||||||
-- End src/controllers/plugins/api/HTTPRequest.hpp
|
-- End src/controllers/plugins/api/HTTPRequest.hpp
|
||||||
|
|
||||||
-- Begin src/common/network/NetworkCommon.hpp
|
-- Begin src/common/network/NetworkCommon.hpp
|
||||||
|
|
||||||
---@alias HTTPMethod integer
|
---@enum c2.HTTPMethod
|
||||||
---@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod }
|
c2.HTTPMethod = {
|
||||||
HTTPMethod = {}
|
Get = {}, ---@type c2.HTTPMethod.Get
|
||||||
|
Post = {}, ---@type c2.HTTPMethod.Post
|
||||||
|
Put = {}, ---@type c2.HTTPMethod.Put
|
||||||
|
Delete = {}, ---@type c2.HTTPMethod.Delete
|
||||||
|
Patch = {}, ---@type c2.HTTPMethod.Patch
|
||||||
|
}
|
||||||
|
|
||||||
-- End src/common/network/NetworkCommon.hpp
|
-- End src/common/network/NetworkCommon.hpp
|
||||||
|
|
||||||
|
@ -245,7 +277,7 @@ function c2.register_command(name, handler) end
|
||||||
|
|
||||||
--- Registers a callback to be invoked when completions for a term are requested.
|
--- Registers a callback to be invoked when completions for a term are requested.
|
||||||
---
|
---
|
||||||
---@param type "CompletionRequested"
|
---@param type c2.EventType.CompletionRequested
|
||||||
---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
||||||
function c2.register_callback(type, func) end
|
function c2.register_callback(type, func) end
|
||||||
|
|
||||||
|
|
|
@ -171,7 +171,7 @@ function cmd_words(ctx)
|
||||||
-- ctx contains:
|
-- ctx contains:
|
||||||
-- words - table of words supplied to the command including the trigger
|
-- words - table of words supplied to the command including the trigger
|
||||||
-- channel - the channel the command is being run in
|
-- channel - the channel the command is being run in
|
||||||
channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
ctx.channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
||||||
end
|
end
|
||||||
|
|
||||||
c2.register_command("/words", cmd_words)
|
c2.register_command("/words", cmd_words)
|
||||||
|
@ -183,7 +183,7 @@ 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)`
|
#### `register_callback(c2.EventType.CompletionRequested, handler)`
|
||||||
|
|
||||||
Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries:
|
Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries:
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ function string.startswith(s, other)
|
||||||
end
|
end
|
||||||
|
|
||||||
c2.register_callback(
|
c2.register_callback(
|
||||||
"CompletionRequested",
|
c2.EventType.CompletionRequested,
|
||||||
function(event)
|
function(event)
|
||||||
if ("!join"):startswith(event.query) then
|
if ("!join"):startswith(event.query) then
|
||||||
---@type CompletionList
|
---@type CompletionList
|
||||||
|
@ -219,15 +219,6 @@ c2.register_callback(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `Platform` enum
|
|
||||||
|
|
||||||
This table describes platforms that can be accessed. Chatterino supports IRC
|
|
||||||
however plugins do not yet have explicit access to get IRC channels objects.
|
|
||||||
The values behind the names may change, do not count on them. It has the
|
|
||||||
following keys:
|
|
||||||
|
|
||||||
- `Twitch`
|
|
||||||
|
|
||||||
#### `ChannelType` enum
|
#### `ChannelType` enum
|
||||||
|
|
||||||
This table describes channel types Chatterino supports. The values behind the
|
This table describes channel types Chatterino supports. The values behind the
|
||||||
|
@ -260,9 +251,9 @@ used on non-Twitch channels. Special channels while marked as
|
||||||
is an actual Twitch chatroom use `Channel:get_type()` instead of
|
is an actual Twitch chatroom use `Channel:get_type()` instead of
|
||||||
`Channel:is_twitch_channel()`.
|
`Channel:is_twitch_channel()`.
|
||||||
|
|
||||||
##### `Channel:by_name(name, platform)`
|
##### `Channel:by_name(name)`
|
||||||
|
|
||||||
Finds a channel given by `name` on `platform` (see [`Platform` enum](#Platform-enum)). Returns the channel or `nil` if not open.
|
Finds a channel given by `name`. Returns the channel or `nil` if not open.
|
||||||
|
|
||||||
Some miscellaneous channels are marked as if they are specifically Twitch channels:
|
Some miscellaneous channels are marked as if they are specifically Twitch channels:
|
||||||
|
|
||||||
|
@ -275,7 +266,7 @@ Some miscellaneous channels are marked as if they are specifically Twitch channe
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch)
|
local pajladas = c2.Channel.by_name("pajlada")
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `Channel:by_twitch_id(id)`
|
##### `Channel:by_twitch_id(id)`
|
||||||
|
@ -363,7 +354,7 @@ pajladas:add_system_message("Hello, world!")
|
||||||
|
|
||||||
Returns `true` if the channel is a Twitch channel, that is its type name has
|
Returns `true` if the channel is a Twitch channel, that is its type name has
|
||||||
the `Twitch` prefix. This returns `true` for special channels like Mentions.
|
the `Twitch` prefix. This returns `true` for special channels like Mentions.
|
||||||
You might want `Channel:get_type() == "Twitch"` if you want to use
|
You might want `Channel:get_type() == c2.ChannelType.Twitch` if you want to use
|
||||||
Twitch-specific functions.
|
Twitch-specific functions.
|
||||||
|
|
||||||
##### `Channel:get_twitch_id()`
|
##### `Channel:get_twitch_id()`
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit f339d2f73730f8fee4412f5e4938717866ecef48
|
Subproject commit 5b5caad7cd57d5ba3ca796bf1521b131d73ca405
|
|
@ -1,48 +1,44 @@
|
||||||
project(lua CXX)
|
project(lua CXX)
|
||||||
|
|
||||||
#[====[
|
#[====[
|
||||||
Updating this list:
|
This list contains all .c files except lua.c and onelua.c
|
||||||
remove all listed files
|
Use the following command from the repository root to get these file:
|
||||||
go to line below, ^y2j4j$@" and then reindent the file names
|
perl -e 'print s/^lib\/lua\///r . "\n" for grep { /\.c$/ && !/(lua|onelua)\.c$/ } glob "lib/lua/src/*.c"'
|
||||||
/LUA_SRC
|
|
||||||
:r!ls lib/lua/src | grep '\.c' | grep -Ev 'lua\.c|onelua\.c' | sed 's#^#src/#'
|
|
||||||
|
|
||||||
#]====]
|
#]====]
|
||||||
set(LUA_SRC
|
set(LUA_SRC
|
||||||
"src/lapi.c"
|
src/lapi.c
|
||||||
"src/lauxlib.c"
|
src/lauxlib.c
|
||||||
"src/lbaselib.c"
|
src/lbaselib.c
|
||||||
"src/lcode.c"
|
src/lcode.c
|
||||||
"src/lcorolib.c"
|
src/lcorolib.c
|
||||||
"src/lctype.c"
|
src/lctype.c
|
||||||
"src/ldblib.c"
|
src/ldblib.c
|
||||||
"src/ldebug.c"
|
src/ldebug.c
|
||||||
"src/ldo.c"
|
src/ldo.c
|
||||||
"src/ldump.c"
|
src/ldump.c
|
||||||
"src/lfunc.c"
|
src/lfunc.c
|
||||||
"src/lgc.c"
|
src/lgc.c
|
||||||
"src/linit.c"
|
src/linit.c
|
||||||
"src/liolib.c"
|
src/liolib.c
|
||||||
"src/llex.c"
|
src/llex.c
|
||||||
"src/lmathlib.c"
|
src/lmathlib.c
|
||||||
"src/lmem.c"
|
src/lmem.c
|
||||||
"src/loadlib.c"
|
src/loadlib.c
|
||||||
"src/lobject.c"
|
src/lobject.c
|
||||||
"src/lopcodes.c"
|
src/lopcodes.c
|
||||||
"src/loslib.c"
|
src/loslib.c
|
||||||
"src/lparser.c"
|
src/lparser.c
|
||||||
"src/lstate.c"
|
src/lstate.c
|
||||||
"src/lstring.c"
|
src/lstring.c
|
||||||
"src/lstrlib.c"
|
src/lstrlib.c
|
||||||
"src/ltable.c"
|
src/ltable.c
|
||||||
"src/ltablib.c"
|
src/ltablib.c
|
||||||
"src/ltests.c"
|
src/ltests.c
|
||||||
"src/ltm.c"
|
src/ltm.c
|
||||||
"src/lua.c"
|
src/lundump.c
|
||||||
"src/lundump.c"
|
src/lutf8lib.c
|
||||||
"src/lutf8lib.c"
|
src/lvm.c
|
||||||
"src/lvm.c"
|
src/lzio.c
|
||||||
"src/lzio.c"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(lua STATIC ${LUA_SRC})
|
add_library(lua STATIC ${LUA_SRC})
|
||||||
|
@ -50,4 +46,14 @@ target_include_directories(lua
|
||||||
PUBLIC
|
PUBLIC
|
||||||
${LUA_INCLUDE_DIRS}
|
${LUA_INCLUDE_DIRS}
|
||||||
)
|
)
|
||||||
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE C)
|
set_target_properties(${liblua} PROPERTIES
|
||||||
|
LANGUAGE CXX
|
||||||
|
LINKER_LANGUAGE CXX
|
||||||
|
CXX_STANDARD 98
|
||||||
|
CXX_EXTENSIONS TRUE
|
||||||
|
)
|
||||||
|
target_compile_options(lua PRIVATE
|
||||||
|
-w # this makes clang shut up about c-as-c++
|
||||||
|
$<$<AND:$<BOOL:${MSVC}>,$<CXX_COMPILER_ID:Clang>>:/EHsc> # enable exceptions in clang-cl
|
||||||
|
)
|
||||||
|
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60
|
Subproject commit 1ab3208a1fceb12fca8f24ba57d6e13c5bff15e3
|
|
@ -1 +1 @@
|
||||||
Subproject commit c58874c1aa5d0619df2c975bcb87433941b46920
|
Subproject commit 4a0a1e599377cdcdc91b0fbbefc312936b48730c
|
1
lib/sol2
Submodule
1
lib/sol2
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 2b0d2fe8ba0074e16b499940c4f3126b9c7d3471
|
|
@ -3,6 +3,7 @@
|
||||||
#include "common/Args.hpp"
|
#include "common/Args.hpp"
|
||||||
#include "mocks/DisabledStreamerMode.hpp"
|
#include "mocks/DisabledStreamerMode.hpp"
|
||||||
#include "mocks/EmptyApplication.hpp"
|
#include "mocks/EmptyApplication.hpp"
|
||||||
|
#include "mocks/TwitchUsers.hpp"
|
||||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||||
#include "singletons/Fonts.hpp"
|
#include "singletons/Fonts.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
@ -55,6 +56,11 @@ public:
|
||||||
return &this->fonts;
|
return &this->fonts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ITwitchUsers *getTwitchUsers() override
|
||||||
|
{
|
||||||
|
return &this->twitchUsers;
|
||||||
|
}
|
||||||
|
|
||||||
BttvLiveUpdates *getBttvLiveUpdates() override
|
BttvLiveUpdates *getBttvLiveUpdates() override
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
@ -71,6 +77,7 @@ public:
|
||||||
DisabledStreamerMode streamerMode;
|
DisabledStreamerMode streamerMode;
|
||||||
Theme theme;
|
Theme theme;
|
||||||
Fonts fonts;
|
Fonts fonts;
|
||||||
|
TwitchUsers twitchUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino::mock
|
} // namespace chatterino::mock
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace chatterino::mock {
|
namespace chatterino::mock {
|
||||||
|
|
||||||
class ChatterinoBadges : public IChatterinoBadges
|
class ChatterinoBadges : public IChatterinoBadges
|
||||||
|
@ -9,9 +11,21 @@ class ChatterinoBadges : public IChatterinoBadges
|
||||||
public:
|
public:
|
||||||
std::optional<EmotePtr> getBadge(const UserId &id) override
|
std::optional<EmotePtr> getBadge(const UserId &id) override
|
||||||
{
|
{
|
||||||
(void)id;
|
auto it = this->users.find(id);
|
||||||
|
if (it != this->users.end())
|
||||||
|
{
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setBadge(UserId id, EmotePtr emote)
|
||||||
|
{
|
||||||
|
this->users.emplace(std::move(id), std::move(emote));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<UserId, EmotePtr> users;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino::mock
|
} // namespace chatterino::mock
|
||||||
|
|
|
@ -350,7 +350,7 @@ public:
|
||||||
// contains a comma
|
// contains a comma
|
||||||
MOCK_METHOD(
|
MOCK_METHOD(
|
||||||
void, getChatters,
|
void, getChatters,
|
||||||
(QString broadcasterID, QString moderatorID, int maxChattersToFetch,
|
(QString broadcasterID, QString moderatorID, size_t maxChattersToFetch,
|
||||||
ResultCallback<HelixChatters> successCallback,
|
ResultCallback<HelixChatters> successCallback,
|
||||||
(FailureCallback<HelixGetChattersError, QString> failureCallback)),
|
(FailureCallback<HelixGetChattersError, QString> failureCallback)),
|
||||||
(override)); // getChatters
|
(override)); // getChatters
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||||
#include "providers/seventv/eventapi/Message.hpp"
|
#include "providers/seventv/eventapi/Message.hpp"
|
||||||
#include "providers/seventv/SeventvEmotes.hpp"
|
#include "providers/seventv/SeventvEmotes.hpp"
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace chatterino::mock {
|
namespace chatterino::mock {
|
||||||
|
|
||||||
class MockTwitchIrcServer : public ITwitchIrcServer
|
class MockTwitchIrcServer : public ITwitchIrcServer
|
||||||
|
@ -67,7 +70,30 @@ public:
|
||||||
std::shared_ptr<Channel> getChannelOrEmptyByID(
|
std::shared_ptr<Channel> getChannelOrEmptyByID(
|
||||||
const QString &channelID) override
|
const QString &channelID) override
|
||||||
{
|
{
|
||||||
return {};
|
// XXX: this is the same as in TwitchIrcServer::getChannelOrEmptyByID
|
||||||
|
for (const auto &[name, weakChannel] : this->mockChannels)
|
||||||
|
{
|
||||||
|
auto channel = weakChannel.lock();
|
||||||
|
if (!channel)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto twitchChannel =
|
||||||
|
std::dynamic_pointer_cast<TwitchChannel>(channel);
|
||||||
|
if (!twitchChannel)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twitchChannel->roomId() == channelID &&
|
||||||
|
twitchChannel->getName().count(':') < 2)
|
||||||
|
{
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Channel::getEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dropSeventvChannel(const QString &userID,
|
void dropSeventvChannel(const QString &userID,
|
||||||
|
@ -123,6 +149,8 @@ public:
|
||||||
ChannelPtr liveChannel;
|
ChannelPtr liveChannel;
|
||||||
ChannelPtr automodChannel;
|
ChannelPtr automodChannel;
|
||||||
QString lastUserThatWhisperedMe{"forsen"};
|
QString lastUserThatWhisperedMe{"forsen"};
|
||||||
|
|
||||||
|
std::unordered_map<QString, std::weak_ptr<Channel>> mockChannels;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino::mock
|
} // namespace chatterino::mock
|
||||||
|
|
24
mocks/include/mocks/TwitchUsers.hpp
Normal file
24
mocks/include/mocks/TwitchUsers.hpp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "providers/twitch/TwitchUser.hpp"
|
||||||
|
#include "providers/twitch/TwitchUsers.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::mock {
|
||||||
|
|
||||||
|
class TwitchUsers : public ITwitchUsers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TwitchUsers() = default;
|
||||||
|
|
||||||
|
std::shared_ptr<TwitchUser> resolveID(const UserId &id)
|
||||||
|
{
|
||||||
|
TwitchUser u = {
|
||||||
|
.id = id.string,
|
||||||
|
.name = {},
|
||||||
|
.displayName = {},
|
||||||
|
};
|
||||||
|
return std::make_shared<TwitchUser>(u);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::mock
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include "controllers/userdata/UserDataController.hpp"
|
#include "controllers/userdata/UserDataController.hpp"
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace chatterino::mock {
|
namespace chatterino::mock {
|
||||||
|
|
||||||
class UserDataController : public IUserDataController
|
class UserDataController : public IUserDataController
|
||||||
|
@ -13,6 +15,11 @@ public:
|
||||||
// If the user does not have any extra data, return none
|
// If the user does not have any extra data, return none
|
||||||
std::optional<UserData> getUser(const QString &userID) const override
|
std::optional<UserData> getUser(const QString &userID) const override
|
||||||
{
|
{
|
||||||
|
auto it = this->userMap.find(userID);
|
||||||
|
if (it != this->userMap.end())
|
||||||
|
{
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +27,21 @@ public:
|
||||||
void setUserColor(const QString &userID,
|
void setUserColor(const QString &userID,
|
||||||
const QString &colorString) override
|
const QString &colorString) override
|
||||||
{
|
{
|
||||||
// do nothing
|
auto it = this->userMap.find(userID);
|
||||||
|
if (it != this->userMap.end())
|
||||||
|
{
|
||||||
|
it->second.color = QColor(colorString);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->userMap.emplace(userID, UserData{
|
||||||
|
.color = QColor(colorString),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<QString, UserData> userMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino::mock
|
} // namespace chatterino::mock
|
||||||
|
|
BIN
resources/avatars/maliByatzes.png
Normal file
BIN
resources/avatars/maliByatzes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -35,7 +35,7 @@ Confuseh | https://github.com/Confuseh |
|
||||||
ch-ems | https://github.com/ch-ems |
|
ch-ems | https://github.com/ch-ems |
|
||||||
Bur0k | https://github.com/Bur0k |
|
Bur0k | https://github.com/Bur0k |
|
||||||
nuuls | https://github.com/nuuls |
|
nuuls | https://github.com/nuuls |
|
||||||
Chronophylos | https://github.com/Chronophylos |
|
Chronophylos | https://github.com/Chronophylos |
|
||||||
Ckath | https://github.com/Ckath |
|
Ckath | https://github.com/Ckath |
|
||||||
matijakevic | https://github.com/matijakevic |
|
matijakevic | https://github.com/matijakevic |
|
||||||
nforro | https://github.com/nforro |
|
nforro | https://github.com/nforro |
|
||||||
|
@ -79,6 +79,7 @@ KleberPF | https://github.com/KleberPF |
|
||||||
nealxm | https://github.com/nealxm | :/avatars/nealxm.png
|
nealxm | https://github.com/nealxm | :/avatars/nealxm.png
|
||||||
Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png
|
Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png
|
||||||
JakeRYW | https://github.com/JakeRYW | :/avatars/jakeryw.png
|
JakeRYW | https://github.com/JakeRYW | :/avatars/jakeryw.png
|
||||||
|
maliByatzes | https://github.com/maliByatzes | :/avatars/maliByatzes.png
|
||||||
|
|
||||||
# If you are a contributor add yourself above this line
|
# If you are a contributor add yourself above this line
|
||||||
|
|
||||||
|
|
BIN
resources/twitch/sharedChat.png
Normal file
BIN
resources/twitch/sharedChat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
19
scripts/make_luals_meta.py
Normal file → Executable file
19
scripts/make_luals_meta.py
Normal file → Executable file
|
@ -1,3 +1,4 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
This script generates docs/plugin-meta.lua. It accepts no arguments
|
This script generates docs/plugin-meta.lua. It accepts no arguments
|
||||||
|
|
||||||
|
@ -196,7 +197,7 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper):
|
||||||
if not comments[0].startswith("@"):
|
if not comments[0].startswith("@"):
|
||||||
out.write(f"--- {comments[0]}\n---\n")
|
out.write(f"--- {comments[0]}\n---\n")
|
||||||
comments = comments[1:]
|
comments = comments[1:]
|
||||||
params = []
|
params: list[str] = []
|
||||||
for comment in comments[:-1]:
|
for comment in comments[:-1]:
|
||||||
if not comment.startswith("@lua"):
|
if not comment.startswith("@lua"):
|
||||||
panic(path, line, f"Invalid function specification - got '{comment}'")
|
panic(path, line, f"Invalid function specification - got '{comment}'")
|
||||||
|
@ -209,7 +210,7 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper):
|
||||||
panic(path, line, f"Invalid function exposure - got '{comments[-1]}'")
|
panic(path, line, f"Invalid function exposure - got '{comments[-1]}'")
|
||||||
name = comments[-1].split(" ", 1)[1]
|
name = comments[-1].split(" ", 1)[1]
|
||||||
printmsg(path, line, f"function {name}")
|
printmsg(path, line, f"function {name}")
|
||||||
lua_params = ", ".join(params)
|
lua_params = ", ".join(p.removesuffix("?") for p in params)
|
||||||
out.write(f"function {name}({lua_params}) end\n\n")
|
out.write(f"function {name}({lua_params}) end\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,17 +243,19 @@ def read_file(path: Path, out: TextIOWrapper):
|
||||||
)
|
)
|
||||||
name = header[0].split(" ", 1)[1]
|
name = header[0].split(" ", 1)[1]
|
||||||
printmsg(path, reader.line_no(), f"enum {name}")
|
printmsg(path, reader.line_no(), f"enum {name}")
|
||||||
out.write(f"---@alias {name} integer\n")
|
|
||||||
if header_comment:
|
if header_comment:
|
||||||
out.write(f"--- {header_comment}\n")
|
out.write(f"--- {header_comment}\n")
|
||||||
out.write("---@type { ")
|
out.write(f"---@enum {name}\n")
|
||||||
|
out.write(f"{name} = {{\n")
|
||||||
out.write(
|
out.write(
|
||||||
", ".join(
|
"\n".join(
|
||||||
[f"{variant}: {name}" for variant in reader.read_enum_variants()]
|
[
|
||||||
|
f" {variant} = {{}}, ---@type {name}.{variant}"
|
||||||
|
for variant in reader.read_enum_variants()
|
||||||
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
out.write(" }\n")
|
out.write("\n}\n\n")
|
||||||
out.write(f"{name} = {{}}\n\n")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# class
|
# class
|
||||||
|
|
|
@ -2,6 +2,7 @@ set(LIBRARY_PROJECT "${PROJECT_NAME}-lib")
|
||||||
set(VERSION_PROJECT "${LIBRARY_PROJECT}-version")
|
set(VERSION_PROJECT "${LIBRARY_PROJECT}-version")
|
||||||
set(EXECUTABLE_PROJECT "${PROJECT_NAME}")
|
set(EXECUTABLE_PROJECT "${PROJECT_NAME}")
|
||||||
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00)
|
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00)
|
||||||
|
add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x050F00)
|
||||||
|
|
||||||
# registers the native messageing host
|
# registers the native messageing host
|
||||||
option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF)
|
option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF)
|
||||||
|
@ -39,6 +40,7 @@ set(SOURCE_FILES
|
||||||
common/WindowDescriptors.cpp
|
common/WindowDescriptors.cpp
|
||||||
common/WindowDescriptors.hpp
|
common/WindowDescriptors.hpp
|
||||||
|
|
||||||
|
common/enums/MessageContext.hpp
|
||||||
common/enums/MessageOverflow.hpp
|
common/enums/MessageOverflow.hpp
|
||||||
|
|
||||||
common/network/NetworkCommon.cpp
|
common/network/NetworkCommon.cpp
|
||||||
|
@ -225,24 +227,28 @@ set(SOURCE_FILES
|
||||||
controllers/pings/MutedChannelModel.cpp
|
controllers/pings/MutedChannelModel.cpp
|
||||||
controllers/pings/MutedChannelModel.hpp
|
controllers/pings/MutedChannelModel.hpp
|
||||||
|
|
||||||
|
|
||||||
controllers/plugins/api/ChannelRef.cpp
|
controllers/plugins/api/ChannelRef.cpp
|
||||||
controllers/plugins/api/ChannelRef.hpp
|
controllers/plugins/api/ChannelRef.hpp
|
||||||
controllers/plugins/api/IOWrapper.cpp
|
controllers/plugins/api/EventType.hpp
|
||||||
controllers/plugins/api/IOWrapper.hpp
|
|
||||||
controllers/plugins/api/HTTPRequest.cpp
|
controllers/plugins/api/HTTPRequest.cpp
|
||||||
controllers/plugins/api/HTTPRequest.hpp
|
controllers/plugins/api/HTTPRequest.hpp
|
||||||
controllers/plugins/api/HTTPResponse.cpp
|
controllers/plugins/api/HTTPResponse.cpp
|
||||||
controllers/plugins/api/HTTPResponse.hpp
|
controllers/plugins/api/HTTPResponse.hpp
|
||||||
|
controllers/plugins/api/IOWrapper.cpp
|
||||||
|
controllers/plugins/api/IOWrapper.hpp
|
||||||
controllers/plugins/LuaAPI.cpp
|
controllers/plugins/LuaAPI.cpp
|
||||||
controllers/plugins/LuaAPI.hpp
|
controllers/plugins/LuaAPI.hpp
|
||||||
controllers/plugins/PluginPermission.cpp
|
|
||||||
controllers/plugins/PluginPermission.hpp
|
|
||||||
controllers/plugins/Plugin.cpp
|
|
||||||
controllers/plugins/Plugin.hpp
|
|
||||||
controllers/plugins/PluginController.hpp
|
|
||||||
controllers/plugins/PluginController.cpp
|
|
||||||
controllers/plugins/LuaUtilities.cpp
|
controllers/plugins/LuaUtilities.cpp
|
||||||
controllers/plugins/LuaUtilities.hpp
|
controllers/plugins/LuaUtilities.hpp
|
||||||
|
controllers/plugins/PluginController.cpp
|
||||||
|
controllers/plugins/PluginController.hpp
|
||||||
|
controllers/plugins/Plugin.cpp
|
||||||
|
controllers/plugins/Plugin.hpp
|
||||||
|
controllers/plugins/PluginPermission.cpp
|
||||||
|
controllers/plugins/PluginPermission.hpp
|
||||||
|
controllers/plugins/SolTypes.cpp
|
||||||
|
controllers/plugins/SolTypes.hpp
|
||||||
|
|
||||||
controllers/sound/ISoundController.hpp
|
controllers/sound/ISoundController.hpp
|
||||||
controllers/sound/MiniaudioBackend.cpp
|
controllers/sound/MiniaudioBackend.cpp
|
||||||
|
@ -277,6 +283,9 @@ set(SOURCE_FILES
|
||||||
messages/MessageElement.cpp
|
messages/MessageElement.cpp
|
||||||
messages/MessageElement.hpp
|
messages/MessageElement.hpp
|
||||||
messages/MessageFlag.hpp
|
messages/MessageFlag.hpp
|
||||||
|
messages/MessageSimilarity.cpp
|
||||||
|
messages/MessageSimilarity.hpp
|
||||||
|
messages/MessageSink.hpp
|
||||||
messages/MessageThread.cpp
|
messages/MessageThread.cpp
|
||||||
messages/MessageThread.hpp
|
messages/MessageThread.hpp
|
||||||
|
|
||||||
|
@ -409,6 +418,8 @@ set(SOURCE_FILES
|
||||||
providers/twitch/TwitchEmotes.hpp
|
providers/twitch/TwitchEmotes.hpp
|
||||||
providers/twitch/TwitchHelpers.cpp
|
providers/twitch/TwitchHelpers.cpp
|
||||||
providers/twitch/TwitchHelpers.hpp
|
providers/twitch/TwitchHelpers.hpp
|
||||||
|
providers/twitch/TwitchIrc.cpp
|
||||||
|
providers/twitch/TwitchIrc.hpp
|
||||||
providers/twitch/TwitchIrcServer.cpp
|
providers/twitch/TwitchIrcServer.cpp
|
||||||
providers/twitch/TwitchIrcServer.hpp
|
providers/twitch/TwitchIrcServer.hpp
|
||||||
providers/twitch/TwitchUser.cpp
|
providers/twitch/TwitchUser.cpp
|
||||||
|
@ -497,6 +508,8 @@ set(SOURCE_FILES
|
||||||
util/InitUpdateButton.hpp
|
util/InitUpdateButton.hpp
|
||||||
util/IpcQueue.cpp
|
util/IpcQueue.cpp
|
||||||
util/IpcQueue.hpp
|
util/IpcQueue.hpp
|
||||||
|
util/IrcHelpers.cpp
|
||||||
|
util/IrcHelpers.hpp
|
||||||
util/LayoutHelper.cpp
|
util/LayoutHelper.cpp
|
||||||
util/LayoutHelper.hpp
|
util/LayoutHelper.hpp
|
||||||
util/LoadPixmap.cpp
|
util/LoadPixmap.cpp
|
||||||
|
@ -518,6 +531,8 @@ set(SOURCE_FILES
|
||||||
util/Twitch.hpp
|
util/Twitch.hpp
|
||||||
util/TypeName.hpp
|
util/TypeName.hpp
|
||||||
util/Variant.hpp
|
util/Variant.hpp
|
||||||
|
util/VectorMessageSink.cpp
|
||||||
|
util/VectorMessageSink.hpp
|
||||||
util/WidgetHelpers.cpp
|
util/WidgetHelpers.cpp
|
||||||
util/WidgetHelpers.hpp
|
util/WidgetHelpers.hpp
|
||||||
util/WindowsHelper.cpp
|
util/WindowsHelper.cpp
|
||||||
|
@ -787,7 +802,7 @@ target_link_libraries(${LIBRARY_PROJECT}
|
||||||
$<$<BOOL:${WIN32}>:Wtsapi32>
|
$<$<BOOL:${WIN32}>:Wtsapi32>
|
||||||
)
|
)
|
||||||
if (CHATTERINO_PLUGINS)
|
if (CHATTERINO_PLUGINS)
|
||||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua)
|
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (BUILD_WITH_QTKEYCHAIN)
|
if (BUILD_WITH_QTKEYCHAIN)
|
||||||
|
@ -969,6 +984,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
|
||||||
IRC_STATIC
|
IRC_STATIC
|
||||||
IRC_NAMESPACE=Communi
|
IRC_NAMESPACE=Communi
|
||||||
$<$<BOOL:${WIN32}>:_WIN32_WINNT=0x0A00> # Windows 10
|
$<$<BOOL:${WIN32}>:_WIN32_WINNT=0x0A00> # Windows 10
|
||||||
|
$<$<BOOL:${BUILD_TESTS}>:CHATTERINO_WITH_TESTS>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (USE_SYSTEM_QTKEYCHAIN)
|
if (USE_SYSTEM_QTKEYCHAIN)
|
||||||
|
|
|
@ -129,6 +129,10 @@
|
||||||
# include <unordered_set>
|
# include <unordered_set>
|
||||||
# include <vector>
|
# include <vector>
|
||||||
|
|
||||||
|
# ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifndef UNUSED
|
# ifndef UNUSED
|
||||||
# define UNUSED(x) (void)(x)
|
# define UNUSED(x) (void)(x)
|
||||||
# endif
|
# endif
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
|
#include "messages/MessageSimilarity.hpp"
|
||||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "singletons/Logging.hpp"
|
#include "singletons/Logging.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
@ -121,10 +123,10 @@ void Channel::addSystemMessage(const QString &contents)
|
||||||
this->addMessage(msg, MessageContext::Original);
|
this->addMessage(msg, MessageContext::Original);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Channel::addOrReplaceTimeout(MessagePtr message)
|
void Channel::addOrReplaceTimeout(MessagePtr message, QTime now)
|
||||||
{
|
{
|
||||||
addOrReplaceChannelTimeout(
|
addOrReplaceChannelTimeout(
|
||||||
this->getMessageSnapshot(), std::move(message), QTime::currentTime(),
|
this->getMessageSnapshot(), std::move(message), now,
|
||||||
[this](auto /*idx*/, auto msg, auto replacement) {
|
[this](auto /*idx*/, auto msg, auto replacement) {
|
||||||
this->replaceMessage(msg, replacement);
|
this->replaceMessage(msg, replacement);
|
||||||
},
|
},
|
||||||
|
@ -253,21 +255,33 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Channel::replaceMessage(MessagePtr message, MessagePtr replacement)
|
void Channel::replaceMessage(const MessagePtr &message,
|
||||||
|
const MessagePtr &replacement)
|
||||||
{
|
{
|
||||||
int index = this->messages_.replaceItem(message, replacement);
|
int index = this->messages_.replaceItem(message, replacement);
|
||||||
|
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
{
|
{
|
||||||
this->messageReplaced.invoke((size_t)index, replacement);
|
this->messageReplaced.invoke((size_t)index, message, replacement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Channel::replaceMessage(size_t index, MessagePtr replacement)
|
void Channel::replaceMessage(size_t index, const MessagePtr &replacement)
|
||||||
{
|
{
|
||||||
if (this->messages_.replaceItem(index, replacement))
|
MessagePtr prev;
|
||||||
|
if (this->messages_.replaceItem(index, replacement, &prev))
|
||||||
{
|
{
|
||||||
this->messageReplaced.invoke(index, replacement);
|
this->messageReplaced.invoke(index, prev, replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Channel::replaceMessage(size_t hint, const MessagePtr &message,
|
||||||
|
const MessagePtr &replacement)
|
||||||
|
{
|
||||||
|
auto index = this->messages_.replaceItem(hint, message, replacement);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
this->messageReplaced.invoke(hint, message, replacement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,10 +301,15 @@ void Channel::clearMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
MessagePtr Channel::findMessage(QString messageID)
|
MessagePtr Channel::findMessage(QString messageID)
|
||||||
|
{
|
||||||
|
return this->findMessageByID(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessagePtr Channel::findMessageByID(QStringView messageID)
|
||||||
{
|
{
|
||||||
MessagePtr res;
|
MessagePtr res;
|
||||||
|
|
||||||
if (auto msg = this->messages_.rfind([&messageID](const MessagePtr &msg) {
|
if (auto msg = this->messages_.rfind([messageID](const MessagePtr &msg) {
|
||||||
return msg->id == messageID;
|
return msg->id == messageID;
|
||||||
});
|
});
|
||||||
msg)
|
msg)
|
||||||
|
@ -301,6 +320,19 @@ MessagePtr Channel::findMessage(QString messageID)
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Channel::applySimilarityFilters(const MessagePtr &message) const
|
||||||
|
{
|
||||||
|
setSimilarityFlags(message, this->messages_.getSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageSinkTraits Channel::sinkTraits() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
MessageSinkTrait::AddMentionsToGlobalChannel,
|
||||||
|
MessageSinkTrait::RequiresKnownChannelPointReward,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bool Channel::canSendMessage() const
|
bool Channel::canSendMessage() const
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/enums/MessageContext.hpp"
|
||||||
#include "controllers/completion/TabCompletionModel.hpp"
|
#include "controllers/completion/TabCompletionModel.hpp"
|
||||||
#include "messages/LimitedQueue.hpp"
|
#include "messages/LimitedQueue.hpp"
|
||||||
#include "messages/MessageFlag.hpp"
|
#include "messages/MessageFlag.hpp"
|
||||||
|
#include "messages/MessageSink.hpp"
|
||||||
|
|
||||||
#include <magic_enum/magic_enum.hpp>
|
#include <magic_enum/magic_enum.hpp>
|
||||||
#include <pajlada/signals/signal.hpp>
|
#include <pajlada/signals/signal.hpp>
|
||||||
|
@ -26,15 +28,7 @@ enum class TimeoutStackStyle : int {
|
||||||
Default = DontStackBeyondUserMessage,
|
Default = DontStackBeyondUserMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Context of the message being added to a channel
|
class Channel : public std::enable_shared_from_this<Channel>, public MessageSink
|
||||||
enum class MessageContext {
|
|
||||||
/// This message is the original
|
|
||||||
Original,
|
|
||||||
/// This message is a repost of a message that has already been added in a channel
|
|
||||||
Repost,
|
|
||||||
};
|
|
||||||
|
|
||||||
class Channel : public std::enable_shared_from_this<Channel>
|
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// This is for Lua. See scripts/make_luals_meta.py
|
// This is for Lua. See scripts/make_luals_meta.py
|
||||||
|
@ -55,7 +49,7 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Channel(const QString &name, Type type);
|
explicit Channel(const QString &name, Type type);
|
||||||
virtual ~Channel();
|
~Channel() override;
|
||||||
|
|
||||||
// SIGNALS
|
// SIGNALS
|
||||||
pajlada::Signals::Signal<const QString &, const QString &, bool &>
|
pajlada::Signals::Signal<const QString &, const QString &, bool &>
|
||||||
|
@ -66,7 +60,9 @@ public:
|
||||||
pajlada::Signals::Signal<MessagePtr &, std::optional<MessageFlags>>
|
pajlada::Signals::Signal<MessagePtr &, std::optional<MessageFlags>>
|
||||||
messageAppended;
|
messageAppended;
|
||||||
pajlada::Signals::Signal<std::vector<MessagePtr> &> messagesAddedAtStart;
|
pajlada::Signals::Signal<std::vector<MessagePtr> &> messagesAddedAtStart;
|
||||||
pajlada::Signals::Signal<size_t, MessagePtr &> messageReplaced;
|
/// (index, prev-message, replacement)
|
||||||
|
pajlada::Signals::Signal<size_t, const MessagePtr &, const MessagePtr &>
|
||||||
|
messageReplaced;
|
||||||
/// Invoked when some number of messages were filled in using time received
|
/// Invoked when some number of messages were filled in using time received
|
||||||
pajlada::Signals::Signal<const std::vector<MessagePtr> &> filledInMessages;
|
pajlada::Signals::Signal<const std::vector<MessagePtr> &> filledInMessages;
|
||||||
pajlada::Signals::NoArgSignal destroyed;
|
pajlada::Signals::NoArgSignal destroyed;
|
||||||
|
@ -85,8 +81,9 @@ public:
|
||||||
// overridingFlags can be filled in with flags that should be used instead
|
// overridingFlags can be filled in with flags that should be used instead
|
||||||
// of the message's flags. This is useful in case a flag is specific to a
|
// of the message's flags. This is useful in case a flag is specific to a
|
||||||
// type of split
|
// type of split
|
||||||
void addMessage(MessagePtr message, MessageContext context,
|
void addMessage(
|
||||||
std::optional<MessageFlags> overridingFlags = std::nullopt);
|
MessagePtr message, MessageContext context,
|
||||||
|
std::optional<MessageFlags> overridingFlags = std::nullopt) final;
|
||||||
void addMessagesAtStart(const std::vector<MessagePtr> &messages_);
|
void addMessagesAtStart(const std::vector<MessagePtr> &messages_);
|
||||||
|
|
||||||
void addSystemMessage(const QString &contents);
|
void addSystemMessage(const QString &contents);
|
||||||
|
@ -94,19 +91,28 @@ public:
|
||||||
/// Inserts the given messages in order by Message::serverReceivedTime.
|
/// Inserts the given messages in order by Message::serverReceivedTime.
|
||||||
void fillInMissingMessages(const std::vector<MessagePtr> &messages);
|
void fillInMissingMessages(const std::vector<MessagePtr> &messages);
|
||||||
|
|
||||||
void addOrReplaceTimeout(MessagePtr message);
|
void addOrReplaceTimeout(MessagePtr message, QTime now) final;
|
||||||
void disableAllMessages();
|
void disableAllMessages() final;
|
||||||
void replaceMessage(MessagePtr message, MessagePtr replacement);
|
void replaceMessage(const MessagePtr &message,
|
||||||
void replaceMessage(size_t index, MessagePtr replacement);
|
const MessagePtr &replacement);
|
||||||
|
void replaceMessage(size_t index, const MessagePtr &replacement);
|
||||||
|
void replaceMessage(size_t hint, const MessagePtr &message,
|
||||||
|
const MessagePtr &replacement);
|
||||||
void deleteMessage(QString messageID);
|
void deleteMessage(QString messageID);
|
||||||
|
|
||||||
/// Removes all messages from this channel and invokes #messagesCleared
|
/// Removes all messages from this channel and invokes #messagesCleared
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
|
|
||||||
MessagePtr findMessage(QString messageID);
|
[[deprecated("Use findMessageByID instead")]] MessagePtr findMessage(
|
||||||
|
QString messageID);
|
||||||
|
MessagePtr findMessageByID(QStringView messageID) final;
|
||||||
|
|
||||||
bool hasMessages() const;
|
bool hasMessages() const;
|
||||||
|
|
||||||
|
void applySimilarityFilters(const MessagePtr &message) const final;
|
||||||
|
|
||||||
|
MessageSinkTraits sinkTraits() const final;
|
||||||
|
|
||||||
// CHANNEL INFO
|
// CHANNEL INFO
|
||||||
virtual bool canSendMessage() const;
|
virtual bool canSendMessage() const;
|
||||||
virtual bool isWritable() const; // whether split input will be usable
|
virtual bool isWritable() const; // whether split input will be usable
|
||||||
|
@ -165,30 +171,3 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
||||||
template <>
|
|
||||||
constexpr magic_enum::customize::customize_t
|
|
||||||
magic_enum::customize::enum_name<chatterino::Channel::Type>(
|
|
||||||
chatterino::Channel::Type value) noexcept
|
|
||||||
{
|
|
||||||
using Type = chatterino::Channel::Type;
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case Type::Twitch:
|
|
||||||
return "twitch";
|
|
||||||
case Type::TwitchWhispers:
|
|
||||||
return "whispers";
|
|
||||||
case Type::TwitchWatching:
|
|
||||||
return "watching";
|
|
||||||
case Type::TwitchMentions:
|
|
||||||
return "mentions";
|
|
||||||
case Type::TwitchLive:
|
|
||||||
return "live";
|
|
||||||
case Type::TwitchAutomod:
|
|
||||||
return "automod";
|
|
||||||
case Type::Misc:
|
|
||||||
return "misc";
|
|
||||||
default:
|
|
||||||
return default_tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "util/QMagicEnum.hpp"
|
#include "util/QMagicEnum.hpp"
|
||||||
|
|
||||||
#include <pajlada/settings.hpp>
|
#include <pajlada/settings.hpp>
|
||||||
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -55,6 +56,7 @@ using DoubleSetting = ChatterinoSetting<double>;
|
||||||
using IntSetting = ChatterinoSetting<int>;
|
using IntSetting = ChatterinoSetting<int>;
|
||||||
using StringSetting = ChatterinoSetting<std::string>;
|
using StringSetting = ChatterinoSetting<std::string>;
|
||||||
using QStringSetting = ChatterinoSetting<QString>;
|
using QStringSetting = ChatterinoSetting<QString>;
|
||||||
|
using QSizeSetting = ChatterinoSetting<QSize>;
|
||||||
|
|
||||||
template <typename Enum>
|
template <typename Enum>
|
||||||
class EnumSetting
|
class EnumSetting
|
||||||
|
@ -71,7 +73,6 @@ public:
|
||||||
_registerSetting(this->getData());
|
_registerSetting(this->getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T2>
|
|
||||||
EnumSetting<Enum> &operator=(Enum newValue)
|
EnumSetting<Enum> &operator=(Enum newValue)
|
||||||
{
|
{
|
||||||
this->setValue(Underlying(newValue));
|
this->setValue(Underlying(newValue));
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
enum class ProviderId { Twitch, Irc };
|
enum class ProviderId { // NOLINT(performance-enum-size)
|
||||||
|
Twitch,
|
||||||
|
};
|
||||||
//
|
//
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -87,7 +87,8 @@ public:
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
assert(index >= 0 && index <= this->items_.size());
|
assert(index >= 0 &&
|
||||||
|
index <= static_cast<int>(this->items_.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
this->items_.insert(this->items_.begin() + index, item);
|
this->items_.insert(this->items_.begin() + index, item);
|
||||||
|
@ -116,7 +117,7 @@ public:
|
||||||
void removeAt(int index, void *caller = nullptr)
|
void removeAt(int index, void *caller = nullptr)
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
assert(index >= 0 && index < int(this->items_.size()));
|
assert(index >= 0 && index < static_cast<int>(this->items_.size()));
|
||||||
|
|
||||||
T item = this->items_[index];
|
T item = this->items_[index];
|
||||||
this->items_.erase(this->items_.begin() + index);
|
this->items_.erase(this->items_.begin() + index);
|
||||||
|
@ -132,13 +133,14 @@ public:
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
|
|
||||||
for (int index = 0; index < this->items_.size(); ++index)
|
for (size_t index = 0; index < this->items_.size(); ++index)
|
||||||
{
|
{
|
||||||
T item = this->items_[index];
|
T item = this->items_[index];
|
||||||
if (matcher(item))
|
if (matcher(item))
|
||||||
{
|
{
|
||||||
this->items_.erase(this->items_.begin() + index);
|
this->items_.erase(this->items_.begin() + index);
|
||||||
SignalVectorItemEvent<T> args{item, index, caller};
|
SignalVectorItemEvent<T> args{item, static_cast<int>(index),
|
||||||
|
caller};
|
||||||
this->itemRemoved.invoke(args);
|
this->itemRemoved.invoke(args);
|
||||||
this->itemsChanged_();
|
this->itemsChanged_();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -43,7 +43,7 @@ public:
|
||||||
}
|
}
|
||||||
// get row index
|
// get row index
|
||||||
int index = this->getModelIndexFromVectorIndex(args.index);
|
int index = this->getModelIndexFromVectorIndex(args.index);
|
||||||
assert(index >= 0 && index <= this->rows_.size());
|
assert(index >= 0 && index <= static_cast<int>(this->rows_.size()));
|
||||||
|
|
||||||
// get row items
|
// get row items
|
||||||
std::vector<QStandardItem *> row = this->createRow();
|
std::vector<QStandardItem *> row = this->createRow();
|
||||||
|
@ -75,7 +75,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
int row = this->getModelIndexFromVectorIndex(args.index);
|
int row = this->getModelIndexFromVectorIndex(args.index);
|
||||||
assert(row >= 0 && row <= this->rows_.size());
|
assert(row >= 0 && row <= static_cast<int>(this->rows_.size()));
|
||||||
|
|
||||||
// remove row
|
// remove row
|
||||||
std::vector<QStandardItem *> items = this->rows_[row].items;
|
std::vector<QStandardItem *> items = this->rows_[row].items;
|
||||||
|
@ -130,7 +130,8 @@ public:
|
||||||
{
|
{
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
int column = index.column();
|
int column = index.column();
|
||||||
if (row < 0 || column < 0 || row >= this->rows_.size() ||
|
if (row < 0 || column < 0 ||
|
||||||
|
row >= static_cast<int>(this->rows_.size()) ||
|
||||||
column >= this->columnCount_)
|
column >= this->columnCount_)
|
||||||
{
|
{
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
@ -144,7 +145,8 @@ public:
|
||||||
{
|
{
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
int column = index.column();
|
int column = index.column();
|
||||||
if (row < 0 || column < 0 || row >= this->rows_.size() ||
|
if (row < 0 || column < 0 ||
|
||||||
|
row >= static_cast<int>(this->rows_.size()) ||
|
||||||
column >= this->columnCount_)
|
column >= this->columnCount_)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -152,7 +154,7 @@ public:
|
||||||
|
|
||||||
Row &rowItem = this->rows_[row];
|
Row &rowItem = this->rows_[row];
|
||||||
|
|
||||||
assert(this->columnCount_ == rowItem.items.size());
|
assert(this->columnCount_ == static_cast<int>(rowItem.items.size()));
|
||||||
|
|
||||||
auto &cell = rowItem.items[column];
|
auto &cell = rowItem.items[column];
|
||||||
|
|
||||||
|
@ -167,7 +169,7 @@ public:
|
||||||
int vecRow = this->getVectorIndexFromModelIndex(row);
|
int vecRow = this->getVectorIndexFromModelIndex(row);
|
||||||
// TODO: This is only a safety-thing for when we modify data that's being modified right now.
|
// TODO: This is only a safety-thing for when we modify data that's being modified right now.
|
||||||
// It should not be necessary, but it would require some rethinking about this surrounding logic
|
// It should not be necessary, but it would require some rethinking about this surrounding logic
|
||||||
if (vecRow >= this->vector_->readOnly()->size())
|
if (vecRow >= static_cast<int>(this->vector_->readOnly()->size()))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -224,18 +226,19 @@ public:
|
||||||
{
|
{
|
||||||
int row = index.row(), column = index.column();
|
int row = index.row(), column = index.column();
|
||||||
|
|
||||||
if (row < 0 || column < 0 || row >= this->rows_.size() ||
|
if (row < 0 || column < 0 ||
|
||||||
|
row >= static_cast<int>(this->rows_.size()) ||
|
||||||
column >= this->columnCount_)
|
column >= this->columnCount_)
|
||||||
{
|
{
|
||||||
return Qt::NoItemFlags;
|
return Qt::NoItemFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
|
assert(row >= 0 && row < static_cast<int>(this->rows_.size()) &&
|
||||||
column < this->columnCount_);
|
column >= 0 && column < this->columnCount_);
|
||||||
|
|
||||||
const auto &rowItem = this->rows_[row];
|
const auto &rowItem = this->rows_[row];
|
||||||
|
|
||||||
assert(this->columnCount_ == rowItem.items.size());
|
assert(this->columnCount_ == static_cast<int>(rowItem.items.size()));
|
||||||
|
|
||||||
return rowItem.items[column]->flags();
|
return rowItem.items[column]->flags();
|
||||||
}
|
}
|
||||||
|
@ -267,7 +270,8 @@ public:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(sourceRow >= 0 && sourceRow < this->rows_.size());
|
assert(sourceRow >= 0 &&
|
||||||
|
sourceRow < static_cast<int>(this->rows_.size()));
|
||||||
|
|
||||||
int signalVectorRow = this->getVectorIndexFromModelIndex(sourceRow);
|
int signalVectorRow = this->getVectorIndexFromModelIndex(sourceRow);
|
||||||
this->beginMoveRows(sourceParent, sourceRow, sourceRow,
|
this->beginMoveRows(sourceParent, sourceRow, sourceRow,
|
||||||
|
@ -294,7 +298,7 @@ public:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(row >= 0 && row < this->rows_.size());
|
assert(row >= 0 && row < static_cast<int>(this->rows_.size()));
|
||||||
|
|
||||||
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
|
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
|
||||||
this->vector_->removeAt(signalVectorRow);
|
this->vector_->removeAt(signalVectorRow);
|
||||||
|
@ -337,8 +341,10 @@ public:
|
||||||
int from = data->data("chatterino_row_id").toInt();
|
int from = data->data("chatterino_row_id").toInt();
|
||||||
int to = parent.row();
|
int to = parent.row();
|
||||||
|
|
||||||
int vectorFrom = this->getVectorIndexFromModelIndex(from);
|
auto vectorFrom =
|
||||||
int vectorTo = this->getVectorIndexFromModelIndex(to);
|
static_cast<size_t>(this->getVectorIndexFromModelIndex(from));
|
||||||
|
auto vectorTo =
|
||||||
|
static_cast<size_t>(this->getVectorIndexFromModelIndex(to));
|
||||||
|
|
||||||
if (vectorFrom < 0 || vectorFrom > this->vector_->raw().size() ||
|
if (vectorFrom < 0 || vectorFrom > this->vector_->raw().size() ||
|
||||||
vectorTo < 0 || vectorTo > this->vector_->raw().size())
|
vectorTo < 0 || vectorTo > this->vector_->raw().size())
|
||||||
|
@ -402,7 +408,7 @@ protected:
|
||||||
|
|
||||||
void insertCustomRow(std::vector<QStandardItem *> row, int index)
|
void insertCustomRow(std::vector<QStandardItem *> row, int index)
|
||||||
{
|
{
|
||||||
assert(index >= 0 && index <= this->rows_.size());
|
assert(index >= 0 && index <= static_cast<int>(this->rows_.size()));
|
||||||
|
|
||||||
this->beginInsertRows(QModelIndex(), index, index);
|
this->beginInsertRows(QModelIndex(), index, index);
|
||||||
this->rows_.insert(this->rows_.begin() + index,
|
this->rows_.insert(this->rows_.begin() + index,
|
||||||
|
@ -412,7 +418,7 @@ protected:
|
||||||
|
|
||||||
void removeCustomRow(int index)
|
void removeCustomRow(int index)
|
||||||
{
|
{
|
||||||
assert(index >= 0 && index <= this->rows_.size());
|
assert(index >= 0 && index <= static_cast<int>(this->rows_.size()));
|
||||||
assert(this->rows_[index].isCustomRow);
|
assert(this->rows_[index].isCustomRow);
|
||||||
|
|
||||||
this->beginRemoveRows(QModelIndex(), index, index);
|
this->beginRemoveRows(QModelIndex(), index, index);
|
||||||
|
|
13
src/common/enums/MessageContext.hpp
Normal file
13
src/common/enums/MessageContext.hpp
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
/// Context of the message being added to a channel
|
||||||
|
enum class MessageContext {
|
||||||
|
/// This message is the original
|
||||||
|
Original,
|
||||||
|
/// This message is a repost of a message that has already been added in a channel
|
||||||
|
Repost,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -16,7 +16,7 @@ using NetworkErrorCallback = std::function<void(NetworkResult)>;
|
||||||
using NetworkFinallyCallback = std::function<void()>;
|
using NetworkFinallyCallback = std::function<void()>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exposeenum HTTPMethod
|
* @exposeenum c2.HTTPMethod
|
||||||
*/
|
*/
|
||||||
enum class NetworkRequestType {
|
enum class NetworkRequestType {
|
||||||
Get,
|
Get,
|
||||||
|
|
|
@ -79,6 +79,7 @@ QString listEnvironmentVariables(const CommandContext &ctx)
|
||||||
QStringList debugMessages{
|
QStringList debugMessages{
|
||||||
"recentMessagesApiUrl: " + env.recentMessagesApiUrl,
|
"recentMessagesApiUrl: " + env.recentMessagesApiUrl,
|
||||||
"linkResolverUrl: " + env.linkResolverUrl,
|
"linkResolverUrl: " + env.linkResolverUrl,
|
||||||
|
"proxyUrl: " + env.proxyUrl.value_or("N/A"),
|
||||||
"twitchServerHost: " + env.twitchServerHost,
|
"twitchServerHost: " + env.twitchServerHost,
|
||||||
"twitchServerPort: " + QString::number(env.twitchServerPort),
|
"twitchServerPort: " + QString::number(env.twitchServerPort),
|
||||||
"twitchServerSecure: " + QString::number(env.twitchServerSecure),
|
"twitchServerSecure: " + QString::number(env.twitchServerSecure),
|
||||||
|
|
|
@ -37,7 +37,7 @@ void UnifiedSource::addToListModel(GenericListModel &model,
|
||||||
source->addToListModel(model, maxCount - used);
|
source->addToListModel(model, maxCount - used);
|
||||||
// Calculate how many items have been added so far
|
// Calculate how many items have been added so far
|
||||||
used = model.rowCount() - startingSize;
|
used = model.rowCount() - startingSize;
|
||||||
if (used >= maxCount)
|
if (used >= static_cast<int>(maxCount))
|
||||||
{
|
{
|
||||||
// Used up all of limit
|
// Used up all of limit
|
||||||
break;
|
break;
|
||||||
|
@ -58,15 +58,15 @@ void UnifiedSource::addToStringList(QStringList &list, size_t maxCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to only add maxCount elements in total.
|
// Make sure to only add maxCount elements in total.
|
||||||
int startingSize = list.size();
|
auto startingSize = list.size();
|
||||||
int used = 0;
|
QStringList::size_type used = 0;
|
||||||
|
|
||||||
for (const auto &source : this->sources_)
|
for (const auto &source : this->sources_)
|
||||||
{
|
{
|
||||||
source->addToStringList(list, maxCount - used, isFirstWord);
|
source->addToStringList(list, maxCount - used, isFirstWord);
|
||||||
// Calculate how many items have been added so far
|
// Calculate how many items have been added so far
|
||||||
used = list.size() - startingSize;
|
used = list.size() - startingSize;
|
||||||
if (used >= maxCount)
|
if (used >= static_cast<QStringList::size_type>(maxCount))
|
||||||
{
|
{
|
||||||
// Used up all of limit
|
// Used up all of limit
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,12 +1,134 @@
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
|
#include "common/Literals.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrc.hpp"
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace chatterino::literals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes (only) the replacement of @a match in @a source.
|
||||||
|
* The parts before and after the match in @a source are ignored.
|
||||||
|
*
|
||||||
|
* Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced
|
||||||
|
* with the string captured by the corresponding capturing group.
|
||||||
|
* This function should only be used if the regex contains capturing groups.
|
||||||
|
*
|
||||||
|
* Since Qt doesn't provide a way of replacing a single match with some replacement
|
||||||
|
* while supporting both capturing groups and lookahead/-behind in the regex,
|
||||||
|
* this is included here. It's essentially the implementation of
|
||||||
|
* QString::replace(const QRegularExpression &, const QString &).
|
||||||
|
* @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703
|
||||||
|
*/
|
||||||
|
QString makeRegexReplacement(QStringView source,
|
||||||
|
const QRegularExpression ®ex,
|
||||||
|
const QRegularExpressionMatch &match,
|
||||||
|
const QString &replacement)
|
||||||
|
{
|
||||||
|
using SizeType = QString::size_type;
|
||||||
|
struct QStringCapture {
|
||||||
|
SizeType pos;
|
||||||
|
SizeType len;
|
||||||
|
int captureNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
qsizetype numCaptures = regex.captureCount();
|
||||||
|
|
||||||
|
// 1. build the backreferences list, holding where the backreferences
|
||||||
|
// are in the replacement string
|
||||||
|
QVarLengthArray<QStringCapture> backReferences;
|
||||||
|
|
||||||
|
SizeType replacementLength = replacement.size();
|
||||||
|
for (SizeType i = 0; i < replacementLength - 1; i++)
|
||||||
|
{
|
||||||
|
if (replacement[i] != u'\\')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int no = replacement[i + 1].digitValue();
|
||||||
|
if (no <= 0 || no > numCaptures)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringCapture backReference{.pos = i, .len = 2};
|
||||||
|
|
||||||
|
if (i < replacementLength - 2)
|
||||||
|
{
|
||||||
|
int secondDigit = replacement[i + 2].digitValue();
|
||||||
|
if (secondDigit != -1 && ((no * 10) + secondDigit) <= numCaptures)
|
||||||
|
{
|
||||||
|
no = (no * 10) + secondDigit;
|
||||||
|
++backReference.len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backReference.captureNumber = no;
|
||||||
|
backReferences.append(backReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. iterate on the matches.
|
||||||
|
// For every match, copy the replacement string in chunks
|
||||||
|
// with the proper replacements for the backreferences
|
||||||
|
|
||||||
|
// length of the new string, with all the replacements
|
||||||
|
SizeType newLength = 0;
|
||||||
|
QVarLengthArray<QStringView> chunks;
|
||||||
|
QStringView replacementView{replacement};
|
||||||
|
|
||||||
|
// Initially: empty, as we only care about the replacement
|
||||||
|
SizeType len = 0;
|
||||||
|
SizeType lastEnd = 0;
|
||||||
|
for (const QStringCapture &backReference : std::as_const(backReferences))
|
||||||
|
{
|
||||||
|
// part of "replacement" before the backreference
|
||||||
|
len = backReference.pos - lastEnd;
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
chunks << replacementView.mid(lastEnd, len);
|
||||||
|
newLength += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// backreference itself
|
||||||
|
len = match.capturedLength(backReference.captureNumber);
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
chunks << source.mid(
|
||||||
|
match.capturedStart(backReference.captureNumber), len);
|
||||||
|
newLength += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEnd = backReference.pos + backReference.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the last part of the replacement string
|
||||||
|
len = replacementView.size() - lastEnd;
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
chunks << replacementView.mid(lastEnd, len);
|
||||||
|
newLength += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. assemble the chunks together
|
||||||
|
QString dst;
|
||||||
|
dst.reserve(newLength);
|
||||||
|
for (const QStringView &chunk : std::as_const(chunks))
|
||||||
|
{
|
||||||
|
dst += chunk;
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
||||||
|
@ -65,4 +187,187 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void processIgnorePhrases(const std::vector<IgnorePhrase> &phrases,
|
||||||
|
QString &content,
|
||||||
|
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
|
||||||
|
{
|
||||||
|
using SizeType = QString::size_type;
|
||||||
|
|
||||||
|
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
|
||||||
|
// all emotes outside the range come before `it`
|
||||||
|
// all emotes in the range start at `it`
|
||||||
|
auto it = std::partition(
|
||||||
|
twitchEmotes.begin(), twitchEmotes.end(),
|
||||||
|
[pos, len](const auto &item) {
|
||||||
|
// returns true for emotes outside the range
|
||||||
|
return !((item.start >= pos) && item.start < (pos + len));
|
||||||
|
});
|
||||||
|
std::vector<TwitchEmoteOccurrence> emotesInRange(it,
|
||||||
|
twitchEmotes.end());
|
||||||
|
twitchEmotes.erase(it, twitchEmotes.end());
|
||||||
|
return emotesInRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) {
|
||||||
|
for (auto &item : twitchEmotes)
|
||||||
|
{
|
||||||
|
auto &index = item.start;
|
||||||
|
if (index >= pos)
|
||||||
|
{
|
||||||
|
index += by;
|
||||||
|
item.end += by;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase,
|
||||||
|
const auto &midrepl,
|
||||||
|
SizeType startIndex) {
|
||||||
|
if (!phrase.containsEmote())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
auto words = midrepl.tokenize(u' ');
|
||||||
|
#else
|
||||||
|
auto words = midrepl.split(' ');
|
||||||
|
#endif
|
||||||
|
SizeType pos = 0;
|
||||||
|
for (const auto &word : words)
|
||||||
|
{
|
||||||
|
for (const auto &emote : phrase.getEmotes())
|
||||||
|
{
|
||||||
|
if (word == emote.first.string)
|
||||||
|
{
|
||||||
|
if (emote.second == nullptr)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoTwitch)
|
||||||
|
<< "emote null" << emote.first.string;
|
||||||
|
}
|
||||||
|
twitchEmotes.push_back(TwitchEmoteOccurrence{
|
||||||
|
static_cast<int>(startIndex + pos),
|
||||||
|
static_cast<int>(startIndex + pos +
|
||||||
|
emote.first.string.length()),
|
||||||
|
emote.second,
|
||||||
|
emote.first,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += word.length() + 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
|
||||||
|
SizeType length, const QString &replacement) {
|
||||||
|
auto removedEmotes = removeEmotesInRange(from, length);
|
||||||
|
content.replace(from, length, replacement);
|
||||||
|
auto wordStart = from;
|
||||||
|
while (wordStart > 0)
|
||||||
|
{
|
||||||
|
if (content[wordStart - 1] == ' ')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--wordStart;
|
||||||
|
}
|
||||||
|
auto wordEnd = from + replacement.length();
|
||||||
|
while (wordEnd < content.length())
|
||||||
|
{
|
||||||
|
if (content[wordEnd] == ' ')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++wordEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
shiftIndicesAfter(static_cast<int>(from + length),
|
||||||
|
static_cast<int>(replacement.length() - length));
|
||||||
|
|
||||||
|
auto midExtendedRef =
|
||||||
|
QStringView{content}.mid(wordStart, wordEnd - wordStart);
|
||||||
|
|
||||||
|
for (auto &emote : removedEmotes)
|
||||||
|
{
|
||||||
|
if (emote.ptr == nullptr)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoTwitch)
|
||||||
|
<< "Invalid emote occurrence" << emote.name.string;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QRegularExpression emoteregex(
|
||||||
|
"\\b" + emote.name.string + "\\b",
|
||||||
|
QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||||
|
auto match = emoteregex.matchView(midExtendedRef);
|
||||||
|
#else
|
||||||
|
auto match = emoteregex.match(midExtendedRef);
|
||||||
|
#endif
|
||||||
|
if (match.hasMatch())
|
||||||
|
{
|
||||||
|
emote.start = static_cast<int>(from + match.capturedStart());
|
||||||
|
emote.end = static_cast<int>(from + match.capturedEnd());
|
||||||
|
twitchEmotes.push_back(std::move(emote));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addReplEmotes(phrase, midExtendedRef, wordStart);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &phrase : phrases)
|
||||||
|
{
|
||||||
|
if (phrase.isBlock())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &pattern = phrase.getPattern();
|
||||||
|
if (pattern.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (phrase.isRegex())
|
||||||
|
{
|
||||||
|
const auto ®ex = phrase.getRegex();
|
||||||
|
if (!regex.isValid())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegularExpressionMatch match;
|
||||||
|
size_t iterations = 0;
|
||||||
|
SizeType from = 0;
|
||||||
|
while ((from = content.indexOf(regex, from, &match)) != -1)
|
||||||
|
{
|
||||||
|
auto replacement = phrase.getReplace();
|
||||||
|
if (regex.captureCount() > 0)
|
||||||
|
{
|
||||||
|
replacement = makeRegexReplacement(content, regex, match,
|
||||||
|
replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceMessageAt(phrase, from, match.capturedLength(),
|
||||||
|
replacement);
|
||||||
|
from += phrase.getReplace().length();
|
||||||
|
iterations++;
|
||||||
|
if (iterations >= 128)
|
||||||
|
{
|
||||||
|
content = u"Too many replacements - check your ignores!"_s;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeType from = 0;
|
||||||
|
while ((from = content.indexOf(pattern, from,
|
||||||
|
phrase.caseSensitivity())) != -1)
|
||||||
|
{
|
||||||
|
replaceMessageAt(phrase, from, pattern.length(),
|
||||||
|
phrase.getReplace());
|
||||||
|
from += phrase.getReplace().length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -2,8 +2,13 @@
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
class IgnorePhrase;
|
||||||
|
struct TwitchEmoteOccurrence;
|
||||||
|
|
||||||
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
||||||
|
|
||||||
struct IgnoredMessageParameters {
|
struct IgnoredMessageParameters {
|
||||||
|
@ -16,4 +21,17 @@ struct IgnoredMessageParameters {
|
||||||
|
|
||||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms);
|
bool isIgnoredMessage(IgnoredMessageParameters &¶ms);
|
||||||
|
|
||||||
|
/// @brief Processes replacement ignore-phrases for a message
|
||||||
|
///
|
||||||
|
/// @param phrases A list of IgnorePhrases to process. Block phrases as well as
|
||||||
|
/// invalid phrases are ignored.
|
||||||
|
/// @param content The message text. This gets altered by replacements.
|
||||||
|
/// @param twitchEmotes A list of emotes present in the message. Occurrences
|
||||||
|
/// that have been removed from the message will also be
|
||||||
|
/// removed in this list. Similarly, if new emotes are added
|
||||||
|
/// from a replacement, this list gets updated as well.
|
||||||
|
void processIgnorePhrases(const std::vector<IgnorePhrase> &phrases,
|
||||||
|
QString &content,
|
||||||
|
std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -95,11 +95,11 @@ bool IgnorePhrase::containsEmote() const
|
||||||
{
|
{
|
||||||
if (!this->emotesChecked_)
|
if (!this->emotesChecked_)
|
||||||
{
|
{
|
||||||
const auto &accvec = getApp()->getAccounts()->twitch.accounts;
|
auto accemotes =
|
||||||
for (const auto &acc : accvec)
|
getApp()->getAccounts()->twitch.getCurrent()->accessEmotes();
|
||||||
|
if (*accemotes)
|
||||||
{
|
{
|
||||||
const auto &accemotes = *acc->accessEmotes();
|
for (const auto &emote : **accemotes)
|
||||||
for (const auto &emote : *accemotes)
|
|
||||||
{
|
{
|
||||||
if (this->replace_.contains(emote.first.string,
|
if (this->replace_.contains(emote.first.string,
|
||||||
Qt::CaseSensitive))
|
Qt::CaseSensitive))
|
||||||
|
|
|
@ -3,34 +3,45 @@
|
||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
# include "messages/MessageBuilder.hpp"
|
# include "controllers/plugins/SolTypes.hpp" // for lua operations on QString{,List} for CompletionList
|
||||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
# include <lualib.h>
|
# include <lualib.h>
|
||||||
}
|
|
||||||
# include <QFileInfo>
|
# include <QFileInfo>
|
||||||
|
# include <QList>
|
||||||
# include <QLoggingCategory>
|
# include <QLoggingCategory>
|
||||||
# include <QTextCodec>
|
# include <QTextCodec>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
# include <sol/protected_function_result.hpp>
|
||||||
|
# include <sol/reference.hpp>
|
||||||
|
# include <sol/stack.hpp>
|
||||||
|
# include <sol/state_view.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
# include <sol/variadic_args.hpp>
|
||||||
|
# include <sol/variadic_results.hpp>
|
||||||
|
|
||||||
|
# include <stdexcept>
|
||||||
|
# include <string>
|
||||||
|
# include <utility>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
|
||||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc)
|
void logHelper(lua_State *L, Plugin *pl, QDebug stream,
|
||||||
|
const sol::variadic_args &args)
|
||||||
{
|
{
|
||||||
stream.noquote();
|
stream.noquote();
|
||||||
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
||||||
for (int i = 1; i <= argc; i++)
|
for (const auto &arg : args)
|
||||||
{
|
{
|
||||||
stream << lua::toString(L, i);
|
stream << lua::toString(L, arg.stack_index());
|
||||||
|
// Remove this from our stack
|
||||||
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
lua_pop(L, argc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||||
|
@ -63,195 +74,92 @@ QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||||
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
int c2_register_command(lua_State *L)
|
CompletionList::CompletionList(const sol::table &table)
|
||||||
|
: values(table.get<QStringList>("values"))
|
||||||
|
, hideOthers(table["hide_others"])
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "internal error: no plugin");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString name;
|
|
||||||
if (!lua::peek(L, &name, 1))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get command name (1st arg of register_command, "
|
|
||||||
"expected a string)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (lua_isnoneornil(L, 2))
|
|
||||||
{
|
|
||||||
luaL_error(L, "missing argument for register_command: function "
|
|
||||||
"\"pointer\"");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto callbackSavedName = QString("c2commandcb-%1").arg(name);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str());
|
|
||||||
auto ok = pl->registerCommand(name, callbackSavedName);
|
|
||||||
|
|
||||||
// delete both name and callback
|
|
||||||
lua_pop(L, 2);
|
|
||||||
|
|
||||||
lua::push(L, ok);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int c2_register_callback(lua_State *L)
|
sol::table toTable(lua_State *L, const CompletionEvent &ev)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
return sol::state_view(L).create_table_with(
|
||||||
if (pl == nullptr)
|
"query", ev.query, //
|
||||||
{
|
"full_text_content", ev.full_text_content, //
|
||||||
luaL_error(L, "internal error: no plugin");
|
"cursor_position", ev.cursor_position, //
|
||||||
return 0;
|
"is_first_word", ev.is_first_word //
|
||||||
}
|
);
|
||||||
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 typeName = magic_enum::enum_name(evtType);
|
|
||||||
std::string callbackSavedName;
|
|
||||||
callbackSavedName.reserve(5 + typeName.size());
|
|
||||||
callbackSavedName += "c2cb-";
|
|
||||||
callbackSavedName += typeName;
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str());
|
|
||||||
|
|
||||||
lua_pop(L, 2);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int c2_log(lua_State *L)
|
void c2_register_callback(ThisPluginState L, EventType evtType,
|
||||||
|
sol::protected_function callback)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
L.plugin()->callbacks[evtType] = std::move(callback);
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "c2_log: internal error: no plugin?");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
auto logc = lua_gettop(L) - 1;
|
|
||||||
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
|
||||||
LogLevel lvl{};
|
|
||||||
if (!lua::pop(L, &lvl, 1))
|
|
||||||
{
|
|
||||||
luaL_error(L, "Invalid log level, use one from c2.LogLevel.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
QDebug stream = qdebugStreamForLogLevel(lvl);
|
|
||||||
logHelper(L, pl, stream, logc);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int c2_later(lua_State *L)
|
void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
lua::StackGuard guard(L);
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
{
|
||||||
return luaL_error(L, "c2.later: internal error: no plugin?");
|
QDebug stream = qdebugStreamForLogLevel(lvl);
|
||||||
}
|
logHelper(L, L.plugin(), stream, args);
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "c2.later expects two arguments (a callback that takes no "
|
|
||||||
"arguments and returns nothing and a number the time in "
|
|
||||||
"milliseconds to wait)\n");
|
|
||||||
}
|
|
||||||
int time{};
|
|
||||||
if (!lua::pop(L, &time))
|
|
||||||
{
|
|
||||||
return luaL_error(L, "cannot get time (2nd arg of c2.later, "
|
|
||||||
"expected a number)");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!lua_isfunction(L, lua_gettop(L)))
|
void c2_later(ThisPluginState L, sol::protected_function callback, int time)
|
||||||
|
{
|
||||||
|
if (time <= 0)
|
||||||
{
|
{
|
||||||
return luaL_error(L, "cannot get callback (1st arg of c2.later, "
|
throw std::runtime_error(
|
||||||
"expected a function)");
|
"c2.later time must be strictly greater than zero.");
|
||||||
}
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
|
||||||
auto *timer = new QTimer();
|
auto *timer = new QTimer();
|
||||||
timer->setInterval(time);
|
timer->setInterval(time);
|
||||||
auto id = pl->addTimeout(timer);
|
timer->setSingleShot(true);
|
||||||
|
auto id = L.plugin()->addTimeout(timer);
|
||||||
auto name = QString("timeout_%1").arg(id);
|
auto name = QString("timeout_%1").arg(id);
|
||||||
auto *coro = lua_newthread(L);
|
|
||||||
|
|
||||||
QObject::connect(timer, &QTimer::timeout, [pl, coro, name, timer]() {
|
sol::state_view main = sol::main_thread(L);
|
||||||
timer->deleteLater();
|
|
||||||
pl->removeTimeout(timer);
|
|
||||||
int nres{};
|
|
||||||
lua_resume(coro, nullptr, 0, &nres);
|
|
||||||
|
|
||||||
lua_pushnil(coro);
|
sol::thread thread = sol::thread::create(main);
|
||||||
lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
sol::protected_function cb(thread.state(), callback);
|
||||||
if (lua_gettop(coro) != 0)
|
main.registry()[name.toStdString()] = thread;
|
||||||
{
|
|
||||||
stackDump(coro,
|
QObject::connect(
|
||||||
pl->id +
|
timer, &QTimer::timeout,
|
||||||
": timer returned a value, this shouldn't happen "
|
[pl = L.plugin(), name, timer, cb, thread, main]() {
|
||||||
"and is probably a plugin bug");
|
timer->deleteLater();
|
||||||
}
|
pl->removeTimeout(timer);
|
||||||
});
|
sol::protected_function_result res = cb();
|
||||||
stackDump(L, "before setfield");
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
if (res.return_count() != 0)
|
||||||
lua_xmove(L, coro, 1); // move function to thread
|
{
|
||||||
|
stackDump(thread.lua_state(),
|
||||||
|
pl->id +
|
||||||
|
": timer returned a value, this shouldn't happen "
|
||||||
|
"and is probably a plugin bug");
|
||||||
|
}
|
||||||
|
main.registry()[name.toStdString()] = sol::nil;
|
||||||
|
});
|
||||||
timer->start();
|
timer->start();
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int g_load(lua_State *L)
|
// TODO: Add tests for this once we run tests in debug mode
|
||||||
|
sol::variadic_results g_load(ThisPluginState s, sol::object data)
|
||||||
{
|
{
|
||||||
# ifdef NDEBUG
|
# ifdef NDEBUG
|
||||||
luaL_error(L, "load() is only usable in debug mode");
|
(void)data;
|
||||||
return 0;
|
(void)s;
|
||||||
|
throw std::runtime_error("load() is only usable in debug mode");
|
||||||
# else
|
# else
|
||||||
auto countArgs = lua_gettop(L);
|
|
||||||
QByteArray data;
|
|
||||||
if (lua::peek(L, &data, 1))
|
|
||||||
{
|
|
||||||
auto *utf8 = QTextCodec::codecForName("UTF-8");
|
|
||||||
QTextCodec::ConverterState state;
|
|
||||||
utf8->toUnicode(data.constData(), data.size(), &state);
|
|
||||||
if (state.invalidChars != 0)
|
|
||||||
{
|
|
||||||
luaL_error(L, "invalid utf-8 in load() is not allowed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
luaL_error(L, "using reader function in load() is not allowed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < countArgs; i++)
|
// If you're modifying this PLEASE verify it works, Sol is very annoying about serialization
|
||||||
{
|
// - Mm2PL
|
||||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
sol::state_view lua(s);
|
||||||
}
|
auto load = lua.registry()["real_load"];
|
||||||
|
sol::protected_function_result ret = load(data, "=(load)", "t");
|
||||||
// fetch load and call it
|
return ret;
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_load");
|
|
||||||
|
|
||||||
for (int i = 0; i < countArgs; i++)
|
|
||||||
{
|
|
||||||
lua_geti(L, LUA_REGISTRYINDEX, i);
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
lua_call(L, countArgs, LUA_MULTRET);
|
|
||||||
|
|
||||||
return lua_gettop(L);
|
|
||||||
# endif
|
# endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +228,7 @@ int searcherAbsolute(lua_State *L)
|
||||||
int searcherRelative(lua_State *L)
|
int searcherRelative(lua_State *L)
|
||||||
{
|
{
|
||||||
lua_Debug dbg;
|
lua_Debug dbg;
|
||||||
lua_getstack(L, 1, &dbg);
|
lua_getstack(L, 2, &dbg);
|
||||||
lua_getinfo(L, "S", &dbg);
|
lua_getinfo(L, "S", &dbg);
|
||||||
auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen);
|
auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen);
|
||||||
if (currentFile.startsWith("@"))
|
if (currentFile.startsWith("@"))
|
||||||
|
@ -346,22 +254,14 @@ int searcherRelative(lua_State *L)
|
||||||
return loadfile(L, filename);
|
return loadfile(L, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
int g_print(lua_State *L)
|
void g_print(ThisPluginState L, sol::variadic_args args)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "c2_print: internal error: no plugin?");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
auto argc = lua_gettop(L);
|
|
||||||
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
||||||
auto stream =
|
auto stream =
|
||||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
||||||
.debug());
|
.debug());
|
||||||
logHelper(L, pl, stream, argc);
|
logHelper(L, L.plugin(), stream, args);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
# include "controllers/plugins/Plugin.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
# include <QList>
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
|
|
||||||
# include <QString>
|
# include <QString>
|
||||||
|
# include <sol/table.hpp>
|
||||||
|
|
||||||
# include <cassert>
|
# include <cassert>
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <vector>
|
|
||||||
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
|
@ -30,11 +30,8 @@ namespace chatterino::lua::api {
|
||||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exposeenum c2.EventType
|
* @includefile controllers/plugins/api/EventType.hpp
|
||||||
*/
|
*/
|
||||||
enum class EventType {
|
|
||||||
CompletionRequested,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@class CommandContext
|
* @lua@class CommandContext
|
||||||
|
@ -46,10 +43,12 @@ enum class EventType {
|
||||||
* @lua@class CompletionList
|
* @lua@class CompletionList
|
||||||
*/
|
*/
|
||||||
struct CompletionList {
|
struct CompletionList {
|
||||||
|
CompletionList(const sol::table &);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@field values string[] The completions
|
* @lua@field values string[] The completions
|
||||||
*/
|
*/
|
||||||
std::vector<QString> values{};
|
QStringList values;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
* @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
||||||
|
@ -79,6 +78,8 @@ struct CompletionEvent {
|
||||||
bool is_first_word{};
|
bool is_first_word{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sol::table toTable(lua_State *L, const CompletionEvent &ev);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @includefile common/Channel.hpp
|
* @includefile common/Channel.hpp
|
||||||
* @includefile controllers/plugins/api/ChannelRef.hpp
|
* @includefile controllers/plugins/api/ChannelRef.hpp
|
||||||
|
@ -95,16 +96,16 @@ struct CompletionEvent {
|
||||||
* @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
|
* @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
|
||||||
* @exposed c2.register_command
|
* @exposed c2.register_command
|
||||||
*/
|
*/
|
||||||
int c2_register_command(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a callback to be invoked when completions for a term are requested.
|
* Registers a callback to be invoked when completions for a term are requested.
|
||||||
*
|
*
|
||||||
* @lua@param type "CompletionRequested"
|
* @lua@param type c2.EventType.CompletionRequested
|
||||||
* @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
* @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
||||||
* @exposed c2.register_callback
|
* @exposed c2.register_callback
|
||||||
*/
|
*/
|
||||||
int c2_register_callback(lua_State *L);
|
void c2_register_callback(ThisPluginState L, EventType evtType,
|
||||||
|
sol::protected_function callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a message to the Chatterino log.
|
* Writes a message to the Chatterino log.
|
||||||
|
@ -113,7 +114,7 @@ int c2_register_callback(lua_State *L);
|
||||||
* @lua@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
* @lua@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
||||||
* @exposed c2.log
|
* @exposed c2.log
|
||||||
*/
|
*/
|
||||||
int c2_log(lua_State *L);
|
void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls callback around msec milliseconds later. Does not freeze Chatterino.
|
* Calls callback around msec milliseconds later. Does not freeze Chatterino.
|
||||||
|
@ -122,11 +123,11 @@ int c2_log(lua_State *L);
|
||||||
* @lua@param msec number How long to wait.
|
* @lua@param msec number How long to wait.
|
||||||
* @exposed c2.later
|
* @exposed c2.later
|
||||||
*/
|
*/
|
||||||
int c2_later(lua_State *L);
|
void c2_later(ThisPluginState L, sol::protected_function callback, int time);
|
||||||
|
|
||||||
// These ones are global
|
// These ones are global
|
||||||
int g_load(lua_State *L);
|
sol::variadic_results g_load(ThisPluginState s, sol::object data);
|
||||||
int g_print(lua_State *L);
|
void g_print(ThisPluginState L, sol::variadic_args args);
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
// This is for require() exposed as an element of package.searchers
|
// This is for require() exposed as an element of package.searchers
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
|
||||||
# include "common/Channel.hpp"
|
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandContext.hpp"
|
|
||||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
|
||||||
|
|
||||||
# include <climits>
|
# include <climits>
|
||||||
# include <cstdlib>
|
# include <cstdlib>
|
||||||
|
@ -79,9 +73,6 @@ 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)";
|
||||||
}
|
}
|
||||||
|
@ -93,18 +84,6 @@ QString humanErrorText(lua_State *L, int errCode)
|
||||||
return errName;
|
return errName;
|
||||||
}
|
}
|
||||||
|
|
||||||
StackIdx pushEmptyArray(lua_State *L, int countArray)
|
|
||||||
{
|
|
||||||
lua_createtable(L, countArray, 0);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx pushEmptyTable(lua_State *L, int countProperties)
|
|
||||||
{
|
|
||||||
lua_createtable(L, 0, countProperties);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const QString &str)
|
StackIdx push(lua_State *L, const QString &str)
|
||||||
{
|
{
|
||||||
return lua::push(L, str.toStdString());
|
return lua::push(L, str.toStdString());
|
||||||
|
@ -116,82 +95,6 @@ StackIdx push(lua_State *L, const std::string &str)
|
||||||
return lua_gettop(L);
|
return lua_gettop(L);
|
||||||
}
|
}
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const CommandContext &ctx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L, 1);
|
|
||||||
auto outIdx = pushEmptyTable(L, 2);
|
|
||||||
|
|
||||||
push(L, ctx.words);
|
|
||||||
lua_setfield(L, outIdx, "words");
|
|
||||||
|
|
||||||
push(L, ctx.channel);
|
|
||||||
lua_setfield(L, outIdx, "channel");
|
|
||||||
|
|
||||||
return outIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const bool &b)
|
|
||||||
{
|
|
||||||
lua_pushboolean(L, int(b));
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const int &b)
|
|
||||||
{
|
|
||||||
lua_pushinteger(L, b);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const api::CompletionEvent &ev)
|
|
||||||
{
|
|
||||||
auto idx = pushEmptyTable(L, 4);
|
|
||||||
# define PUSH(field) \
|
|
||||||
lua::push(L, ev.field); \
|
|
||||||
lua_setfield(L, idx, #field)
|
|
||||||
PUSH(query);
|
|
||||||
PUSH(full_text_content);
|
|
||||||
PUSH(cursor_position);
|
|
||||||
PUSH(is_first_word);
|
|
||||||
# undef PUSH
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, int *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
if (lua_isnumber(L, idx) == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = lua_tointeger(L, idx);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, bool *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
if (!lua_isboolean(L, idx))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = bool(lua_toboolean(L, idx));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, double *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
int ok{0};
|
|
||||||
auto v = lua_tonumberx(L, idx, &ok);
|
|
||||||
if (ok != 0)
|
|
||||||
{
|
|
||||||
*out = v;
|
|
||||||
}
|
|
||||||
return ok != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, QString *out, StackIdx idx)
|
bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||||
{
|
{
|
||||||
StackGuard guard(L);
|
StackGuard guard(L);
|
||||||
|
@ -209,57 +112,6 @@ bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
size_t len{0};
|
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
|
||||||
if (str == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (len >= INT_MAX)
|
|
||||||
{
|
|
||||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
|
||||||
}
|
|
||||||
*out = QByteArray(str, int(len));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, std::string *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
size_t len{0};
|
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
|
||||||
if (str == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (len >= INT_MAX)
|
|
||||||
{
|
|
||||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
|
||||||
}
|
|
||||||
*out = std::string(str, len);
|
|
||||||
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,37 +2,20 @@
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
# include "common/QLogging.hpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# 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 <sol/state_view.hpp>
|
||||||
|
|
||||||
# include <cassert>
|
# include <cassert>
|
||||||
# include <optional>
|
|
||||||
# include <string>
|
# include <string>
|
||||||
# include <string_view>
|
# include <string_view>
|
||||||
# include <type_traits>
|
# include <type_traits>
|
||||||
# include <variant>
|
|
||||||
# include <vector>
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
class QJsonObject;
|
|
||||||
namespace chatterino {
|
|
||||||
struct CommandContext;
|
|
||||||
} // namespace chatterino
|
|
||||||
|
|
||||||
namespace chatterino::lua {
|
namespace chatterino::lua {
|
||||||
|
|
||||||
namespace api {
|
|
||||||
struct CompletionList;
|
|
||||||
struct CompletionEvent;
|
|
||||||
} // 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)
|
||||||
*
|
*
|
||||||
|
@ -40,6 +23,9 @@ constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
|
||||||
*/
|
*/
|
||||||
void stackDump(lua_State *L, const QString &tag);
|
void stackDump(lua_State *L, const QString &tag);
|
||||||
|
|
||||||
|
// This is for calling stackDump out of gdb as it's not easy to create a QString there
|
||||||
|
const QString GDB_DUMMY = "GDB_DUMMY";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Converts a lua error code and potentially string on top of the stack into a human readable message
|
* @brief Converts a lua error code and potentially string on top of the stack into a human readable message
|
||||||
*/
|
*/
|
||||||
|
@ -50,33 +36,11 @@ QString humanErrorText(lua_State *L, int errCode);
|
||||||
*/
|
*/
|
||||||
using StackIdx = int;
|
using StackIdx = int;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Creates a table with countArray array properties on the Lua stack
|
|
||||||
* @return stack index of the newly created table
|
|
||||||
*/
|
|
||||||
StackIdx pushEmptyArray(lua_State *L, int countArray);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Creates a table with countProperties named properties on the Lua stack
|
|
||||||
* @return stack index of the newly created table
|
|
||||||
*/
|
|
||||||
StackIdx pushEmptyTable(lua_State *L, int countProperties);
|
|
||||||
|
|
||||||
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 int &b);
|
|
||||||
StackIdx push(lua_State *L, const api::CompletionEvent &ev);
|
|
||||||
|
|
||||||
// returns OK?
|
// returns OK?
|
||||||
bool peek(lua_State *L, int *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, bool *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, double *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
||||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, std::string *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, api::CompletionList *out, StackIdx idx = -1);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Converts Lua object at stack index idx to a string.
|
* @brief Converts Lua object at stack index idx to a string.
|
||||||
|
@ -140,246 +104,29 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// TEMPLATES
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
StackIdx push(lua_State *L, std::optional<T> val)
|
|
||||||
{
|
|
||||||
if (val.has_value())
|
|
||||||
{
|
|
||||||
return lua::push(L, *val);
|
|
||||||
}
|
|
||||||
lua_pushnil(L);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
template <typename T,
|
|
||||||
typename std::enable_if<std::is_enum_v<T>, bool>::type = true>
|
|
||||||
bool peek(lua_State *L, T *out, StackIdx idx = -1)
|
|
||||||
{
|
|
||||||
std::string tmp;
|
|
||||||
if (!lua::peek(L, &tmp, idx))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::optional<T> opt = magic_enum::enum_cast<T>(tmp);
|
|
||||||
if (opt.has_value())
|
|
||||||
{
|
|
||||||
*out = opt.value();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a vector<T> to Lua and pushes it onto the stack.
|
|
||||||
*
|
|
||||||
* Needs StackIdx push(lua_State*, T); to work.
|
|
||||||
*
|
|
||||||
* @return Stack index of newly created table.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
StackIdx push(lua_State *L, std::vector<T> vec)
|
|
||||||
{
|
|
||||||
auto out = pushEmptyArray(L, vec.size());
|
|
||||||
int i = 1;
|
|
||||||
for (const auto &el : vec)
|
|
||||||
{
|
|
||||||
push(L, el);
|
|
||||||
lua_seti(L, out, i);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a QList<T> to Lua and pushes it onto the stack.
|
|
||||||
*
|
|
||||||
* Needs StackIdx push(lua_State*, T); to work.
|
|
||||||
*
|
|
||||||
* @return Stack index of newly created table.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
StackIdx push(lua_State *L, QList<T> vec)
|
|
||||||
{
|
|
||||||
auto out = pushEmptyArray(L, vec.size());
|
|
||||||
int i = 1;
|
|
||||||
for (const auto &el : vec)
|
|
||||||
{
|
|
||||||
push(L, el);
|
|
||||||
lua_seti(L, out, i);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts an enum given by T to Lua (into a string) and pushes it onto the stack.
|
|
||||||
*
|
|
||||||
* @return Stack index of newly created string.
|
|
||||||
*/
|
|
||||||
template <typename T, typename std::enable_if_t<std::is_enum_v<T>, bool> = true>
|
|
||||||
StackIdx push(lua_State *L, T inp)
|
|
||||||
{
|
|
||||||
std::string_view name = magic_enum::enum_name<T>(inp);
|
|
||||||
return lua::push(L, std::string(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a Lua object into c++ and removes it from the stack.
|
|
||||||
* If peek fails, the object is still removed from the stack.
|
|
||||||
*
|
|
||||||
* Relies on bool peek(lua_State*, T*, StackIdx) existing.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
|
||||||
{
|
|
||||||
StackGuard guard(L, -1);
|
|
||||||
auto ok = peek(L, out, idx);
|
|
||||||
if (idx < 0)
|
|
||||||
{
|
|
||||||
idx = lua_gettop(L) + idx + 1;
|
|
||||||
}
|
|
||||||
lua_remove(L, idx);
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates a table mapping enum names to unique values.
|
* @brief Creates a table mapping enum names to unique values.
|
||||||
*
|
*
|
||||||
* Values in this table may change.
|
* Values in this table may change.
|
||||||
*
|
*
|
||||||
* @returns stack index of newly created table
|
* @returns Sol reference to the table
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T>
|
||||||
StackIdx pushEnumTable(lua_State *L)
|
requires std::is_enum_v<T>
|
||||||
|
sol::table createEnumTable(sol::state_view &lua)
|
||||||
{
|
{
|
||||||
// std::array<T, _>
|
constexpr auto values = magic_enum::enum_values<T>();
|
||||||
auto values = magic_enum::enum_values<T>();
|
auto out = lua.create_table(0, values.size());
|
||||||
StackIdx out = lua::pushEmptyTable(L, values.size());
|
|
||||||
for (const T v : values)
|
for (const T v : values)
|
||||||
{
|
{
|
||||||
std::string_view name = magic_enum::enum_name<T>(v);
|
std::string_view name = magic_enum::enum_name<T>(v);
|
||||||
std::string str(name);
|
std::string str(name);
|
||||||
|
|
||||||
lua::push(L, str);
|
out.raw_set(str, v);
|
||||||
lua_setfield(L, out, str.c_str());
|
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|
|
@ -7,14 +7,13 @@
|
||||||
# include "controllers/plugins/PluginPermission.hpp"
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
# include "util/QMagicEnum.hpp"
|
# include "util/QMagicEnum.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
|
||||||
# include <magic_enum/magic_enum.hpp>
|
# include <magic_enum/magic_enum.hpp>
|
||||||
# include <QJsonArray>
|
# include <QJsonArray>
|
||||||
# include <QJsonObject>
|
# include <QJsonObject>
|
||||||
# include <QLoggingCategory>
|
# include <QLoggingCategory>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
# include <algorithm>
|
# include <algorithm>
|
||||||
# include <unordered_map>
|
# include <unordered_map>
|
||||||
|
@ -190,7 +189,8 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
bool Plugin::registerCommand(const QString &name,
|
||||||
|
sol::protected_function function)
|
||||||
{
|
{
|
||||||
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
||||||
{
|
{
|
||||||
|
@ -202,7 +202,7 @@ bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this->ownedCommands.insert({name, functionName});
|
this->ownedCommands.emplace(name, std::move(function));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,14 +223,24 @@ Plugin::~Plugin()
|
||||||
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
||||||
timer->deleteLater();
|
timer->deleteLater();
|
||||||
}
|
}
|
||||||
|
this->httpRequests.clear();
|
||||||
qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size()
|
qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size()
|
||||||
<< "timers for plugin" << this->id
|
<< "timers for plugin" << this->id
|
||||||
<< "while destroying the object";
|
<< "while destroying the object";
|
||||||
this->activeTimeouts.clear();
|
this->activeTimeouts.clear();
|
||||||
if (this->state_ != nullptr)
|
if (this->state_ != nullptr)
|
||||||
{
|
{
|
||||||
|
// clearing this after the state is gone is not safe to do
|
||||||
|
this->ownedCommands.clear();
|
||||||
|
this->callbacks.clear();
|
||||||
lua_close(this->state_);
|
lua_close(this->state_);
|
||||||
}
|
}
|
||||||
|
assert(this->ownedCommands.empty() &&
|
||||||
|
"This must be empty or destructor of sol::protected_function would "
|
||||||
|
"explode malloc structures later");
|
||||||
|
assert(this->callbacks.empty() &&
|
||||||
|
"This must be empty or destructor of sol::protected_function would "
|
||||||
|
"explode malloc structures later");
|
||||||
}
|
}
|
||||||
int Plugin::addTimeout(QTimer *timer)
|
int Plugin::addTimeout(QTimer *timer)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/network/NetworkCommon.hpp"
|
# include "controllers/plugins/api/EventType.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/api/HTTPRequest.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginPermission.hpp"
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@
|
||||||
# include <QString>
|
# include <QString>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
# include <semver/semver.hpp>
|
# include <semver/semver.hpp>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
|
# include <memory>
|
||||||
|
# include <optional>
|
||||||
# include <unordered_map>
|
# include <unordered_map>
|
||||||
# include <unordered_set>
|
# include <unordered_set>
|
||||||
# include <vector>
|
# include <vector>
|
||||||
|
@ -56,6 +59,8 @@ struct PluginMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit PluginMeta(const QJsonObject &obj);
|
explicit PluginMeta(const QJsonObject &obj);
|
||||||
|
// This is for tests
|
||||||
|
PluginMeta() = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Plugin
|
class Plugin
|
||||||
|
@ -75,13 +80,18 @@ public:
|
||||||
|
|
||||||
~Plugin();
|
~Plugin();
|
||||||
|
|
||||||
|
Plugin(const Plugin &) = delete;
|
||||||
|
Plugin(Plugin &&) = delete;
|
||||||
|
Plugin &operator=(const Plugin &) = delete;
|
||||||
|
Plugin &operator=(Plugin &&) = delete;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Perform all necessary tasks to bind a command name to this plugin
|
* @brief Perform all necessary tasks to bind a command name to this plugin
|
||||||
* @param name name of the command to create
|
* @param name name of the command to create
|
||||||
* @param functionName name of the function that should be called when the command is executed
|
* @param function the function that should be called when the command is executed
|
||||||
* @return true if addition succeeded, false otherwise (for example because the command name is already taken)
|
* @return true if addition succeeded, false otherwise (for example because the command name is already taken)
|
||||||
*/
|
*/
|
||||||
bool registerCommand(const QString &name, const QString &functionName);
|
bool registerCommand(const QString &name, sol::protected_function function);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get names of all commands belonging to this plugin
|
* @brief Get names of all commands belonging to this plugin
|
||||||
|
@ -98,35 +108,19 @@ public:
|
||||||
return this->loadDirectory_.absoluteFilePath("data");
|
return this->loadDirectory_.absoluteFilePath("data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
std::optional<sol::protected_function> getCompletionCallback()
|
||||||
using LuaCompletionCallback =
|
|
||||||
lua::CallbackFunction<lua::api::CompletionList,
|
|
||||||
lua::api::CompletionEvent>;
|
|
||||||
std::optional<LuaCompletionCallback> getCompletionCallback()
|
|
||||||
{
|
{
|
||||||
if (this->state_ == nullptr || !this->error_.isNull())
|
if (this->state_ == nullptr || !this->error_.isNull())
|
||||||
{
|
{
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
// this uses magic enum to help automatic tooling find usages
|
auto it =
|
||||||
auto typeName =
|
this->callbacks.find(lua::api::EventType::CompletionRequested);
|
||||||
magic_enum::enum_name(lua::api::EventType::CompletionRequested);
|
if (it == this->callbacks.end())
|
||||||
std::string cbName;
|
|
||||||
cbName.reserve(5 + typeName.size());
|
|
||||||
cbName += "c2cb-";
|
|
||||||
cbName += typeName;
|
|
||||||
auto typ =
|
|
||||||
lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str());
|
|
||||||
if (typ != LUA_TFUNCTION)
|
|
||||||
{
|
{
|
||||||
lua_pop(this->state_, 1);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
return it->second;
|
||||||
// move
|
|
||||||
return std::make_optional<lua::CallbackFunction<
|
|
||||||
lua::api::CompletionList, lua::api::CompletionEvent>>(
|
|
||||||
this->state_, lua_gettop(this->state_));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,18 +137,25 @@ public:
|
||||||
bool hasFSPermissionFor(bool write, const QString &path);
|
bool hasFSPermissionFor(bool write, const QString &path);
|
||||||
bool hasHTTPPermissionFor(const QUrl &url);
|
bool hasHTTPPermissionFor(const QUrl &url);
|
||||||
|
|
||||||
|
std::map<lua::api::EventType, sol::protected_function> callbacks;
|
||||||
|
|
||||||
|
// In-flight HTTP Requests
|
||||||
|
// This is a lifetime hack to ensure they get deleted with the plugin. This relies on the Plugin getting deleted on reload!
|
||||||
|
std::vector<std::shared_ptr<lua::api::HTTPRequest>> httpRequests;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir loadDirectory_;
|
QDir loadDirectory_;
|
||||||
lua_State *state_;
|
lua_State *state_;
|
||||||
|
|
||||||
QString error_;
|
QString error_;
|
||||||
|
|
||||||
// maps command name -> function name
|
// maps command name -> function
|
||||||
std::unordered_map<QString, QString> ownedCommands;
|
std::unordered_map<QString, sol::protected_function> ownedCommands;
|
||||||
std::vector<QTimer *> activeTimeouts;
|
std::vector<QTimer *> activeTimeouts;
|
||||||
int lastTimerId = 0;
|
int lastTimerId = 0;
|
||||||
|
|
||||||
friend class PluginController;
|
friend class PluginController;
|
||||||
|
friend class PluginControllerAccess; // this is for tests
|
||||||
};
|
};
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -13,16 +13,20 @@
|
||||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "messages/MessageBuilder.hpp"
|
# include "messages/MessageBuilder.hpp"
|
||||||
# include "singletons/Paths.hpp"
|
# include "singletons/Paths.hpp"
|
||||||
# include "singletons/Settings.hpp"
|
# include "singletons/Settings.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
# include <lualib.h>
|
# include <lualib.h>
|
||||||
}
|
|
||||||
# include <QJsonDocument>
|
# include <QJsonDocument>
|
||||||
|
# include <sol/overload.hpp>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
# include <sol/variadic_args.hpp>
|
||||||
|
# include <sol/variadic_results.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
@ -113,10 +117,11 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
void PluginController::openLibrariesFor(Plugin *plugin)
|
||||||
const QDir &pluginDir)
|
|
||||||
{
|
{
|
||||||
|
auto *L = plugin->state_;
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
|
sol::state_view lua(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},
|
||||||
|
@ -124,8 +129,6 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||||
|
|
||||||
luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||||
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
||||||
// luaL_Reg{LUA_IOLIBNAME, luaopen_io},
|
|
||||||
// - explicit fs access, needs wrapper with permissions, no usage ideas yet
|
|
||||||
// luaL_Reg{LUA_OSLIBNAME, luaopen_os},
|
// luaL_Reg{LUA_OSLIBNAME, luaopen_os},
|
||||||
// - fs access
|
// - fs access
|
||||||
// - environ access
|
// - environ access
|
||||||
|
@ -147,155 +150,100 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||||
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
|
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
auto r = lua.registry();
|
||||||
static const luaL_Reg c2Lib[] = {
|
auto g = lua.globals();
|
||||||
{"register_command", lua::api::c2_register_command},
|
auto c2 = lua.create_table();
|
||||||
{"register_callback", lua::api::c2_register_callback},
|
g["c2"] = c2;
|
||||||
{"log", lua::api::c2_log},
|
|
||||||
{"later", lua::api::c2_later},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
lua_pushglobaltable(L);
|
|
||||||
auto gtable = lua_gettop(L);
|
|
||||||
|
|
||||||
// count of elements in C2LIB + LogLevel + EventType
|
|
||||||
auto c2libIdx = lua::pushEmptyTable(L, 8);
|
|
||||||
|
|
||||||
luaL_setfuncs(L, c2Lib, 0);
|
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "LogLevel");
|
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::EventType>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "EventType");
|
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::LPlatform>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "Platform");
|
|
||||||
|
|
||||||
lua::pushEnumTable<Channel::Type>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "ChannelType");
|
|
||||||
|
|
||||||
lua::pushEnumTable<NetworkRequestType>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "HTTPMethod");
|
|
||||||
|
|
||||||
// Initialize metatables for objects
|
|
||||||
lua::api::ChannelRef::createMetatable(L);
|
|
||||||
lua_setfield(L, c2libIdx, "Channel");
|
|
||||||
|
|
||||||
lua::api::HTTPRequest::createMetatable(L);
|
|
||||||
lua_setfield(L, c2libIdx, "HTTPRequest");
|
|
||||||
|
|
||||||
lua::api::HTTPResponse::createMetatable(L);
|
|
||||||
lua_setfield(L, c2libIdx, "HTTPResponse");
|
|
||||||
|
|
||||||
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?
|
||||||
|
|
||||||
// possibly randomize this name at runtime to prevent some attacks?
|
|
||||||
|
|
||||||
# ifndef NDEBUG
|
# ifndef NDEBUG
|
||||||
lua_getfield(L, gtable, "load");
|
lua.registry()["real_load"] = lua.globals()["load"];
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
|
||||||
# endif
|
# endif
|
||||||
|
// See chatterino::lua::api::g_load implementation
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
g["loadfile"] = sol::nil;
|
||||||
static const luaL_Reg replacementFuncs[] = {
|
g["dofile"] = sol::nil;
|
||||||
{"load", lua::api::g_load},
|
|
||||||
{"print", lua::api::g_print},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
luaL_setfuncs(L, replacementFuncs, 0);
|
|
||||||
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_setfield(L, gtable, "loadfile");
|
|
||||||
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_setfield(L, gtable, "dofile");
|
|
||||||
|
|
||||||
// set up package lib
|
// 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 package = g["package"];
|
||||||
auto table = lua_gettop(L);
|
package["cpath"] = "";
|
||||||
lua_getfield(L, -1, "remove");
|
package["path"] = "";
|
||||||
lua_remove(L, table);
|
|
||||||
}
|
|
||||||
auto remove = lua_gettop(L);
|
|
||||||
|
|
||||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
sol::protected_function tbremove = g["table"]["remove"];
|
||||||
for (int i = 0; i < 3; i++)
|
|
||||||
|
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||||
|
sol::table searchers = package["searchers"];
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
tbremove(searchers);
|
||||||
|
}
|
||||||
|
searchers.add(&lua::api::searcherRelative);
|
||||||
|
searchers.add(&lua::api::searcherAbsolute);
|
||||||
|
}
|
||||||
|
// set up io lib
|
||||||
{
|
{
|
||||||
lua_pushvalue(L, remove);
|
auto c2io = lua.create_table();
|
||||||
lua_getfield(L, package, "searchers");
|
auto realio = r[lua::api::REG_REAL_IO_NAME];
|
||||||
lua_pcall(L, 1, 0, 0);
|
c2io["type"] = realio["type"];
|
||||||
|
g["io"] = c2io;
|
||||||
|
// prevent plugins getting direct access to realio
|
||||||
|
r[LUA_LOADED_TABLE]["io"] = c2io;
|
||||||
|
|
||||||
|
// Don't give plugins the option to shit into our stdio
|
||||||
|
r["_IO_input"] = sol::nil;
|
||||||
|
r["_IO_output"] = sol::nil;
|
||||||
}
|
}
|
||||||
lua_pop(L, 1); // get rid of remove
|
PluginController::initSol(lua, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
lua_getfield(L, package, "searchers");
|
// TODO: investigate if `plugin` can ever point to an invalid plugin,
|
||||||
lua_pushcclosure(L, lua::api::searcherRelative, 0);
|
// especially in cases when the plugin is errored.
|
||||||
lua_seti(L, -2, 2);
|
void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
|
||||||
|
{
|
||||||
|
auto g = lua.globals();
|
||||||
|
// Do not capture plugin->state_ in lambdas, this makes the functions unusable in callbacks
|
||||||
|
g.set_function("print", &lua::api::g_print);
|
||||||
|
g.set_function("load", &lua::api::g_load);
|
||||||
|
|
||||||
lua::push(L, QString(pluginDir.absolutePath()));
|
sol::table c2 = g["c2"];
|
||||||
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
c2.set_function("register_command",
|
||||||
lua_seti(L, -2, 3);
|
[plugin](const QString &name, sol::protected_function cb) {
|
||||||
lua_pop(L, 2); // remove package, package.searchers
|
return plugin->registerCommand(name, std::move(cb));
|
||||||
|
});
|
||||||
|
c2.set_function("register_callback", &lua::api::c2_register_callback);
|
||||||
|
c2.set_function("log", &lua::api::c2_log);
|
||||||
|
c2.set_function("later", &lua::api::c2_later);
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
lua::api::ChannelRef::createUserType(c2);
|
||||||
static const luaL_Reg ioLib[] = {
|
lua::api::HTTPResponse::createUserType(c2);
|
||||||
{"close", lua::api::io_close},
|
lua::api::HTTPRequest::createUserType(c2);
|
||||||
{"flush", lua::api::io_flush},
|
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
|
||||||
{"input", lua::api::io_input},
|
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
|
||||||
{"lines", lua::api::io_lines},
|
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
|
||||||
{"open", lua::api::io_open},
|
c2["LogLevel"] = lua::createEnumTable<lua::api::LogLevel>(lua);
|
||||||
{"output", lua::api::io_output},
|
|
||||||
{"popen", lua::api::io_popen}, // stub
|
|
||||||
{"read", lua::api::io_read},
|
|
||||||
{"tmpfile", lua::api::io_tmpfile}, // stub
|
|
||||||
{"write", lua::api::io_write},
|
|
||||||
// type = realio.type
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
// TODO: io.popen stub
|
|
||||||
auto iolibIdx = lua::pushEmptyTable(L, 1);
|
|
||||||
luaL_setfuncs(L, ioLib, 0);
|
|
||||||
|
|
||||||
// set ourio.type = realio.type
|
sol::table io = g["io"];
|
||||||
lua_pushvalue(L, iolibIdx);
|
io.set_function(
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
"open", sol::overload(&lua::api::io_open, &lua::api::io_open_modeless));
|
||||||
lua_getfield(L, -1, "type");
|
io.set_function("lines", sol::overload(&lua::api::io_lines,
|
||||||
lua_remove(L, -2); // remove realio
|
&lua::api::io_lines_noargs));
|
||||||
lua_setfield(L, iolibIdx, "type");
|
io.set_function("input", sol::overload(&lua::api::io_input_argless,
|
||||||
lua_pop(L, 1); // still have iolib on top of stack
|
&lua::api::io_input_name,
|
||||||
|
&lua::api::io_input_file));
|
||||||
lua_pushvalue(L, iolibIdx);
|
io.set_function("output", sol::overload(&lua::api::io_output_argless,
|
||||||
lua_setfield(L, gtable, "io");
|
&lua::api::io_output_name,
|
||||||
|
&lua::api::io_output_file));
|
||||||
lua_pushvalue(L, iolibIdx);
|
io.set_function("close", sol::overload(&lua::api::io_close_argless,
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME);
|
&lua::api::io_close_file));
|
||||||
|
io.set_function("flush", sol::overload(&lua::api::io_flush_argless,
|
||||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
|
&lua::api::io_flush_file));
|
||||||
lua_pushvalue(L, iolibIdx);
|
io.set_function("read", &lua::api::io_read);
|
||||||
lua_setfield(L, -2, "io");
|
io.set_function("write", &lua::api::io_write);
|
||||||
|
io.set_function("popen", &lua::api::io_popen);
|
||||||
lua_pop(L, 3); // remove gtable, iolib, LOADED
|
io.set_function("tmpfile", &lua::api::io_tmpfile);
|
||||||
|
|
||||||
// Don't give plugins the option to shit into our stdio
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
|
||||||
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
|
@ -314,7 +262,7 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
<< " because safe mode is enabled.";
|
<< " because safe mode is enabled.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PluginController::openLibrariesFor(l, meta, pluginDir);
|
PluginController::openLibrariesFor(temp);
|
||||||
|
|
||||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||||
!getSettings()->pluginsEnabled)
|
!getSettings()->pluginsEnabled)
|
||||||
|
@ -345,17 +293,13 @@ bool PluginController::reload(const QString &id)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (it->second->state_ != nullptr)
|
|
||||||
{
|
|
||||||
lua_close(it->second->state_);
|
|
||||||
it->second->state_ = nullptr;
|
|
||||||
}
|
|
||||||
for (const auto &[cmd, _] : it->second->ownedCommands)
|
for (const auto &[cmd, _] : it->second->ownedCommands)
|
||||||
{
|
{
|
||||||
getApp()->getCommands()->unregisterPluginCommand(cmd);
|
getApp()->getCommands()->unregisterPluginCommand(cmd);
|
||||||
}
|
}
|
||||||
it->second->ownedCommands.clear();
|
|
||||||
QDir loadDir = it->second->loadDirectory_;
|
QDir loadDir = it->second->loadDirectory_;
|
||||||
|
// Since Plugin owns the state, it will clean up everything related to it
|
||||||
this->plugins_.erase(id);
|
this->plugins_.erase(id);
|
||||||
this->tryLoadFromDir(loadDir);
|
this->tryLoadFromDir(loadDir);
|
||||||
return true;
|
return true;
|
||||||
|
@ -369,27 +313,36 @@ QString PluginController::tryExecPluginCommand(const QString &commandName,
|
||||||
if (auto it = plugin->ownedCommands.find(commandName);
|
if (auto it = plugin->ownedCommands.find(commandName);
|
||||||
it != plugin->ownedCommands.end())
|
it != plugin->ownedCommands.end())
|
||||||
{
|
{
|
||||||
const auto &funcName = it->second;
|
sol::state_view lua(plugin->state_);
|
||||||
|
sol::table args = lua.create_table_with(
|
||||||
|
"words", ctx.words, //
|
||||||
|
"channel", lua::api::ChannelRef(ctx.channel) //
|
||||||
|
);
|
||||||
|
|
||||||
auto *L = plugin->state_;
|
auto result =
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str());
|
lua::tryCall<std::optional<QString>>(it->second, args);
|
||||||
lua::push(L, ctx);
|
if (!result)
|
||||||
|
|
||||||
auto res = lua_pcall(L, 1, 0, 0);
|
|
||||||
if (res != LUA_OK)
|
|
||||||
{
|
{
|
||||||
ctx.channel->addSystemMessage("Lua error: " +
|
ctx.channel->addSystemMessage(
|
||||||
lua::humanErrorText(L, res));
|
QStringView(
|
||||||
return "";
|
u"Failed to evaluate command from plugin %1: %2")
|
||||||
|
.arg(plugin->meta.name, result.error()));
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
|
auto opt = result.value();
|
||||||
|
if (!opt)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return *opt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
qCCritical(chatterinoLua)
|
qCCritical(chatterinoLua)
|
||||||
<< "Something's seriously up, no plugin owns command" << commandName
|
<< "Something's seriously up, no plugin owns command" << commandName
|
||||||
<< "yet a call to execute it came in";
|
<< "yet a call to execute it came in";
|
||||||
assert(false && "missing plugin command owner");
|
assert(false && "missing plugin command owner");
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PluginController::isPluginEnabled(const QString &id)
|
bool PluginController::isPluginEnabled(const QString &id)
|
||||||
|
@ -435,32 +388,31 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
lua::StackGuard guard(pl->state_);
|
|
||||||
|
|
||||||
auto opt = pl->getCompletionCallback();
|
auto opt = pl->getCompletionCallback();
|
||||||
if (opt)
|
if (opt)
|
||||||
{
|
{
|
||||||
qCDebug(chatterinoLua)
|
qCDebug(chatterinoLua)
|
||||||
<< "Processing custom completions from plugin" << name;
|
<< "Processing custom completions from plugin" << name;
|
||||||
auto &cb = *opt;
|
auto &cb = *opt;
|
||||||
auto errOrList = cb(lua::api::CompletionEvent{
|
sol::state_view view(pl->state_);
|
||||||
.query = query,
|
auto errOrList = lua::tryCall<sol::table>(
|
||||||
.full_text_content = fullTextContent,
|
cb,
|
||||||
.cursor_position = cursorPosition,
|
toTable(pl->state_, lua::api::CompletionEvent{
|
||||||
.is_first_word = isFirstWord,
|
.query = query,
|
||||||
});
|
.full_text_content = fullTextContent,
|
||||||
if (std::holds_alternative<int>(errOrList))
|
.cursor_position = cursorPosition,
|
||||||
|
.is_first_word = isFirstWord,
|
||||||
|
}));
|
||||||
|
if (!errOrList.has_value())
|
||||||
{
|
{
|
||||||
guard.handled();
|
|
||||||
int err = std::get<int>(errOrList);
|
|
||||||
qCDebug(chatterinoLua)
|
qCDebug(chatterinoLua)
|
||||||
<< "Got error from plugin " << pl->meta.name
|
<< "Got error from plugin " << pl->meta.name
|
||||||
<< " while refreshing tab completion: "
|
<< " while refreshing tab completion: "
|
||||||
<< lua::humanErrorText(pl->state_, err);
|
<< errOrList.get_unexpected().error();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto list = std::get<lua::api::CompletionList>(errOrList);
|
auto list = lua::api::CompletionList(*errOrList);
|
||||||
if (list.hideOthers)
|
if (list.hideOthers)
|
||||||
{
|
{
|
||||||
results = QStringList(list.values.begin(), list.values.end());
|
results = QStringList(list.values.begin(), list.values.end());
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# include <QJsonArray>
|
# include <QJsonArray>
|
||||||
# include <QJsonObject>
|
# include <QJsonObject>
|
||||||
# include <QString>
|
# include <QString>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
# include <algorithm>
|
# include <algorithm>
|
||||||
# include <map>
|
# include <map>
|
||||||
|
@ -66,11 +67,16 @@ private:
|
||||||
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(Plugin *plugin);
|
||||||
const QDir &pluginDir);
|
|
||||||
|
static void initSol(sol::state_view &lua, Plugin *plugin);
|
||||||
|
|
||||||
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_;
|
||||||
|
|
||||||
|
// This is for tests, pay no attention
|
||||||
|
friend class PluginControllerAccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -10,6 +10,8 @@ namespace chatterino {
|
||||||
|
|
||||||
struct PluginPermission {
|
struct PluginPermission {
|
||||||
explicit PluginPermission(const QJsonObject &obj);
|
explicit PluginPermission(const QJsonObject &obj);
|
||||||
|
// This is for tests
|
||||||
|
PluginPermission() = default;
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
FilesystemRead,
|
FilesystemRead,
|
||||||
|
|
131
src/controllers/plugins/SolTypes.cpp
Normal file
131
src/controllers/plugins/SolTypes.cpp
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
|
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
|
# include <QObject>
|
||||||
|
# include <sol/thread.hpp>
|
||||||
|
namespace chatterino::lua {
|
||||||
|
|
||||||
|
Plugin *ThisPluginState::plugin()
|
||||||
|
{
|
||||||
|
if (this->plugptr_ != nullptr)
|
||||||
|
{
|
||||||
|
return this->plugptr_;
|
||||||
|
}
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(this->state_);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: missing plugin");
|
||||||
|
}
|
||||||
|
this->plugptr_ = pl;
|
||||||
|
return pl;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
// NOLINTBEGIN(readability-named-parameter)
|
||||||
|
// QString
|
||||||
|
bool sol_lua_check(sol::types<QString>, lua_State *L, int index,
|
||||||
|
std::function<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<const char *>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString sol_lua_get(sol::types<QString>, lua_State *L, int index,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
auto str = sol::stack::get<std::string_view>(L, index, tracking);
|
||||||
|
return QString::fromUtf8(str.data(), static_cast<qsizetype>(str.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<QString>, lua_State *L, const QString &value)
|
||||||
|
{
|
||||||
|
return sol::stack::push(L, value.toUtf8().data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// QStringList
|
||||||
|
bool sol_lua_check(sol::types<QStringList>, lua_State *L, int index,
|
||||||
|
std::function<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<sol::table>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList sol_lua_get(sol::types<QStringList>, lua_State *L, int index,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
sol::table table = sol::stack::get<sol::table>(L, index, tracking);
|
||||||
|
QStringList result;
|
||||||
|
result.reserve(static_cast<qsizetype>(table.size()));
|
||||||
|
for (size_t i = 1; i < table.size() + 1; i++)
|
||||||
|
{
|
||||||
|
result.append(table.get<QString>(i));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<QStringList>, lua_State *L,
|
||||||
|
const QStringList &value)
|
||||||
|
{
|
||||||
|
sol::table table = sol::table::create(L, static_cast<int>(value.size()));
|
||||||
|
for (const QString &str : value)
|
||||||
|
{
|
||||||
|
table.add(str);
|
||||||
|
}
|
||||||
|
return sol::stack::push(L, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
// QByteArray
|
||||||
|
bool sol_lua_check(sol::types<QByteArray>, lua_State *L, int index,
|
||||||
|
std::function<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<const char *>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray sol_lua_get(sol::types<QByteArray>, lua_State *L, int index,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
auto str = sol::stack::get<std::string_view>(L, index, tracking);
|
||||||
|
return QByteArray::fromRawData(str.data(), str.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<QByteArray>, lua_State *L, const QByteArray &value)
|
||||||
|
{
|
||||||
|
return sol::stack::push(L,
|
||||||
|
std::string_view(value.constData(), value.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace chatterino::lua {
|
||||||
|
|
||||||
|
// ThisPluginState
|
||||||
|
|
||||||
|
bool sol_lua_check(sol::types<chatterino::lua::ThisPluginState>,
|
||||||
|
lua_State * /*L*/, int /* index*/,
|
||||||
|
std::function<sol::check_handler_type> /* handler*/,
|
||||||
|
sol::stack::record & /*tracking*/)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatterino::lua::ThisPluginState sol_lua_get(
|
||||||
|
sol::types<chatterino::lua::ThisPluginState>, lua_State *L, int /*index*/,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
tracking.use(0);
|
||||||
|
return {L};
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<chatterino::lua::ThisPluginState>, lua_State *L,
|
||||||
|
const chatterino::lua::ThisPluginState &value)
|
||||||
|
{
|
||||||
|
return sol::stack::push(L, sol::thread(L, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
// NOLINTEND(readability-named-parameter)
|
||||||
|
|
||||||
|
#endif
|
170
src/controllers/plugins/SolTypes.hpp
Normal file
170
src/controllers/plugins/SolTypes.hpp
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
#pragma once
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "util/QMagicEnum.hpp"
|
||||||
|
# include "util/TypeName.hpp"
|
||||||
|
|
||||||
|
# include <nonstd/expected.hpp>
|
||||||
|
# include <QObject>
|
||||||
|
# include <QString>
|
||||||
|
# include <QStringBuilder>
|
||||||
|
# include <QStringList>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
|
namespace chatterino::detail {
|
||||||
|
|
||||||
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
template <typename T>
|
||||||
|
constexpr bool IsOptional = false;
|
||||||
|
template <typename T>
|
||||||
|
constexpr bool IsOptional<std::optional<T>> = true;
|
||||||
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
|
} // namespace chatterino::detail
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Plugin;
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
namespace chatterino::lua {
|
||||||
|
|
||||||
|
class ThisPluginState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ThisPluginState(lua_State *Ls)
|
||||||
|
: plugptr_(nullptr)
|
||||||
|
, state_(Ls)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
operator lua_State *() const noexcept
|
||||||
|
{
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_State *operator->() const noexcept
|
||||||
|
{
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
lua_State *state() const noexcept
|
||||||
|
{
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin *plugin();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Plugin *plugptr_;
|
||||||
|
lua_State *state_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Attempts to call @a function with @a args
|
||||||
|
///
|
||||||
|
/// @a T is expected to be returned.
|
||||||
|
/// If `void` is specified, the returned values
|
||||||
|
/// are ignored.
|
||||||
|
/// `std::optional<T>` means nil|LuaEquiv<T> (or zero returns)
|
||||||
|
/// A return type that doesn't match returns an error
|
||||||
|
template <typename T, typename... Args>
|
||||||
|
inline nonstd::expected_lite::expected<T, QString> tryCall(
|
||||||
|
const sol::protected_function &function, Args &&...args)
|
||||||
|
{
|
||||||
|
sol::protected_function_result result =
|
||||||
|
function(std::forward<Args>(args)...);
|
||||||
|
if (!result.valid())
|
||||||
|
{
|
||||||
|
sol::error err = result;
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
QString::fromUtf8(err.what()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, void>)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if constexpr (detail::IsOptional<T>)
|
||||||
|
{
|
||||||
|
if (result.return_count() == 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.return_count() > 1)
|
||||||
|
{
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
u"Expected one value to be returned but " %
|
||||||
|
QString::number(result.return_count()) %
|
||||||
|
u" values were returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if constexpr (detail::IsOptional<T>)
|
||||||
|
{
|
||||||
|
// we want to error on anything that is not nil|T,
|
||||||
|
// std::optional<T> in sol means "give me a T or if it does not match nullopt"
|
||||||
|
if (result.get_type() == sol::type::nil)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto ret = result.get<T>();
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
{
|
||||||
|
auto t = type_name<T>();
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
u"Expected " % QLatin1String(t.data(), t.size()) %
|
||||||
|
u" to be returned but " %
|
||||||
|
qmagicenum::enumName(result.get_type()) %
|
||||||
|
u" was returned");
|
||||||
|
}
|
||||||
|
return *ret;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto ret = result.get<std::optional<T>>();
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
{
|
||||||
|
auto t = type_name<T>();
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
u"Expected " % QLatin1String(t.data(), t.size()) %
|
||||||
|
u" to be returned but " %
|
||||||
|
qmagicenum::enumName(result.get_type()) %
|
||||||
|
u" was returned");
|
||||||
|
}
|
||||||
|
return *ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::runtime_error &e)
|
||||||
|
{
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
QString::fromUtf8(e.what()));
|
||||||
|
}
|
||||||
|
// non other exceptions we let it explode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||||
|
# define SOL_STACK_FUNCTIONS(TYPE) \
|
||||||
|
bool sol_lua_check(sol::types<TYPE>, lua_State *L, int index, \
|
||||||
|
std::function<sol::check_handler_type> handler, \
|
||||||
|
sol::stack::record &tracking); \
|
||||||
|
TYPE sol_lua_get(sol::types<TYPE>, lua_State *L, int index, \
|
||||||
|
sol::stack::record &tracking); \
|
||||||
|
int sol_lua_push(sol::types<TYPE>, lua_State *L, const TYPE &value);
|
||||||
|
|
||||||
|
SOL_STACK_FUNCTIONS(chatterino::lua::ThisPluginState)
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
SOL_STACK_FUNCTIONS(QString)
|
||||||
|
SOL_STACK_FUNCTIONS(QStringList)
|
||||||
|
SOL_STACK_FUNCTIONS(QByteArray)
|
||||||
|
|
||||||
|
# undef SOL_STACK_FUNCTIONS
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,397 +1,224 @@
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
|
||||||
|
# include "Application.hpp"
|
||||||
# include "common/Channel.hpp"
|
# include "common/Channel.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
# include "controllers/commands/CommandController.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
# include "messages/MessageBuilder.hpp"
|
|
||||||
# include "providers/twitch/TwitchChannel.hpp"
|
# include "providers/twitch/TwitchChannel.hpp"
|
||||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
|
||||||
extern "C" {
|
# include <sol/sol.hpp>
|
||||||
# include <lauxlib.h>
|
|
||||||
# include <lua.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
# include <cassert>
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <optional>
|
# include <optional>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(*vararg)
|
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
ChannelRef::ChannelRef(const std::shared_ptr<Channel> &chan)
|
||||||
static const luaL_Reg CHANNEL_REF_METHODS[] = {
|
: weak(chan)
|
||||||
{"is_valid", &ChannelRef::is_valid},
|
|
||||||
{"get_name", &ChannelRef::get_name},
|
|
||||||
{"get_type", &ChannelRef::get_type},
|
|
||||||
{"get_display_name", &ChannelRef::get_display_name},
|
|
||||||
{"send_message", &ChannelRef::send_message},
|
|
||||||
{"add_system_message", &ChannelRef::add_system_message},
|
|
||||||
{"is_twitch_channel", &ChannelRef::is_twitch_channel},
|
|
||||||
|
|
||||||
// Twitch
|
|
||||||
{"get_room_modes", &ChannelRef::get_room_modes},
|
|
||||||
{"get_stream_status", &ChannelRef::get_stream_status},
|
|
||||||
{"get_twitch_id", &ChannelRef::get_twitch_id},
|
|
||||||
{"is_broadcaster", &ChannelRef::is_broadcaster},
|
|
||||||
{"is_mod", &ChannelRef::is_mod},
|
|
||||||
{"is_vip", &ChannelRef::is_vip},
|
|
||||||
|
|
||||||
// misc
|
|
||||||
{"__tostring", &ChannelRef::to_string},
|
|
||||||
|
|
||||||
// static
|
|
||||||
{"by_name", &ChannelRef::get_by_name},
|
|
||||||
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
void ChannelRef::createMetatable(lua_State *L)
|
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 1);
|
|
||||||
|
|
||||||
luaL_newmetatable(L, "c2.Channel");
|
|
||||||
lua_pushstring(L, "__index");
|
|
||||||
lua_pushvalue(L, -2); // clone metatable
|
|
||||||
lua_settable(L, -3); // metatable.__index = metatable
|
|
||||||
|
|
||||||
// Generic IWeakResource stuff
|
|
||||||
lua_pushstring(L, "__gc");
|
|
||||||
lua_pushcfunction(
|
|
||||||
L, (&WeakPtrUserData<UserData::Type::Channel, ChannelRef>::destroy));
|
|
||||||
lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy
|
|
||||||
|
|
||||||
luaL_setfuncs(L, CHANNEL_REF_METHODS, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
std::shared_ptr<Channel> ChannelRef::strong()
|
||||||
{
|
{
|
||||||
if (lua_gettop(L) < 1)
|
auto c = this->weak.lock();
|
||||||
|
if (!c)
|
||||||
{
|
{
|
||||||
luaL_error(L, "Called c2.Channel method without a channel object");
|
throw std::runtime_error(
|
||||||
return nullptr;
|
"Expired c2.Channel used - use c2.Channel:is_valid() to "
|
||||||
|
"check validity");
|
||||||
}
|
}
|
||||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<TwitchChannel> ChannelRef::twitch()
|
||||||
|
{
|
||||||
|
auto c = std::dynamic_pointer_cast<TwitchChannel>(this->weak.lock());
|
||||||
|
if (!c)
|
||||||
{
|
{
|
||||||
luaL_error(
|
throw std::runtime_error(
|
||||||
L, "Called c2.Channel method with a non-userdata 'self' argument");
|
"Expired or non-twitch c2.Channel used - use "
|
||||||
return nullptr;
|
"c2.Channel:is_valid() and c2.Channe:is_twitch_channel()");
|
||||||
}
|
}
|
||||||
// luaL_checkudata is no-return if check fails
|
return c;
|
||||||
auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel");
|
}
|
||||||
auto *data =
|
|
||||||
WeakPtrUserData<UserData::Type::Channel, Channel>::from(checked);
|
bool ChannelRef::is_valid()
|
||||||
if (data == nullptr)
|
{
|
||||||
{
|
return !this->weak.expired();
|
||||||
luaL_error(L,
|
}
|
||||||
"Called c2.Channel method with an invalid channel pointer");
|
|
||||||
return nullptr;
|
QString ChannelRef::get_name()
|
||||||
}
|
{
|
||||||
lua_pop(L, 1);
|
return this->strong()->getName();
|
||||||
if (data->target.expired())
|
}
|
||||||
{
|
|
||||||
if (!expiredOk)
|
Channel::Type ChannelRef::get_type()
|
||||||
|
{
|
||||||
|
return this->strong()->getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChannelRef::get_display_name()
|
||||||
|
{
|
||||||
|
return this->strong()->getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChannelRef::send_message(QString text, sol::variadic_args va)
|
||||||
|
{
|
||||||
|
bool execCommands = [&] {
|
||||||
|
if (va.size() >= 1)
|
||||||
{
|
{
|
||||||
luaL_error(L,
|
return va.get<bool>();
|
||||||
"Usage of expired c2.Channel object. Underlying "
|
|
||||||
"resource was freed. Use Channel:is_valid() to check");
|
|
||||||
}
|
}
|
||||||
return nullptr;
|
return false;
|
||||||
}
|
}();
|
||||||
return data->target.lock();
|
text = text.replace('\n', ' ');
|
||||||
}
|
auto chan = this->strong();
|
||||||
|
if (execCommands)
|
||||||
std::shared_ptr<TwitchChannel> ChannelRef::getTwitchOrError(lua_State *L)
|
|
||||||
{
|
|
||||||
auto ref = ChannelRef::getOrError(L);
|
|
||||||
auto ptr = dynamic_pointer_cast<TwitchChannel>(ref);
|
|
||||||
if (ptr == nullptr)
|
|
||||||
{
|
{
|
||||||
luaL_error(L,
|
text = getApp()->getCommands()->execCommand(text, chan, false);
|
||||||
"c2.Channel Twitch-only operation on non-Twitch channel.");
|
|
||||||
}
|
}
|
||||||
return ptr;
|
chan->sendMessage(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::is_valid(lua_State *L)
|
void ChannelRef::add_system_message(QString text)
|
||||||
{
|
{
|
||||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
text = text.replace('\n', ' ');
|
||||||
lua::push(L, that != nullptr);
|
this->strong()->addSystemMessage(text);
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_name(lua_State *L)
|
bool ChannelRef::is_twitch_channel()
|
||||||
{
|
{
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
return this->strong()->isTwitchChannel();
|
||||||
lua::push(L, that->getName());
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_type(lua_State *L)
|
sol::table ChannelRef::get_room_modes(sol::this_state state)
|
||||||
{
|
{
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
return toTable(state.L, *this->twitch()->accessRoomModes());
|
||||||
lua::push(L, that->getType());
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_display_name(lua_State *L)
|
sol::table ChannelRef::get_stream_status(sol::this_state state)
|
||||||
{
|
{
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
return toTable(state.L, *this->twitch()->accessStreamStatus());
|
||||||
lua::push(L, that->getDisplayName());
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::send_message(lua_State *L)
|
QString ChannelRef::get_twitch_id()
|
||||||
{
|
{
|
||||||
if (lua_gettop(L) != 2 && lua_gettop(L) != 3)
|
return this->twitch()->roomId();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_broadcaster()
|
||||||
|
{
|
||||||
|
return this->twitch()->isBroadcaster();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_mod()
|
||||||
|
{
|
||||||
|
return this->twitch()->isMod();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_vip()
|
||||||
|
{
|
||||||
|
return this->twitch()->isVip();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChannelRef::to_string()
|
||||||
|
{
|
||||||
|
auto chan = this->weak.lock();
|
||||||
|
if (!chan)
|
||||||
{
|
{
|
||||||
luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message "
|
return "<c2.Channel expired>";
|
||||||
"text and optionally execute_commands flag)");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
bool execcmds = false;
|
return QStringView(u"<c2.Channel %1>").arg(chan->getName());
|
||||||
if (lua_gettop(L) == 3)
|
}
|
||||||
|
|
||||||
|
std::optional<ChannelRef> ChannelRef::get_by_name(const QString &name)
|
||||||
|
{
|
||||||
|
auto chan = getApp()->getTwitch()->getChannelOrEmpty(name);
|
||||||
|
if (chan->isEmpty())
|
||||||
{
|
{
|
||||||
if (!lua::pop(L, &execcmds))
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return chan;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ChannelRef> ChannelRef::get_by_twitch_id(const QString &id)
|
||||||
|
{
|
||||||
|
auto chan = getApp()->getTwitch()->getChannelOrEmptyByID(id);
|
||||||
|
if (chan->isEmpty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return chan;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChannelRef::createUserType(sol::table &c2)
|
||||||
|
{
|
||||||
|
// clang-format off
|
||||||
|
c2.new_usertype<ChannelRef>(
|
||||||
|
"Channel", sol::no_constructor,
|
||||||
|
// meta methods
|
||||||
|
sol::meta_method::to_string, &ChannelRef::to_string,
|
||||||
|
|
||||||
|
// Channel
|
||||||
|
"is_valid", &ChannelRef::is_valid,
|
||||||
|
"get_name",&ChannelRef::get_name,
|
||||||
|
"get_type", &ChannelRef::get_type,
|
||||||
|
"get_display_name", &ChannelRef::get_display_name,
|
||||||
|
"send_message", &ChannelRef::send_message,
|
||||||
|
"add_system_message", &ChannelRef::add_system_message,
|
||||||
|
"is_twitch_channel", &ChannelRef::is_twitch_channel,
|
||||||
|
|
||||||
|
// TwitchChannel
|
||||||
|
"get_room_modes", &ChannelRef::get_room_modes,
|
||||||
|
"get_stream_status", &ChannelRef::get_stream_status,
|
||||||
|
"get_twitch_id", &ChannelRef::get_twitch_id,
|
||||||
|
"is_broadcaster", &ChannelRef::is_broadcaster,
|
||||||
|
"is_mod", &ChannelRef::is_mod,
|
||||||
|
"is_vip", &ChannelRef::is_vip,
|
||||||
|
|
||||||
|
// static
|
||||||
|
"by_name", &ChannelRef::get_by_name,
|
||||||
|
"by_twitch_id", &ChannelRef::get_by_twitch_id
|
||||||
|
);
|
||||||
|
// clang-format on
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes)
|
||||||
|
{
|
||||||
|
auto maybe = [](int value) {
|
||||||
|
if (value >= 0)
|
||||||
{
|
{
|
||||||
luaL_error(L, "cannot get execute_commands (2nd argument of "
|
return std::optional{value};
|
||||||
"Channel:send_message)");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
return std::optional<int>{};
|
||||||
|
|
||||||
QString text;
|
|
||||||
if (!lua::pop(L, &text))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get text (1st argument of Channel:send_message)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
|
|
||||||
text = text.replace('\n', ' ');
|
|
||||||
if (execcmds)
|
|
||||||
{
|
|
||||||
text = getApp()->getCommands()->execCommand(text, that, false);
|
|
||||||
}
|
|
||||||
that->sendMessage(text);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::add_system_message(lua_State *L)
|
|
||||||
{
|
|
||||||
// needs to account for the hidden self argument
|
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Channel:add_system_message needs exactly 1 argument (message "
|
|
||||||
"text)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString text;
|
|
||||||
if (!lua::pop(L, &text))
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "cannot get text (1st argument of Channel:add_system_message)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
text = text.replace('\n', ' ');
|
|
||||||
that->addSystemMessage(text);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_twitch_channel(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
lua::push(L, that->isTwitchChannel());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_room_modes(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
const auto m = tc->accessRoomModes();
|
|
||||||
const auto modes = LuaRoomModes{
|
|
||||||
.unique_chat = m->r9k,
|
|
||||||
.subscriber_only = m->submode,
|
|
||||||
.emotes_only = m->emoteOnly,
|
|
||||||
.follower_only = (m->followerOnly == -1)
|
|
||||||
? std::nullopt
|
|
||||||
: std::optional(m->followerOnly),
|
|
||||||
.slow_mode =
|
|
||||||
(m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode),
|
|
||||||
|
|
||||||
};
|
};
|
||||||
lua::push(L, modes);
|
// clang-format off
|
||||||
return 1;
|
return sol::table::create_with(L,
|
||||||
|
"subscriber_only", modes.submode,
|
||||||
|
"unique_chat", modes.r9k,
|
||||||
|
"emotes_only", modes.emoteOnly,
|
||||||
|
"follower_only", maybe(modes.followerOnly),
|
||||||
|
"slow_mode", maybe(modes.slowMode)
|
||||||
|
);
|
||||||
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_stream_status(lua_State *L)
|
sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status)
|
||||||
{
|
{
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
// clang-format off
|
||||||
const auto s = tc->accessStreamStatus();
|
return sol::table::create_with(L,
|
||||||
const auto status = LuaStreamStatus{
|
"live", status.live,
|
||||||
.live = s->live,
|
"viewer_count", status.viewerCount,
|
||||||
.viewer_count = static_cast<int>(s->viewerCount),
|
"title", status.title,
|
||||||
.uptime = s->uptimeSeconds,
|
"game_name", status.game,
|
||||||
.title = s->title,
|
"game_id", status.gameId,
|
||||||
.game_name = s->game,
|
"uptime", status.uptimeSeconds
|
||||||
.game_id = s->gameId,
|
);
|
||||||
};
|
// clang-format on
|
||||||
lua::push(L, status);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_twitch_id(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->roomId());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_broadcaster(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->isBroadcaster());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_mod(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->isMod());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_vip(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->isVip());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_by_name(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel "
|
|
||||||
"name and platform)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
LPlatform platform{};
|
|
||||||
if (!lua::pop(L, &platform))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, "
|
|
||||||
"expected a string)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString name;
|
|
||||||
if (!lua::pop(L, &name))
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"cannot get channel name (1st argument of Channel.by_name, "
|
|
||||||
"expected a string)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
auto chn = getApp()->getTwitch()->getChannelOrEmpty(name);
|
|
||||||
lua::push(L, chn);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_by_twitch_id(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) != 1)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner "
|
|
||||||
"id)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString id;
|
|
||||||
if (!lua::pop(L, &id))
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"cannot get channel name (1st argument of Channel.by_name, "
|
|
||||||
"expected a string)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
auto chn = getApp()->getTwitch()->getChannelOrEmptyByID(id);
|
|
||||||
|
|
||||||
lua::push(L, chn);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::to_string(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
|
||||||
if (that == nullptr)
|
|
||||||
{
|
|
||||||
lua_pushstring(L, "<c2.Channel expired>");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString formated = QString("<c2.Channel %1>").arg(that->getName());
|
|
||||||
lua::push(L, formated);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
// NOLINTEND(*vararg)
|
|
||||||
//
|
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes)
|
|
||||||
{
|
|
||||||
auto out = lua::pushEmptyTable(L, 6);
|
|
||||||
# define PUSH(field) \
|
|
||||||
lua::push(L, modes.field); \
|
|
||||||
lua_setfield(L, out, #field)
|
|
||||||
PUSH(unique_chat);
|
|
||||||
PUSH(subscriber_only);
|
|
||||||
PUSH(emotes_only);
|
|
||||||
PUSH(follower_only);
|
|
||||||
PUSH(slow_mode);
|
|
||||||
# undef PUSH
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status)
|
|
||||||
{
|
|
||||||
auto out = lua::pushEmptyTable(L, 6);
|
|
||||||
# define PUSH(field) \
|
|
||||||
lua::push(L, status.field); \
|
|
||||||
lua_setfield(L, out, #field)
|
|
||||||
PUSH(live);
|
|
||||||
PUSH(viewer_count);
|
|
||||||
PUSH(uptime);
|
|
||||||
PUSH(title);
|
|
||||||
PUSH(game_name);
|
|
||||||
PUSH(game_id);
|
|
||||||
# undef PUSH
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, ChannelPtr chn)
|
|
||||||
{
|
|
||||||
using namespace chatterino::lua::api;
|
|
||||||
|
|
||||||
if (chn->isEmpty())
|
|
||||||
{
|
|
||||||
lua_pushnil(L);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
WeakPtrUserData<UserData::Type::Channel, Channel>::create(
|
|
||||||
L, chn->weak_from_this());
|
|
||||||
luaL_getmetatable(L, "c2.Channel");
|
|
||||||
lua_setmetatable(L, -2);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,48 +1,24 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/Channel.hpp"
|
# include "common/Channel.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
|
||||||
# include "providers/twitch/TwitchChannel.hpp"
|
# include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
|
||||||
# include <optional>
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(readability-identifier-naming)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This enum describes a platform for the purpose of searching for a channel.
|
* @includefile providers/twitch/TwitchChannel.hpp
|
||||||
* Currently only Twitch is supported because identifying IRC channels is tricky.
|
|
||||||
* @exposeenum c2.Platform
|
|
||||||
*/
|
*/
|
||||||
enum class LPlatform {
|
|
||||||
Twitch,
|
|
||||||
//IRC,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@class c2.Channel
|
* @lua@class c2.Channel
|
||||||
*/
|
*/
|
||||||
struct ChannelRef {
|
struct ChannelRef {
|
||||||
static void createMetatable(lua_State *L);
|
|
||||||
friend class chatterino::PluginController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr.
|
|
||||||
* If the object given is not a userdatum or the pointer inside that
|
|
||||||
* userdatum doesn't point to a Channel, a lua error is thrown.
|
|
||||||
*
|
|
||||||
* @param expiredOk Should an expired return nullptr instead of erroring
|
|
||||||
*/
|
|
||||||
static ChannelPtr getOrError(lua_State *L, bool expiredOk = false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Casts the result of getOrError to std::shared_ptr<TwitchChannel>
|
|
||||||
* if that fails thows a lua error.
|
|
||||||
*/
|
|
||||||
static std::shared_ptr<TwitchChannel> getTwitchOrError(lua_State *L);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
ChannelRef(const std::shared_ptr<Channel> &chan);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel this object points to is valid.
|
* Returns true if the channel this object points to is valid.
|
||||||
* If the object expired, returns false
|
* If the object expired, returns false
|
||||||
|
@ -51,7 +27,7 @@ public:
|
||||||
* @lua@return boolean success
|
* @lua@return boolean success
|
||||||
* @exposed c2.Channel:is_valid
|
* @exposed c2.Channel:is_valid
|
||||||
*/
|
*/
|
||||||
static int is_valid(lua_State *L);
|
bool is_valid();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel's name. This is the lowercase login name.
|
* Gets the channel's name. This is the lowercase login name.
|
||||||
|
@ -59,7 +35,7 @@ public:
|
||||||
* @lua@return string name
|
* @lua@return string name
|
||||||
* @exposed c2.Channel:get_name
|
* @exposed c2.Channel:get_name
|
||||||
*/
|
*/
|
||||||
static int get_name(lua_State *L);
|
QString get_name();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel's type
|
* Gets the channel's type
|
||||||
|
@ -67,7 +43,7 @@ public:
|
||||||
* @lua@return c2.ChannelType
|
* @lua@return c2.ChannelType
|
||||||
* @exposed c2.Channel:get_type
|
* @exposed c2.Channel:get_type
|
||||||
*/
|
*/
|
||||||
static int get_type(lua_State *L);
|
Channel::Type get_type();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||||
|
@ -75,17 +51,17 @@ public:
|
||||||
* @lua@return string name
|
* @lua@return string name
|
||||||
* @exposed c2.Channel:get_display_name
|
* @exposed c2.Channel:get_display_name
|
||||||
*/
|
*/
|
||||||
static int get_display_name(lua_State *L);
|
QString get_display_name();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to the target channel.
|
* Sends a message to the target channel.
|
||||||
* Note that this does not execute client-commands.
|
* Note that this does not execute client-commands.
|
||||||
*
|
*
|
||||||
* @lua@param message string
|
* @lua@param message string
|
||||||
* @lua@param execute_commands boolean Should commands be run on the text?
|
* @lua@param execute_commands? boolean Should commands be run on the text?
|
||||||
* @exposed c2.Channel:send_message
|
* @exposed c2.Channel:send_message
|
||||||
*/
|
*/
|
||||||
static int send_message(lua_State *L);
|
void send_message(QString text, sol::variadic_args va);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a system message client-side
|
* Adds a system message client-side
|
||||||
|
@ -93,7 +69,7 @@ public:
|
||||||
* @lua@param message string
|
* @lua@param message string
|
||||||
* @exposed c2.Channel:add_system_message
|
* @exposed c2.Channel:add_system_message
|
||||||
*/
|
*/
|
||||||
static int add_system_message(lua_State *L);
|
void add_system_message(QString text);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true for twitch channels.
|
* Returns true for twitch channels.
|
||||||
|
@ -103,7 +79,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_twitch_channel
|
* @exposed c2.Channel:is_twitch_channel
|
||||||
*/
|
*/
|
||||||
static int is_twitch_channel(lua_State *L);
|
bool is_twitch_channel();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Twitch Channel specific functions
|
* Twitch Channel specific functions
|
||||||
|
@ -115,7 +91,7 @@ public:
|
||||||
* @lua@return RoomModes
|
* @lua@return RoomModes
|
||||||
* @exposed c2.Channel:get_room_modes
|
* @exposed c2.Channel:get_room_modes
|
||||||
*/
|
*/
|
||||||
static int get_room_modes(lua_State *L);
|
sol::table get_room_modes(sol::this_state state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the stream status.
|
* Returns a copy of the stream status.
|
||||||
|
@ -123,7 +99,7 @@ public:
|
||||||
* @lua@return StreamStatus
|
* @lua@return StreamStatus
|
||||||
* @exposed c2.Channel:get_stream_status
|
* @exposed c2.Channel:get_stream_status
|
||||||
*/
|
*/
|
||||||
static int get_stream_status(lua_State *L);
|
sol::table get_stream_status(sol::this_state state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Twitch user ID of the owner of the channel.
|
* Returns the Twitch user ID of the owner of the channel.
|
||||||
|
@ -131,7 +107,7 @@ public:
|
||||||
* @lua@return string
|
* @lua@return string
|
||||||
* @exposed c2.Channel:get_twitch_id
|
* @exposed c2.Channel:get_twitch_id
|
||||||
*/
|
*/
|
||||||
static int get_twitch_id(lua_State *L);
|
QString get_twitch_id();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel is a Twitch channel and the user owns it
|
* Returns true if the channel is a Twitch channel and the user owns it
|
||||||
|
@ -139,7 +115,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_broadcaster
|
* @exposed c2.Channel:is_broadcaster
|
||||||
*/
|
*/
|
||||||
static int is_broadcaster(lua_State *L);
|
bool is_broadcaster();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||||
|
@ -148,7 +124,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_mod
|
* @exposed c2.Channel:is_mod
|
||||||
*/
|
*/
|
||||||
static int is_mod(lua_State *L);
|
bool is_mod();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||||
|
@ -157,7 +133,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_vip
|
* @exposed c2.Channel:is_vip
|
||||||
*/
|
*/
|
||||||
static int is_vip(lua_State *L);
|
bool is_vip();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Misc
|
* Misc
|
||||||
|
@ -167,7 +143,7 @@ public:
|
||||||
* @lua@return string
|
* @lua@return string
|
||||||
* @exposed c2.Channel:__tostring
|
* @exposed c2.Channel:__tostring
|
||||||
*/
|
*/
|
||||||
static int to_string(lua_State *L);
|
QString to_string();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static functions
|
* Static functions
|
||||||
|
@ -184,11 +160,10 @@ public:
|
||||||
* - /automod
|
* - /automod
|
||||||
*
|
*
|
||||||
* @lua@param name string Which channel are you looking for?
|
* @lua@param name string Which channel are you looking for?
|
||||||
* @lua@param platform c2.Platform Where to search for the channel?
|
|
||||||
* @lua@return c2.Channel?
|
* @lua@return c2.Channel?
|
||||||
* @exposed c2.Channel.by_name
|
* @exposed c2.Channel.by_name
|
||||||
*/
|
*/
|
||||||
static int get_by_name(lua_State *L);
|
static std::optional<ChannelRef> get_by_name(const QString &name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a channel by the Twitch user ID of its owner.
|
* Finds a channel by the Twitch user ID of its owner.
|
||||||
|
@ -197,79 +172,24 @@ public:
|
||||||
* @lua@return c2.Channel?
|
* @lua@return c2.Channel?
|
||||||
* @exposed c2.Channel.by_twitch_id
|
* @exposed c2.Channel.by_twitch_id
|
||||||
*/
|
*/
|
||||||
static int get_by_twitch_id(lua_State *L);
|
static std::optional<ChannelRef> get_by_twitch_id(const QString &id);
|
||||||
};
|
|
||||||
|
|
||||||
// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals
|
static void createUserType(sol::table &c2);
|
||||||
/**
|
|
||||||
* @lua@class RoomModes
|
|
||||||
*/
|
|
||||||
struct LuaRoomModes {
|
|
||||||
/**
|
|
||||||
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
|
||||||
*/
|
|
||||||
bool unique_chat = false;
|
|
||||||
|
|
||||||
/**
|
private:
|
||||||
* @lua@field subscriber_only boolean
|
std::weak_ptr<Channel> weak;
|
||||||
*/
|
|
||||||
bool subscriber_only = false;
|
|
||||||
|
|
||||||
/**
|
/// Locks the weak pointer and throws if the pointer expired
|
||||||
* @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
std::shared_ptr<Channel> strong();
|
||||||
*/
|
|
||||||
bool emotes_only = false;
|
|
||||||
|
|
||||||
/**
|
/// Locks the weak pointer and throws if the pointer is invalid
|
||||||
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
|
std::shared_ptr<TwitchChannel> twitch();
|
||||||
*/
|
|
||||||
std::optional<int> follower_only;
|
|
||||||
/**
|
|
||||||
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
|
||||||
*/
|
|
||||||
std::optional<int> slow_mode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@class StreamStatus
|
|
||||||
*/
|
|
||||||
struct LuaStreamStatus {
|
|
||||||
/**
|
|
||||||
* @lua@field live boolean
|
|
||||||
*/
|
|
||||||
bool live = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field viewer_count number
|
|
||||||
*/
|
|
||||||
int viewer_count = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field uptime number Seconds since the stream started.
|
|
||||||
*/
|
|
||||||
int uptime = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field title string Stream title or last stream title
|
|
||||||
*/
|
|
||||||
QString title;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field game_name string
|
|
||||||
*/
|
|
||||||
QString game_name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field game_id string
|
|
||||||
*/
|
|
||||||
QString game_id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
|
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes);
|
||||||
|
sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status);
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes);
|
|
||||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status);
|
|
||||||
StackIdx push(lua_State *L, ChannelPtr chn);
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
14
src/controllers/plugins/api/EventType.hpp
Normal file
14
src/controllers/plugins/api/EventType.hpp
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exposeenum c2.EventType
|
||||||
|
*/
|
||||||
|
enum class EventType {
|
||||||
|
CompletionRequested,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
#endif
|
|
@ -6,402 +6,173 @@
|
||||||
# include "common/network/NetworkRequest.hpp"
|
# include "common/network/NetworkRequest.hpp"
|
||||||
# include "common/network/NetworkResult.hpp"
|
# include "common/network/NetworkResult.hpp"
|
||||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "util/DebugCount.hpp"
|
# include "util/DebugCount.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
# include <QChar>
|
||||||
|
# include <QLoggingCategory>
|
||||||
# include <QRandomGenerator>
|
# include <QRandomGenerator>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
# include <sol/raii.hpp>
|
||||||
|
# include <sol/state_view.hpp>
|
||||||
|
# include <sol/table.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <optional>
|
||||||
|
# include <stdexcept>
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
# include <vector>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(*vararg)
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
|
||||||
static const luaL_Reg HTTP_REQUEST_METHODS[] = {
|
|
||||||
{"on_success", &HTTPRequest::on_success_wrap},
|
|
||||||
{"on_error", &HTTPRequest::on_error_wrap},
|
|
||||||
{"finally", &HTTPRequest::finally_wrap},
|
|
||||||
|
|
||||||
{"execute", &HTTPRequest::execute_wrap},
|
void HTTPRequest::createUserType(sol::table &c2)
|
||||||
{"set_timeout", &HTTPRequest::set_timeout_wrap},
|
|
||||||
{"set_payload", &HTTPRequest::set_payload_wrap},
|
|
||||||
{"set_header", &HTTPRequest::set_header_wrap},
|
|
||||||
// static
|
|
||||||
{"create", &HTTPRequest::create},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
std::shared_ptr<HTTPRequest> HTTPRequest::getOrError(lua_State *L,
|
|
||||||
StackIdx where)
|
|
||||||
{
|
{
|
||||||
if (lua_gettop(L) < 1)
|
c2.new_usertype<HTTPRequest>( //
|
||||||
{
|
"HTTPRequest", sol::no_constructor, //
|
||||||
// The nullptr is there just to appease the compiler, luaL_error is no return
|
sol::meta_method::to_string, &HTTPRequest::to_string, //
|
||||||
luaL_error(L, "Called c2.HTTPRequest method without a request object");
|
|
||||||
return nullptr;
|
"on_success", &HTTPRequest::on_success, //
|
||||||
}
|
"on_error", &HTTPRequest::on_error, //
|
||||||
if (lua_isuserdata(L, where) == 0)
|
"finally", &HTTPRequest::finally, //
|
||||||
{
|
|
||||||
luaL_error(
|
"set_timeout", &HTTPRequest::set_timeout, //
|
||||||
L,
|
"set_payload", &HTTPRequest::set_payload, //
|
||||||
"Called c2.HTTPRequest method with a non-userdata 'self' argument");
|
"set_header", &HTTPRequest::set_header, //
|
||||||
return nullptr;
|
"execute", &HTTPRequest::execute, //
|
||||||
}
|
|
||||||
// luaL_checkudata is no-return if check fails
|
"create", &HTTPRequest::create //
|
||||||
auto *checked = luaL_checkudata(L, where, "c2.HTTPRequest");
|
);
|
||||||
auto *data =
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPRequest, HTTPRequest>::from(
|
|
||||||
checked);
|
|
||||||
if (data == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Called c2.HTTPRequest method with an invalid pointer");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
lua_remove(L, where);
|
|
||||||
if (data->target == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Internal error: SharedPtrUserData<UserData::Type::HTTPRequest, "
|
|
||||||
"HTTPRequest>::target was null. This is a Chatterino bug!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (data->target->done)
|
|
||||||
{
|
|
||||||
luaL_error(L, "This c2.HTTPRequest has already been executed!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return data->target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPRequest::createMetatable(lua_State *L)
|
void HTTPRequest::on_success(sol::protected_function func)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 1);
|
this->cbSuccess = std::make_optional(func);
|
||||||
|
|
||||||
luaL_newmetatable(L, "c2.HTTPRequest");
|
|
||||||
lua_pushstring(L, "__index");
|
|
||||||
lua_pushvalue(L, -2); // clone metatable
|
|
||||||
lua_settable(L, -3); // metatable.__index = metatable
|
|
||||||
|
|
||||||
// Generic ISharedResource stuff
|
|
||||||
lua_pushstring(L, "__gc");
|
|
||||||
lua_pushcfunction(L, (&SharedPtrUserData<UserData::Type::HTTPRequest,
|
|
||||||
HTTPRequest>::destroy));
|
|
||||||
lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy
|
|
||||||
|
|
||||||
luaL_setfuncs(L, HTTP_REQUEST_METHODS, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_success_wrap(lua_State *L)
|
void HTTPRequest::on_error(sol::protected_function func)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, -2);
|
this->cbError = std::make_optional(func);
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->on_success(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_success(lua_State *L)
|
void HTTPRequest::set_timeout(int timeout)
|
||||||
{
|
{
|
||||||
auto top = lua_gettop(L);
|
this->timeout_ = timeout;
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_success needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
if (!lua_isfunction(L, top))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_success needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
auto shared = this->pushPrivate(L);
|
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_setfield(L, shared, "success"); // this deletes the function copy
|
|
||||||
lua_pop(L, 2); // delete the table and function original
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_error_wrap(lua_State *L)
|
void HTTPRequest::finally(sol::protected_function func)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, -2);
|
this->cbFinally = std::make_optional(func);
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->on_error(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_error(lua_State *L)
|
void HTTPRequest::set_payload(QByteArray payload)
|
||||||
{
|
{
|
||||||
auto top = lua_gettop(L);
|
this->req_ = std::move(this->req_).payload(payload);
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_error needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
if (!lua_isfunction(L, top))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_error needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
auto shared = this->pushPrivate(L);
|
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_setfield(L, shared, "error"); // this deletes the function copy
|
|
||||||
lua_pop(L, 2); // delete the table and function original
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::set_timeout_wrap(lua_State *L)
|
// name and value may be random bytes
|
||||||
|
void HTTPRequest::set_header(QByteArray name, QByteArray value)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, -2);
|
this->req_ = std::move(this->req_).header(name, value);
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->set_timeout(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::set_timeout(lua_State *L)
|
std::shared_ptr<HTTPRequest> HTTPRequest::create(sol::this_state L,
|
||||||
|
NetworkRequestType method,
|
||||||
|
QString url)
|
||||||
{
|
{
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_timeout needs 1 argument (a number of "
|
|
||||||
"milliseconds after which the request will time out)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int temporary = -1;
|
|
||||||
if (!lua::pop(L, &temporary))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_timeout failed to get timeout, expected a "
|
|
||||||
"positive integer");
|
|
||||||
}
|
|
||||||
if (temporary <= 0)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_timeout failed to get timeout, expected a "
|
|
||||||
"positive integer");
|
|
||||||
}
|
|
||||||
this->timeout_ = temporary;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::finally_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -2);
|
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->finally(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::finally(lua_State *L)
|
|
||||||
{
|
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback "
|
|
||||||
"that takes nothing and doesn't return anything)");
|
|
||||||
}
|
|
||||||
if (!lua_isfunction(L, top))
|
|
||||||
{
|
|
||||||
return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback "
|
|
||||||
"that takes nothing and doesn't return anything)");
|
|
||||||
}
|
|
||||||
auto shared = this->pushPrivate(L);
|
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_setfield(L, shared, "finally"); // this deletes the function copy
|
|
||||||
lua_pop(L, 2); // delete the table and function original
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_payload_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -2);
|
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->set_payload(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_payload(lua_State *L)
|
|
||||||
{
|
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_payload needs 1 argument (a string payload)");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string temporary;
|
|
||||||
if (!lua::pop(L, &temporary))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_payload failed to get payload, expected a "
|
|
||||||
"string");
|
|
||||||
}
|
|
||||||
this->req_ =
|
|
||||||
std::move(this->req_).payload(QByteArray::fromStdString(temporary));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_header_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -3);
|
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->set_header(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_header(lua_State *L)
|
|
||||||
{
|
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 2)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_header needs 2 arguments (a header name "
|
|
||||||
"and a value)");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string value;
|
|
||||||
if (!lua::pop(L, &value))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "cannot get value (2nd argument of HTTPRequest:set_header)");
|
|
||||||
}
|
|
||||||
std::string name;
|
|
||||||
if (!lua::pop(L, &name))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "cannot get name (1st argument of HTTPRequest:set_header)");
|
|
||||||
}
|
|
||||||
this->req_ = std::move(this->req_)
|
|
||||||
.header(QByteArray::fromStdString(name),
|
|
||||||
QByteArray::fromStdString(value));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::create(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -1);
|
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest.create needs exactly 2 arguments (method "
|
|
||||||
"and url)");
|
|
||||||
}
|
|
||||||
QString url;
|
|
||||||
if (!lua::pop(L, &url))
|
|
||||||
{
|
|
||||||
return luaL_error(L,
|
|
||||||
"cannot get url (2nd argument of HTTPRequest.create, "
|
|
||||||
"expected a string)");
|
|
||||||
}
|
|
||||||
auto parsedurl = QUrl(url);
|
auto parsedurl = QUrl(url);
|
||||||
if (!parsedurl.isValid())
|
if (!parsedurl.isValid())
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error(
|
||||||
L, "cannot parse url (2nd argument of HTTPRequest.create, "
|
"cannot parse url (2nd argument of HTTPRequest.create, "
|
||||||
"got invalid url in argument)");
|
"got invalid url in argument)");
|
||||||
}
|
|
||||||
NetworkRequestType method{};
|
|
||||||
if (!lua::pop(L, &method))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "cannot get method (1st argument of HTTPRequest.create, "
|
|
||||||
"expected a string)");
|
|
||||||
}
|
}
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (!pl->hasHTTPPermissionFor(parsedurl))
|
if (!pl->hasHTTPPermissionFor(parsedurl))
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error(
|
||||||
L, "Plugin does not have permission to send HTTP requests "
|
"Plugin does not have permission to send HTTP requests "
|
||||||
"to this URL");
|
"to this URL");
|
||||||
}
|
}
|
||||||
NetworkRequest r(parsedurl, method);
|
NetworkRequest r(parsedurl, method);
|
||||||
lua::push(
|
return std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r));
|
||||||
L, std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r)));
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::execute_wrap(lua_State *L)
|
void HTTPRequest::execute(sol::this_state L)
|
||||||
{
|
{
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
if (this->done)
|
||||||
return ptr->execute(L);
|
{
|
||||||
}
|
throw std::runtime_error(
|
||||||
|
"Cannot execute this c2.HTTPRequest, it was executed already!");
|
||||||
int HTTPRequest::execute(lua_State *L)
|
}
|
||||||
{
|
|
||||||
auto shared = this->shared_from_this();
|
|
||||||
this->done = true;
|
this->done = true;
|
||||||
|
|
||||||
|
// this keeps the object alive even if Lua were to forget about it,
|
||||||
|
auto hack = this->weak_from_this();
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
pl->httpRequests.push_back(this->shared_from_this());
|
||||||
|
|
||||||
std::move(this->req_)
|
std::move(this->req_)
|
||||||
.onSuccess([shared, L](const NetworkResult &res) {
|
.onSuccess([L, hack](const NetworkResult &res) {
|
||||||
|
auto self = hack.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!self->cbSuccess.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
auto *thread = lua_newthread(L);
|
(*self->cbSuccess)(HTTPResponse(res));
|
||||||
|
self->cbSuccess = std::nullopt;
|
||||||
auto priv = shared->pushPrivate(thread);
|
|
||||||
lua_getfield(thread, priv, "success");
|
|
||||||
auto cb = lua_gettop(thread);
|
|
||||||
if (lua_isfunction(thread, cb))
|
|
||||||
{
|
|
||||||
lua::push(thread, std::make_shared<HTTPResponse>(res));
|
|
||||||
// one arg, no return, no msgh
|
|
||||||
lua_pcall(thread, 1, 0, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pop(thread, 1); // remove callback
|
|
||||||
}
|
|
||||||
lua_closethread(thread, nullptr);
|
|
||||||
lua_pop(L, 1); // remove thread from L
|
|
||||||
})
|
})
|
||||||
.onError([shared, L](const NetworkResult &res) {
|
.onError([L, hack](const NetworkResult &res) {
|
||||||
|
auto self = hack.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!self->cbError.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
auto *thread = lua_newthread(L);
|
(*self->cbError)(HTTPResponse(res));
|
||||||
|
self->cbError = std::nullopt;
|
||||||
auto priv = shared->pushPrivate(thread);
|
|
||||||
lua_getfield(thread, priv, "error");
|
|
||||||
auto cb = lua_gettop(thread);
|
|
||||||
if (lua_isfunction(thread, cb))
|
|
||||||
{
|
|
||||||
lua::push(thread, std::make_shared<HTTPResponse>(res));
|
|
||||||
// one arg, no return, no msgh
|
|
||||||
lua_pcall(thread, 1, 0, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pop(thread, 1); // remove callback
|
|
||||||
}
|
|
||||||
lua_closethread(thread, nullptr);
|
|
||||||
lua_pop(L, 1); // remove thread from L
|
|
||||||
})
|
})
|
||||||
.finally([shared, L]() {
|
.finally([L, hack]() {
|
||||||
|
auto self = hack.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
// this could happen if the plugin was deleted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
for (auto it = pl->httpRequests.begin();
|
||||||
|
it < pl->httpRequests.end(); it++)
|
||||||
|
{
|
||||||
|
if (*it == self)
|
||||||
|
{
|
||||||
|
pl->httpRequests.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->cbFinally.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
auto *thread = lua_newthread(L);
|
(*self->cbFinally)();
|
||||||
|
self->cbFinally = std::nullopt;
|
||||||
auto priv = shared->pushPrivate(thread);
|
|
||||||
lua_getfield(thread, priv, "finally");
|
|
||||||
auto cb = lua_gettop(thread);
|
|
||||||
if (lua_isfunction(thread, cb))
|
|
||||||
{
|
|
||||||
// no args, no return, no msgh
|
|
||||||
lua_pcall(thread, 0, 0, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pop(thread, 1); // remove callback
|
|
||||||
}
|
|
||||||
// remove our private data
|
|
||||||
lua_pushnil(thread);
|
|
||||||
lua_setfield(thread, LUA_REGISTRYINDEX,
|
|
||||||
shared->privateKey.toStdString().c_str());
|
|
||||||
lua_closethread(thread, nullptr);
|
|
||||||
lua_pop(L, 1); // remove thread from L
|
|
||||||
|
|
||||||
// we removed our private table, forget the key for it
|
|
||||||
shared->privateKey = QString();
|
|
||||||
})
|
})
|
||||||
.timeout(this->timeout_)
|
.timeout(this->timeout_)
|
||||||
.execute();
|
.execute();
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
|
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
|
||||||
|
@ -418,34 +189,10 @@ HTTPRequest::~HTTPRequest()
|
||||||
// but that's better than accessing a possibly invalid lua_State pointer.
|
// but that's better than accessing a possibly invalid lua_State pointer.
|
||||||
}
|
}
|
||||||
|
|
||||||
StackIdx HTTPRequest::pushPrivate(lua_State *L)
|
QString HTTPRequest::to_string()
|
||||||
{
|
{
|
||||||
if (this->privateKey.isEmpty())
|
return "<HTTPRequest>";
|
||||||
{
|
|
||||||
this->privateKey = QString("HTTPRequestPrivate%1")
|
|
||||||
.arg(QRandomGenerator::system()->generate());
|
|
||||||
pushEmptyTable(L, 4);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX,
|
|
||||||
this->privateKey.toStdString().c_str());
|
|
||||||
}
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, this->privateKey.toStdString().c_str());
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTEND(*vararg)
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
namespace chatterino::lua {
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPRequest> request)
|
|
||||||
{
|
|
||||||
using namespace chatterino::lua::api;
|
|
||||||
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPRequest, HTTPRequest>::create(
|
|
||||||
L, std::move(request));
|
|
||||||
luaL_getmetatable(L, "c2.HTTPRequest");
|
|
||||||
lua_setmetatable(L, -2);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,19 +2,25 @@
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/network/NetworkRequest.hpp"
|
# include "common/network/NetworkRequest.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
class PluginController;
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(readability-identifier-naming)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@alias HTTPCallback fun(result: HTTPResponse): nil
|
* @lua@alias c2.HTTPCallback fun(result: c2.HTTPResponse): nil
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@class HTTPRequest
|
* @lua@class c2.HTTPRequest
|
||||||
*/
|
*/
|
||||||
class HTTPRequest : public std::enable_shared_from_this<HTTPRequest>
|
class HTTPRequest : public std::enable_shared_from_this<HTTPRequest>
|
||||||
{
|
{
|
||||||
|
@ -33,33 +39,19 @@ public:
|
||||||
private:
|
private:
|
||||||
NetworkRequest req_;
|
NetworkRequest req_;
|
||||||
|
|
||||||
static void createMetatable(lua_State *L);
|
static void createUserType(sol::table &c2);
|
||||||
friend class chatterino::PluginController;
|
friend class chatterino::PluginController;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPRequest
|
|
||||||
*
|
|
||||||
* If the object given is not a userdatum or the pointer inside that
|
|
||||||
* userdatum doesn't point to a HTTPRequest, a lua error is thrown.
|
|
||||||
*
|
|
||||||
* This function always returns a non-null pointer.
|
|
||||||
*/
|
|
||||||
static std::shared_ptr<HTTPRequest> getOrError(lua_State *L,
|
|
||||||
StackIdx where = -1);
|
|
||||||
/**
|
|
||||||
* Pushes the private table onto the lua stack.
|
|
||||||
*
|
|
||||||
* This might create it if it doesn't exist.
|
|
||||||
*/
|
|
||||||
StackIdx pushPrivate(lua_State *L);
|
|
||||||
|
|
||||||
// This is the key in the registry the private table it held at (if it exists)
|
// This is the key in the registry the private table it held at (if it exists)
|
||||||
// This might be a null QString if the request has already been executed or
|
// This might be a null QString if the request has already been executed or
|
||||||
// the table wasn't created yet.
|
// the table wasn't created yet.
|
||||||
QString privateKey;
|
|
||||||
int timeout_ = 10'000;
|
int timeout_ = 10'000;
|
||||||
bool done = false;
|
bool done = false;
|
||||||
|
|
||||||
|
std::optional<sol::protected_function> cbSuccess;
|
||||||
|
std::optional<sol::protected_function> cbError;
|
||||||
|
std::optional<sol::protected_function> cbFinally;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
|
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
|
||||||
// - the static wrapper function is called
|
// - the static wrapper function is called
|
||||||
|
@ -69,65 +61,63 @@ public:
|
||||||
/**
|
/**
|
||||||
* Sets the success callback
|
* Sets the success callback
|
||||||
*
|
*
|
||||||
* @lua@param callback HTTPCallback Function to call when the HTTP request succeeds
|
* @lua@param callback c2.HTTPCallback Function to call when the HTTP request succeeds
|
||||||
* @exposed HTTPRequest:on_success
|
* @exposed c2.HTTPRequest:on_success
|
||||||
*/
|
*/
|
||||||
static int on_success_wrap(lua_State *L);
|
void on_success(sol::protected_function func);
|
||||||
int on_success(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the failure callback
|
* Sets the failure callback
|
||||||
*
|
*
|
||||||
* @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
|
* @lua@param callback c2.HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
|
||||||
* @exposed HTTPRequest:on_error
|
* @exposed c2.HTTPRequest:on_error
|
||||||
*/
|
*/
|
||||||
static int on_error_wrap(lua_State *L);
|
void on_error(sol::protected_function func);
|
||||||
int on_error(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the finally callback
|
* Sets the finally callback
|
||||||
*
|
*
|
||||||
* @lua@param callback fun(): nil Function to call when the HTTP request finishes
|
* @lua@param callback fun(): nil Function to call when the HTTP request finishes
|
||||||
* @exposed HTTPRequest:finally
|
* @exposed c2.HTTPRequest:finally
|
||||||
*/
|
*/
|
||||||
static int finally_wrap(lua_State *L);
|
void finally(sol::protected_function func);
|
||||||
int finally(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the timeout
|
* Sets the timeout
|
||||||
*
|
*
|
||||||
* @lua@param timeout integer How long in milliseconds until the times out
|
* @lua@param timeout integer How long in milliseconds until the times out
|
||||||
* @exposed HTTPRequest:set_timeout
|
* @exposed c2.HTTPRequest:set_timeout
|
||||||
*/
|
*/
|
||||||
static int set_timeout_wrap(lua_State *L);
|
void set_timeout(int timeout);
|
||||||
int set_timeout(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the request payload
|
* Sets the request payload
|
||||||
*
|
*
|
||||||
* @lua@param data string
|
* @lua@param data string
|
||||||
* @exposed HTTPRequest:set_payload
|
* @exposed c2.HTTPRequest:set_payload
|
||||||
*/
|
*/
|
||||||
static int set_payload_wrap(lua_State *L);
|
void set_payload(QByteArray payload);
|
||||||
int set_payload(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a header in the request
|
* Sets a header in the request
|
||||||
*
|
*
|
||||||
* @lua@param name string
|
* @lua@param name string
|
||||||
* @lua@param value string
|
* @lua@param value string
|
||||||
* @exposed HTTPRequest:set_header
|
* @exposed c2.HTTPRequest:set_header
|
||||||
*/
|
*/
|
||||||
static int set_header_wrap(lua_State *L);
|
void set_header(QByteArray name, QByteArray value);
|
||||||
int set_header(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the HTTP request
|
* Executes the HTTP request
|
||||||
*
|
*
|
||||||
* @exposed HTTPRequest:execute
|
* @exposed c2.HTTPRequest:execute
|
||||||
*/
|
*/
|
||||||
static int execute_wrap(lua_State *L);
|
void execute(sol::this_state L);
|
||||||
int execute(lua_State *L);
|
/**
|
||||||
|
* @lua@return string
|
||||||
|
* @exposed c2.HTTPRequest:__tostring
|
||||||
|
*/
|
||||||
|
QString to_string();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static functions
|
* Static functions
|
||||||
|
@ -136,13 +126,15 @@ public:
|
||||||
/**
|
/**
|
||||||
* Creates a new HTTPRequest
|
* Creates a new HTTPRequest
|
||||||
*
|
*
|
||||||
* @lua@param method HTTPMethod Method to use
|
* @lua@param method c2.HTTPMethod Method to use
|
||||||
* @lua@param url string Where to send the request to
|
* @lua@param url string Where to send the request to
|
||||||
*
|
*
|
||||||
* @lua@return HTTPRequest
|
* @lua@return c2.HTTPRequest
|
||||||
* @exposed HTTPRequest.create
|
* @exposed c2.HTTPRequest.create
|
||||||
*/
|
*/
|
||||||
static int create(lua_State *L);
|
static std::shared_ptr<HTTPRequest> create(sol::this_state L,
|
||||||
|
NetworkRequestType method,
|
||||||
|
QString url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
|
@ -2,77 +2,28 @@
|
||||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||||
|
|
||||||
# include "common/network/NetworkResult.hpp"
|
# include "common/network/NetworkResult.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "util/DebugCount.hpp"
|
# include "util/DebugCount.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
}
|
# include <sol/raii.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(*vararg)
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
|
||||||
static const luaL_Reg HTTP_RESPONSE_METHODS[] = {
|
|
||||||
{"data", &HTTPResponse::data_wrap},
|
|
||||||
{"status", &HTTPResponse::status_wrap},
|
|
||||||
{"error", &HTTPResponse::error_wrap},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
void HTTPResponse::createMetatable(lua_State *L)
|
void HTTPResponse::createUserType(sol::table &c2)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 1);
|
c2.new_usertype<HTTPResponse>( //
|
||||||
|
"HTTPResponse", sol::no_constructor,
|
||||||
|
// metamethods
|
||||||
|
sol::meta_method::to_string, &HTTPResponse::to_string, //
|
||||||
|
|
||||||
luaL_newmetatable(L, "c2.HTTPResponse");
|
"data", &HTTPResponse::data, //
|
||||||
lua_pushstring(L, "__index");
|
"status", &HTTPResponse::status, //
|
||||||
lua_pushvalue(L, -2); // clone metatable
|
"error", &HTTPResponse::error //
|
||||||
lua_settable(L, -3); // metatable.__index = metatable
|
);
|
||||||
|
|
||||||
// Generic ISharedResource stuff
|
|
||||||
lua_pushstring(L, "__gc");
|
|
||||||
lua_pushcfunction(L, (&SharedPtrUserData<UserData::Type::HTTPResponse,
|
|
||||||
HTTPResponse>::destroy));
|
|
||||||
lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy
|
|
||||||
|
|
||||||
luaL_setfuncs(L, HTTP_RESPONSE_METHODS, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<HTTPResponse> HTTPResponse::getOrError(lua_State *L,
|
|
||||||
StackIdx where)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) < 1)
|
|
||||||
{
|
|
||||||
// The nullptr is there just to appease the compiler, luaL_error is no return
|
|
||||||
luaL_error(L, "Called c2.HTTPResponse method without a request object");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (lua_isuserdata(L, where) == 0)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Called c2.HTTPResponse method with a non-userdata "
|
|
||||||
"'self' argument");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
// luaL_checkudata is no-return if check fails
|
|
||||||
auto *checked = luaL_checkudata(L, where, "c2.HTTPResponse");
|
|
||||||
auto *data =
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPResponse, HTTPResponse>::from(
|
|
||||||
checked);
|
|
||||||
if (data == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Called c2.HTTPResponse method with an invalid pointer");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
lua_remove(L, where);
|
|
||||||
if (data->target == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L,
|
|
||||||
"Internal error: SharedPtrUserData<UserData::Type::HTTPResponse, "
|
|
||||||
"HTTPResponse>::target was null. This is a Chatterino bug!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return data->target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPResponse::HTTPResponse(NetworkResult res)
|
HTTPResponse::HTTPResponse(NetworkResult res)
|
||||||
|
@ -85,60 +36,30 @@ HTTPResponse::~HTTPResponse()
|
||||||
DebugCount::decrease("lua::api::HTTPResponse");
|
DebugCount::decrease("lua::api::HTTPResponse");
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::data_wrap(lua_State *L)
|
QByteArray HTTPResponse::data()
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
return this->result_.getData();
|
||||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
|
||||||
return ptr->data(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::data(lua_State *L)
|
std::optional<int> HTTPResponse::status()
|
||||||
{
|
{
|
||||||
lua::push(L, this->result_.getData().toStdString());
|
return this->result_.status();
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::status_wrap(lua_State *L)
|
QString HTTPResponse::error()
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
return this->result_.formatError();
|
||||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
|
||||||
return ptr->status(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::status(lua_State *L)
|
QString HTTPResponse::to_string()
|
||||||
{
|
{
|
||||||
lua::push(L, this->result_.status());
|
if (this->status().has_value())
|
||||||
return 1;
|
{
|
||||||
|
return QStringView(u"<c2.HTTPResponse status %1>")
|
||||||
|
.arg(QString::number(*this->status()));
|
||||||
|
}
|
||||||
|
return "<c2.HTTPResponse no status>";
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::error_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
|
||||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
|
||||||
return ptr->error(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPResponse::error(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::push(L, this->result_.formatError());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOLINTEND(*vararg)
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request)
|
|
||||||
{
|
|
||||||
using namespace chatterino::lua::api;
|
|
||||||
|
|
||||||
// Prepare table
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPResponse, HTTPResponse>::create(
|
|
||||||
L, std::move(request));
|
|
||||||
luaL_getmetatable(L, "c2.HTTPResponse");
|
|
||||||
lua_setmetatable(L, -2);
|
|
||||||
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/network/NetworkResult.hpp"
|
# include "common/network/NetworkResult.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
|
# include <lua.h>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
class PluginController;
|
class PluginController;
|
||||||
|
@ -16,9 +15,9 @@ namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(readability-identifier-naming)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@class HTTPResponse
|
* @lua@class c2.HTTPResponse
|
||||||
*/
|
*/
|
||||||
class HTTPResponse : public std::enable_shared_from_this<HTTPResponse>
|
class HTTPResponse
|
||||||
{
|
{
|
||||||
NetworkResult result_;
|
NetworkResult result_;
|
||||||
|
|
||||||
|
@ -31,50 +30,46 @@ public:
|
||||||
~HTTPResponse();
|
~HTTPResponse();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void createMetatable(lua_State *L);
|
static void createUserType(sol::table &c2);
|
||||||
friend class chatterino::PluginController;
|
friend class chatterino::PluginController;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPResponse
|
|
||||||
*
|
|
||||||
* If the object given is not a userdatum or the pointer inside that
|
|
||||||
* userdatum doesn't point to a HTTPResponse, a lua error is thrown.
|
|
||||||
*
|
|
||||||
* This function always returns a non-null pointer.
|
|
||||||
*/
|
|
||||||
static std::shared_ptr<HTTPResponse> getOrError(lua_State *L,
|
|
||||||
StackIdx where = -1);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Returns the data. This is not guaranteed to be encoded using any
|
* Returns the data. This is not guaranteed to be encoded using any
|
||||||
* particular encoding scheme. It's just the bytes the server returned.
|
* particular encoding scheme. It's just the bytes the server returned.
|
||||||
*
|
*
|
||||||
* @exposed HTTPResponse:data
|
* @lua@return string
|
||||||
|
* @lua@nodiscard
|
||||||
|
* @exposed c2.HTTPResponse:data
|
||||||
*/
|
*/
|
||||||
static int data_wrap(lua_State *L);
|
QByteArray data();
|
||||||
int data(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status code.
|
* Returns the status code.
|
||||||
*
|
*
|
||||||
* @exposed HTTPResponse:status
|
* @lua@return number|nil
|
||||||
|
* @lua@nodiscard
|
||||||
|
* @exposed c2.HTTPResponse:status
|
||||||
*/
|
*/
|
||||||
static int status_wrap(lua_State *L);
|
std::optional<int> status();
|
||||||
int status(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A somewhat human readable description of an error if such happened
|
* A somewhat human readable description of an error if such happened
|
||||||
* @exposed HTTPResponse:error
|
*
|
||||||
|
* @lua@return string
|
||||||
|
* @lua@nodiscard
|
||||||
|
* @exposed c2.HTTPResponse:error
|
||||||
*/
|
*/
|
||||||
|
QString error();
|
||||||
|
|
||||||
static int error_wrap(lua_State *L);
|
/**
|
||||||
int error(lua_State *L);
|
* @lua@return string
|
||||||
|
* @lua@nodiscard
|
||||||
|
* @exposed c2.HTTPResponse:__tostring
|
||||||
|
*/
|
||||||
|
QString to_string();
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request);
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,15 +2,17 @@
|
||||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
# include <QString>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
# include <cerrno>
|
# include <cerrno>
|
||||||
|
# include <stdexcept>
|
||||||
|
# include <utility>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
|
@ -91,45 +93,28 @@ struct LuaFileMode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
int ioError(lua_State *L, const QString &value, int errnoequiv)
|
sol::variadic_results ioError(lua_State *L, const QString &value,
|
||||||
|
int errnoequiv)
|
||||||
{
|
{
|
||||||
lua_pushnil(L);
|
sol::variadic_results out;
|
||||||
lua::push(L, value);
|
out.push_back(sol::nil);
|
||||||
lua::push(L, errnoequiv);
|
out.push_back(sol::make_object(L, value.toStdString()));
|
||||||
return 3;
|
out.push_back({L, sol::in_place_type<int>, errnoequiv});
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTBEGIN(*vararg)
|
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||||
int io_open(lua_State *L)
|
QString strmode)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (pl == nullptr)
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
luaL_error(L, "internal error: no plugin");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
LuaFileMode mode;
|
LuaFileMode mode(strmode);
|
||||||
if (lua_gettop(L) == 2)
|
if (!mode.error.isEmpty())
|
||||||
{
|
{
|
||||||
// we have a mode
|
throw std::runtime_error(mode.error.toStdString());
|
||||||
QString smode;
|
|
||||||
if (!lua::pop(L, &smode))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L,
|
|
||||||
"io.open mode (2nd argument) must be a string or not present");
|
|
||||||
}
|
|
||||||
mode = LuaFileMode(smode);
|
|
||||||
if (!mode.error.isEmpty())
|
|
||||||
{
|
|
||||||
return luaL_error(L, mode.error.toStdString().c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QString filename;
|
|
||||||
if (!lua::pop(L, &filename))
|
|
||||||
{
|
|
||||||
return luaL_error(L,
|
|
||||||
"io.open filename (1st argument) must be a string");
|
|
||||||
}
|
}
|
||||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||||
auto abs = file.absoluteFilePath();
|
auto abs = file.absoluteFilePath();
|
||||||
|
@ -144,39 +129,35 @@ int io_open(lua_State *L)
|
||||||
"Plugin does not have permissions to access given file.",
|
"Plugin does not have permissions to access given file.",
|
||||||
EACCES);
|
EACCES);
|
||||||
}
|
}
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "open");
|
sol::state_view lua(L);
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
auto open = lua.registry()[REG_REAL_IO_NAME]["open"];
|
||||||
lua::push(L, abs);
|
sol::protected_function_result res =
|
||||||
lua::push(L, mode.toString());
|
open(abs.toStdString(), mode.toString().toStdString());
|
||||||
lua_call(L, 2, 3);
|
return res;
|
||||||
return 3;
|
}
|
||||||
|
sol::variadic_results io_open_modeless(sol::this_state L, QString filename)
|
||||||
|
{
|
||||||
|
return io_open(L, std::move(filename), "r");
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_lines(lua_State *L)
|
sol::variadic_results io_lines_noargs(sol::this_state L)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||||
|
sol::protected_function_result res = lines();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::variadic_results io_lines(sol::this_state L, QString filename,
|
||||||
|
sol::variadic_args args)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (pl == nullptr)
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
luaL_error(L, "internal error: no plugin");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (lua_gettop(L) == 0)
|
|
||||||
{
|
|
||||||
// io.lines() case, just call realio.lines
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "lines");
|
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
|
||||||
lua_call(L, 0, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString filename;
|
|
||||||
if (!lua::pop(L, &filename))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L,
|
|
||||||
"io.lines filename (1st argument) must be a string or not present");
|
|
||||||
}
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||||
auto abs = file.absoluteFilePath();
|
auto abs = file.absoluteFilePath();
|
||||||
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||||
|
@ -185,191 +166,168 @@ int io_lines(lua_State *L)
|
||||||
bool ok = pl->hasFSPermissionFor(false, abs);
|
bool ok = pl->hasFSPermissionFor(false, abs);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
{
|
{
|
||||||
return ioError(L,
|
throw std::runtime_error(
|
||||||
"Plugin does not have permissions to access given file.",
|
"Plugin does not have permissions to access given file.");
|
||||||
EACCES);
|
|
||||||
}
|
}
|
||||||
// Our stack looks like this:
|
|
||||||
// - {...}[1]
|
|
||||||
// - {...}[2]
|
|
||||||
// ...
|
|
||||||
// We want:
|
|
||||||
// - REG[REG_REAL_IO_NAME].lines
|
|
||||||
// - absolute file path
|
|
||||||
// - {...}[1]
|
|
||||||
// - {...}[2]
|
|
||||||
// ...
|
|
||||||
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||||
lua_getfield(L, -1, "lines");
|
sol::protected_function_result res = lines(abs.toStdString(), args);
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
return res;
|
||||||
lua_insert(L, 1); // move function to start of stack
|
|
||||||
lua::push(L, abs);
|
|
||||||
lua_insert(L, 2); // move file name just after the function
|
|
||||||
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
sol::variadic_results io_input_argless(sol::this_state L)
|
||||||
|
{
|
||||||
// This is the code for both io.input and io.output
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
int globalFileCommon(lua_State *L, bool output)
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
throw std::runtime_error("internal error: no plugin");
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "internal error: no plugin");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// Three signature cases:
|
|
||||||
// io.input()
|
|
||||||
// io.input(file)
|
|
||||||
// io.input(name)
|
|
||||||
if (lua_gettop(L) == 0)
|
|
||||||
{
|
|
||||||
// We have no arguments, call realio.input()
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
if (output)
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "output");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "input");
|
|
||||||
}
|
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
|
||||||
lua_call(L, 0, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (lua_gettop(L) != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(L, "Too many arguments given to io.input().");
|
|
||||||
}
|
|
||||||
// Now check if we have a file or name
|
|
||||||
auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE);
|
|
||||||
if (p == nullptr)
|
|
||||||
{
|
|
||||||
// this is not a file handle, send it to open
|
|
||||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "open");
|
|
||||||
lua_remove(L, -2); // remove io
|
|
||||||
|
|
||||||
lua_pushvalue(L, 1); // dupe arg
|
|
||||||
if (output)
|
|
||||||
{
|
|
||||||
lua_pushstring(L, "w");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pushstring(L, "r");
|
|
||||||
}
|
|
||||||
lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w')
|
|
||||||
// if this isn't a string ourio.open errors
|
|
||||||
|
|
||||||
// this leaves us with:
|
|
||||||
// 1. arg
|
|
||||||
// 2. new_file
|
|
||||||
lua_remove(L, 1); // remove arg, replacing it with new_file
|
|
||||||
}
|
|
||||||
|
|
||||||
// file handle, pass it off to realio.input
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
if (output)
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "output");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "input");
|
|
||||||
}
|
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
|
||||||
lua_pushvalue(L, 1); // duplicate arg
|
|
||||||
lua_call(L, 1, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
|
||||||
} // namespace
|
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||||
|
sol::protected_function_result res = func();
|
||||||
int io_input(lua_State *L)
|
return res;
|
||||||
{
|
|
||||||
return globalFileCommon(L, false);
|
|
||||||
}
|
}
|
||||||
|
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file)
|
||||||
int io_output(lua_State *L)
|
|
||||||
{
|
{
|
||||||
return globalFileCommon(L, true);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
}
|
if (pl == nullptr)
|
||||||
|
|
||||||
int io_close(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) > 1)
|
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error("internal error: no plugin");
|
||||||
L, "Too many arguments for io.close. Expected one or zero.");
|
|
||||||
}
|
}
|
||||||
if (lua_gettop(L) == 0)
|
sol::state_view lua(L);
|
||||||
|
|
||||||
|
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||||
|
sol::protected_function_result res = func(file);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
sol::variadic_results io_input_name(sol::this_state L, QString filename)
|
||||||
|
{
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
}
|
}
|
||||||
lua_getfield(L, -1, "close");
|
sol::state_view lua(L);
|
||||||
lua_pushvalue(L, -2);
|
auto res = io_open(L, std::move(filename), "r");
|
||||||
lua_call(L, 1, 0);
|
if (res.size() != 1)
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int io_flush(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) > 1)
|
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error(res.at(1).as<std::string>());
|
||||||
L, "Too many arguments for io.flush. Expected one or zero.");
|
|
||||||
}
|
}
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
auto obj = res.at(0);
|
||||||
lua_getfield(L, -1, "flush");
|
if (obj.get_type() != sol::type::userdata)
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_call(L, 1, 0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int io_read(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) > 1)
|
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error("a file must be a userdata.");
|
||||||
L, "Too many arguments for io.read. Expected one or zero.");
|
|
||||||
}
|
}
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
return io_input_file(L, obj);
|
||||||
lua_getfield(L, -1, "read");
|
|
||||||
lua_insert(L, 1);
|
|
||||||
lua_insert(L, 2);
|
|
||||||
lua_call(L, lua_gettop(L) - 1, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_write(lua_State *L)
|
sol::variadic_results io_output_argless(sol::this_state L)
|
||||||
{
|
{
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
lua_getfield(L, -1, "write");
|
if (pl == nullptr)
|
||||||
lua_insert(L, 1);
|
{
|
||||||
lua_insert(L, 2);
|
throw std::runtime_error("internal error: no plugin");
|
||||||
// (input)
|
}
|
||||||
// (input).read
|
sol::state_view lua(L);
|
||||||
// args
|
|
||||||
lua_call(L, lua_gettop(L) - 1, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int io_popen(lua_State *L)
|
auto func = lua.registry()[REG_REAL_IO_NAME]["output"];
|
||||||
|
sol::protected_function_result res = func();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
sol::variadic_results io_output_file(sol::this_state L, sol::userdata file)
|
||||||
{
|
{
|
||||||
return luaL_error(L, "io.popen: This function is a stub!");
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
}
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: no plugin");
|
||||||
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
|
||||||
int io_tmpfile(lua_State *L)
|
auto func = lua.registry()[REG_REAL_IO_NAME]["output"];
|
||||||
|
sol::protected_function_result res = func(file);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
sol::variadic_results io_output_name(sol::this_state L, QString filename)
|
||||||
{
|
{
|
||||||
return luaL_error(L, "io.tmpfile: This function is a stub!");
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: no plugin");
|
||||||
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto res = io_open(L, std::move(filename), "w");
|
||||||
|
if (res.size() != 1)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(res.at(1).as<std::string>());
|
||||||
|
}
|
||||||
|
auto obj = res.at(0);
|
||||||
|
if (obj.get_type() != sol::type::userdata)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: a file must be a userdata.");
|
||||||
|
}
|
||||||
|
return io_output_file(L, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTEND(*vararg)
|
bool io_close_argless(sol::this_state L)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto out = lua.registry()["_IO_output"];
|
||||||
|
return io_close_file(L, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool io_close_file(sol::this_state L, sol::userdata file)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
return file["close"](file);
|
||||||
|
}
|
||||||
|
|
||||||
|
void io_flush_argless(sol::this_state L)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto out = lua.registry()["_IO_output"];
|
||||||
|
io_flush_file(L, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void io_flush_file(sol::this_state L, sol::userdata file)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
file["flush"](file);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::variadic_results io_read(sol::this_state L, sol::variadic_args args)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto inp = lua.registry()["_IO_input"];
|
||||||
|
if (!inp.is<sol::userdata>())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Input not set to a file");
|
||||||
|
}
|
||||||
|
sol::protected_function read = inp["read"];
|
||||||
|
return read(inp, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::variadic_results io_write(sol::this_state L, sol::variadic_args args)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto out = lua.registry()["_IO_output"];
|
||||||
|
if (!out.is<sol::userdata>())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Output not set to a file");
|
||||||
|
}
|
||||||
|
sol::protected_function write = out["write"];
|
||||||
|
return write(out, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void io_popen()
|
||||||
|
{
|
||||||
|
throw std::runtime_error("io.popen: This function is a stub!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void io_tmpfile()
|
||||||
|
{
|
||||||
|
throw std::runtime_error("io.tmpfile: This function is a stub!");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include <QString>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
# include <sol/variadic_args.hpp>
|
||||||
|
# include <sol/variadic_results.hpp>
|
||||||
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
|
|
||||||
|
@ -8,7 +12,6 @@ namespace chatterino::lua::api {
|
||||||
// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality.
|
// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality.
|
||||||
|
|
||||||
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
||||||
const char *const REG_C2_IO_NAME = "c2io";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file.
|
* Opens a file.
|
||||||
|
@ -20,7 +23,9 @@ const char *const REG_C2_IO_NAME = "c2io";
|
||||||
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
||||||
* @exposed io.open
|
* @exposed io.open
|
||||||
*/
|
*/
|
||||||
int io_open(lua_State *L);
|
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||||
|
QString strmode);
|
||||||
|
sol::variadic_results io_open_modeless(sol::this_state L, QString filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
||||||
|
@ -32,7 +37,9 @@ int io_open(lua_State *L);
|
||||||
* @lua@param ...
|
* @lua@param ...
|
||||||
* @exposed io.lines
|
* @exposed io.lines
|
||||||
*/
|
*/
|
||||||
int io_lines(lua_State *L);
|
sol::variadic_results io_lines(sol::this_state L, QString filename,
|
||||||
|
sol::variadic_args args);
|
||||||
|
sol::variadic_results io_lines_noargs(sol::this_state L);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
||||||
|
@ -42,7 +49,9 @@ int io_lines(lua_State *L);
|
||||||
* @lua@return nil|FILE*
|
* @lua@return nil|FILE*
|
||||||
* @exposed io.input
|
* @exposed io.input
|
||||||
*/
|
*/
|
||||||
int io_input(lua_State *L);
|
sol::variadic_results io_input_argless(sol::this_state L);
|
||||||
|
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file);
|
||||||
|
sol::variadic_results io_input_name(sol::this_state L, QString filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file and sets it as default output or if given no arguments returns the default output
|
* Opens a file and sets it as default output or if given no arguments returns the default output
|
||||||
|
@ -52,7 +61,9 @@ int io_input(lua_State *L);
|
||||||
* @lua@return nil|FILE*
|
* @lua@return nil|FILE*
|
||||||
* @exposed io.output
|
* @exposed io.output
|
||||||
*/
|
*/
|
||||||
int io_output(lua_State *L);
|
sol::variadic_results io_output_argless(sol::this_state L);
|
||||||
|
sol::variadic_results io_output_file(sol::this_state L, sol::userdata file);
|
||||||
|
sol::variadic_results io_output_name(sol::this_state L, QString filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes given file or io.output() if not given.
|
* Closes given file or io.output() if not given.
|
||||||
|
@ -61,7 +72,8 @@ int io_output(lua_State *L);
|
||||||
* @lua@param nil|FILE*
|
* @lua@param nil|FILE*
|
||||||
* @exposed io.close
|
* @exposed io.close
|
||||||
*/
|
*/
|
||||||
int io_close(lua_State *L);
|
bool io_close_argless(sol::this_state L);
|
||||||
|
bool io_close_file(sol::this_state L, sol::userdata file);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flushes the buffer for given file or io.output() if not given.
|
* Flushes the buffer for given file or io.output() if not given.
|
||||||
|
@ -70,7 +82,8 @@ int io_close(lua_State *L);
|
||||||
* @lua@param nil|FILE*
|
* @lua@param nil|FILE*
|
||||||
* @exposed io.flush
|
* @exposed io.flush
|
||||||
*/
|
*/
|
||||||
int io_flush(lua_State *L);
|
void io_flush_argless(sol::this_state L);
|
||||||
|
void io_flush_file(sol::this_state L, sol::userdata file);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads some data from the default input file
|
* Reads some data from the default input file
|
||||||
|
@ -79,7 +92,7 @@ int io_flush(lua_State *L);
|
||||||
* @lua@param nil|string
|
* @lua@param nil|string
|
||||||
* @exposed io.read
|
* @exposed io.read
|
||||||
*/
|
*/
|
||||||
int io_read(lua_State *L);
|
sol::variadic_results io_read(sol::this_state L, sol::variadic_args args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes some data to the default output file
|
* Writes some data to the default output file
|
||||||
|
@ -88,10 +101,10 @@ int io_read(lua_State *L);
|
||||||
* @lua@param nil|string
|
* @lua@param nil|string
|
||||||
* @exposed io.write
|
* @exposed io.write
|
||||||
*/
|
*/
|
||||||
int io_write(lua_State *L);
|
sol::variadic_results io_write(sol::this_state L, sol::variadic_args args);
|
||||||
|
|
||||||
int io_popen(lua_State *L);
|
void io_popen();
|
||||||
int io_tmpfile(lua_State *L);
|
void io_tmpfile();
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -196,12 +197,12 @@ public:
|
||||||
std::unique_lock lock(this->mutex_);
|
std::unique_lock lock(this->mutex_);
|
||||||
|
|
||||||
Equals eq;
|
Equals eq;
|
||||||
for (int i = 0; i < this->buffer_.size(); ++i)
|
for (size_t i = 0; i < this->buffer_.size(); ++i)
|
||||||
{
|
{
|
||||||
if (eq(this->buffer_[i], needle))
|
if (eq(this->buffer_[i], needle))
|
||||||
{
|
{
|
||||||
this->buffer_[i] = replacement;
|
this->buffer_[i] = replacement;
|
||||||
return i;
|
return static_cast<int>(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -212,9 +213,10 @@ public:
|
||||||
*
|
*
|
||||||
* @param[in] index the index of the item to replace
|
* @param[in] index the index of the item to replace
|
||||||
* @param[in] replacement the item to put in place of the item at index
|
* @param[in] replacement the item to put in place of the item at index
|
||||||
|
* @param[out] prev (optional) the item located at @a index before replacing
|
||||||
* @return true if a replacement took place
|
* @return true if a replacement took place
|
||||||
*/
|
*/
|
||||||
bool replaceItem(size_t index, const T &replacement)
|
bool replaceItem(size_t index, const T &replacement, T *prev = nullptr)
|
||||||
{
|
{
|
||||||
std::unique_lock lock(this->mutex_);
|
std::unique_lock lock(this->mutex_);
|
||||||
|
|
||||||
|
@ -223,10 +225,46 @@ public:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->buffer_[index] = replacement;
|
if (prev)
|
||||||
|
{
|
||||||
|
*prev = std::exchange(this->buffer_[index], replacement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->buffer_[index] = replacement;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Replace the needle with the given item
|
||||||
|
*
|
||||||
|
* @param hint A hint on where the needle _might_ be
|
||||||
|
* @param[in] needle the item to search for
|
||||||
|
* @param[in] replacement the item to replace needle with
|
||||||
|
* @return the index of the replaced item, or -1 if no replacement took place
|
||||||
|
*/
|
||||||
|
int replaceItem(size_t hint, const T &needle, const T &replacement)
|
||||||
|
{
|
||||||
|
std::unique_lock lock(this->mutex_);
|
||||||
|
|
||||||
|
if (hint < this->buffer_.size() && this->buffer_[hint] == needle)
|
||||||
|
{
|
||||||
|
this->buffer_[hint] = replacement;
|
||||||
|
return static_cast<int>(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < this->buffer_.size(); ++i)
|
||||||
|
{
|
||||||
|
if (this->buffer_[i] == needle)
|
||||||
|
{
|
||||||
|
this->buffer_[i] = replacement;
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Inserts the given item before another item
|
* @brief Inserts the given item before another item
|
||||||
*
|
*
|
||||||
|
@ -315,6 +353,32 @@ public:
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find an item with a hint
|
||||||
|
*
|
||||||
|
* @param hint A hint on where the needle _might_ be
|
||||||
|
* @param predicate that will used to find the item
|
||||||
|
* @return the item and its index or none if it's not found
|
||||||
|
*/
|
||||||
|
std::optional<std::pair<size_t, T>> find(size_t hint, auto &&predicate)
|
||||||
|
{
|
||||||
|
std::unique_lock lock(this->mutex_);
|
||||||
|
|
||||||
|
if (hint < this->buffer_.size() && predicate(this->buffer_[hint]))
|
||||||
|
{
|
||||||
|
return std::pair{hint, this->buffer_[hint]};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < this->buffer_.size(); i++)
|
||||||
|
{
|
||||||
|
if (predicate(this->buffer_[i]))
|
||||||
|
{
|
||||||
|
return std::pair{i, this->buffer_[i]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the first item matching a predicate, checking in reverse
|
* @brief Returns the first item matching a predicate, checking in reverse
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ScrollbarHighlight;
|
||||||
|
|
||||||
struct Message;
|
struct Message;
|
||||||
using MessagePtr = std::shared_ptr<const Message>;
|
using MessagePtr = std::shared_ptr<const Message>;
|
||||||
|
using MessagePtrMut = std::shared_ptr<Message>;
|
||||||
struct Message {
|
struct Message {
|
||||||
Message();
|
Message();
|
||||||
~Message();
|
~Message();
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,8 +14,6 @@
|
||||||
|
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
|
||||||
#include <tuple>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
@ -31,6 +29,7 @@ struct AutomodUserAction;
|
||||||
struct AutomodInfoAction;
|
struct AutomodInfoAction;
|
||||||
struct Message;
|
struct Message;
|
||||||
using MessagePtr = std::shared_ptr<const Message>;
|
using MessagePtr = std::shared_ptr<const Message>;
|
||||||
|
using MessagePtrMut = std::shared_ptr<Message>;
|
||||||
|
|
||||||
class MessageElement;
|
class MessageElement;
|
||||||
class TextElement;
|
class TextElement;
|
||||||
|
@ -45,6 +44,7 @@ struct HelixVip;
|
||||||
using HelixModerator = HelixVip;
|
using HelixModerator = HelixVip;
|
||||||
struct ChannelPointReward;
|
struct ChannelPointReward;
|
||||||
struct DeleteAction;
|
struct DeleteAction;
|
||||||
|
struct TwitchEmoteOccurrence;
|
||||||
|
|
||||||
namespace linkparser {
|
namespace linkparser {
|
||||||
struct Parsed;
|
struct Parsed;
|
||||||
|
@ -65,6 +65,7 @@ struct LiveUpdatesUpdateEmoteSetMessageTag {
|
||||||
struct ImageUploaderResultTag {
|
struct ImageUploaderResultTag {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
const SystemMessageTag systemMessage{};
|
const SystemMessageTag systemMessage{};
|
||||||
const TimeoutMessageTag timeoutMessage{};
|
const TimeoutMessageTag timeoutMessage{};
|
||||||
const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{};
|
const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{};
|
||||||
|
@ -75,6 +76,7 @@ const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{};
|
||||||
// This signifies that you want to construct a message containing the result of
|
// This signifies that you want to construct a message containing the result of
|
||||||
// a successful image upload.
|
// a successful image upload.
|
||||||
const ImageUploaderResultTag imageUploaderResultMessage{};
|
const ImageUploaderResultTag imageUploaderResultMessage{};
|
||||||
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
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);
|
||||||
|
@ -86,39 +88,22 @@ struct MessageParseArgs {
|
||||||
bool trimSubscriberUsername = false;
|
bool trimSubscriberUsername = false;
|
||||||
bool isStaffOrBroadcaster = false;
|
bool isStaffOrBroadcaster = false;
|
||||||
bool isSubscriptionMessage = false;
|
bool isSubscriptionMessage = false;
|
||||||
|
bool allowIgnore = true;
|
||||||
|
bool isAction = false;
|
||||||
QString channelPointRewardId = "";
|
QString channelPointRewardId = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TwitchEmoteOccurrence {
|
struct HighlightAlert {
|
||||||
int start;
|
QUrl customSound;
|
||||||
int end;
|
bool playSound = false;
|
||||||
EmotePtr ptr;
|
bool windowAlert = false;
|
||||||
EmoteName name;
|
|
||||||
|
|
||||||
bool operator==(const TwitchEmoteOccurrence &other) const
|
|
||||||
{
|
|
||||||
return std::tie(this->start, this->end, this->ptr, this->name) ==
|
|
||||||
std::tie(other.start, other.end, other.ptr, other.name);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class MessageBuilder
|
class MessageBuilder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/// Build a message without a base IRC message.
|
/// Build a message without a base IRC message.
|
||||||
MessageBuilder();
|
MessageBuilder();
|
||||||
|
|
||||||
/// Build a message based on an incoming IRC PRIVMSG
|
|
||||||
explicit MessageBuilder(Channel *_channel,
|
|
||||||
const Communi::IrcPrivateMessage *_ircMessage,
|
|
||||||
const MessageParseArgs &_args);
|
|
||||||
|
|
||||||
/// Build a message based on an incoming IRC message (e.g. notice)
|
|
||||||
explicit MessageBuilder(Channel *_channel,
|
|
||||||
const Communi::IrcMessage *_ircMessage,
|
|
||||||
const MessageParseArgs &_args, QString content,
|
|
||||||
bool isAction);
|
|
||||||
|
|
||||||
MessageBuilder(SystemMessageTag, const QString &text,
|
MessageBuilder(SystemMessageTag, const QString &text,
|
||||||
const QTime &time = QTime::currentTime());
|
const QTime &time = QTime::currentTime());
|
||||||
MessageBuilder(TimeoutMessageTag, const QString &timeoutUser,
|
MessageBuilder(TimeoutMessageTag, const QString &timeoutUser,
|
||||||
|
@ -163,17 +148,10 @@ public:
|
||||||
|
|
||||||
~MessageBuilder() = default;
|
~MessageBuilder() = default;
|
||||||
|
|
||||||
QString userName;
|
|
||||||
|
|
||||||
/// The Twitch Channel the message was received in
|
|
||||||
TwitchChannel *twitchChannel = nullptr;
|
|
||||||
/// The Twitch Channel the message was sent in, according to the Shared Chat feature
|
|
||||||
TwitchChannel *sourceChannel = nullptr;
|
|
||||||
|
|
||||||
Message *operator->();
|
Message *operator->();
|
||||||
Message &message();
|
Message &message();
|
||||||
MessagePtr release();
|
MessagePtrMut release();
|
||||||
std::weak_ptr<Message> weakOf();
|
std::weak_ptr<const Message> weakOf();
|
||||||
|
|
||||||
void append(std::unique_ptr<MessageElement> element);
|
void append(std::unique_ptr<MessageElement> element);
|
||||||
void addLink(const linkparser::Parsed &parsedLink, const QString &source);
|
void addLink(const linkparser::Parsed &parsedLink, const QString &source);
|
||||||
|
@ -190,14 +168,8 @@ public:
|
||||||
return pointer;
|
return pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool isIgnored() const;
|
static void triggerHighlights(const Channel *channel,
|
||||||
bool isIgnoredReply() const;
|
const HighlightAlert &alert);
|
||||||
void triggerHighlights();
|
|
||||||
MessagePtr build();
|
|
||||||
|
|
||||||
void setThread(std::shared_ptr<MessageThread> thread);
|
|
||||||
void setParent(MessagePtr parent);
|
|
||||||
void setMessageOffset(int offset);
|
|
||||||
|
|
||||||
void appendChannelPointRewardMessage(const ChannelPointReward &reward,
|
void appendChannelPointRewardMessage(const ChannelPointReward &reward,
|
||||||
bool isMod, bool isBroadcaster);
|
bool isMod, bool isBroadcaster);
|
||||||
|
@ -237,110 +209,133 @@ public:
|
||||||
static MessagePtr makeLowTrustUpdateMessage(
|
static MessagePtr makeLowTrustUpdateMessage(
|
||||||
const PubSubLowTrustUsersMessage &action);
|
const PubSubLowTrustUsersMessage &action);
|
||||||
|
|
||||||
static std::unordered_map<QString, QString> parseBadgeInfoTag(
|
/// @brief Builds a message out of an `ircMessage`.
|
||||||
const QVariantMap &tags);
|
///
|
||||||
|
/// Building a message won't cause highlights to be triggered. They will
|
||||||
|
/// only be parsed. To trigger highlights (play sound etc.), use
|
||||||
|
/// triggerHighlights().
|
||||||
|
///
|
||||||
|
/// @param channel The channel this message was sent to. Must not be
|
||||||
|
/// `nullptr`.
|
||||||
|
/// @param ircMessage The original message. This can be any message
|
||||||
|
/// (PRIVMSG, USERNOTICE, etc.). Its content is not
|
||||||
|
/// accessed through this parameter but through `content`,
|
||||||
|
/// as the content might be inside a tag (e.g. gifts in a
|
||||||
|
/// USERNOTICE).
|
||||||
|
/// @param args Arguments from parsing a chat message.
|
||||||
|
/// @param content The message text. This isn't always the entire text. In
|
||||||
|
/// replies, the leading mention can be cut off.
|
||||||
|
/// See `messageOffset`.
|
||||||
|
/// @param messageOffset Starting offset to be used on index-based
|
||||||
|
/// operations on `content` such as parsing emotes.
|
||||||
|
/// For example:
|
||||||
|
/// ircMessage = "@hi there"
|
||||||
|
/// content = "there"
|
||||||
|
/// messageOffset_ = 4
|
||||||
|
/// The index 6 would resolve to 6 - 4 = 2 => 'e'
|
||||||
|
/// @param thread The reply thread this message is part of. If there's no
|
||||||
|
/// thread, this is an empty `shared_ptr`.
|
||||||
|
/// @param parent The direct parent this message is replying to. This does
|
||||||
|
/// not need to be the `thread`s root. If this message isn't
|
||||||
|
/// replying to anything, this is an empty `shared_ptr`.
|
||||||
|
///
|
||||||
|
/// @returns The built message and a highlight result. If the message is
|
||||||
|
/// ignored (e.g. from a blocked user), then the returned pointer
|
||||||
|
/// will be en empty `shared_ptr`.
|
||||||
|
static std::pair<MessagePtrMut, HighlightAlert> makeIrcMessage(
|
||||||
|
Channel *channel, const Communi::IrcMessage *ircMessage,
|
||||||
|
const MessageParseArgs &args, QString content,
|
||||||
|
QString::size_type messageOffset,
|
||||||
|
const std::shared_ptr<MessageThread> &thread = {},
|
||||||
|
const MessagePtr &parent = {});
|
||||||
|
|
||||||
// Parses "badges" tag which contains a comma separated list of key-value elements
|
static MessagePtrMut makeSystemMessageWithUser(
|
||||||
static std::vector<Badge> parseBadgeTag(const QVariantMap &tags);
|
const QString &text, const QString &loginName,
|
||||||
|
const QString &displayName, const MessageColor &userColor,
|
||||||
|
const QTime &time);
|
||||||
|
|
||||||
static std::vector<TwitchEmoteOccurrence> parseTwitchEmotes(
|
static MessagePtrMut makeSubgiftMessage(const QString &text,
|
||||||
const QVariantMap &tags, const QString &originalMessage,
|
const QVariantMap &tags,
|
||||||
int messageOffset);
|
const QTime &time);
|
||||||
|
|
||||||
static void processIgnorePhrases(
|
private:
|
||||||
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
|
struct TextState {
|
||||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
TwitchChannel *twitchChannel = nullptr;
|
||||||
|
bool hasBits = false;
|
||||||
|
bool bitsStacked = false;
|
||||||
|
int bitsLeft = 0;
|
||||||
|
};
|
||||||
|
void addEmoji(const EmotePtr &emote);
|
||||||
|
void addTextOrEmote(TextState &state, QString string);
|
||||||
|
|
||||||
protected:
|
Outcome tryAppendCheermote(TextState &state, const QString &string);
|
||||||
void addTextOrEmoji(EmotePtr emote);
|
Outcome tryAppendEmote(TwitchChannel *twitchChannel, const EmoteName &name);
|
||||||
void addTextOrEmoji(const QString &string_);
|
|
||||||
|
|
||||||
bool isEmpty() const;
|
bool isEmpty() const;
|
||||||
MessageElement &back();
|
MessageElement &back();
|
||||||
std::unique_ptr<MessageElement> releaseBack();
|
std::unique_ptr<MessageElement> releaseBack();
|
||||||
|
|
||||||
MessageColor textColor_ = MessageColor::Text;
|
|
||||||
|
|
||||||
// Helper method that emplaces some text stylized as system text
|
// Helper method that emplaces some text stylized as system text
|
||||||
// and then appends that text to the QString parameter "toUpdate".
|
// and then appends that text to the QString parameter "toUpdate".
|
||||||
// Returns the TextElement that was emplaced.
|
// Returns the TextElement that was emplaced.
|
||||||
TextElement *emplaceSystemTextAndUpdate(const QString &text,
|
TextElement *emplaceSystemTextAndUpdate(const QString &text,
|
||||||
QString &toUpdate);
|
QString &toUpdate);
|
||||||
|
|
||||||
std::shared_ptr<Message> message_;
|
|
||||||
|
|
||||||
void parse();
|
void parse();
|
||||||
void parseUsernameColor();
|
void parseUsernameColor(const QVariantMap &tags, const QString &userID);
|
||||||
void parseUsername();
|
void parseUsername(const Communi::IrcMessage *ircMessage,
|
||||||
void parseMessageID();
|
TwitchChannel *twitchChannel,
|
||||||
void parseRoomID();
|
bool trimSubscriberUsername);
|
||||||
|
void parseMessageID(const QVariantMap &tags);
|
||||||
|
|
||||||
|
/// Parses the room-ID this message was received in
|
||||||
|
///
|
||||||
|
/// @returns The room-ID
|
||||||
|
static QString parseRoomID(const QVariantMap &tags,
|
||||||
|
TwitchChannel *twitchChannel);
|
||||||
|
|
||||||
|
/// Parses the shared-chat information from this message.
|
||||||
|
///
|
||||||
|
/// @param tags The tags of the received message
|
||||||
|
/// @param twitchChannel The channel this message was received in
|
||||||
|
/// @returns The source channel - the channel this message originated from.
|
||||||
|
/// If there's no channel currently open, @a twitchChannel is
|
||||||
|
/// returned.
|
||||||
|
TwitchChannel *parseSharedChatInfo(const QVariantMap &tags,
|
||||||
|
TwitchChannel *twitchChannel);
|
||||||
|
|
||||||
// Parse & build thread information into the message
|
// Parse & build thread information into the message
|
||||||
// Will read information from thread_ or from IRC tags
|
// Will read information from thread_ or from IRC tags
|
||||||
void parseThread();
|
void parseThread(const QString &messageContent, const QVariantMap &tags,
|
||||||
|
const Channel *channel,
|
||||||
|
const std::shared_ptr<MessageThread> &thread,
|
||||||
|
const MessagePtr &parent);
|
||||||
// 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
|
||||||
void parseHighlights();
|
HighlightAlert parseHighlights(const QVariantMap &tags,
|
||||||
void appendChannelName();
|
const QString &originalMessage,
|
||||||
void appendUsername();
|
const MessageParseArgs &args);
|
||||||
|
|
||||||
/// Return the Twitch Channel this message originated from
|
void appendChannelName(const Channel *channel);
|
||||||
///
|
void appendUsername(const QVariantMap &tags, const MessageParseArgs &args);
|
||||||
/// Useful to handle messages from the "Shared Chat" feature
|
|
||||||
///
|
|
||||||
/// Can return nullptr
|
|
||||||
const TwitchChannel *getSourceChannel() const;
|
|
||||||
|
|
||||||
std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote(
|
|
||||||
const EmoteName &name) const;
|
|
||||||
Outcome tryAppendEmote(const EmoteName &name);
|
|
||||||
|
|
||||||
void addWords(const QStringList &words,
|
void addWords(const QStringList &words,
|
||||||
const std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
const std::vector<TwitchEmoteOccurrence> &twitchEmotes,
|
||||||
|
TextState &state);
|
||||||
|
|
||||||
void appendTwitchBadges();
|
void appendTwitchBadges(const QVariantMap &tags,
|
||||||
void appendChatterinoBadges();
|
TwitchChannel *twitchChannel);
|
||||||
void appendFfzBadges();
|
void appendChatterinoBadges(const QString &userID);
|
||||||
void appendSeventvBadges();
|
void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID);
|
||||||
Outcome tryParseCheermote(const QString &string);
|
void appendSeventvBadges(const QString &userID);
|
||||||
|
|
||||||
bool shouldAddModerationElements() const;
|
[[nodiscard]] static bool isIgnored(const QString &originalMessage,
|
||||||
|
const QString &userID,
|
||||||
|
const Channel *channel);
|
||||||
|
|
||||||
QString roomID_;
|
std::shared_ptr<Message> message_;
|
||||||
bool hasBits_ = false;
|
MessageColor textColor_ = MessageColor::Text;
|
||||||
QString bits;
|
|
||||||
int bitsLeft{};
|
|
||||||
bool bitsStacked = false;
|
|
||||||
bool historicalMessage_ = false;
|
|
||||||
std::shared_ptr<MessageThread> thread_;
|
|
||||||
MessagePtr parent_;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starting offset to be used on index-based operations on `originalMessage_`.
|
|
||||||
*
|
|
||||||
* For example:
|
|
||||||
* originalMessage_ = "there"
|
|
||||||
* messageOffset_ = 4
|
|
||||||
* (the irc message is "hey there")
|
|
||||||
*
|
|
||||||
* then the index 6 would resolve to 6 - 4 = 2 => 'e'
|
|
||||||
*/
|
|
||||||
int messageOffset_ = 0;
|
|
||||||
|
|
||||||
QString userId_;
|
|
||||||
bool senderIsBroadcaster{};
|
|
||||||
|
|
||||||
Channel *channel = nullptr;
|
|
||||||
const Communi::IrcMessage *ircMessage;
|
|
||||||
MessageParseArgs args;
|
|
||||||
const QVariantMap tags;
|
|
||||||
QString originalMessage_;
|
|
||||||
|
|
||||||
const bool action_{};
|
|
||||||
|
|
||||||
QColor usernameColor_ = {153, 153, 153};
|
QColor usernameColor_ = {153, 153, 153};
|
||||||
|
|
||||||
bool highlightAlert_ = false;
|
|
||||||
bool highlightSound_ = false;
|
|
||||||
std::optional<QUrl> highlightSoundCustomUrl_{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -66,6 +66,10 @@ enum class MessageElementFlag : int64_t {
|
||||||
BitsStatic = (1LL << 11),
|
BitsStatic = (1LL << 11),
|
||||||
BitsAnimated = (1LL << 12),
|
BitsAnimated = (1LL << 12),
|
||||||
|
|
||||||
|
// Slot 0: Twitch
|
||||||
|
// - Shared Channel indicator badge
|
||||||
|
BadgeSharedChannel = (1LL << 37),
|
||||||
|
|
||||||
// Slot 1: Twitch
|
// Slot 1: Twitch
|
||||||
// - Staff badge
|
// - Staff badge
|
||||||
// - Admin badge
|
// - Admin badge
|
||||||
|
@ -119,7 +123,7 @@ enum class MessageElementFlag : int64_t {
|
||||||
|
|
||||||
Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority |
|
Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority |
|
||||||
BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV |
|
BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV |
|
||||||
BadgeFfz,
|
BadgeFfz | BadgeSharedChannel,
|
||||||
|
|
||||||
ChannelName = (1LL << 20),
|
ChannelName = (1LL << 20),
|
||||||
|
|
||||||
|
|
121
src/messages/MessageSimilarity.cpp
Normal file
121
src/messages/MessageSimilarity.cpp
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
#include "messages/MessageSimilarity.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "messages/LimitedQueueSnapshot.hpp" // IWYU pragma: keep
|
||||||
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
float relativeSimilarity(QStringView str1, QStringView str2)
|
||||||
|
{
|
||||||
|
using SizeType = QStringView::size_type;
|
||||||
|
|
||||||
|
// Longest Common Substring Problem
|
||||||
|
std::vector<std::vector<int>> tree(str1.size(),
|
||||||
|
std::vector<int>(str2.size(), 0));
|
||||||
|
int z = 0;
|
||||||
|
|
||||||
|
for (SizeType i = 0; i < str1.size(); ++i)
|
||||||
|
{
|
||||||
|
for (SizeType j = 0; j < str2.size(); ++j)
|
||||||
|
{
|
||||||
|
if (str1[i] == str2[j])
|
||||||
|
{
|
||||||
|
if (i == 0 || j == 0)
|
||||||
|
{
|
||||||
|
tree[i][j] = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tree[i][j] = tree[i - 1][j - 1] + 1;
|
||||||
|
}
|
||||||
|
z = std::max(tree[i][j], z);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tree[i][j] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that no div by 0
|
||||||
|
if (z == 0)
|
||||||
|
{
|
||||||
|
return 0.F;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto div = std::max<>({static_cast<SizeType>(1), str1.size(), str2.size()});
|
||||||
|
|
||||||
|
return float(z) / float(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <std::ranges::bidirectional_range T>
|
||||||
|
float inMessages(const MessagePtr &msg, const T &messages)
|
||||||
|
{
|
||||||
|
float similarityPercent = 0.0F;
|
||||||
|
|
||||||
|
for (const auto &prevMsg :
|
||||||
|
messages | std::views::reverse |
|
||||||
|
std::views::take(getSettings()->hideSimilarMaxMessagesToCheck))
|
||||||
|
{
|
||||||
|
if (prevMsg->parseTime.secsTo(QTime::currentTime()) >=
|
||||||
|
getSettings()->hideSimilarMaxDelay)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (getSettings()->hideSimilarBySameUser &&
|
||||||
|
msg->loginName != prevMsg->loginName)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
similarityPercent = std::max(
|
||||||
|
similarityPercent,
|
||||||
|
relativeSimilarity(msg->messageText, prevMsg->messageText));
|
||||||
|
}
|
||||||
|
|
||||||
|
return similarityPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
template <std::ranges::bidirectional_range T>
|
||||||
|
void setSimilarityFlags(const MessagePtr &message, const T &messages)
|
||||||
|
{
|
||||||
|
if (getSettings()->similarityEnabled)
|
||||||
|
{
|
||||||
|
bool isMyself =
|
||||||
|
message->loginName ==
|
||||||
|
getApp()->getAccounts()->twitch.getCurrent()->getUserName();
|
||||||
|
bool hideMyself = getSettings()->hideSimilarMyself;
|
||||||
|
|
||||||
|
if (isMyself && !hideMyself)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inMessages(message, messages) > getSettings()->similarityPercentage)
|
||||||
|
{
|
||||||
|
message->flags.set(MessageFlag::Similar);
|
||||||
|
if (getSettings()->colorSimilarDisabled)
|
||||||
|
{
|
||||||
|
message->flags.set(MessageFlag::Disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template void setSimilarityFlags<std::vector<MessagePtr>>(
|
||||||
|
const MessagePtr &msg, const std::vector<MessagePtr> &messages);
|
||||||
|
template void setSimilarityFlags<LimitedQueueSnapshot<MessagePtr>>(
|
||||||
|
const MessagePtr &msg, const LimitedQueueSnapshot<MessagePtr> &messages);
|
||||||
|
|
||||||
|
} // namespace chatterino
|
11
src/messages/MessageSimilarity.hpp
Normal file
11
src/messages/MessageSimilarity.hpp
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "messages/Message.hpp"
|
||||||
|
|
||||||
|
#include <ranges>
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
template <std::ranges::bidirectional_range T>
|
||||||
|
void setSimilarityFlags(const MessagePtr &message, const T &messages);
|
||||||
|
|
||||||
|
} // namespace chatterino
|
67
src/messages/MessageSink.hpp
Normal file
67
src/messages/MessageSink.hpp
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/enums/MessageContext.hpp"
|
||||||
|
#include "common/FlagsEnum.hpp"
|
||||||
|
#include "messages/MessageFlag.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
class QStringView;
|
||||||
|
class QTime;
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct Message;
|
||||||
|
using MessagePtr = std::shared_ptr<const Message>;
|
||||||
|
|
||||||
|
enum class MessageSinkTrait : uint8_t {
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// Messages with the `Highlighted` and `ShowInMentions` flags should be
|
||||||
|
/// added to the global mentions channel when encountered.
|
||||||
|
AddMentionsToGlobalChannel = 1 << 0,
|
||||||
|
|
||||||
|
/// A channel-point redemption whose reward is not yet known should not be
|
||||||
|
/// added to this sink, but queued in the corresponding TwitchChannel
|
||||||
|
/// (`addQueuedRedemption`).
|
||||||
|
RequiresKnownChannelPointReward = 1 << 1,
|
||||||
|
};
|
||||||
|
using MessageSinkTraits = FlagsEnum<MessageSinkTrait>;
|
||||||
|
|
||||||
|
/// A generic interface for a managed buffer of `Message`s
|
||||||
|
class MessageSink
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~MessageSink() = default;
|
||||||
|
|
||||||
|
/// Add a message to this sink
|
||||||
|
///
|
||||||
|
/// @param message The message to add (non-null)
|
||||||
|
/// @param ctx The context in which this message is being added.
|
||||||
|
/// @param overridingFlags
|
||||||
|
virtual void addMessage(
|
||||||
|
MessagePtr message, MessageContext ctx,
|
||||||
|
std::optional<MessageFlags> overridingFlags = std::nullopt) = 0;
|
||||||
|
|
||||||
|
/// Adds a timeout message or merges it into an existing one
|
||||||
|
virtual void addOrReplaceTimeout(MessagePtr clearchatMessage,
|
||||||
|
QTime now) = 0;
|
||||||
|
|
||||||
|
/// Flags all messages as `Disabled`
|
||||||
|
virtual void disableAllMessages() = 0;
|
||||||
|
|
||||||
|
/// Searches for similar messages and flags this message as similar
|
||||||
|
/// (based on the current settings).
|
||||||
|
virtual void applySimilarityFilters(const MessagePtr &message) const = 0;
|
||||||
|
|
||||||
|
/// @brief Searches for a message by an ID
|
||||||
|
///
|
||||||
|
/// If there is no message found, an empty shared-pointer is returned.
|
||||||
|
virtual MessagePtr findMessageByID(QStringView id) = 0;
|
||||||
|
|
||||||
|
/// Behaviour to be exercised when parsing/building messages for this sink.
|
||||||
|
virtual MessageSinkTraits sinkTraits() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -212,7 +212,8 @@ void MessageLayoutContainer::breakLine()
|
||||||
this->lineStart_ = this->elements_.size();
|
this->lineStart_ = this->elements_.size();
|
||||||
// this->currentX = (int)(this->scale * 8);
|
// this->currentX = (int)(this->scale * 8);
|
||||||
|
|
||||||
if (this->canCollapse() && this->line_ + 1 >= maxUncollapsedLines())
|
if (this->canCollapse() &&
|
||||||
|
static_cast<int>(this->line_ + 1) >= maxUncollapsedLines())
|
||||||
{
|
{
|
||||||
this->canAddMessages_ = false;
|
this->canAddMessages_ = false;
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -514,9 +514,10 @@ int TextLayoutElement::getXFromIndex(size_t index)
|
||||||
else if (index < static_cast<size_t>(this->getText().size()))
|
else if (index < static_cast<size_t>(this->getText().size()))
|
||||||
{
|
{
|
||||||
int x = 0;
|
int x = 0;
|
||||||
for (int i = 0; i < index; i++)
|
for (size_t i = 0; i < index; i++)
|
||||||
{
|
{
|
||||||
x += metrics.horizontalAdvance(this->getText()[i]);
|
x += metrics.horizontalAdvance(
|
||||||
|
this->getText()[static_cast<QString::size_type>(i)]);
|
||||||
}
|
}
|
||||||
return x + this->getRect().left();
|
return x + this->getRect().left();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "providers/ffz/FfzBadges.hpp"
|
#include "providers/ffz/FfzBadges.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
#include "common/network/NetworkRequest.hpp"
|
#include "common/network/NetworkRequest.hpp"
|
||||||
#include "common/network/NetworkResult.hpp"
|
#include "common/network/NetworkResult.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
|
@ -109,4 +110,30 @@ void FfzBadges::load()
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FfzBadges::registerBadge(int badgeID, Badge badge)
|
||||||
|
{
|
||||||
|
assert(getApp()->isTest());
|
||||||
|
|
||||||
|
std::unique_lock lock(this->mutex_);
|
||||||
|
|
||||||
|
this->badges.emplace(badgeID, std::move(badge));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FfzBadges::assignBadgeToUser(const UserId &userID, int badgeID)
|
||||||
|
{
|
||||||
|
assert(getApp()->isTest());
|
||||||
|
|
||||||
|
std::unique_lock lock(this->mutex_);
|
||||||
|
|
||||||
|
auto it = this->userBadges.find(userID.string);
|
||||||
|
if (it != this->userBadges.end())
|
||||||
|
{
|
||||||
|
it->second.emplace(badgeID);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->userBadges.emplace(userID.string, std::set{badgeID});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -30,6 +30,9 @@ public:
|
||||||
std::vector<Badge> getUserBadges(const UserId &id);
|
std::vector<Badge> getUserBadges(const UserId &id);
|
||||||
std::optional<Badge> getBadge(int badgeID) const;
|
std::optional<Badge> getBadge(int badgeID) const;
|
||||||
|
|
||||||
|
void registerBadge(int badgeID, Badge badge);
|
||||||
|
void assignBadgeToUser(const UserId &userID, int badgeID);
|
||||||
|
|
||||||
void load();
|
void load();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
#include "common/Env.hpp"
|
#include "common/Env.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
|
#include "util/VectorMessageSink.hpp"
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
@ -40,7 +42,13 @@ std::vector<Communi::IrcMessage *> parseRecentMessages(
|
||||||
std::vector<MessagePtr> buildRecentMessages(
|
std::vector<MessagePtr> buildRecentMessages(
|
||||||
std::vector<Communi::IrcMessage *> &messages, Channel *channel)
|
std::vector<Communi::IrcMessage *> &messages, Channel *channel)
|
||||||
{
|
{
|
||||||
std::vector<MessagePtr> allBuiltMessages;
|
VectorMessageSink sink({}, MessageFlag::RecentMessage);
|
||||||
|
|
||||||
|
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel);
|
||||||
|
if (!twitchChannel)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
for (auto *message : messages)
|
for (auto *message : messages)
|
||||||
{
|
{
|
||||||
|
@ -58,24 +66,16 @@ std::vector<MessagePtr> buildRecentMessages(
|
||||||
auto msg = makeSystemMessage(
|
auto msg = makeSystemMessage(
|
||||||
QLocale().toString(msgDate, QLocale::LongFormat),
|
QLocale().toString(msgDate, QLocale::LongFormat),
|
||||||
QTime(0, 0));
|
QTime(0, 0));
|
||||||
msg->flags.set(MessageFlag::RecentMessage);
|
sink.addMessage(msg, MessageContext::Original);
|
||||||
allBuiltMessages.emplace_back(msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto builtMessages = IrcMessageHandler::parseMessageWithReply(
|
IrcMessageHandler::parseMessageInto(message, sink, twitchChannel);
|
||||||
channel, message, allBuiltMessages);
|
|
||||||
|
|
||||||
for (const auto &builtMessage : builtMessages)
|
|
||||||
{
|
|
||||||
builtMessage->flags.set(MessageFlag::RecentMessage);
|
|
||||||
allBuiltMessages.emplace_back(builtMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
message->deleteLater();
|
message->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
return allBuiltMessages;
|
return std::move(sink).takeMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -59,7 +59,7 @@ void SeventvBadges::registerBadge(const QJsonObject &badgeJson)
|
||||||
|
|
||||||
auto emote = Emote{
|
auto emote = Emote{
|
||||||
.name = EmoteName{},
|
.name = EmoteName{},
|
||||||
.images = SeventvEmotes::createImageSet(badgeJson),
|
.images = SeventvEmotes::createImageSet(badgeJson, true),
|
||||||
.tooltip = Tooltip{badgeJson["tooltip"].toString()},
|
.tooltip = Tooltip{badgeJson["tooltip"].toString()},
|
||||||
.homePage = Url{},
|
.homePage = Url{},
|
||||||
.id = EmoteId{badgeID},
|
.id = EmoteId{badgeID},
|
||||||
|
|
|
@ -79,7 +79,8 @@ bool isZeroWidthRecommended(const QJsonObject &emoteData)
|
||||||
Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal)
|
Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal)
|
||||||
{
|
{
|
||||||
return Tooltip{QString("%1<br>%2 7TV Emote<br>By: %3")
|
return Tooltip{QString("%1<br>%2 7TV Emote<br>By: %3")
|
||||||
.arg(name, isGlobal ? "Global" : "Channel",
|
.arg(name.toHtmlEscaped(),
|
||||||
|
isGlobal ? "Global" : "Channel",
|
||||||
author.isEmpty() ? "<deleted>" : author)};
|
author.isEmpty() ? "<deleted>" : author)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +88,8 @@ Tooltip createAliasedTooltip(const QString &name, const QString &baseName,
|
||||||
const QString &author, bool isGlobal)
|
const QString &author, bool isGlobal)
|
||||||
{
|
{
|
||||||
return Tooltip{QString("%1<br>Alias of %2<br>%3 7TV Emote<br>By: %4")
|
return Tooltip{QString("%1<br>Alias of %2<br>%3 7TV Emote<br>By: %4")
|
||||||
.arg(name, baseName, isGlobal ? "Global" : "Channel",
|
.arg(name.toHtmlEscaped(), baseName.toHtmlEscaped(),
|
||||||
|
isGlobal ? "Global" : "Channel",
|
||||||
author.isEmpty() ? "<deleted>" : author)};
|
author.isEmpty() ? "<deleted>" : author)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,12 +108,18 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote,
|
||||||
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
|
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
|
||||||
author.string, isGlobal)
|
author.string, isGlobal)
|
||||||
: createTooltip(emoteName.string, author.string, isGlobal);
|
: createTooltip(emoteName.string, author.string, isGlobal);
|
||||||
auto imageSet = SeventvEmotes::createImageSet(emoteData);
|
auto imageSet = SeventvEmotes::createImageSet(emoteData, false);
|
||||||
|
|
||||||
auto emote =
|
auto emote = Emote({
|
||||||
Emote({emoteName, imageSet, tooltip,
|
emoteName,
|
||||||
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth, emoteId,
|
imageSet,
|
||||||
author, makeConditionedOptional(aliasedName, baseEmoteName)});
|
tooltip,
|
||||||
|
Url{EMOTE_LINK_FORMAT.arg(emoteId.string)},
|
||||||
|
zeroWidth,
|
||||||
|
emoteId,
|
||||||
|
author,
|
||||||
|
makeConditionedOptional(aliasedName, baseEmoteName),
|
||||||
|
});
|
||||||
|
|
||||||
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
|
return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()};
|
||||||
}
|
}
|
||||||
|
@ -427,7 +435,8 @@ void SeventvEmotes::getEmoteSet(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData)
|
ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData,
|
||||||
|
bool useStatic)
|
||||||
{
|
{
|
||||||
auto host = emoteData["host"].toObject();
|
auto host = emoteData["host"].toObject();
|
||||||
// "//cdn.7tv[...]"
|
// "//cdn.7tv[...]"
|
||||||
|
@ -463,9 +472,21 @@ ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData)
|
||||||
baseWidth = width;
|
baseWidth = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto image = Image::fromUrl(
|
auto name = [&] {
|
||||||
{QString("https:%1/%2").arg(baseUrl, file["name"].toString())},
|
if (useStatic)
|
||||||
scale, {static_cast<int>(width), file["height"].toInt(16)});
|
{
|
||||||
|
auto staticName = file["static_name"].toString();
|
||||||
|
if (!staticName.isEmpty())
|
||||||
|
{
|
||||||
|
return staticName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file["name"].toString();
|
||||||
|
}();
|
||||||
|
|
||||||
|
auto image =
|
||||||
|
Image::fromUrl({QString("https:%1/%2").arg(baseUrl, name)}, scale,
|
||||||
|
{static_cast<int>(width), file["height"].toInt(16)});
|
||||||
|
|
||||||
sizes.at(nextSize) = image;
|
sizes.at(nextSize) = image;
|
||||||
nextSize++;
|
nextSize++;
|
||||||
|
|
|
@ -153,8 +153,10 @@ public:
|
||||||
* Creates an image set from a 7TV emote or badge.
|
* Creates an image set from a 7TV emote or badge.
|
||||||
*
|
*
|
||||||
* @param emoteData { host: { files: [], url } }
|
* @param emoteData { host: { files: [], url } }
|
||||||
|
* @param useStatic use static version if possible
|
||||||
*/
|
*/
|
||||||
static ImageSet createImageSet(const QJsonObject &emoteData);
|
static ImageSet createImageSet(const QJsonObject &emoteData,
|
||||||
|
bool useStatic);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
Atomic<std::shared_ptr<const EmoteMap>> global_;
|
||||||
|
|
|
@ -233,6 +233,10 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case SubscriptionType::ResetEntitlement: {
|
||||||
|
// unhandled (not clear what we'd do here yet)
|
||||||
|
}
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
qCDebug(chatterinoSeventvEventAPI)
|
qCDebug(chatterinoSeventvEventAPI)
|
||||||
<< "Unknown subscription type:"
|
<< "Unknown subscription type:"
|
||||||
|
|
|
@ -27,6 +27,7 @@ enum class SubscriptionType {
|
||||||
CreateEntitlement,
|
CreateEntitlement,
|
||||||
UpdateEntitlement,
|
UpdateEntitlement,
|
||||||
DeleteEntitlement,
|
DeleteEntitlement,
|
||||||
|
ResetEntitlement,
|
||||||
|
|
||||||
INVALID,
|
INVALID,
|
||||||
};
|
};
|
||||||
|
@ -119,6 +120,8 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
|
||||||
return "entitlement.update";
|
return "entitlement.update";
|
||||||
case SubscriptionType::DeleteEntitlement:
|
case SubscriptionType::DeleteEntitlement:
|
||||||
return "entitlement.delete";
|
return "entitlement.delete";
|
||||||
|
case SubscriptionType::ResetEntitlement:
|
||||||
|
return "entitlement.reset";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return default_tag;
|
return default_tag;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -16,6 +16,7 @@ struct Message;
|
||||||
using MessagePtr = std::shared_ptr<const Message>;
|
using MessagePtr = std::shared_ptr<const Message>;
|
||||||
class TwitchChannel;
|
class TwitchChannel;
|
||||||
class TwitchMessageBuilder;
|
class TwitchMessageBuilder;
|
||||||
|
class MessageSink;
|
||||||
|
|
||||||
struct ClearChatMessage {
|
struct ClearChatMessage {
|
||||||
MessagePtr message;
|
MessagePtr message;
|
||||||
|
@ -33,30 +34,34 @@ public:
|
||||||
* Parse an IRC message into 0 or more Chatterino messages
|
* Parse an IRC message into 0 or more Chatterino messages
|
||||||
* Takes previously loaded messages into consideration to add reply contexts
|
* Takes previously loaded messages into consideration to add reply contexts
|
||||||
**/
|
**/
|
||||||
static std::vector<MessagePtr> parseMessageWithReply(
|
static void parseMessageInto(Communi::IrcMessage *message,
|
||||||
Channel *channel, Communi::IrcMessage *message,
|
MessageSink &sink, TwitchChannel *channel);
|
||||||
std::vector<MessagePtr> &otherLoaded);
|
|
||||||
|
|
||||||
void handlePrivMessage(Communi::IrcPrivateMessage *message,
|
void handlePrivMessage(Communi::IrcPrivateMessage *message,
|
||||||
ITwitchIrcServer &twitchServer);
|
ITwitchIrcServer &twitchServer);
|
||||||
|
static void parsePrivMessageInto(Communi::IrcPrivateMessage *message,
|
||||||
|
MessageSink &sink, TwitchChannel *channel);
|
||||||
|
|
||||||
void handleRoomStateMessage(Communi::IrcMessage *message);
|
void handleRoomStateMessage(Communi::IrcMessage *message);
|
||||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||||
void handleWhisperMessage(Communi::IrcMessage *ircMessage);
|
|
||||||
|
|
||||||
|
void handleWhisperMessage(Communi::IrcMessage *ircMessage);
|
||||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||||
ITwitchIrcServer &twitchServer);
|
ITwitchIrcServer &twitchServer);
|
||||||
|
static void parseUserNoticeMessageInto(Communi::IrcMessage *message,
|
||||||
|
MessageSink &sink,
|
||||||
|
TwitchChannel *channel);
|
||||||
|
|
||||||
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||||
|
|
||||||
void handleJoinMessage(Communi::IrcMessage *message);
|
void handleJoinMessage(Communi::IrcMessage *message);
|
||||||
void handlePartMessage(Communi::IrcMessage *message);
|
void handlePartMessage(Communi::IrcMessage *message);
|
||||||
|
|
||||||
void addMessage(Communi::IrcMessage *message, const ChannelPtr &chan,
|
static void addMessage(Communi::IrcMessage *message, MessageSink &sink,
|
||||||
const QString &originalContent, ITwitchIrcServer &server,
|
TwitchChannel *chan, const QString &originalContent,
|
||||||
bool isSub, bool isAction);
|
ITwitchIrcServer &twitch, bool isSub, bool isAction);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static float similarity(const MessagePtr &msg,
|
static float similarity(const MessagePtr &msg,
|
||||||
|
|
|
@ -174,6 +174,17 @@ void TwitchAccount::unblockUser(const QString &userId, const QObject *caller,
|
||||||
std::move(onFailure));
|
std::move(onFailure));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchAccount::blockUserLocally(const QString &userID)
|
||||||
|
{
|
||||||
|
assertInGuiThread();
|
||||||
|
assert(getApp()->isTest());
|
||||||
|
|
||||||
|
TwitchUser blockedUser;
|
||||||
|
blockedUser.id = userID;
|
||||||
|
this->ignores_.insert(blockedUser);
|
||||||
|
this->ignoresUserIds_.insert(blockedUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
const std::unordered_set<TwitchUser> &TwitchAccount::blocks() const
|
const std::unordered_set<TwitchUser> &TwitchAccount::blocks() const
|
||||||
{
|
{
|
||||||
assertInGuiThread();
|
assertInGuiThread();
|
||||||
|
@ -336,6 +347,12 @@ SharedAccessGuard<std::shared_ptr<const EmoteMap>> TwitchAccount::accessEmotes()
|
||||||
return this->emotes_.accessConst();
|
return this->emotes_.accessConst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchAccount::setEmotes(std::shared_ptr<const EmoteMap> emotes)
|
||||||
|
{
|
||||||
|
assert(getApp()->isTest());
|
||||||
|
*this->emotes_.access() = std::move(emotes);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<EmotePtr> TwitchAccount::twitchEmote(const EmoteName &name) const
|
std::optional<EmotePtr> TwitchAccount::twitchEmote(const EmoteName &name) const
|
||||||
{
|
{
|
||||||
auto emotes = this->emotes_.accessConst();
|
auto emotes = this->emotes_.accessConst();
|
||||||
|
|
|
@ -71,6 +71,8 @@ public:
|
||||||
std::function<void()> onSuccess,
|
std::function<void()> onSuccess,
|
||||||
std::function<void()> onFailure);
|
std::function<void()> onFailure);
|
||||||
|
|
||||||
|
void blockUserLocally(const QString &userID);
|
||||||
|
|
||||||
[[nodiscard]] const std::unordered_set<TwitchUser> &blocks() const;
|
[[nodiscard]] const std::unordered_set<TwitchUser> &blocks() const;
|
||||||
[[nodiscard]] const std::unordered_set<QString> &blockedUserIds() const;
|
[[nodiscard]] const std::unordered_set<QString> &blockedUserIds() const;
|
||||||
|
|
||||||
|
@ -83,16 +85,21 @@ public:
|
||||||
/// Returns true if the account has access to the given emote set
|
/// Returns true if the account has access to the given emote set
|
||||||
bool hasEmoteSet(const EmoteSetId &id) const;
|
bool hasEmoteSet(const EmoteSetId &id) const;
|
||||||
|
|
||||||
/// Return a map of emote sets the account has access to
|
/// Returns a map of emote sets the account has access to
|
||||||
///
|
///
|
||||||
/// Key being the emote set ID, and contents being information about the emote set
|
/// Key being the emote set ID, and contents being information about the emote set
|
||||||
/// and the emotes contained in the emote set
|
/// and the emotes contained in the emote set
|
||||||
SharedAccessGuard<std::shared_ptr<const TwitchEmoteSetMap>>
|
SharedAccessGuard<std::shared_ptr<const TwitchEmoteSetMap>>
|
||||||
accessEmoteSets() const;
|
accessEmoteSets() const;
|
||||||
|
|
||||||
/// Return a map of emotes the account has access to
|
/// Returns a map of emotes the account has access to
|
||||||
SharedAccessGuard<std::shared_ptr<const EmoteMap>> accessEmotes() const;
|
SharedAccessGuard<std::shared_ptr<const EmoteMap>> accessEmotes() const;
|
||||||
|
|
||||||
|
/// Sets the emotes this account has access to
|
||||||
|
///
|
||||||
|
/// This should only be used in tests.
|
||||||
|
void setEmotes(std::shared_ptr<const EmoteMap> emotes);
|
||||||
|
|
||||||
/// Return the emote by emote name if the account has access to the emote
|
/// Return the emote by emote name if the account has access to the emote
|
||||||
std::optional<EmotePtr> twitchEmote(const EmoteName &name) const;
|
std::optional<EmotePtr> twitchEmote(const EmoteName &name) const;
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,10 @@ public:
|
||||||
|
|
||||||
void loadTwitchBadges();
|
void loadTwitchBadges();
|
||||||
|
|
||||||
private:
|
|
||||||
/// Loads the badges shipped with Chatterino (twitch-badges.json)
|
/// Loads the badges shipped with Chatterino (twitch-badges.json)
|
||||||
void loadLocalBadges();
|
void loadLocalBadges();
|
||||||
|
|
||||||
|
private:
|
||||||
void loaded();
|
void loaded();
|
||||||
void loadEmoteImage(const QString &name, const ImagePtr &image,
|
void loadEmoteImage(const QString &name, const ImagePtr &image,
|
||||||
BadgeIconCallback &&callback);
|
BadgeIconCallback &&callback);
|
||||||
|
|
|
@ -452,8 +452,8 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
|
||||||
if (reward.id == msg.rewardID)
|
if (reward.id == msg.rewardID)
|
||||||
{
|
{
|
||||||
IrcMessageHandler::instance().addMessage(
|
IrcMessageHandler::instance().addMessage(
|
||||||
msg.message.get(), shared_from_this(),
|
msg.message.get(), *this, this, msg.originalContent,
|
||||||
msg.originalContent, *server, false, false);
|
*server, false, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -462,6 +462,14 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::addKnownChannelPointReward(const ChannelPointReward &reward)
|
||||||
|
{
|
||||||
|
assert(getApp()->isTest());
|
||||||
|
|
||||||
|
auto channelPointRewards = this->channelPointRewards_.access();
|
||||||
|
channelPointRewards->try_emplace(reward.id, reward);
|
||||||
|
}
|
||||||
|
|
||||||
bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId)
|
bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId)
|
||||||
{
|
{
|
||||||
const auto &pointRewards = this->channelPointRewards_.accessConst();
|
const auto &pointRewards = this->channelPointRewards_.accessConst();
|
||||||
|
@ -1348,8 +1356,6 @@ void TwitchChannel::loadRecentMessages()
|
||||||
{
|
{
|
||||||
msgs.push_back(msg);
|
msgs.push_back(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
tc->addRecentChatter(msg->displayName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getApp()->getTwitch()->getMentionsChannel()->fillInMissingMessages(
|
getApp()->getTwitch()->getMentionsChannel()->fillInMissingMessages(
|
||||||
|
@ -1560,7 +1566,7 @@ void TwitchChannel::refreshBadges()
|
||||||
getHelix()->getChannelBadges(
|
getHelix()->getChannelBadges(
|
||||||
this->roomId(),
|
this->roomId(),
|
||||||
// successCallback
|
// successCallback
|
||||||
[this, weak = weakOf<Channel>(this)](auto channelBadges) {
|
[this, weak = weakOf<Channel>(this)](const auto &channelBadges) {
|
||||||
auto shared = weak.lock();
|
auto shared = weak.lock();
|
||||||
if (!shared)
|
if (!shared)
|
||||||
{
|
{
|
||||||
|
@ -1568,31 +1574,7 @@ void TwitchChannel::refreshBadges()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto badgeSets = this->badgeSets_.access();
|
this->addTwitchBadgeSets(channelBadges);
|
||||||
|
|
||||||
for (const auto &badgeSet : channelBadges.badgeSets)
|
|
||||||
{
|
|
||||||
const auto &setID = badgeSet.setID;
|
|
||||||
for (const auto &version : badgeSet.versions)
|
|
||||||
{
|
|
||||||
auto emote = Emote{
|
|
||||||
.name = EmoteName{},
|
|
||||||
.images =
|
|
||||||
ImageSet{
|
|
||||||
Image::fromUrl(version.imageURL1x, 1,
|
|
||||||
BASE_BADGE_SIZE),
|
|
||||||
Image::fromUrl(version.imageURL2x, .5,
|
|
||||||
BASE_BADGE_SIZE * 2),
|
|
||||||
Image::fromUrl(version.imageURL4x, .25,
|
|
||||||
BASE_BADGE_SIZE * 4),
|
|
||||||
},
|
|
||||||
.tooltip = Tooltip{version.title},
|
|
||||||
.homePage = version.clickURL,
|
|
||||||
};
|
|
||||||
(*badgeSets)[setID][version.id] =
|
|
||||||
std::make_shared<Emote>(emote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// failureCallback
|
// failureCallback
|
||||||
[this, weak = weakOf<Channel>(this)](auto error, auto message) {
|
[this, weak = weakOf<Channel>(this)](auto error, auto message) {
|
||||||
|
@ -1623,6 +1605,33 @@ void TwitchChannel::refreshBadges()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::addTwitchBadgeSets(const HelixChannelBadges &channelBadges)
|
||||||
|
{
|
||||||
|
auto badgeSets = this->badgeSets_.access();
|
||||||
|
|
||||||
|
for (const auto &badgeSet : channelBadges.badgeSets)
|
||||||
|
{
|
||||||
|
const auto &setID = badgeSet.setID;
|
||||||
|
for (const auto &version : badgeSet.versions)
|
||||||
|
{
|
||||||
|
auto emote = Emote{
|
||||||
|
.name = EmoteName{},
|
||||||
|
.images =
|
||||||
|
ImageSet{
|
||||||
|
Image::fromUrl(version.imageURL1x, 1, BASE_BADGE_SIZE),
|
||||||
|
Image::fromUrl(version.imageURL2x, .5,
|
||||||
|
BASE_BADGE_SIZE * 2),
|
||||||
|
Image::fromUrl(version.imageURL4x, .25,
|
||||||
|
BASE_BADGE_SIZE * 4),
|
||||||
|
},
|
||||||
|
.tooltip = Tooltip{version.title},
|
||||||
|
.homePage = version.clickURL,
|
||||||
|
};
|
||||||
|
(*badgeSets)[setID][version.id] = std::make_shared<Emote>(emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TwitchChannel::refreshCheerEmotes()
|
void TwitchChannel::refreshCheerEmotes()
|
||||||
{
|
{
|
||||||
getHelix()->getCheermotes(
|
getHelix()->getCheermotes(
|
||||||
|
@ -1635,74 +1644,75 @@ void TwitchChannel::refreshCheerEmotes()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<CheerEmoteSet> emoteSets;
|
this->setCheerEmoteSets(cheermoteSets);
|
||||||
|
|
||||||
for (const auto &set : cheermoteSets)
|
|
||||||
{
|
|
||||||
auto cheerEmoteSet = CheerEmoteSet();
|
|
||||||
cheerEmoteSet.regex = QRegularExpression(
|
|
||||||
"^" + set.prefix + "([1-9][0-9]*)$",
|
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
|
||||||
|
|
||||||
for (const auto &tier : set.tiers)
|
|
||||||
{
|
|
||||||
CheerEmote cheerEmote;
|
|
||||||
|
|
||||||
cheerEmote.color = QColor(tier.color);
|
|
||||||
cheerEmote.minBits = tier.minBits;
|
|
||||||
cheerEmote.regex = cheerEmoteSet.regex;
|
|
||||||
|
|
||||||
// TODO(pajlada): We currently hardcode dark here :|
|
|
||||||
// We will continue to do so for now since we haven't had to
|
|
||||||
// solve that anywhere else
|
|
||||||
|
|
||||||
// Combine the prefix (e.g. BibleThump) with the tier (1, 100 etc.)
|
|
||||||
auto emoteTooltip =
|
|
||||||
set.prefix + tier.id + "<br>Twitch Cheer Emote";
|
|
||||||
auto makeImageSet = [](const HelixCheermoteImage &image) {
|
|
||||||
return ImageSet{
|
|
||||||
Image::fromUrl(image.imageURL1x, 1.0,
|
|
||||||
BASE_BADGE_SIZE),
|
|
||||||
Image::fromUrl(image.imageURL2x, 0.5,
|
|
||||||
BASE_BADGE_SIZE * 2),
|
|
||||||
Image::fromUrl(image.imageURL4x, 0.25,
|
|
||||||
BASE_BADGE_SIZE * 4),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
cheerEmote.animatedEmote = std::make_shared<Emote>(Emote{
|
|
||||||
.name = EmoteName{"cheer emote"},
|
|
||||||
.images = makeImageSet(tier.darkAnimated),
|
|
||||||
.tooltip = Tooltip{emoteTooltip},
|
|
||||||
.homePage = Url{},
|
|
||||||
});
|
|
||||||
cheerEmote.staticEmote = std::make_shared<Emote>(Emote{
|
|
||||||
.name = EmoteName{"cheer emote"},
|
|
||||||
.images = makeImageSet(tier.darkStatic),
|
|
||||||
.tooltip = Tooltip{emoteTooltip},
|
|
||||||
.homePage = Url{},
|
|
||||||
});
|
|
||||||
|
|
||||||
cheerEmoteSet.cheerEmotes.emplace_back(
|
|
||||||
std::move(cheerEmote));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort cheermotes by cost
|
|
||||||
std::sort(cheerEmoteSet.cheerEmotes.begin(),
|
|
||||||
cheerEmoteSet.cheerEmotes.end(),
|
|
||||||
[](const auto &lhs, const auto &rhs) {
|
|
||||||
return lhs.minBits > rhs.minBits;
|
|
||||||
});
|
|
||||||
|
|
||||||
emoteSets.emplace_back(std::move(cheerEmoteSet));
|
|
||||||
}
|
|
||||||
|
|
||||||
*this->cheerEmoteSets_.access() = std::move(emoteSets);
|
|
||||||
},
|
},
|
||||||
[] {
|
[] {
|
||||||
// Failure
|
// Failure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::setCheerEmoteSets(
|
||||||
|
const std::vector<HelixCheermoteSet> &cheermoteSets)
|
||||||
|
{
|
||||||
|
std::vector<CheerEmoteSet> emoteSets;
|
||||||
|
|
||||||
|
for (const auto &set : cheermoteSets)
|
||||||
|
{
|
||||||
|
auto cheerEmoteSet = CheerEmoteSet();
|
||||||
|
cheerEmoteSet.regex =
|
||||||
|
QRegularExpression("^" + set.prefix + "([1-9][0-9]*)$",
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
|
for (const auto &tier : set.tiers)
|
||||||
|
{
|
||||||
|
CheerEmote cheerEmote;
|
||||||
|
|
||||||
|
cheerEmote.color = QColor(tier.color);
|
||||||
|
cheerEmote.minBits = tier.minBits;
|
||||||
|
cheerEmote.regex = cheerEmoteSet.regex;
|
||||||
|
|
||||||
|
// TODO(pajlada): We currently hardcode dark here :|
|
||||||
|
// We will continue to do so for now since we haven't had to
|
||||||
|
// solve that anywhere else
|
||||||
|
|
||||||
|
// Combine the prefix (e.g. BibleThump) with the tier (1, 100 etc.)
|
||||||
|
auto emoteTooltip = set.prefix + tier.id + "<br>Twitch Cheer Emote";
|
||||||
|
auto makeImageSet = [](const HelixCheermoteImage &image) {
|
||||||
|
return ImageSet{
|
||||||
|
Image::fromUrl(image.imageURL1x, 1.0, BASE_BADGE_SIZE),
|
||||||
|
Image::fromUrl(image.imageURL2x, 0.5, BASE_BADGE_SIZE * 2),
|
||||||
|
Image::fromUrl(image.imageURL4x, 0.25, BASE_BADGE_SIZE * 4),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
cheerEmote.animatedEmote = std::make_shared<Emote>(Emote{
|
||||||
|
.name = EmoteName{u"cheer emote"_s},
|
||||||
|
.images = makeImageSet(tier.darkAnimated),
|
||||||
|
.tooltip = Tooltip{emoteTooltip},
|
||||||
|
.homePage = Url{},
|
||||||
|
});
|
||||||
|
cheerEmote.staticEmote = std::make_shared<Emote>(Emote{
|
||||||
|
.name = EmoteName{u"cheer emote"_s},
|
||||||
|
.images = makeImageSet(tier.darkStatic),
|
||||||
|
.tooltip = Tooltip{emoteTooltip},
|
||||||
|
.homePage = Url{},
|
||||||
|
});
|
||||||
|
|
||||||
|
cheerEmoteSet.cheerEmotes.emplace_back(std::move(cheerEmote));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort cheermotes by cost
|
||||||
|
std::sort(cheerEmoteSet.cheerEmotes.begin(),
|
||||||
|
cheerEmoteSet.cheerEmotes.end(),
|
||||||
|
[](const auto &lhs, const auto &rhs) {
|
||||||
|
return lhs.minBits > rhs.minBits;
|
||||||
|
});
|
||||||
|
|
||||||
|
emoteSets.emplace_back(std::move(cheerEmoteSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
*this->cheerEmoteSets_.access() = std::move(emoteSets);
|
||||||
|
}
|
||||||
|
|
||||||
void TwitchChannel::createClip()
|
void TwitchChannel::createClip()
|
||||||
{
|
{
|
||||||
if (!this->isLive())
|
if (!this->isLive())
|
||||||
|
@ -1859,6 +1869,12 @@ std::vector<FfzBadges::Badge> TwitchChannel::ffzChannelBadges(
|
||||||
return badges;
|
return badges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::setFfzChannelBadges(FfzChannelBadgeMap map)
|
||||||
|
{
|
||||||
|
this->tgFfzChannelBadges_.guard();
|
||||||
|
this->ffzChannelBadges_ = std::move(map);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
|
std::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
|
||||||
{
|
{
|
||||||
return this->ffzCustomModBadge_.get();
|
return this->ffzCustomModBadge_.get();
|
||||||
|
@ -1869,6 +1885,16 @@ std::optional<EmotePtr> TwitchChannel::ffzCustomVipBadge() const
|
||||||
return this->ffzCustomVipBadge_.get();
|
return this->ffzCustomVipBadge_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::setFfzCustomModBadge(std::optional<EmotePtr> badge)
|
||||||
|
{
|
||||||
|
this->ffzCustomModBadge_.set(std::move(badge));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TwitchChannel::setFfzCustomVipBadge(std::optional<EmotePtr> badge)
|
||||||
|
{
|
||||||
|
this->ffzCustomVipBadge_.set(std::move(badge));
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string) const
|
std::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string) const
|
||||||
{
|
{
|
||||||
auto sets = this->cheerEmoteSets_.access();
|
auto sets = this->cheerEmoteSets_.access();
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
class TestIrcMessageHandlerP;
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
enum class HighlightState;
|
enum class HighlightState;
|
||||||
|
@ -51,6 +53,9 @@ struct ChannelPointReward;
|
||||||
class MessageThread;
|
class MessageThread;
|
||||||
struct CheerEmoteSet;
|
struct CheerEmoteSet;
|
||||||
struct HelixStream;
|
struct HelixStream;
|
||||||
|
struct HelixCheermoteSet;
|
||||||
|
struct HelixGlobalBadges;
|
||||||
|
using HelixChannelBadges = HelixGlobalBadges;
|
||||||
|
|
||||||
class TwitchIrcServer;
|
class TwitchIrcServer;
|
||||||
|
|
||||||
|
@ -59,22 +64,55 @@ const int MAX_QUEUED_REDEMPTIONS = 16;
|
||||||
class TwitchChannel final : public Channel, public ChannelChatters
|
class TwitchChannel final : public Channel, public ChannelChatters
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @lua@class StreamStatus
|
||||||
|
*/
|
||||||
struct StreamStatus {
|
struct StreamStatus {
|
||||||
|
/**
|
||||||
|
* @lua@field live boolean
|
||||||
|
*/
|
||||||
bool live = false;
|
bool live = false;
|
||||||
bool rerun = false;
|
bool rerun = false;
|
||||||
|
/**
|
||||||
|
* @lua@field viewer_count number
|
||||||
|
*/
|
||||||
unsigned viewerCount = 0;
|
unsigned viewerCount = 0;
|
||||||
|
/**
|
||||||
|
* @lua@field title string Stream title or last stream title
|
||||||
|
*/
|
||||||
QString title;
|
QString title;
|
||||||
|
/**
|
||||||
|
* @lua@field game_name string
|
||||||
|
*/
|
||||||
QString game;
|
QString game;
|
||||||
|
/**
|
||||||
|
* @lua@field game_id string
|
||||||
|
*/
|
||||||
QString gameId;
|
QString gameId;
|
||||||
QString uptime;
|
QString uptime;
|
||||||
|
/**
|
||||||
|
* @lua@field uptime number Seconds since the stream started.
|
||||||
|
*/
|
||||||
int uptimeSeconds = 0;
|
int uptimeSeconds = 0;
|
||||||
QString streamType;
|
QString streamType;
|
||||||
QString streamId;
|
QString streamId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@class RoomModes
|
||||||
|
*/
|
||||||
struct RoomModes {
|
struct RoomModes {
|
||||||
|
/**
|
||||||
|
* @lua@field subscriber_only boolean
|
||||||
|
*/
|
||||||
bool submode = false;
|
bool submode = false;
|
||||||
|
/**
|
||||||
|
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||||
|
*/
|
||||||
bool r9k = false;
|
bool r9k = false;
|
||||||
|
/**
|
||||||
|
* @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||||
|
*/
|
||||||
bool emoteOnly = false;
|
bool emoteOnly = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,6 +121,8 @@ public:
|
||||||
* Special cases:
|
* Special cases:
|
||||||
* -1 = follower mode off
|
* -1 = follower mode off
|
||||||
* 0 = follower mode on, no time requirement
|
* 0 = follower mode on, no time requirement
|
||||||
|
*
|
||||||
|
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
|
||||||
**/
|
**/
|
||||||
int followerOnly = -1;
|
int followerOnly = -1;
|
||||||
|
|
||||||
|
@ -90,6 +130,8 @@ public:
|
||||||
* @brief Number of seconds required to wait before typing emotes
|
* @brief Number of seconds required to wait before typing emotes
|
||||||
*
|
*
|
||||||
* 0 = slow mode off
|
* 0 = slow mode off
|
||||||
|
*
|
||||||
|
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||||
**/
|
**/
|
||||||
int slowMode = 0;
|
int slowMode = 0;
|
||||||
};
|
};
|
||||||
|
@ -195,9 +237,15 @@ public:
|
||||||
* Returns a list of channel-specific FrankerFaceZ badges for the given user
|
* Returns a list of channel-specific FrankerFaceZ badges for the given user
|
||||||
*/
|
*/
|
||||||
std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const;
|
std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const;
|
||||||
|
void setFfzChannelBadges(FfzChannelBadgeMap map);
|
||||||
|
void setFfzCustomModBadge(std::optional<EmotePtr> badge);
|
||||||
|
void setFfzCustomVipBadge(std::optional<EmotePtr> badge);
|
||||||
|
|
||||||
|
void addTwitchBadgeSets(const HelixChannelBadges &channelBadges);
|
||||||
|
|
||||||
// Cheers
|
// Cheers
|
||||||
std::optional<CheerEmote> cheerEmote(const QString &string) const;
|
std::optional<CheerEmote> cheerEmote(const QString &string) const;
|
||||||
|
void setCheerEmoteSets(const std::vector<HelixCheermoteSet> &cheermoteSets);
|
||||||
|
|
||||||
// Replies
|
// Replies
|
||||||
/**
|
/**
|
||||||
|
@ -243,6 +291,10 @@ public:
|
||||||
* This will look at queued up partial messages, and if one is found it will add the queued up partial messages fully hydrated.
|
* This will look at queued up partial messages, and if one is found it will add the queued up partial messages fully hydrated.
|
||||||
**/
|
**/
|
||||||
void addChannelPointReward(const ChannelPointReward &reward);
|
void addChannelPointReward(const ChannelPointReward &reward);
|
||||||
|
/// Adds @a reward to the known rewards
|
||||||
|
///
|
||||||
|
/// Unlike in #addChannelPointReward(), no message will be sent.
|
||||||
|
void addKnownChannelPointReward(const ChannelPointReward &reward);
|
||||||
bool isChannelPointRewardKnown(const QString &rewardId);
|
bool isChannelPointRewardKnown(const QString &rewardId);
|
||||||
std::optional<ChannelPointReward> channelPointReward(
|
std::optional<ChannelPointReward> channelPointReward(
|
||||||
const QString &rewardId) const;
|
const QString &rewardId) const;
|
||||||
|
@ -449,6 +501,7 @@ private:
|
||||||
friend class MessageBuilder;
|
friend class MessageBuilder;
|
||||||
friend class IrcMessageHandler;
|
friend class IrcMessageHandler;
|
||||||
friend class Commands_E2E_Test;
|
friend class Commands_E2E_Test;
|
||||||
|
friend class ::TestIrcMessageHandlerP;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -494,17 +494,14 @@ TwitchEmoteSetMeta getTwitchEmoteSetMeta(const HelixChannelEmote &emote)
|
||||||
return u"x-c2-globals"_s;
|
return u"x-c2-globals"_s;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!emote.setID.isEmpty())
|
// some bit emote-sets have an id, but we want to combine them into a
|
||||||
|
// single set
|
||||||
|
if (isBits)
|
||||||
{
|
{
|
||||||
return emote.setID;
|
return TWITCH_BIT_EMOTE_SET_PREFIX % emote.ownerID;
|
||||||
}
|
}
|
||||||
|
// isSub
|
||||||
if (isSub)
|
return TWITCH_SUB_EMOTE_SET_PREFIX % emote.ownerID;
|
||||||
{
|
|
||||||
return TWITCH_SUB_EMOTE_SET_PREFIX % emote.ownerID;
|
|
||||||
}
|
|
||||||
// isBits
|
|
||||||
return TWITCH_BIT_EMOTE_SET_PREFIX % emote.ownerID;
|
|
||||||
}();
|
}();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
166
src/providers/twitch/TwitchIrc.cpp
Normal file
166
src/providers/twitch/TwitchIrc.cpp
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
#include "providers/twitch/TwitchIrc.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
|
#include "common/Aliases.hpp"
|
||||||
|
#include "common/QLogging.hpp"
|
||||||
|
#include "singletons/Emotes.hpp"
|
||||||
|
#include "util/IrcHelpers.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
void appendTwitchEmoteOccurrences(const QString &emote,
|
||||||
|
std::vector<TwitchEmoteOccurrence> &vec,
|
||||||
|
const std::vector<int> &correctPositions,
|
||||||
|
const QString &originalMessage,
|
||||||
|
int messageOffset)
|
||||||
|
{
|
||||||
|
auto *app = getApp();
|
||||||
|
if (!emote.contains(':'))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parameters = emote.split(':');
|
||||||
|
|
||||||
|
if (parameters.length() < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto id = EmoteId{parameters.at(0)};
|
||||||
|
|
||||||
|
auto occurrences = parameters.at(1).split(',');
|
||||||
|
|
||||||
|
for (const QString &occurrence : occurrences)
|
||||||
|
{
|
||||||
|
auto coords = occurrence.split('-');
|
||||||
|
|
||||||
|
if (coords.length() < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto from = coords.at(0).toUInt() - messageOffset;
|
||||||
|
auto to = coords.at(1).toUInt() - messageOffset;
|
||||||
|
auto maxPositions = correctPositions.size();
|
||||||
|
if (from > to || to >= maxPositions)
|
||||||
|
{
|
||||||
|
// Emote coords are out of range
|
||||||
|
qCDebug(chatterinoTwitch)
|
||||||
|
<< "Emote coords" << from << "-" << to << "are out of range ("
|
||||||
|
<< maxPositions << ")";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto start = correctPositions[from];
|
||||||
|
auto end = correctPositions[to];
|
||||||
|
if (start > end || start < 0 || end > originalMessage.length())
|
||||||
|
{
|
||||||
|
// Emote coords are out of range from the modified character positions
|
||||||
|
qCDebug(chatterinoTwitch) << "Emote coords" << from << "-" << to
|
||||||
|
<< "are out of range after offsets ("
|
||||||
|
<< originalMessage.length() << ")";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto name = EmoteName{originalMessage.mid(start, end - start + 1)};
|
||||||
|
TwitchEmoteOccurrence emoteOccurrence{
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
app->getEmotes()->getTwitchEmotes()->getOrCreateEmote(id, name),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
if (emoteOccurrence.ptr == nullptr)
|
||||||
|
{
|
||||||
|
qCDebug(chatterinoTwitch)
|
||||||
|
<< "nullptr" << emoteOccurrence.name.string;
|
||||||
|
}
|
||||||
|
vec.push_back(std::move(emoteOccurrence));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
std::unordered_map<QString, QString> parseBadgeInfoTag(const QVariantMap &tags)
|
||||||
|
{
|
||||||
|
std::unordered_map<QString, QString> infoMap;
|
||||||
|
|
||||||
|
auto infoIt = tags.constFind("badge-info");
|
||||||
|
if (infoIt == tags.end())
|
||||||
|
{
|
||||||
|
return infoMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
for (const QString &badge : info)
|
||||||
|
{
|
||||||
|
infoMap.emplace(slashKeyValue(badge));
|
||||||
|
}
|
||||||
|
|
||||||
|
return infoMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Badge> parseBadgeTag(const QVariantMap &tags)
|
||||||
|
{
|
||||||
|
std::vector<Badge> b;
|
||||||
|
|
||||||
|
auto badgesIt = tags.constFind("badges");
|
||||||
|
if (badgesIt == tags.end())
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
for (const QString &badge : badges)
|
||||||
|
{
|
||||||
|
if (!badge.contains('/'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pair = slashKeyValue(badge);
|
||||||
|
b.emplace_back(Badge{pair.first, pair.second});
|
||||||
|
}
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TwitchEmoteOccurrence> parseTwitchEmotes(const QVariantMap &tags,
|
||||||
|
const QString &content,
|
||||||
|
int messageOffset)
|
||||||
|
{
|
||||||
|
// Twitch emotes
|
||||||
|
std::vector<TwitchEmoteOccurrence> twitchEmotes;
|
||||||
|
|
||||||
|
auto emotesTag = tags.find("emotes");
|
||||||
|
|
||||||
|
if (emotesTag == tags.end())
|
||||||
|
{
|
||||||
|
return twitchEmotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList emoteString = emotesTag.value().toString().split('/');
|
||||||
|
std::vector<int> correctPositions;
|
||||||
|
for (int i = 0; i < content.size(); ++i)
|
||||||
|
{
|
||||||
|
if (!content.at(i).isLowSurrogate())
|
||||||
|
{
|
||||||
|
correctPositions.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const QString &emote : emoteString)
|
||||||
|
{
|
||||||
|
appendTwitchEmoteOccurrences(emote, twitchEmotes, correctPositions,
|
||||||
|
content, messageOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return twitchEmotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
67
src/providers/twitch/TwitchIrc.hpp
Normal file
67
src/providers/twitch/TwitchIrc.hpp
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "messages/Emote.hpp"
|
||||||
|
#include "providers/twitch/TwitchBadge.hpp"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct TwitchEmoteOccurrence {
|
||||||
|
int start;
|
||||||
|
int end;
|
||||||
|
EmotePtr ptr;
|
||||||
|
EmoteName name;
|
||||||
|
|
||||||
|
bool operator==(const TwitchEmoteOccurrence &other) const
|
||||||
|
{
|
||||||
|
return std::tie(this->start, this->end, this->ptr, this->name) ==
|
||||||
|
std::tie(other.start, other.end, other.ptr, other.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Parses the `badge-info` tag of an IRC message
|
||||||
|
///
|
||||||
|
/// The `badge-info` tag maps badge-names to a value. Subscriber badges, for
|
||||||
|
/// example, are mapped to the number of months the chatter is subscribed for.
|
||||||
|
///
|
||||||
|
/// **Example**:
|
||||||
|
/// `badge-info=subscriber/22` would be parsed as `{ subscriber => 22 }`
|
||||||
|
///
|
||||||
|
/// @param tags The tags of the IRC message
|
||||||
|
/// @returns A map of badge-names to their values
|
||||||
|
std::unordered_map<QString, QString> parseBadgeInfoTag(const QVariantMap &tags);
|
||||||
|
|
||||||
|
/// @brief Parses the `badges` tag of an IRC message
|
||||||
|
///
|
||||||
|
/// The `badges` tag contains a comma separated list of key-value elements which
|
||||||
|
/// make up the name and version of each badge.
|
||||||
|
///
|
||||||
|
/// **Example**:
|
||||||
|
/// `badges=broadcaster/1,subscriber/18` would be parsed as
|
||||||
|
/// `[(broadcaster, 1), (subscriber, 18)]`
|
||||||
|
///
|
||||||
|
/// @param tags The tags of the IRC message
|
||||||
|
/// @returns A list of badges (name and version)
|
||||||
|
std::vector<Badge> parseBadgeTag(const QVariantMap &tags);
|
||||||
|
|
||||||
|
/// @brief Parses Twitch emotes in an IRC message
|
||||||
|
///
|
||||||
|
/// @param tags The tags of the IRC message
|
||||||
|
/// @param content The message text. This might be shortened due to skipping
|
||||||
|
/// content at the start. `messageOffset` describes this offset.
|
||||||
|
/// @param messageOffset The offset of `content` compared to the original
|
||||||
|
/// message text. Used for calculating indices into the
|
||||||
|
/// message. An offset of 3, for example, indicates that
|
||||||
|
/// `content` excludes the first three characters of the
|
||||||
|
/// original message (`@a foo` (original message) -> `foo`
|
||||||
|
/// (content)).
|
||||||
|
/// @returns A list of emotes and their positions
|
||||||
|
std::vector<TwitchEmoteOccurrence> parseTwitchEmotes(const QVariantMap &tags,
|
||||||
|
const QString &content,
|
||||||
|
int messageOffset);
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -312,7 +312,7 @@ void TwitchIrcServer::initialize()
|
||||||
postToThread([chan, action] {
|
postToThread([chan, action] {
|
||||||
MessageBuilder msg(action);
|
MessageBuilder msg(action);
|
||||||
msg->flags.set(MessageFlag::PubSub);
|
msg->flags.set(MessageFlag::PubSub);
|
||||||
chan->addOrReplaceTimeout(msg.release());
|
chan->addOrReplaceTimeout(msg.release(), QTime::currentTime());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1909,7 +1909,7 @@ void Helix::updateChatSettings(
|
||||||
|
|
||||||
void Helix::onFetchChattersSuccess(
|
void Helix::onFetchChattersSuccess(
|
||||||
std::shared_ptr<HelixChatters> finalChatters, QString broadcasterID,
|
std::shared_ptr<HelixChatters> finalChatters, QString broadcasterID,
|
||||||
QString moderatorID, int maxChattersToFetch,
|
QString moderatorID, size_t maxChattersToFetch,
|
||||||
ResultCallback<HelixChatters> successCallback,
|
ResultCallback<HelixChatters> successCallback,
|
||||||
FailureCallback<HelixGetChattersError, QString> failureCallback,
|
FailureCallback<HelixGetChattersError, QString> failureCallback,
|
||||||
HelixChatters chatters)
|
HelixChatters chatters)
|
||||||
|
@ -2022,7 +2022,7 @@ void Helix::fetchChatters(
|
||||||
|
|
||||||
void Helix::onFetchModeratorsSuccess(
|
void Helix::onFetchModeratorsSuccess(
|
||||||
std::shared_ptr<std::vector<HelixModerator>> finalModerators,
|
std::shared_ptr<std::vector<HelixModerator>> finalModerators,
|
||||||
QString broadcasterID, int maxModeratorsToFetch,
|
QString broadcasterID, size_t maxModeratorsToFetch,
|
||||||
ResultCallback<std::vector<HelixModerator>> successCallback,
|
ResultCallback<std::vector<HelixModerator>> successCallback,
|
||||||
FailureCallback<HelixGetModeratorsError, QString> failureCallback,
|
FailureCallback<HelixGetModeratorsError, QString> failureCallback,
|
||||||
HelixModerators moderators)
|
HelixModerators moderators)
|
||||||
|
@ -2459,7 +2459,7 @@ void Helix::sendWhisper(
|
||||||
|
|
||||||
// https://dev.twitch.tv/docs/api/reference#get-chatters
|
// https://dev.twitch.tv/docs/api/reference#get-chatters
|
||||||
void Helix::getChatters(
|
void Helix::getChatters(
|
||||||
QString broadcasterID, QString moderatorID, int maxChattersToFetch,
|
QString broadcasterID, QString moderatorID, size_t maxChattersToFetch,
|
||||||
ResultCallback<HelixChatters> successCallback,
|
ResultCallback<HelixChatters> successCallback,
|
||||||
FailureCallback<HelixGetChattersError, QString> failureCallback)
|
FailureCallback<HelixGetChattersError, QString> failureCallback)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1073,7 +1073,7 @@ public:
|
||||||
// This will follow the returned cursor and return up to `maxChattersToFetch` chatters
|
// This will follow the returned cursor and return up to `maxChattersToFetch` chatters
|
||||||
// https://dev.twitch.tv/docs/api/reference#get-chatters
|
// https://dev.twitch.tv/docs/api/reference#get-chatters
|
||||||
virtual void getChatters(
|
virtual void getChatters(
|
||||||
QString broadcasterID, QString moderatorID, int maxChattersToFetch,
|
QString broadcasterID, QString moderatorID, size_t maxChattersToFetch,
|
||||||
ResultCallback<HelixChatters> successCallback,
|
ResultCallback<HelixChatters> successCallback,
|
||||||
FailureCallback<HelixGetChattersError, QString> failureCallback) = 0;
|
FailureCallback<HelixGetChattersError, QString> failureCallback) = 0;
|
||||||
|
|
||||||
|
@ -1417,7 +1417,7 @@ public:
|
||||||
// This will follow the returned cursor and return up to `maxChattersToFetch` chatters
|
// This will follow the returned cursor and return up to `maxChattersToFetch` chatters
|
||||||
// https://dev.twitch.tv/docs/api/reference#get-chatters
|
// https://dev.twitch.tv/docs/api/reference#get-chatters
|
||||||
void getChatters(
|
void getChatters(
|
||||||
QString broadcasterID, QString moderatorID, int maxChattersToFetch,
|
QString broadcasterID, QString moderatorID, size_t maxChattersToFetch,
|
||||||
ResultCallback<HelixChatters> successCallback,
|
ResultCallback<HelixChatters> successCallback,
|
||||||
FailureCallback<HelixGetChattersError, QString> failureCallback) final;
|
FailureCallback<HelixGetChattersError, QString> failureCallback) final;
|
||||||
|
|
||||||
|
@ -1505,7 +1505,7 @@ protected:
|
||||||
// Recursive boy
|
// Recursive boy
|
||||||
void onFetchChattersSuccess(
|
void onFetchChattersSuccess(
|
||||||
std::shared_ptr<HelixChatters> finalChatters, QString broadcasterID,
|
std::shared_ptr<HelixChatters> finalChatters, QString broadcasterID,
|
||||||
QString moderatorID, int maxChattersToFetch,
|
QString moderatorID, size_t maxChattersToFetch,
|
||||||
ResultCallback<HelixChatters> successCallback,
|
ResultCallback<HelixChatters> successCallback,
|
||||||
FailureCallback<HelixGetChattersError, QString> failureCallback,
|
FailureCallback<HelixGetChattersError, QString> failureCallback,
|
||||||
HelixChatters chatters);
|
HelixChatters chatters);
|
||||||
|
@ -1520,7 +1520,7 @@ protected:
|
||||||
// Recursive boy
|
// Recursive boy
|
||||||
void onFetchModeratorsSuccess(
|
void onFetchModeratorsSuccess(
|
||||||
std::shared_ptr<std::vector<HelixModerator>> finalModerators,
|
std::shared_ptr<std::vector<HelixModerator>> finalModerators,
|
||||||
QString broadcasterID, int maxModeratorsToFetch,
|
QString broadcasterID, size_t maxModeratorsToFetch,
|
||||||
ResultCallback<std::vector<HelixModerator>> successCallback,
|
ResultCallback<std::vector<HelixModerator>> successCallback,
|
||||||
FailureCallback<HelixGetModeratorsError, QString> failureCallback,
|
FailureCallback<HelixGetModeratorsError, QString> failureCallback,
|
||||||
HelixModerators moderators);
|
HelixModerators moderators);
|
||||||
|
|
|
@ -212,6 +212,10 @@ public:
|
||||||
BoolSetting useCustomFfzVipBadges = {
|
BoolSetting useCustomFfzVipBadges = {
|
||||||
"/appearance/badges/useCustomFfzVipBadges", true};
|
"/appearance/badges/useCustomFfzVipBadges", true};
|
||||||
BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true};
|
BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true};
|
||||||
|
QSizeSetting lastPopupSize = {
|
||||||
|
"/appearance/lastPopup/size",
|
||||||
|
{300, 500},
|
||||||
|
};
|
||||||
|
|
||||||
/// Behaviour
|
/// Behaviour
|
||||||
BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages",
|
BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages",
|
||||||
|
|
|
@ -195,6 +195,7 @@ void WindowManager::updateWordTypeMask()
|
||||||
flags.set(settings->animateEmotes ? MEF::BitsAnimated : MEF::BitsStatic);
|
flags.set(settings->animateEmotes ? MEF::BitsAnimated : MEF::BitsStatic);
|
||||||
|
|
||||||
// badges
|
// badges
|
||||||
|
flags.set(MEF::BadgeSharedChannel);
|
||||||
flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority
|
flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority
|
||||||
: MEF::None);
|
: MEF::None);
|
||||||
flags.set(settings->showBadgesPredictions ? MEF::BadgePredictions
|
flags.set(settings->showBadgesPredictions ? MEF::BadgePredictions
|
||||||
|
|
|
@ -26,6 +26,7 @@ void GIFTimer::initialize()
|
||||||
|
|
||||||
QObject::connect(&this->timer, &QTimer::timeout, [this] {
|
QObject::connect(&this->timer, &QTimer::timeout, [this] {
|
||||||
if (getSettings()->animationsWhenFocused &&
|
if (getSettings()->animationsWhenFocused &&
|
||||||
|
this->openOverlayWindows_ == 0 &&
|
||||||
QApplication::activeWindow() == nullptr)
|
QApplication::activeWindow() == nullptr)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -18,9 +18,21 @@ public:
|
||||||
return this->position_;
|
return this->position_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void registerOpenOverlayWindow()
|
||||||
|
{
|
||||||
|
this->openOverlayWindows_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterOpenOverlayWindow()
|
||||||
|
{
|
||||||
|
assert(this->openOverlayWindows_ >= 1);
|
||||||
|
this->openOverlayWindows_--;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTimer timer;
|
QTimer timer;
|
||||||
long unsigned position_{};
|
long unsigned position_{};
|
||||||
|
size_t openOverlayWindows_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
|
|
||||||
|
#include "Application.hpp"
|
||||||
#include "providers/twitch/TwitchCommon.hpp"
|
#include "providers/twitch/TwitchCommon.hpp"
|
||||||
|
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
@ -301,4 +302,16 @@ QString unescapeZeroWidthJoiner(QString escaped)
|
||||||
return escaped;
|
return escaped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QLocale getSystemLocale()
|
||||||
|
{
|
||||||
|
#ifdef CHATTERINO_WITH_TESTS
|
||||||
|
if (getApp()->isTest())
|
||||||
|
{
|
||||||
|
return {QLocale::English};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return QLocale::system();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue