mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Add basic lua scripting capabilities (#4341)
The scripting capabilities is locked behind a cmake flag, and is not enabled by default. Co-authored-by: nerix <nerixdev@outlook.de> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
5836073d52
commit
5ba809804e
|
@ -53,3 +53,7 @@ CheckOptions:
|
|||
value: camelBack
|
||||
- key: readability-implicit-bool-conversion.AllowPointerConditions
|
||||
value: true
|
||||
|
||||
# Lua state
|
||||
- key: readability-identifier-naming.LocalPointerIgnoredRegexp
|
||||
value: ^L$
|
||||
|
|
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
|
@ -27,6 +27,7 @@ jobs:
|
|||
qt-version: [5.15.2, 5.12.12]
|
||||
pch: [true]
|
||||
force-lto: [false]
|
||||
plugins: [false]
|
||||
skip_artifact: ["no"]
|
||||
crashpad: [true]
|
||||
include:
|
||||
|
@ -45,12 +46,13 @@ jobs:
|
|||
qt-version: 6.2.4
|
||||
pch: false
|
||||
force-lto: false
|
||||
# Test for disabling Precompiled Headers & enabling link-time optimization
|
||||
# Test for disabling Precompiled Headers & enabling link-time optimization and plugins
|
||||
- os: ubuntu-22.04
|
||||
qt-version: 5.15.2
|
||||
pch: false
|
||||
force-lto: true
|
||||
skip_artifact: "yes"
|
||||
plugins: true
|
||||
# Test for disabling crashpad on Windows
|
||||
- os: windows-latest
|
||||
qt-version: 5.15.2
|
||||
|
@ -58,6 +60,7 @@ jobs:
|
|||
force-lto: true
|
||||
skip_artifact: "yes"
|
||||
crashpad: false
|
||||
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
|
@ -67,6 +70,12 @@ jobs:
|
|||
echo "C2_ENABLE_LTO=ON" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: Enable plugin support
|
||||
if: matrix.plugins == true
|
||||
run: |
|
||||
echo "C2_PLUGINS=ON" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: Set Crashpad
|
||||
if: matrix.crashpad == true
|
||||
run: |
|
||||
|
@ -160,6 +169,7 @@ jobs:
|
|||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} `
|
||||
-DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" `
|
||||
-DCHATTERINO_LTO="$Env:C2_ENABLE_LTO" `
|
||||
-DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" `
|
||||
-DBUILD_WITH_QT6="$Env:C2_BUILD_WITH_QT6" `
|
||||
..
|
||||
set cl=/MP
|
||||
|
@ -246,6 +256,7 @@ jobs:
|
|||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=On \
|
||||
-DCHATTERINO_LTO="$C2_ENABLE_LTO" \
|
||||
-DCHATTERINO_PLUGINS="$C2_PLUGINS" \
|
||||
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
|
||||
..
|
||||
make -j"$(nproc)"
|
||||
|
@ -310,6 +321,7 @@ jobs:
|
|||
-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl \
|
||||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
|
||||
-DCHATTERINO_LTO="$C2_ENABLE_LTO" \
|
||||
-DCHATTERINO_PLUGINS="$C2_PLUGINS" \
|
||||
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
|
||||
..
|
||||
make -j"$(sysctl -n hw.logicalcpu)"
|
||||
|
@ -331,7 +343,6 @@ jobs:
|
|||
with:
|
||||
name: chatterino-osx-${{ matrix.qt-version }}.dmg
|
||||
path: build/chatterino-osx.dmg
|
||||
|
||||
create-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -35,6 +35,9 @@
|
|||
[submodule "lib/miniaudio"]
|
||||
path = lib/miniaudio
|
||||
url = https://github.com/mackron/miniaudio.git
|
||||
[submodule "lib/lua/src"]
|
||||
path = lib/lua/src
|
||||
url = https://github.com/lua/lua
|
||||
[submodule "lib/crashpad"]
|
||||
path = lib/crashpad
|
||||
url = https://github.com/getsentry/crashpad
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
- Dev: Only log debug messages when NDEBUG is not defined. (#4442)
|
||||
- Dev: Cleaned up theme related code. (#4450)
|
||||
- Dev: Ensure tests have default-initialized settings. (#4498)
|
||||
- Dev: Add scripting capabilities with Lua (#4341)
|
||||
- Dev: Conan 2.0 is now used instead of Conan 1.0. (#4417)
|
||||
|
||||
## 2.4.2
|
||||
|
|
|
@ -25,6 +25,7 @@ option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF)
|
|||
option(BUILD_TRANSLATIONS "" OFF)
|
||||
option(BUILD_SHARED_LIBS "" OFF)
|
||||
option(CHATTERINO_LTO "Enable LTO for all targets" OFF)
|
||||
option(CHATTERINO_PLUGINS "Enable EXPERIMENTAL plugin support in Chatterino" OFF)
|
||||
|
||||
if(CHATTERINO_LTO)
|
||||
include(CheckIPOSupported)
|
||||
|
@ -156,6 +157,11 @@ else()
|
|||
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/settings" EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
if (CHATTERINO_PLUGINS)
|
||||
set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
|
||||
add_subdirectory(lib/lua)
|
||||
endif()
|
||||
|
||||
if (BUILD_WITH_CRASHPAD)
|
||||
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
|
22
docs/chatterino.d.ts
vendored
Normal file
22
docs/chatterino.d.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
/** @noSelfInFile */
|
||||
|
||||
declare module c2 {
|
||||
enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
class CommandContext {
|
||||
words: String[];
|
||||
channel_name: String;
|
||||
}
|
||||
|
||||
function log(level: LogLevel, ...data: any[]): void;
|
||||
function register_command(
|
||||
name: String,
|
||||
handler: (ctx: CommandContext) => void
|
||||
): boolean;
|
||||
function send_msg(channel: String, text: String): boolean;
|
||||
function system_msg(channel: String, text: String): boolean;
|
||||
}
|
49
docs/plugin-info.schema.json
Normal file
49
docs/plugin-info.schema.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"$id": "https://raw.githubusercontent.com/Chatterino/chatterino2/master/docs/plugin-info.schema.json",
|
||||
"title": "Plugin info",
|
||||
"description": "Describes a Chatterino2 plugin (draft)",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Plugin name shown to the user."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Plugin description shown to the user."
|
||||
},
|
||||
"authors": {
|
||||
"type": "array",
|
||||
"description": "An array of authors of this Plugin.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"description": "Optional URL to your Plugin's homepage. This could be your GitHub repo for example."
|
||||
},
|
||||
"tags": {
|
||||
"description": "Something that could in the future be used to find your plugin.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"examples": ["moderation", "utility", "commands"]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Semver version string, for more info see https://semver.org.",
|
||||
"examples": ["0.0.1", "1.0.0-rc.1"]
|
||||
},
|
||||
"license": {
|
||||
"type": "string",
|
||||
"description": "A small description of your license.",
|
||||
"examples": ["MIT", "GPL-2.0-or-later"]
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "authors", "version", "license"]
|
||||
}
|
177
docs/wip-plugins.md
Normal file
177
docs/wip-plugins.md
Normal file
|
@ -0,0 +1,177 @@
|
|||
# Plugins
|
||||
|
||||
If Chatterino is compiled with the `CHATTERINO_PLUGINS` CMake option, it can
|
||||
load and execute Lua files. Note that while there are attempts at making this
|
||||
decently safe, we cannot guarantee safety.
|
||||
|
||||
## Plugin structure
|
||||
|
||||
Chatterino searches for plugins in the `Plugins` directory in the app data, right next to `Settings` and `Logs`.
|
||||
|
||||
Each plugin should have its own directory.
|
||||
|
||||
```
|
||||
Chatterino Plugins dir/
|
||||
└── plugin_name/
|
||||
├── init.lua
|
||||
└── info.json
|
||||
```
|
||||
|
||||
`init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`import` global function](#importfilename=).
|
||||
|
||||
`info.json` contains metadata about the plugin, like its name, description,
|
||||
authors, homepage link, tags, version, license name. The version field **must**
|
||||
be [semver 2.0](https://semver.org/) compliant. The general idea of `info.json`
|
||||
will not change however the exact contents probably will, for example with
|
||||
permission system ideas.
|
||||
Example file:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/Chatterino/chatterino2/master/docs/plugin-info.schema.json",
|
||||
"name": "Test plugin",
|
||||
"description": "This plugin is for testing stuff.",
|
||||
"authors": "Mm2PL",
|
||||
"homepage": "https://github.com/Chatterino/Chatterino2",
|
||||
"tags": ["test"],
|
||||
"version": "0.0.0",
|
||||
"license": "MIT"
|
||||
}
|
||||
```
|
||||
|
||||
An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plugin](https://github.com/Mm2PL/Chatterino-test-plugin)
|
||||
|
||||
## Plugins with Typescript
|
||||
|
||||
If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io)
|
||||
to typecheck your plugins. There is a `chatterino.d.ts` file describing the API
|
||||
in this directory. However this has several drawbacks like harder debugging at
|
||||
runtime.
|
||||
|
||||
## API
|
||||
|
||||
The following parts of the Lua standard library are loaded:
|
||||
|
||||
- `_G` (most globals)
|
||||
- `table`
|
||||
- `string`
|
||||
- `math`
|
||||
- `utf8`
|
||||
|
||||
The official manual for them is available [here](https://www.lua.org/manual/5.4/manual.html#6).
|
||||
|
||||
### Chatterino API
|
||||
|
||||
All Chatterino functions are exposed in a global table called `c2`. The following members are available:
|
||||
|
||||
#### `log(level, args...)`
|
||||
|
||||
Writes a message to the Chatterino log. The `level` argument should be a
|
||||
`LogLevel` member. All `args` should be convertible to a string with
|
||||
`tostring()`.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
c2.log(c2.LogLevel.Warning, "Hello, this should show up in the Chatterino log by default")
|
||||
|
||||
c2.log(c2.LogLevel.Debug, "Hello world")
|
||||
-- Equivalent to doing qCDebug(chatterinoLua) << "[pluginDirectory:Plugin Name]" << "Hello, world"; from C++
|
||||
```
|
||||
|
||||
#### `LogLevel` enum
|
||||
|
||||
This table describes log levels available to Lua Plugins. The values behind the names may change, do not count on them. It has the following keys:
|
||||
|
||||
- `Debug`
|
||||
- `Info`
|
||||
- `Warning`
|
||||
- `Critical`
|
||||
|
||||
#### `register_command(name, handler)`
|
||||
|
||||
Registers a new command called `name` which when executed will call `handler`.
|
||||
Returns `true` if everything went ok, `false` if there already exists another
|
||||
command with this name.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
function cmdWords(ctx)
|
||||
-- ctx contains:
|
||||
-- words - table of words supplied to the command including the trigger
|
||||
-- channelName - name of the channel the command is being run in
|
||||
c2.system_msg(ctx.channelName, "Words are: " .. table.concat(ctx.words, " "))
|
||||
end
|
||||
|
||||
c2.register_command("/words", cmdWords)
|
||||
```
|
||||
|
||||
Limitations/known issues:
|
||||
|
||||
- Commands registered in functions, not in the global scope might not show up in the settings UI,
|
||||
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).
|
||||
|
||||
#### `send_msg(channel, text)`
|
||||
|
||||
Sends a message to `channel` with the specified text. Also executes commands.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
function cmdShout(ctx)
|
||||
table.remove(ctx.words, 1)
|
||||
local output = table.concat(ctx.words, " ")
|
||||
c2.send_msg(ctx.channelName, string.upper(output))
|
||||
end
|
||||
c2.register_command("/shout", cmdShout)
|
||||
```
|
||||
|
||||
Limitations/Known issues:
|
||||
|
||||
- It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
||||
|
||||
#### `system_msg(channel, text)`
|
||||
|
||||
Creates a system message and adds it to the twitch channel specified by
|
||||
`channel`. Returns `true` if everything went ok, `false` otherwise. It will
|
||||
throw an error if the number of arguments received doesn't match what it
|
||||
expects.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
local ok = c2.system_msg("pajlada", "test")
|
||||
if (not ok)
|
||||
-- channel not found
|
||||
end
|
||||
```
|
||||
|
||||
### Changed globals
|
||||
|
||||
#### `load(chunk [, chunkname [, mode [, env]]])`
|
||||
|
||||
This function is only available if Chatterino is compiled in debug mode. It is meant for debugging with little exception.
|
||||
This function behaves really similarity to Lua's `load`, however it does not allow for bytecode to be executed.
|
||||
It achieves this by forcing all inputs to be encoded with `UTF-8`.
|
||||
|
||||
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-load)
|
||||
|
||||
#### `import(filename)`
|
||||
|
||||
This function mimics Lua's `dofile` however relative paths are relative to your plugin's directory.
|
||||
You are restricted to loading files in your plugin's directory. You cannot load files with bytecode inside.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
import("stuff.lua") -- executes Plugins/name/stuff.lua
|
||||
import("./stuff.lua") -- executes Plugins/name/stuff.lua
|
||||
import("../stuff.lua") -- tries to load Plugins/stuff.lua and errors
|
||||
import("luac.out") -- tried to load Plugins/name/luac.out and errors because it contains non-utf8 data
|
||||
```
|
||||
|
||||
#### `print(Args...)`
|
||||
|
||||
The `print` global function is equivalent to calling `c2.log(c2.LogLevel.Debug, Args...)`
|
53
lib/lua/CMakeLists.txt
Normal file
53
lib/lua/CMakeLists.txt
Normal file
|
@ -0,0 +1,53 @@
|
|||
project(lua CXX)
|
||||
|
||||
#[====[
|
||||
Updating this list:
|
||||
remove all listed files
|
||||
go to line below, ^y2j4j$@" and then reindent the file names
|
||||
/LUA_SRC
|
||||
:r!ls lib/lua/src | grep '\.c' | grep -Ev 'lua\.c|onelua\.c' | sed 's#^#src/#'
|
||||
|
||||
#]====]
|
||||
set(LUA_SRC
|
||||
"src/lapi.c"
|
||||
"src/lauxlib.c"
|
||||
"src/lbaselib.c"
|
||||
"src/lcode.c"
|
||||
"src/lcorolib.c"
|
||||
"src/lctype.c"
|
||||
"src/ldblib.c"
|
||||
"src/ldebug.c"
|
||||
"src/ldo.c"
|
||||
"src/ldump.c"
|
||||
"src/lfunc.c"
|
||||
"src/lgc.c"
|
||||
"src/linit.c"
|
||||
"src/liolib.c"
|
||||
"src/llex.c"
|
||||
"src/lmathlib.c"
|
||||
"src/lmem.c"
|
||||
"src/loadlib.c"
|
||||
"src/lobject.c"
|
||||
"src/lopcodes.c"
|
||||
"src/loslib.c"
|
||||
"src/lparser.c"
|
||||
"src/lstate.c"
|
||||
"src/lstring.c"
|
||||
"src/lstrlib.c"
|
||||
"src/ltable.c"
|
||||
"src/ltablib.c"
|
||||
"src/ltests.c"
|
||||
"src/ltm.c"
|
||||
"src/lua.c"
|
||||
"src/lundump.c"
|
||||
"src/lutf8lib.c"
|
||||
"src/lvm.c"
|
||||
"src/lzio.c"
|
||||
)
|
||||
|
||||
add_library(lua STATIC ${LUA_SRC})
|
||||
target_include_directories(lua
|
||||
PUBLIC
|
||||
${LUA_INCLUDE_DIRS}
|
||||
)
|
||||
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX)
|
1
lib/lua/src
Submodule
1
lib/lua/src
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 5d708c3f9cae12820e415d4f89c9eacbe2ab964b
|
21
resources/licenses/fluenticons.txt
Normal file
21
resources/licenses/fluenticons.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
7
resources/licenses/lua.txt
Normal file
7
resources/licenses/lua.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
Copyright © 1994–2021 Lua.org, PUC-Rio.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
13
resources/settings/plugins.svg
Normal file
13
resources/settings/plugins.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<!-- Created using Krita: https://krita.org -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:krita="http://krita.org/namespaces/svg/krita"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
width="1.728pt"
|
||||
height="1.728pt"
|
||||
viewBox="0 0 1.728 1.728">
|
||||
<defs/>
|
||||
<path id="shape0" transform="matrix(0.072 0 0 0.072 0.143115521579684 0.144884750976744)" fill="#ffffff" stroke="#000000" stroke-width="0.72" stroke-linecap="square" stroke-linejoin="bevel" d="M19.7193 1.69471C19.9014 1.50618 20.0022 1.25341 20 0.991311C19.9977 0.72921 19.8924 0.478233 19.7071 0.292893C19.5218 0.107553 19.2708 0.00231534 19.0087 3.77513e-05C18.7466 -0.00223983 18.4938 0.0986205 18.3053 0.280712L16.5083 2.07771C15.6935 1.57117 14.7307 1.35503 13.7775 1.46467C12.8244 1.57431 11.9358 2.00339 11.2573 2.68171L10.1893 3.75071C9.86149 4.07874 9.67716 4.52397 9.67716 4.98771C9.67716 5.45145 9.86149 5.89668 10.1893 6.22471L13.7743 9.81071C13.9915 10.0279 14.2621 10.1842 14.5588 10.2637C14.8555 10.3432 15.168 10.3432 15.4647 10.2637C15.7615 10.1842 16.0321 10.0279 16.2493 9.81071L17.3173 8.74271C17.9955 8.06436 18.4246 7.17606 18.5345 6.22312C18.6443 5.27017 18.4284 4.30755 17.9223 3.49271ZM8.71928 9.69471C8.90138 9.50618 9.00224 9.25341 8.99996 8.99131C8.99768 8.72921 8.89244 8.47823 8.7071 8.29289C8.52176 8.10755 8.27079 8.00232 8.00869 8.00004C7.74658 7.99776 7.49382 8.09862 7.30528 8.28071L5.83528 9.75071L5.54228 9.45771C5.40171 9.31732 5.21096 9.23837 5.01228 9.23837C4.81361 9.23837 4.62286 9.31732 4.48228 9.45771L2.70728 11.2327C2.02905 11.9111 1.59993 12.7994 1.49011 13.7523C1.38029 14.7052 1.59612 15.6679 2.10228 16.4827L0.305284 18.2807C0.151702 18.429 0.0495919 18.6227 0.0139927 18.8333C-0.0216064 19.0438 0.0111426 19.2603 0.107417 19.4509C0.203692 19.6415 0.358521 19.7963 0.549105 19.8926C0.739689 19.9889 0.956189 20.0216 1.16672 19.986C1.37725 19.9504 1.57095 19.8483 1.71928 19.6947L3.51728 17.8977C4.33212 18.4039 5.29475 18.6197 6.24769 18.5099C7.20063 18.4001 8.08894 17.9709 8.76728 17.2927L10.5423 15.5177C10.6827 15.3771 10.7616 15.1864 10.7616 14.9877C10.7616 14.789 10.6827 14.5983 10.5423 14.4577L10.2493 14.1647L11.7193 12.6947C11.9014 12.5062 12.0022 12.2534 12 11.9913C11.9977 11.7292 11.8924 11.4782 11.7071 11.2929C11.5218 11.1076 11.2708 11.0023 11.0087 11C10.7466 10.9978 10.4938 11.0986 10.3053 11.2807L8.83528 12.7507L7.24928 11.1647Z" sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccc"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -10,6 +10,9 @@
|
|||
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
#include "controllers/notifications/NotificationController.hpp"
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
#endif
|
||||
#include "controllers/sound/SoundController.hpp"
|
||||
#include "controllers/userdata/UserDataController.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
|
@ -85,6 +88,9 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
, seventvBadges(&this->emplace<SeventvBadges>())
|
||||
, userData(&this->emplace<UserDataController>())
|
||||
, sound(&this->emplace<SoundController>())
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
, plugins(&this->emplace<PluginController>())
|
||||
#endif
|
||||
, logging(&this->emplace<Logging>())
|
||||
{
|
||||
this->instance = this;
|
||||
|
|
|
@ -20,6 +20,9 @@ class HotkeyController;
|
|||
class IUserDataController;
|
||||
class UserDataController;
|
||||
class SoundController;
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
class PluginController;
|
||||
#endif
|
||||
|
||||
class Theme;
|
||||
class WindowManager;
|
||||
|
@ -95,6 +98,10 @@ public:
|
|||
UserDataController *const userData{};
|
||||
SoundController *const sound{};
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginController *const plugins{};
|
||||
#endif
|
||||
|
||||
/*[[deprecated]]*/ Logging *const logging{};
|
||||
|
||||
Theme *getThemes() override
|
||||
|
|
|
@ -136,6 +136,15 @@ set(SOURCE_FILES
|
|||
controllers/pings/MutedChannelModel.cpp
|
||||
controllers/pings/MutedChannelModel.hpp
|
||||
|
||||
controllers/plugins/LuaAPI.cpp
|
||||
controllers/plugins/LuaAPI.hpp
|
||||
controllers/plugins/Plugin.cpp
|
||||
controllers/plugins/Plugin.hpp
|
||||
controllers/plugins/PluginController.hpp
|
||||
controllers/plugins/PluginController.cpp
|
||||
controllers/plugins/LuaUtilities.cpp
|
||||
controllers/plugins/LuaUtilities.hpp
|
||||
|
||||
controllers/userdata/UserDataController.cpp
|
||||
controllers/userdata/UserDataController.hpp
|
||||
controllers/userdata/UserData.hpp
|
||||
|
@ -545,6 +554,8 @@ set(SOURCE_FILES
|
|||
widgets/settingspages/NicknamesPage.hpp
|
||||
widgets/settingspages/NotificationPage.cpp
|
||||
widgets/settingspages/NotificationPage.hpp
|
||||
widgets/settingspages/PluginsPage.cpp
|
||||
widgets/settingspages/PluginsPage.hpp
|
||||
widgets/settingspages/SettingsPage.cpp
|
||||
widgets/settingspages/SettingsPage.hpp
|
||||
|
||||
|
@ -590,6 +601,14 @@ list(APPEND SOURCE_FILES ${RES_AUTOGEN_FILES})
|
|||
|
||||
add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES})
|
||||
|
||||
if(CHATTERINO_PLUGINS)
|
||||
target_compile_definitions(${LIBRARY_PROJECT}
|
||||
PRIVATE
|
||||
CHATTERINO_HAVE_PLUGINS
|
||||
)
|
||||
message(STATUS "Building Chatterino with lua plugin support enabled.")
|
||||
endif()
|
||||
|
||||
if (CHATTERINO_GENERATE_COVERAGE)
|
||||
include(CodeCoverage)
|
||||
append_coverage_compiler_flags_to_target(${LIBRARY_PROJECT})
|
||||
|
@ -624,6 +643,9 @@ target_link_libraries(${LIBRARY_PROJECT}
|
|||
LRUCache
|
||||
MagicEnum
|
||||
)
|
||||
if (CHATTERINO_PLUGINS)
|
||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua)
|
||||
endif()
|
||||
|
||||
if (BUILD_WITH_QT6)
|
||||
target_link_libraries(${LIBRARY_PROJECT}
|
||||
|
|
|
@ -230,7 +230,12 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
|
|||
{
|
||||
addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
|
||||
}
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
for (const auto &command : getApp()->commands->pluginCommands())
|
||||
{
|
||||
addString(command, TaggedString::PluginCommand);
|
||||
}
|
||||
#endif
|
||||
// Custom Chatterino commands
|
||||
for (const auto &command : getApp()->commands->items)
|
||||
{
|
||||
|
|
|
@ -34,6 +34,9 @@ class CompletionModel : public QAbstractListModel
|
|||
CustomCommand,
|
||||
ChatterinoCommand,
|
||||
TwitchCommand,
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginCommand,
|
||||
#endif
|
||||
};
|
||||
|
||||
TaggedString(QString _string, Type type);
|
||||
|
|
|
@ -24,6 +24,7 @@ Q_LOGGING_CATEGORY(chatterinoIrc, "chatterino.irc", logThreshold);
|
|||
Q_LOGGING_CATEGORY(chatterinoIvr, "chatterino.ivr", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoLiveupdates, "chatterino.liveupdates",
|
||||
logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoLua, "chatterino.lua", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoMain, "chatterino.main", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoMessage, "chatterino.message", logThreshold);
|
||||
Q_LOGGING_CATEGORY(chatterinoNativeMessage, "chatterino.nativemessage",
|
||||
|
|
|
@ -19,6 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoImage);
|
|||
Q_DECLARE_LOGGING_CATEGORY(chatterinoIrc);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoIvr);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoLiveupdates);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoLua);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoMain);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage);
|
||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "controllers/commands/Command.hpp"
|
||||
#include "controllers/commands/CommandContext.hpp"
|
||||
#include "controllers/commands/CommandModel.hpp"
|
||||
#include "controllers/plugins/PluginController.hpp"
|
||||
#include "controllers/userdata/UserDataController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
|
@ -3261,6 +3262,32 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
return text;
|
||||
}
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
bool CommandController::registerPluginCommand(const QString &commandName)
|
||||
{
|
||||
if (this->commands_.contains(commandName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this->commands_[commandName] = [commandName](const CommandContext &ctx) {
|
||||
return getApp()->plugins->tryExecPluginCommand(commandName, ctx);
|
||||
};
|
||||
this->pluginCommands_.append(commandName);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CommandController::unregisterPluginCommand(const QString &commandName)
|
||||
{
|
||||
if (!this->pluginCommands_.contains(commandName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this->pluginCommands_.removeAll(commandName);
|
||||
return this->commands_.erase(commandName) != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void CommandController::registerCommand(const QString &commandName,
|
||||
CommandFunctionVariants commandFunction)
|
||||
{
|
||||
|
|
|
@ -42,6 +42,15 @@ public:
|
|||
const QStringList &words, const Command &command, bool dryRun,
|
||||
ChannelPtr channel, const Message *message = nullptr,
|
||||
std::unordered_map<QString, QString> context = {});
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
bool registerPluginCommand(const QString &commandName);
|
||||
bool unregisterPluginCommand(const QString &commandName);
|
||||
|
||||
const QStringList &pluginCommands()
|
||||
{
|
||||
return this->pluginCommands_;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
void load(Paths &paths);
|
||||
|
@ -73,6 +82,9 @@ private:
|
|||
commandsSetting_;
|
||||
|
||||
QStringList defaultChatterinoCommandAutoCompletions_;
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
QStringList pluginCommands_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
338
src/controllers/plugins/LuaAPI.cpp
Normal file
338
src/controllers/plugins/LuaAPI.cpp
Normal file
|
@ -0,0 +1,338 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <QFileInfo>
|
||||
# include <QLoggingCategory>
|
||||
# include <QTextCodec>
|
||||
|
||||
namespace {
|
||||
using namespace chatterino;
|
||||
|
||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc)
|
||||
{
|
||||
stream.noquote();
|
||||
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
||||
for (int i = 1; i <= argc; i++)
|
||||
{
|
||||
stream << lua::toString(L, i);
|
||||
}
|
||||
lua_pop(L, argc);
|
||||
}
|
||||
|
||||
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||
{
|
||||
auto base =
|
||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName()));
|
||||
|
||||
using LogLevel = lua::api::LogLevel;
|
||||
|
||||
switch (lvl)
|
||||
{
|
||||
case LogLevel::Debug:
|
||||
return base.debug();
|
||||
case LogLevel::Info:
|
||||
return base.info();
|
||||
case LogLevel::Warning:
|
||||
return base.warning();
|
||||
case LogLevel::Critical:
|
||||
return base.critical();
|
||||
default:
|
||||
assert(false && "if this happens magic_enum must have failed us");
|
||||
return {(QString *)nullptr};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// NOLINTBEGIN(*vararg)
|
||||
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
int c2_register_command(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->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_send_msg(lua_State *L)
|
||||
{
|
||||
QString text;
|
||||
QString channel;
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L, "send_msg needs exactly 2 arguments (channel and text)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L, "cannot get text (2nd argument of send_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &channel))
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"cannot get channel (1st argument of send_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin" << pl->id
|
||||
<< "tried to send a message (using send_msg) to channel" << channel
|
||||
<< "which is not known";
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
QString message = text;
|
||||
message = message.replace('\n', ' ');
|
||||
QString outText = getApp()->commands->execCommand(message, chn, false);
|
||||
chn->sendMessage(outText);
|
||||
lua::push(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_system_msg(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L,
|
||||
"system_msg needs exactly 2 arguments (channel and text)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
QString channel;
|
||||
QString text;
|
||||
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"cannot get text (2nd argument of system_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &channel))
|
||||
{
|
||||
luaL_error(L, "cannot get channel (1st argument of system_msg, "
|
||||
"expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin" << pl->id
|
||||
<< "tried to show a system message (using system_msg) in channel"
|
||||
<< channel << "which is not known";
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
chn->addMessage(makeSystemMessage(text));
|
||||
lua::push(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_log(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
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 g_load(lua_State *L)
|
||||
{
|
||||
# ifdef NDEBUG
|
||||
luaL_error(L, "load() is only usable in debug mode");
|
||||
return 0;
|
||||
# else
|
||||
auto countArgs = lua_gettop(L);
|
||||
QByteArray data;
|
||||
if (lua::peek(L, &data, 1))
|
||||
{
|
||||
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++)
|
||||
{
|
||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
||||
}
|
||||
|
||||
// fetch load and call it
|
||||
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
|
||||
}
|
||||
|
||||
int g_import(lua_State *L)
|
||||
{
|
||||
auto countArgs = lua_gettop(L);
|
||||
// Lua allows dofile() which loads from stdin, but this is very useless in our case
|
||||
if (countArgs == 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "it is not allowed to call import() without arguments");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
QString fname;
|
||||
if (!lua::pop(L, &fname))
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: expected a string for a filename");
|
||||
return 1;
|
||||
}
|
||||
auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/");
|
||||
auto file = dir.resolved(fname);
|
||||
|
||||
qCDebug(chatterinoLua) << "plugin" << pl->id << "is trying to load" << file
|
||||
<< "(its dir is" << dir << ")";
|
||||
if (!dir.isParentOf(file))
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: filename must be inside of the "
|
||||
"plugin directory");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto path = file.path(QUrl::FullyDecoded);
|
||||
QFile qf(path);
|
||||
qf.open(QIODevice::ReadOnly);
|
||||
if (qf.size() > 10'000'000)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: size limit of 10MB exceeded, what "
|
||||
"the hell are you doing");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// validate utf-8 to block bytecode exploits
|
||||
auto data = qf.readAll();
|
||||
auto *utf8 = QTextCodec::codecForName("UTF-8");
|
||||
QTextCodec::ConverterState state;
|
||||
utf8->toUnicode(data.constData(), data.size(), &state);
|
||||
if (state.invalidChars != 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "invalid utf-8 in import() target (%s) is not allowed",
|
||||
fname.toStdString().c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// fetch dofile and call it
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
// maybe data race here if symlink was swapped?
|
||||
lua::push(L, path);
|
||||
lua_call(L, 1, LUA_MULTRET);
|
||||
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
int g_print(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->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
|
||||
auto stream =
|
||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
||||
.debug());
|
||||
logHelper(L, pl, stream, argc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
// NOLINTEND(*vararg)
|
||||
#endif
|
28
src/controllers/plugins/LuaAPI.hpp
Normal file
28
src/controllers/plugins/LuaAPI.hpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
struct lua_State;
|
||||
namespace chatterino::lua::api {
|
||||
// names in this namespace reflect what's visible inside Lua and follow the lua naming scheme
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
// Following functions are exposed in c2 table.
|
||||
int c2_register_command(lua_State *L);
|
||||
int c2_send_msg(lua_State *L);
|
||||
int c2_system_msg(lua_State *L);
|
||||
int c2_log(lua_State *L);
|
||||
|
||||
// These ones are global
|
||||
int g_load(lua_State *L);
|
||||
int g_print(lua_State *L);
|
||||
int g_import(lua_State *L);
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
// Exposed as c2.LogLevel
|
||||
// Represents "calls" to qCDebug, qCInfo ...
|
||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
||||
#endif
|
196
src/controllers/plugins/LuaUtilities.cpp
Normal file
196
src/controllers/plugins/LuaUtilities.cpp
Normal file
|
@ -0,0 +1,196 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include "common/Channel.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
|
||||
# include <climits>
|
||||
# include <cstdlib>
|
||||
|
||||
namespace chatterino::lua {
|
||||
|
||||
void stackDump(lua_State *L, const QString &tag)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "--------------------";
|
||||
auto count = lua_gettop(L);
|
||||
if (!tag.isEmpty())
|
||||
{
|
||||
qCDebug(chatterinoLua) << "Tag: " << tag;
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Count elems: " << count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
auto typeint = lua_type(L, i);
|
||||
if (typeint == LUA_TSTRING)
|
||||
{
|
||||
QString str;
|
||||
lua::peek(L, &str, i);
|
||||
qCDebug(chatterinoLua)
|
||||
<< "At" << i << "is a" << lua_typename(L, typeint) << "("
|
||||
<< typeint << "): " << str;
|
||||
}
|
||||
else if (typeint == LUA_TTABLE)
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "At" << i << "is a" << lua_typename(L, typeint) << "("
|
||||
<< typeint << ")"
|
||||
<< "its length is " << lua_rawlen(L, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "At" << i << "is a" << lua_typename(L, typeint) << "("
|
||||
<< typeint << ")";
|
||||
}
|
||||
}
|
||||
qCDebug(chatterinoLua) << "--------------------";
|
||||
}
|
||||
|
||||
QString humanErrorText(lua_State *L, int errCode)
|
||||
{
|
||||
QString errName;
|
||||
switch (errCode)
|
||||
{
|
||||
case LUA_OK:
|
||||
return "ok";
|
||||
case LUA_ERRRUN:
|
||||
errName = "(runtime error)";
|
||||
break;
|
||||
case LUA_ERRMEM:
|
||||
errName = "(memory error)";
|
||||
break;
|
||||
case LUA_ERRERR:
|
||||
errName = "(error while handling another error)";
|
||||
break;
|
||||
case LUA_ERRSYNTAX:
|
||||
errName = "(syntax error)";
|
||||
break;
|
||||
case LUA_YIELD:
|
||||
errName = "(illegal coroutine yield)";
|
||||
break;
|
||||
case LUA_ERRFILE:
|
||||
errName = "(file error)";
|
||||
break;
|
||||
default:
|
||||
errName = "(unknown error type)";
|
||||
}
|
||||
QString errText;
|
||||
if (peek(L, &errText))
|
||||
{
|
||||
errName += " " + errText;
|
||||
}
|
||||
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)
|
||||
{
|
||||
return lua::push(L, str.toStdString());
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const std::string &str)
|
||||
{
|
||||
lua_pushstring(L, str.c_str());
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const CommandContext &ctx)
|
||||
{
|
||||
auto outIdx = pushEmptyTable(L, 2);
|
||||
|
||||
push(L, ctx.words);
|
||||
lua_setfield(L, outIdx, "words");
|
||||
push(L, ctx.channel->getName());
|
||||
lua_setfield(L, outIdx, "channel_name");
|
||||
|
||||
return outIdx;
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const bool &b)
|
||||
{
|
||||
lua_pushboolean(L, int(b));
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, double *out, StackIdx idx)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 = QString::fromUtf8(str, int(len));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
QString toString(lua_State *L, StackIdx idx)
|
||||
{
|
||||
size_t len{};
|
||||
const auto *ptr = luaL_tolstring(L, idx, &len);
|
||||
return QString::fromUtf8(ptr, int(len));
|
||||
}
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
191
src/controllers/plugins/LuaUtilities.hpp
Normal file
191
src/controllers/plugins/LuaUtilities.hpp
Normal file
|
@ -0,0 +1,191 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <magic_enum.hpp>
|
||||
# include <QList>
|
||||
|
||||
# include <string>
|
||||
# include <string_view>
|
||||
# include <type_traits>
|
||||
# include <vector>
|
||||
struct lua_State;
|
||||
class QJsonObject;
|
||||
namespace chatterino {
|
||||
struct CommandContext;
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::lua {
|
||||
|
||||
/**
|
||||
* @brief Dumps the Lua stack into qCDebug(chatterinoLua)
|
||||
*
|
||||
* @param tag is a string to let you know which dump is which when browsing logs
|
||||
*/
|
||||
void stackDump(lua_State *L, const QString &tag);
|
||||
|
||||
/**
|
||||
* @brief Converts a lua error code and potentially string on top of the stack into a human readable message
|
||||
*/
|
||||
QString humanErrorText(lua_State *L, int errCode);
|
||||
|
||||
/**
|
||||
* Represents an index into Lua's stack
|
||||
*/
|
||||
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 std::string &str);
|
||||
StackIdx push(lua_State *L, const bool &b);
|
||||
|
||||
// returns OK?
|
||||
bool peek(lua_State *L, double *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, std::string *out, StackIdx idx = -1);
|
||||
|
||||
/**
|
||||
* @brief Converts Lua object at stack index idx to a string.
|
||||
*/
|
||||
QString toString(lua_State *L, StackIdx idx = -1);
|
||||
|
||||
/// TEMPLATES
|
||||
|
||||
/**
|
||||
* @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, std::enable_if<std::is_enum_v<T>>>
|
||||
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.
|
||||
*
|
||||
* Relies on bool peek(lua_State*, T*, StackIdx) existing.
|
||||
*/
|
||||
template <typename T>
|
||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
||||
{
|
||||
auto ok = peek(L, out, idx);
|
||||
if (ok)
|
||||
{
|
||||
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.
|
||||
*
|
||||
* Values in this table may change.
|
||||
*
|
||||
* @returns stack index of newly created table
|
||||
*/
|
||||
template <typename T>
|
||||
StackIdx pushEnumTable(lua_State *L)
|
||||
{
|
||||
// std::array<T, _>
|
||||
auto values = magic_enum::enum_values<T>();
|
||||
StackIdx out = lua::pushEmptyTable(L, values.size());
|
||||
for (const T v : values)
|
||||
{
|
||||
std::string_view name = magic_enum::enum_name<T>(v);
|
||||
std::string str(name);
|
||||
|
||||
lua::push(L, str);
|
||||
lua_setfield(L, out, str.c_str());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua
|
||||
|
||||
#endif
|
177
src/controllers/plugins/Plugin.cpp
Normal file
177
src/controllers/plugins/Plugin.cpp
Normal file
|
@ -0,0 +1,177 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
# include <magic_enum.hpp>
|
||||
# include <QJsonArray>
|
||||
# include <QJsonObject>
|
||||
|
||||
# include <unordered_map>
|
||||
# include <unordered_set>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PluginMeta::PluginMeta(const QJsonObject &obj)
|
||||
{
|
||||
auto homepageObj = obj.value("homepage");
|
||||
if (homepageObj.isString())
|
||||
{
|
||||
this->homepage = homepageObj.toString();
|
||||
}
|
||||
else if (!homepageObj.isUndefined())
|
||||
{
|
||||
QString type = magic_enum::enum_name(homepageObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("homepage is defined but is not a string (its type is %1)")
|
||||
.arg(type));
|
||||
}
|
||||
auto nameObj = obj.value("name");
|
||||
if (nameObj.isString())
|
||||
{
|
||||
this->name = nameObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(nameObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("name is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto descrObj = obj.value("description");
|
||||
if (descrObj.isString())
|
||||
{
|
||||
this->description = descrObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(descrObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("description is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto authorsObj = obj.value("authors");
|
||||
if (authorsObj.isArray())
|
||||
{
|
||||
auto authorsArr = authorsObj.toArray();
|
||||
for (int i = 0; i < authorsArr.size(); i++)
|
||||
{
|
||||
const auto &t = authorsArr.at(i);
|
||||
if (!t.isString())
|
||||
{
|
||||
QString type = magic_enum::enum_name(t.type()).data();
|
||||
this->errors.push_back(
|
||||
QString("authors element #%1 is not a string (it is a %2)")
|
||||
.arg(i)
|
||||
.arg(type));
|
||||
break;
|
||||
}
|
||||
this->authors.push_back(t.toString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(authorsObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("authors is not an array (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto licenseObj = obj.value("license");
|
||||
if (licenseObj.isString())
|
||||
{
|
||||
this->license = licenseObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(licenseObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("license is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto verObj = obj.value("version");
|
||||
if (verObj.isString())
|
||||
{
|
||||
auto v = semver::from_string_noexcept(verObj.toString().toStdString());
|
||||
if (v.has_value())
|
||||
{
|
||||
this->version = v.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->errors.emplace_back("unable to parse version (use semver)");
|
||||
this->version = semver::version(0, 0, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(verObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("version is not a string (its type is %1)").arg(type));
|
||||
this->version = semver::version(0, 0, 0);
|
||||
}
|
||||
auto tagsObj = obj.value("tags");
|
||||
if (!tagsObj.isUndefined())
|
||||
{
|
||||
if (!tagsObj.isArray())
|
||||
{
|
||||
QString type = magic_enum::enum_name(tagsObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("tags is not an array (its type is %1)").arg(type));
|
||||
return;
|
||||
}
|
||||
|
||||
auto tagsArr = tagsObj.toArray();
|
||||
for (int i = 0; i < tagsArr.size(); i++)
|
||||
{
|
||||
const auto &t = tagsArr.at(i);
|
||||
if (!t.isString())
|
||||
{
|
||||
QString type = magic_enum::enum_name(t.type()).data();
|
||||
this->errors.push_back(
|
||||
QString("tags element #%1 is not a string (its type is %2)")
|
||||
.arg(i)
|
||||
.arg(type));
|
||||
return;
|
||||
}
|
||||
this->tags.push_back(t.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
||||
{
|
||||
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ok = getApp()->commands->registerPluginCommand(name);
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this->ownedCommands.insert({name, functionName});
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unordered_set<QString> Plugin::listRegisteredCommands()
|
||||
{
|
||||
std::unordered_set<QString> out;
|
||||
for (const auto &[name, _] : this->ownedCommands)
|
||||
{
|
||||
out.insert(name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Plugin::~Plugin()
|
||||
{
|
||||
if (this->state_ != nullptr)
|
||||
{
|
||||
lua_close(this->state_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#endif
|
98
src/controllers/plugins/Plugin.hpp
Normal file
98
src/controllers/plugins/Plugin.hpp
Normal file
|
@ -0,0 +1,98 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "Application.hpp"
|
||||
|
||||
# include <QDir>
|
||||
# include <QString>
|
||||
# include <semver/semver.hpp>
|
||||
|
||||
# include <unordered_map>
|
||||
# include <unordered_set>
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct PluginMeta {
|
||||
// for more info on these fields see docs/plugin-info.schema.json
|
||||
|
||||
// display name of the plugin
|
||||
QString name;
|
||||
|
||||
// description shown to the user
|
||||
QString description;
|
||||
|
||||
// plugin authors shown to the user
|
||||
std::vector<QString> authors;
|
||||
|
||||
// license name
|
||||
QString license;
|
||||
|
||||
// version of the plugin
|
||||
semver::version version;
|
||||
|
||||
// optionally a homepage link
|
||||
QString homepage;
|
||||
|
||||
// optionally tags that might help in searching for the plugin
|
||||
std::vector<QString> tags;
|
||||
|
||||
// errors that occurred while parsing info.json
|
||||
std::vector<QString> errors;
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
return this->errors.empty();
|
||||
}
|
||||
|
||||
explicit PluginMeta(const QJsonObject &obj);
|
||||
};
|
||||
|
||||
class Plugin
|
||||
{
|
||||
public:
|
||||
QString id;
|
||||
PluginMeta meta;
|
||||
|
||||
Plugin(QString id, lua_State *state, PluginMeta meta,
|
||||
const QDir &loadDirectory)
|
||||
: id(std::move(id))
|
||||
, meta(std::move(meta))
|
||||
, loadDirectory_(loadDirectory)
|
||||
, state_(state)
|
||||
{
|
||||
}
|
||||
|
||||
~Plugin();
|
||||
|
||||
/**
|
||||
* @brief Perform all necessary tasks to bind a command name to this plugin
|
||||
* @param name name of the command to create
|
||||
* @param functionName name of 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)
|
||||
*/
|
||||
bool registerCommand(const QString &name, const QString &functionName);
|
||||
|
||||
/**
|
||||
* @brief Get names of all commands belonging to this plugin
|
||||
*/
|
||||
std::unordered_set<QString> listRegisteredCommands();
|
||||
|
||||
const QDir &loadDirectory() const
|
||||
{
|
||||
return this->loadDirectory_;
|
||||
}
|
||||
|
||||
private:
|
||||
QDir loadDirectory_;
|
||||
lua_State *state_;
|
||||
|
||||
// maps command name -> function name
|
||||
std::unordered_map<QString, QString> ownedCommands;
|
||||
|
||||
friend class PluginController;
|
||||
};
|
||||
} // namespace chatterino
|
||||
#endif
|
302
src/controllers/plugins/PluginController.cpp
Normal file
302
src/controllers/plugins/PluginController.cpp
Normal file
|
@ -0,0 +1,302 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <QJsonDocument>
|
||||
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void PluginController::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
(void)paths;
|
||||
|
||||
// actuallyInitialize will be called by this connection
|
||||
settings.pluginsEnabled.connect([this](bool enabled) {
|
||||
if (enabled)
|
||||
{
|
||||
this->loadPlugins();
|
||||
}
|
||||
else
|
||||
{
|
||||
// uninitialize plugins
|
||||
this->plugins_.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PluginController::loadPlugins()
|
||||
{
|
||||
this->plugins_.clear();
|
||||
auto dir = QDir(getPaths()->pluginsDirectory);
|
||||
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
||||
for (const auto &info :
|
||||
dir.entryInfoList(QDir::NoFilter | QDir::NoDotAndDotDot))
|
||||
{
|
||||
if (info.isDir())
|
||||
{
|
||||
auto pluginDir = QDir(info.absoluteFilePath());
|
||||
this->tryLoadFromDir(pluginDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||
{
|
||||
// look for init.lua
|
||||
auto index = QFileInfo(pluginDir.filePath("init.lua"));
|
||||
qCDebug(chatterinoLua) << "Looking for init.lua and info.json in"
|
||||
<< pluginDir.path();
|
||||
if (!index.exists())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Missing init.lua in plugin directory:" << pluginDir.path();
|
||||
return false;
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Found init.lua, now looking for info.json!";
|
||||
auto infojson = QFileInfo(pluginDir.filePath("info.json"));
|
||||
if (!infojson.exists())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Missing info.json in plugin directory" << pluginDir.path();
|
||||
return false;
|
||||
}
|
||||
QFile infoFile(infojson.absoluteFilePath());
|
||||
infoFile.open(QIODevice::ReadOnly);
|
||||
auto everything = infoFile.readAll();
|
||||
auto doc = QJsonDocument::fromJson(everything);
|
||||
if (!doc.isObject())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "info.json root is not an object" << pluginDir.path();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto meta = PluginMeta(doc.object());
|
||||
if (!meta.isValid())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin from" << pluginDir << "is invalid because:";
|
||||
for (const auto &why : meta.errors)
|
||||
{
|
||||
qCWarning(chatterinoLua) << "- " << why;
|
||||
}
|
||||
auto plugin = std::make_unique<Plugin>(pluginDir.dirName(), nullptr,
|
||||
meta, pluginDir);
|
||||
this->plugins_.insert({pluginDir.dirName(), std::move(plugin)});
|
||||
return false;
|
||||
}
|
||||
this->load(index, pluginDir, meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PluginController::openLibrariesFor(lua_State *L,
|
||||
const PluginMeta & /*meta*/)
|
||||
{
|
||||
// Stuff to change, remove or hide behind a permission system:
|
||||
static const std::vector<luaL_Reg> loadedlibs = {
|
||||
luaL_Reg{LUA_GNAME, luaopen_base},
|
||||
// - load - don't allow in release mode
|
||||
|
||||
//luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||
// - needs special support
|
||||
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},
|
||||
// - fs access
|
||||
// - environ access
|
||||
// - exit
|
||||
luaL_Reg{LUA_STRLIBNAME, luaopen_string},
|
||||
luaL_Reg{LUA_MATHLIBNAME, luaopen_math},
|
||||
luaL_Reg{LUA_UTF8LIBNAME, luaopen_utf8},
|
||||
};
|
||||
// Warning: Do not add debug library to this, it would make the security of
|
||||
// this a living nightmare due to stuff like registry access
|
||||
// - Mm2PL
|
||||
|
||||
for (const auto ® : loadedlibs)
|
||||
{
|
||||
luaL_requiref(L, reg.name, reg.func, int(true));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg c2Lib[] = {
|
||||
{"system_msg", lua::api::c2_system_msg},
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"send_msg", lua::api::c2_send_msg},
|
||||
{"log", lua::api::c2_log},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
lua_pushglobaltable(L);
|
||||
auto global = lua_gettop(L);
|
||||
|
||||
// count of elements in C2LIB + LogLevel
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 5);
|
||||
|
||||
luaL_setfuncs(L, c2Lib, 0);
|
||||
|
||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
||||
lua_setfield(L, c2libIdx, "LogLevel");
|
||||
|
||||
lua_setfield(L, global, "c2");
|
||||
|
||||
// ban functions
|
||||
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
||||
|
||||
lua_pushglobaltable(L);
|
||||
auto gtable = lua_gettop(L);
|
||||
|
||||
// possibly randomize this name at runtime to prevent some attacks?
|
||||
|
||||
# ifndef NDEBUG
|
||||
lua_getfield(L, gtable, "load");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
# endif
|
||||
|
||||
lua_getfield(L, gtable, "dofile");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg replacementFuncs[] = {
|
||||
{"load", lua::api::g_load},
|
||||
{"print", lua::api::g_print},
|
||||
|
||||
// This function replaces both `dofile` and `require`, see docs/wip-plugins.md for more info
|
||||
{"import", lua::api::g_import},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
luaL_setfuncs(L, replacementFuncs, 0);
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "loadfile");
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "dofile");
|
||||
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta)
|
||||
{
|
||||
lua_State *l = luaL_newstate();
|
||||
PluginController::openLibrariesFor(l, meta);
|
||||
|
||||
auto pluginName = pluginDir.dirName();
|
||||
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
||||
this->plugins_.insert({pluginName, std::move(plugin)});
|
||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||
!getSettings()->pluginsEnabled)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "Skipping loading" << pluginName << "("
|
||||
<< meta.name << ") because it is disabled";
|
||||
return;
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Running lua file:" << index;
|
||||
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||
if (err != 0)
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Failed to load" << pluginName << "plugin from" << index << ": "
|
||||
<< lua::humanErrorText(l, err);
|
||||
return;
|
||||
}
|
||||
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
||||
}
|
||||
|
||||
bool PluginController::reload(const QString &id)
|
||||
{
|
||||
auto it = this->plugins_.find(id);
|
||||
if (it == this->plugins_.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (it->second->state_ != nullptr)
|
||||
{
|
||||
lua_close(it->second->state_);
|
||||
it->second->state_ = nullptr;
|
||||
}
|
||||
for (const auto &[cmd, _] : it->second->ownedCommands)
|
||||
{
|
||||
getApp()->commands->unregisterPluginCommand(cmd);
|
||||
}
|
||||
it->second->ownedCommands.clear();
|
||||
QDir loadDir = it->second->loadDirectory_;
|
||||
this->plugins_.erase(id);
|
||||
this->tryLoadFromDir(loadDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString PluginController::tryExecPluginCommand(const QString &commandName,
|
||||
const CommandContext &ctx)
|
||||
{
|
||||
for (auto &[name, plugin] : this->plugins_)
|
||||
{
|
||||
if (auto it = plugin->ownedCommands.find(commandName);
|
||||
it != plugin->ownedCommands.end())
|
||||
{
|
||||
const auto &funcName = it->second;
|
||||
|
||||
auto *L = plugin->state_;
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str());
|
||||
lua::push(L, ctx);
|
||||
|
||||
auto res = lua_pcall(L, 1, 0, 0);
|
||||
if (res != LUA_OK)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"Lua error: " + lua::humanErrorText(L, res)));
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
qCCritical(chatterinoLua)
|
||||
<< "Something's seriously up, no plugin owns command" << commandName
|
||||
<< "yet a call to execute it came in";
|
||||
assert(false && "missing plugin command owner");
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PluginController::isPluginEnabled(const QString &id)
|
||||
{
|
||||
auto vec = getSettings()->enabledPlugins.getValue();
|
||||
auto it = std::find(vec.begin(), vec.end(), id);
|
||||
return it != vec.end();
|
||||
}
|
||||
|
||||
Plugin *PluginController::getPluginByStatePtr(lua_State *L)
|
||||
{
|
||||
for (auto &[name, plugin] : this->plugins_)
|
||||
{
|
||||
if (plugin->state_ == L)
|
||||
{
|
||||
return plugin.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::map<QString, std::unique_ptr<Plugin>> &PluginController::plugins()
|
||||
const
|
||||
{
|
||||
return this->plugins_;
|
||||
}
|
||||
|
||||
}; // namespace chatterino
|
||||
#endif
|
68
src/controllers/plugins/PluginController.hpp
Normal file
68
src/controllers/plugins/PluginController.hpp
Normal file
|
@ -0,0 +1,68 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "common/Singleton.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
|
||||
# include <QDir>
|
||||
# include <QFileInfo>
|
||||
# include <QJsonArray>
|
||||
# include <QJsonObject>
|
||||
# include <QString>
|
||||
|
||||
# include <algorithm>
|
||||
# include <map>
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Paths;
|
||||
|
||||
class PluginController : public Singleton
|
||||
{
|
||||
public:
|
||||
void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
QString tryExecPluginCommand(const QString &commandName,
|
||||
const CommandContext &ctx);
|
||||
|
||||
// NOTE: this pointer does not own the Plugin, unique_ptr still owns it
|
||||
// This is required to be public because of c functions
|
||||
Plugin *getPluginByStatePtr(lua_State *L);
|
||||
|
||||
const std::map<QString, std::unique_ptr<Plugin>> &plugins() const;
|
||||
|
||||
/**
|
||||
* @brief Reload plugin given by id
|
||||
*
|
||||
* @param id This is the unique identifier of the plugin, the name of the directory it is in
|
||||
*/
|
||||
bool reload(const QString &id);
|
||||
|
||||
/**
|
||||
* @brief Checks settings to tell if a plugin named by id is enabled.
|
||||
*
|
||||
* It is the callers responsibility to check Settings::pluginsEnabled
|
||||
*/
|
||||
static bool isPluginEnabled(const QString &id);
|
||||
|
||||
private:
|
||||
void loadPlugins();
|
||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta);
|
||||
|
||||
// This function adds lua standard libraries into the state
|
||||
static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/);
|
||||
static void loadChatterinoLib(lua_State *l);
|
||||
bool tryLoadFromDir(const QDir &pluginDir);
|
||||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||
};
|
||||
|
||||
}; // namespace chatterino
|
||||
#endif
|
|
@ -141,6 +141,7 @@ void Paths::initSubDirectories()
|
|||
this->messageLogDirectory = makePath("Logs");
|
||||
this->miscDirectory = makePath("Misc");
|
||||
this->twitchProfileAvatars = makePath("ProfileAvatars");
|
||||
this->pluginsDirectory = makePath("Plugins");
|
||||
this->crashdumpDirectory = makePath("Crashes");
|
||||
//QDir().mkdir(this->twitchProfileAvatars + "/twitch");
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@ public:
|
|||
// Profile avatars for Twitch <appDataDirectory>/cache/twitch
|
||||
QString twitchProfileAvatars;
|
||||
|
||||
// Plugin files live here. <appDataDirectory>/Plugins
|
||||
QString pluginsDirectory;
|
||||
|
||||
bool createFolder(const QString &folderPath);
|
||||
bool isPortable();
|
||||
|
||||
|
|
|
@ -528,6 +528,10 @@ public:
|
|||
{"d", 1},
|
||||
{"w", 1}}};
|
||||
|
||||
BoolSetting pluginsEnabled = {"/plugins/supportEnabled", false};
|
||||
ChatterinoSetting<std::vector<QString>> enabledPlugins = {
|
||||
"/plugins/enabledPlugins", {}};
|
||||
|
||||
private:
|
||||
void updateModerationActions();
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "widgets/settingspages/ModerationPage.hpp"
|
||||
#include "widgets/settingspages/NicknamesPage.hpp"
|
||||
#include "widgets/settingspages/NotificationPage.hpp"
|
||||
#include "widgets/settingspages/PluginsPage.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFile>
|
||||
|
@ -216,6 +217,9 @@ void SettingsDialog::addTabs()
|
|||
this->addTab([]{return new ModerationPage;}, "Moderation", ":/settings/moderation.svg", SettingsTabId::Moderation);
|
||||
this->addTab([]{return new NotificationPage;}, "Live Notifications", ":/settings/notification2.svg");
|
||||
this->addTab([]{return new ExternalToolsPage;}, "External tools", ":/settings/externaltools.svg");
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
this->addTab([]{return new PluginsPage;}, "Plugins", ":/settings/plugins.svg");
|
||||
#endif
|
||||
this->ui_.tabContainer->addStretch(1);
|
||||
this->addTab([]{return new AboutPage;}, "About", ":/settings/about.svg", SettingsTabId(), Qt::AlignBottom);
|
||||
// clang-format on
|
||||
|
|
|
@ -114,6 +114,13 @@ AboutPage::AboutPage()
|
|||
addLicense(form.getElement(), "miniaudio",
|
||||
"https://github.com/mackron/miniaudio",
|
||||
":/licenses/miniaudio.txt");
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
addLicense(form.getElement(), "lua", "https://lua.org",
|
||||
":/licenses/lua.txt");
|
||||
addLicense(form.getElement(), "Fluent icons",
|
||||
"https://github.com/microsoft/fluentui-system-icons",
|
||||
":/licenses/fluenticons.txt");
|
||||
#endif
|
||||
#ifdef CHATTERINO_WITH_CRASHPAD
|
||||
addLicense(form.getElement(), "sentry-crashpad",
|
||||
"https://github.com/getsentry/crashpad",
|
||||
|
|
185
src/widgets/settingspages/PluginsPage.cpp
Normal file
185
src/widgets/settingspages/PluginsPage.cpp
Normal file
|
@ -0,0 +1,185 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "widgets/settingspages/PluginsPage.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
# include "util/Helpers.hpp"
|
||||
# include "util/LayoutCreator.hpp"
|
||||
# include "util/RemoveScrollAreaBackground.hpp"
|
||||
|
||||
# include <QCheckBox>
|
||||
# include <QFormLayout>
|
||||
# include <QGroupBox>
|
||||
# include <QLabel>
|
||||
# include <QObject>
|
||||
# include <QPushButton>
|
||||
# include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PluginsPage::PluginsPage()
|
||||
: scrollAreaWidget_(nullptr)
|
||||
, dataFrame_(nullptr)
|
||||
{
|
||||
LayoutCreator<PluginsPage> layoutCreator(this);
|
||||
auto scrollArea = layoutCreator.emplace<QScrollArea>();
|
||||
|
||||
auto widget = scrollArea.emplaceScrollAreaWidget();
|
||||
this->scrollAreaWidget_ = widget;
|
||||
removeScrollAreaBackground(scrollArea.getElement(), widget.getElement());
|
||||
|
||||
auto layout = widget.setLayoutType<QVBoxLayout>();
|
||||
|
||||
{
|
||||
auto group = layout.emplace<QGroupBox>("General plugin settings");
|
||||
this->generalGroup = group.getElement();
|
||||
auto groupLayout = group.setLayoutType<QFormLayout>();
|
||||
auto *description = new QLabel(
|
||||
"You can load plugins by putting them into " +
|
||||
formatRichNamedLink("file:///" + getPaths()->pluginsDirectory,
|
||||
"the Plugins directory") +
|
||||
". Each one is a new directory.");
|
||||
description->setOpenExternalLinks(true);
|
||||
description->setWordWrap(true);
|
||||
description->setStyleSheet("color: #bbb");
|
||||
groupLayout->addRow(description);
|
||||
|
||||
auto *box = this->createCheckBox("Enable plugins",
|
||||
getSettings()->pluginsEnabled);
|
||||
QObject::connect(box, &QCheckBox::released, [this]() {
|
||||
this->rebuildContent();
|
||||
});
|
||||
groupLayout->addRow(box);
|
||||
}
|
||||
|
||||
this->rebuildContent();
|
||||
}
|
||||
|
||||
void PluginsPage::rebuildContent()
|
||||
{
|
||||
if (this->dataFrame_ != nullptr)
|
||||
{
|
||||
this->dataFrame_->deleteLater();
|
||||
this->dataFrame_ = nullptr;
|
||||
}
|
||||
auto frame = LayoutCreator<QFrame>(new QFrame(this));
|
||||
this->dataFrame_ = frame.getElement();
|
||||
this->scrollAreaWidget_.append(this->dataFrame_);
|
||||
auto layout = frame.setLayoutType<QVBoxLayout>();
|
||||
layout->setParent(this->dataFrame_);
|
||||
for (const auto &[id, plugin] : getApp()->plugins->plugins())
|
||||
{
|
||||
auto groupHeaderText =
|
||||
QString("%1 (%2, from %3)")
|
||||
.arg(plugin->meta.name,
|
||||
QString::fromStdString(plugin->meta.version.to_string()),
|
||||
id);
|
||||
auto groupBox = layout.emplace<QGroupBox>(groupHeaderText);
|
||||
groupBox->setParent(this->dataFrame_);
|
||||
auto pluginEntry = groupBox.setLayoutType<QFormLayout>();
|
||||
pluginEntry->setParent(groupBox.getElement());
|
||||
|
||||
if (!plugin->meta.isValid())
|
||||
{
|
||||
QString errors = "<ul>";
|
||||
for (const auto &err : plugin->meta.errors)
|
||||
{
|
||||
errors += "<li>" + err.toHtmlEscaped() + "</li>";
|
||||
}
|
||||
errors += "</ul>";
|
||||
|
||||
auto *warningLabel = new QLabel(
|
||||
"There were errors while loading metadata for this plugin:" +
|
||||
errors,
|
||||
this->dataFrame_);
|
||||
warningLabel->setTextFormat(Qt::RichText);
|
||||
warningLabel->setStyleSheet("color: #f00");
|
||||
pluginEntry->addRow(warningLabel);
|
||||
}
|
||||
|
||||
auto *description =
|
||||
new QLabel(plugin->meta.description, this->dataFrame_);
|
||||
description->setWordWrap(true);
|
||||
description->setStyleSheet("color: #bbb");
|
||||
pluginEntry->addRow(description);
|
||||
|
||||
QString authorsTxt;
|
||||
for (const auto &author : plugin->meta.authors)
|
||||
{
|
||||
if (!authorsTxt.isEmpty())
|
||||
{
|
||||
authorsTxt += ", ";
|
||||
}
|
||||
|
||||
authorsTxt += author;
|
||||
}
|
||||
pluginEntry->addRow("Authors",
|
||||
new QLabel(authorsTxt, this->dataFrame_));
|
||||
|
||||
if (!plugin->meta.homepage.isEmpty())
|
||||
{
|
||||
auto *homepage = new QLabel(formatRichLink(plugin->meta.homepage),
|
||||
this->dataFrame_);
|
||||
homepage->setOpenExternalLinks(true);
|
||||
pluginEntry->addRow("Homepage", homepage);
|
||||
}
|
||||
pluginEntry->addRow("License",
|
||||
new QLabel(plugin->meta.license, this->dataFrame_));
|
||||
|
||||
QString commandsTxt;
|
||||
for (const auto &cmdName : plugin->listRegisteredCommands())
|
||||
{
|
||||
if (!commandsTxt.isEmpty())
|
||||
{
|
||||
commandsTxt += ", ";
|
||||
}
|
||||
|
||||
commandsTxt += cmdName;
|
||||
}
|
||||
pluginEntry->addRow("Commands",
|
||||
new QLabel(commandsTxt, this->dataFrame_));
|
||||
|
||||
if (plugin->meta.isValid())
|
||||
{
|
||||
QString toggleTxt = "Enable";
|
||||
if (PluginController::isPluginEnabled(id))
|
||||
{
|
||||
toggleTxt = "Disable";
|
||||
}
|
||||
|
||||
auto *toggleButton = new QPushButton(toggleTxt, this->dataFrame_);
|
||||
QObject::connect(
|
||||
toggleButton, &QPushButton::pressed, [name = id, this]() {
|
||||
std::vector<QString> val =
|
||||
getSettings()->enabledPlugins.getValue();
|
||||
if (PluginController::isPluginEnabled(name))
|
||||
{
|
||||
val.erase(std::remove(val.begin(), val.end(), name),
|
||||
val.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
val.push_back(name);
|
||||
}
|
||||
getSettings()->enabledPlugins.setValue(val);
|
||||
getApp()->plugins->reload(name);
|
||||
this->rebuildContent();
|
||||
});
|
||||
pluginEntry->addRow(toggleButton);
|
||||
}
|
||||
|
||||
auto *reloadButton = new QPushButton("Reload", this->dataFrame_);
|
||||
QObject::connect(reloadButton, &QPushButton::pressed,
|
||||
[name = id, this]() {
|
||||
getApp()->plugins->reload(name);
|
||||
this->rebuildContent();
|
||||
});
|
||||
pluginEntry->addRow(reloadButton);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
30
src/widgets/settingspages/PluginsPage.hpp
Normal file
30
src/widgets/settingspages/PluginsPage.hpp
Normal file
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "util/LayoutCreator.hpp"
|
||||
# include "widgets/settingspages/SettingsPage.hpp"
|
||||
|
||||
# include <QDebug>
|
||||
# include <QFormLayout>
|
||||
# include <QGroupBox>
|
||||
# include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
class Plugin;
|
||||
|
||||
class PluginsPage : public SettingsPage
|
||||
{
|
||||
public:
|
||||
PluginsPage();
|
||||
|
||||
private:
|
||||
void rebuildContent();
|
||||
|
||||
LayoutCreator<QWidget> scrollAreaWidget_;
|
||||
QGroupBox *generalGroup;
|
||||
QFrame *dataFrame_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
Loading…
Reference in a new issue