Add plugin permissions and IO API (#5231)
This commit is contained in:
@@ -44,6 +44,8 @@
|
|||||||
- Minor: Image links now reflect the scale of their image instead of an internal label. (#5201)
|
- Minor: Image links now reflect the scale of their image instead of an internal label. (#5201)
|
||||||
- Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226)
|
- Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226)
|
||||||
- Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209)
|
- Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209)
|
||||||
|
- Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231)
|
||||||
|
- Minor: Add permissions to experimental plugins feature. (#5231)
|
||||||
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
|
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
|
||||||
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
|
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
|
||||||
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
|
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
|
||||||
|
|||||||
@@ -41,9 +41,21 @@
|
|||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A small description of your license.",
|
"description": "SPDX identifier for license of this plugin. See https://spdx.org/licenses/",
|
||||||
"examples": ["MIT", "GPL-2.0-or-later"]
|
"examples": ["MIT", "GPL-2.0-or-later"]
|
||||||
},
|
},
|
||||||
|
"permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The permissions the plugin needs to work.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"enum": ["FilesystemRead", "FilesystemWrite"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"$schema": { "type": "string" }
|
"$schema": { "type": "string" }
|
||||||
},
|
},
|
||||||
"required": ["name", "description", "authors", "version", "license"]
|
"required": ["name", "description", "authors", "version", "license"]
|
||||||
|
|||||||
+163
-5
@@ -14,7 +14,9 @@ Each plugin should have its own directory.
|
|||||||
Chatterino Plugins dir/
|
Chatterino Plugins dir/
|
||||||
└── plugin_name/
|
└── plugin_name/
|
||||||
├── init.lua
|
├── init.lua
|
||||||
└── info.json
|
├── info.json
|
||||||
|
└── data/
|
||||||
|
└── This is where your data/configs can be dumped
|
||||||
```
|
```
|
||||||
|
|
||||||
`init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`require` global function](#requiremodname).
|
`init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`require` global function](#requiremodname).
|
||||||
@@ -35,12 +37,54 @@ Example file:
|
|||||||
"homepage": "https://github.com/Chatterino/Chatterino2",
|
"homepage": "https://github.com/Chatterino/Chatterino2",
|
||||||
"tags": ["test"],
|
"tags": ["test"],
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"permissions": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plugin](https://github.com/Mm2PL/Chatterino-test-plugin)
|
An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plugin](https://github.com/Mm2PL/Chatterino-test-plugin)
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
Plugins can have permissions associated to them. Unless otherwise noted functions don't require permissions.
|
||||||
|
These are the valid permissions:
|
||||||
|
|
||||||
|
### FilesystemRead
|
||||||
|
|
||||||
|
Allows the plugin to read from its data directory.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
...,
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"type": "FilesystemRead"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FilesystemWrite
|
||||||
|
|
||||||
|
Allows the plugin to write to files and create files in its data directory.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
...,
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"type": "FilesystemWrite"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Plugins with Typescript
|
## Plugins with Typescript
|
||||||
|
|
||||||
If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io)
|
If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io)
|
||||||
@@ -60,9 +104,10 @@ script](../scripts/make_luals_meta.py).
|
|||||||
The following parts of the Lua standard library are loaded:
|
The following parts of the Lua standard library are loaded:
|
||||||
|
|
||||||
- `_G` (most globals)
|
- `_G` (most globals)
|
||||||
- `table`
|
- `io` - except `stdin`, `stdout`, `stderr`. Some functions require permissions.
|
||||||
- `string`
|
|
||||||
- `math`
|
- `math`
|
||||||
|
- `string`
|
||||||
|
- `table`
|
||||||
- `utf8`
|
- `utf8`
|
||||||
|
|
||||||
The official manual for them is available [here](https://www.lua.org/manual/5.4/manual.html#6).
|
The official manual for them is available [here](https://www.lua.org/manual/5.4/manual.html#6).
|
||||||
@@ -325,6 +370,117 @@ Returns `true` if the channel can be moderated by the current user.
|
|||||||
|
|
||||||
Returns `true` if the current user is a VIP in the channel.
|
Returns `true` if the current user is a VIP in the channel.
|
||||||
|
|
||||||
|
### Input/Output API
|
||||||
|
|
||||||
|
These functions are wrappers for Lua's I/O library. Functions on file pointer
|
||||||
|
objects (`FILE*`) are not modified or replaced. [You can read the documentation
|
||||||
|
for them here](https://www.lua.org/manual/5.4/manual.html#pdf-file:close).
|
||||||
|
Chatterino does _not_ give you stdin and stdout as default input and output
|
||||||
|
respectively. The following objects are missing from the `io` table exposed by
|
||||||
|
Chatterino compared to Lua's native library: `stdin`, `stdout`, `stderr`.
|
||||||
|
|
||||||
|
#### `close([file])`
|
||||||
|
|
||||||
|
Closes a file. If not given, `io.output()` is used instead.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.close)
|
||||||
|
|
||||||
|
#### `flush()`
|
||||||
|
|
||||||
|
Flushes `io.output()`.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.flush)
|
||||||
|
|
||||||
|
#### `input([file_or_name])`
|
||||||
|
|
||||||
|
When called with no arguments this function returns the default input file.
|
||||||
|
This variant requires no permissions.
|
||||||
|
|
||||||
|
When called with a file object, it will set the default input file to the one
|
||||||
|
given. This one also requires no permissions.
|
||||||
|
|
||||||
|
When called with a filename as a string, it will open that file for reading.
|
||||||
|
Equivalent to: `io.input(io.open(filename))`. This variant requires
|
||||||
|
the `FilesystemRead` permission and the given file to be within the plugin's
|
||||||
|
data directory.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.input)
|
||||||
|
|
||||||
|
#### `lines([filename, ...])`
|
||||||
|
|
||||||
|
With no arguments this function is equivalent to `io.input():lines("l")`. See
|
||||||
|
[Lua documentation for file:flush()](https://www.lua.org/manual/5.4/manual.html#pdf-file:flush).
|
||||||
|
This variant requires no permissions.
|
||||||
|
|
||||||
|
With `filename` given it is most like `io.open(filename):lines(...)`. This
|
||||||
|
variant requires the `FilesystemRead` permission and the given file to be
|
||||||
|
within the plugin's data directory.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.lines)
|
||||||
|
|
||||||
|
#### `open(filename [, mode])`
|
||||||
|
|
||||||
|
This functions opens the given file with a mode. It requires `filename` to be
|
||||||
|
within the plugin's data directory. A call with no mode given is equivalent to
|
||||||
|
one with `mode="r"`.
|
||||||
|
Depending on the mode this function has slightly different behavior:
|
||||||
|
|
||||||
|
| Mode | Permission | Read? | Write? | Truncate? | Create? |
|
||||||
|
| ----------- | ----------------- | ----- | ------ | --------- | ------- |
|
||||||
|
| `r` read | `FilesystemRead` | Yes | No | No | No |
|
||||||
|
| `w` write | `FilesystemWrite` | No | Yes | Yes | Yes |
|
||||||
|
| `a` append | `FilesystemWrite` | No | Append | No | Yes |
|
||||||
|
| `r+` update | `FilesystemWrite` | Yes | Yes | No | No |
|
||||||
|
| `w+` update | `FilesystemWrite` | Yes | Yes | Yes | Yes |
|
||||||
|
| `a+` update | `FilesystemWrite` | Yes | Append | No | Yes |
|
||||||
|
|
||||||
|
To open a file in binary mode add a `b` at the end of the mode.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.open)
|
||||||
|
|
||||||
|
#### `output([file_or_name])`
|
||||||
|
|
||||||
|
This is identical to [`io.input()`](#inputfile_or_name) but operates on the
|
||||||
|
default output and opens the file in write mode instead. Requires
|
||||||
|
`FilesystemWrite` instead of `FilesystemRead`.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.output)
|
||||||
|
|
||||||
|
#### `popen(exe [, mode])`
|
||||||
|
|
||||||
|
This function is unavailable in Chatterino. Calling it results in an error
|
||||||
|
message to let you know that it's not available, no permissions needed.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.popen)
|
||||||
|
|
||||||
|
#### `read(...)`
|
||||||
|
|
||||||
|
Equivalent to `io.input():read(...)`. See [`io.input()`](#inputfile_or_name)
|
||||||
|
and [`file:read()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:read).
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.read)
|
||||||
|
|
||||||
|
#### `tmpfile()`
|
||||||
|
|
||||||
|
This function is unavailable in Chatterino. Calling it results in an error
|
||||||
|
message to let you know that it's not available, no permissions needed.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.tmpfile)
|
||||||
|
|
||||||
|
#### `type(obj)`
|
||||||
|
|
||||||
|
This functions allows you to tell if the object is a `file`, a `closed file` or
|
||||||
|
a different bit of data.
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.type)
|
||||||
|
|
||||||
|
#### `write(...)`
|
||||||
|
|
||||||
|
Equivalent to `io.output():write(...)`. See [`io.output()`](#outputfile_or_name)
|
||||||
|
and [`file:write()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:write).
|
||||||
|
|
||||||
|
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write)
|
||||||
|
|
||||||
### Changed globals
|
### Changed globals
|
||||||
|
|
||||||
#### `load(chunk [, chunkname [, mode [, env]]])`
|
#### `load(chunk [, chunkname [, mode [, env]]])`
|
||||||
@@ -344,7 +500,8 @@ However, the searcher and load configuration is notably different from the defau
|
|||||||
- `package.path` is not used, in its place are two searchers,
|
- `package.path` is not used, in its place are two searchers,
|
||||||
- when `require()` is used, first a file relative to the currently executing
|
- when `require()` is used, first a file relative to the currently executing
|
||||||
file will be checked, then a file relative to the plugin directory,
|
file will be checked, then a file relative to the plugin directory,
|
||||||
- binary chunks are never loaded
|
- binary chunks are never loaded,
|
||||||
|
- files inside of the plugin `data` directory are never loaded
|
||||||
|
|
||||||
As in normal Lua, dots are converted to the path separators (`'/'` on Linux and Mac, `'\'` on Windows).
|
As in normal Lua, dots are converted to the path separators (`'/'` on Linux and Mac, `'\'` on Windows).
|
||||||
|
|
||||||
@@ -354,6 +511,7 @@ Example:
|
|||||||
require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua
|
require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua
|
||||||
require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua
|
require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua
|
||||||
require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file
|
require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file
|
||||||
|
require("data.file") -- tried to load Plugins/name/data/file.lua and errors because that is not allowed
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `print(Args...)`
|
#### `print(Args...)`
|
||||||
|
|||||||
@@ -223,8 +223,12 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
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/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.cpp
|
||||||
controllers/plugins/Plugin.hpp
|
controllers/plugins/Plugin.hpp
|
||||||
controllers/plugins/PluginController.hpp
|
controllers/plugins/PluginController.hpp
|
||||||
|
|||||||
@@ -266,6 +266,14 @@ int loadfile(lua_State *L, const QString &str)
|
|||||||
L, QString("requested module is outside of the plugin directory"));
|
L, QString("requested module is outside of the plugin directory"));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
auto datadir = QUrl(pl->dataDirectory().canonicalPath() + "/");
|
||||||
|
if (datadir.isParentOf(str))
|
||||||
|
{
|
||||||
|
lua::push(L, QString("requested file is data, not code, see Chatterino "
|
||||||
|
"documentation"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
QFileInfo info(str);
|
QFileInfo info(str);
|
||||||
if (!info.exists())
|
if (!info.exists())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ StackIdx push(lua_State *L, T inp)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Converts a Lua object into c++ and removes it from the stack.
|
* @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.
|
* Relies on bool peek(lua_State*, T*, StackIdx) existing.
|
||||||
*/
|
*/
|
||||||
@@ -291,14 +292,11 @@ bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
|||||||
{
|
{
|
||||||
StackGuard guard(L, -1);
|
StackGuard guard(L, -1);
|
||||||
auto ok = peek(L, out, idx);
|
auto ok = peek(L, out, idx);
|
||||||
if (ok)
|
if (idx < 0)
|
||||||
{
|
{
|
||||||
if (idx < 0)
|
idx = lua_gettop(L) + idx + 1;
|
||||||
{
|
|
||||||
idx = lua_gettop(L) + idx + 1;
|
|
||||||
}
|
|
||||||
lua_remove(L, idx);
|
|
||||||
}
|
}
|
||||||
|
lua_remove(L, idx);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
# include <QJsonArray>
|
# include <QJsonArray>
|
||||||
# include <QJsonObject>
|
# include <QJsonObject>
|
||||||
|
|
||||||
|
# include <algorithm>
|
||||||
# include <unordered_map>
|
# include <unordered_map>
|
||||||
# include <unordered_set>
|
# include <unordered_set>
|
||||||
|
|
||||||
@@ -111,6 +112,48 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
|
|||||||
QString("version is not a string (its type is %1)").arg(type));
|
QString("version is not a string (its type is %1)").arg(type));
|
||||||
this->version = semver::version(0, 0, 0);
|
this->version = semver::version(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
auto permsObj = obj.value("permissions");
|
||||||
|
if (!permsObj.isUndefined())
|
||||||
|
{
|
||||||
|
if (!permsObj.isArray())
|
||||||
|
{
|
||||||
|
QString type = magic_enum::enum_name(permsObj.type()).data();
|
||||||
|
this->errors.emplace_back(
|
||||||
|
QString("permissions is not an array (its type is %1)")
|
||||||
|
.arg(type));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto permsArr = permsObj.toArray();
|
||||||
|
for (int i = 0; i < permsArr.size(); i++)
|
||||||
|
{
|
||||||
|
const auto &t = permsArr.at(i);
|
||||||
|
if (!t.isObject())
|
||||||
|
{
|
||||||
|
QString type = magic_enum::enum_name(t.type()).data();
|
||||||
|
this->errors.push_back(QString("permissions element #%1 is not "
|
||||||
|
"an object (its type is %2)")
|
||||||
|
.arg(i)
|
||||||
|
.arg(type));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto parsed = PluginPermission(t.toObject());
|
||||||
|
if (parsed.isValid())
|
||||||
|
{
|
||||||
|
// ensure no invalid permissions slip through this
|
||||||
|
this->permissions.push_back(parsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (const auto &err : parsed.errors)
|
||||||
|
{
|
||||||
|
this->errors.push_back(
|
||||||
|
QString("permissions element #%1: %2").arg(i).arg(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto tagsObj = obj.value("tags");
|
auto tagsObj = obj.value("tags");
|
||||||
if (!tagsObj.isUndefined())
|
if (!tagsObj.isUndefined())
|
||||||
{
|
{
|
||||||
@@ -201,5 +244,28 @@ void Plugin::removeTimeout(QTimer *timer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Plugin::hasFSPermissionFor(bool write, const QString &path)
|
||||||
|
{
|
||||||
|
auto canon = QUrl(this->dataDirectory().absolutePath() + "/");
|
||||||
|
if (!canon.isParentOf(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using PType = PluginPermission::Type;
|
||||||
|
auto typ = write ? PType::FilesystemWrite : PType::FilesystemRead;
|
||||||
|
|
||||||
|
// XXX: Older compilers don't have support for std::ranges
|
||||||
|
// NOLINTNEXTLINE(readability-use-anyofallof)
|
||||||
|
for (const auto &p : this->meta.permissions)
|
||||||
|
{
|
||||||
|
if (p.type == typ)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
|
|
||||||
# include <QDir>
|
# include <QDir>
|
||||||
# include <QString>
|
# include <QString>
|
||||||
@@ -42,6 +43,8 @@ struct PluginMeta {
|
|||||||
// optionally tags that might help in searching for the plugin
|
// optionally tags that might help in searching for the plugin
|
||||||
std::vector<QString> tags;
|
std::vector<QString> tags;
|
||||||
|
|
||||||
|
std::vector<PluginPermission> permissions;
|
||||||
|
|
||||||
// errors that occurred while parsing info.json
|
// errors that occurred while parsing info.json
|
||||||
std::vector<QString> errors;
|
std::vector<QString> errors;
|
||||||
|
|
||||||
@@ -88,6 +91,11 @@ public:
|
|||||||
return this->loadDirectory_;
|
return this->loadDirectory_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QDir dataDirectory() const
|
||||||
|
{
|
||||||
|
return this->loadDirectory_.absoluteFilePath("data");
|
||||||
|
}
|
||||||
|
|
||||||
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
||||||
using LuaCompletionCallback =
|
using LuaCompletionCallback =
|
||||||
lua::CallbackFunction<lua::api::CompletionList, QString, QString, int,
|
lua::CallbackFunction<lua::api::CompletionList, QString, QString, int,
|
||||||
@@ -130,6 +138,8 @@ public:
|
|||||||
int addTimeout(QTimer *timer);
|
int addTimeout(QTimer *timer);
|
||||||
void removeTimeout(QTimer *timer);
|
void removeTimeout(QTimer *timer);
|
||||||
|
|
||||||
|
bool hasFSPermissionFor(bool write, const QString &path);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir loadDirectory_;
|
QDir loadDirectory_;
|
||||||
lua_State *state_;
|
lua_State *state_;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# include "controllers/commands/CommandContext.hpp"
|
# include "controllers/commands/CommandContext.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
# include "controllers/commands/CommandController.hpp"
|
||||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
# include "controllers/plugins/api/ChannelRef.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 "messages/MessageBuilder.hpp"
|
# include "messages/MessageBuilder.hpp"
|
||||||
@@ -140,6 +141,8 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||||||
luaL_requiref(L, reg.name, reg.func, int(true));
|
luaL_requiref(L, reg.name, reg.func, int(true));
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||||
static const luaL_Reg c2Lib[] = {
|
static const luaL_Reg c2Lib[] = {
|
||||||
@@ -234,8 +237,53 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||||||
lua::push(L, QString(pluginDir.absolutePath()));
|
lua::push(L, QString(pluginDir.absolutePath()));
|
||||||
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
||||||
lua_seti(L, -2, 3);
|
lua_seti(L, -2, 3);
|
||||||
|
lua_pop(L, 2); // remove package, package.searchers
|
||||||
|
|
||||||
lua_pop(L, 3); // remove gtable, package, package.searchers
|
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||||
|
static const luaL_Reg ioLib[] = {
|
||||||
|
{"close", lua::api::io_close},
|
||||||
|
{"flush", lua::api::io_flush},
|
||||||
|
{"input", lua::api::io_input},
|
||||||
|
{"lines", lua::api::io_lines},
|
||||||
|
{"open", lua::api::io_open},
|
||||||
|
{"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
|
||||||
|
lua_pushvalue(L, iolibIdx);
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||||
|
lua_getfield(L, -1, "type");
|
||||||
|
lua_remove(L, -2); // remove realio
|
||||||
|
lua_setfield(L, iolibIdx, "type");
|
||||||
|
lua_pop(L, 1); // still have iolib on top of stack
|
||||||
|
|
||||||
|
lua_pushvalue(L, iolibIdx);
|
||||||
|
lua_setfield(L, gtable, "io");
|
||||||
|
|
||||||
|
lua_pushvalue(L, iolibIdx);
|
||||||
|
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME);
|
||||||
|
|
||||||
|
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
|
||||||
|
lua_pushvalue(L, iolibIdx);
|
||||||
|
lua_setfield(L, -2, "io");
|
||||||
|
|
||||||
|
lua_pop(L, 3); // remove gtable, iolib, LOADED
|
||||||
|
|
||||||
|
// 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,
|
||||||
@@ -263,6 +311,8 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||||||
<< meta.name << ") because it is disabled";
|
<< meta.name << ") because it is disabled";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
temp->dataDirectory().mkpath(".");
|
||||||
|
|
||||||
qCDebug(chatterinoLua) << "Running lua file:" << index;
|
qCDebug(chatterinoLua) << "Running lua file:" << index;
|
||||||
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||||
if (err != 0)
|
if (err != 0)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
|
|
||||||
|
# include <magic_enum/magic_enum.hpp>
|
||||||
|
# include <QJsonObject>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
PluginPermission::PluginPermission(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
auto jsontype = obj.value("type");
|
||||||
|
if (!jsontype.isString())
|
||||||
|
{
|
||||||
|
QString tn = magic_enum::enum_name(jsontype.type()).data();
|
||||||
|
this->errors.emplace_back(QString("permission type is defined but is "
|
||||||
|
"not a string (its type is %1)")
|
||||||
|
.arg(tn));
|
||||||
|
}
|
||||||
|
auto strtype = jsontype.toString().toStdString();
|
||||||
|
auto opt = magic_enum::enum_cast<PluginPermission::Type>(
|
||||||
|
strtype, magic_enum::case_insensitive);
|
||||||
|
if (!opt.has_value())
|
||||||
|
{
|
||||||
|
this->errors.emplace_back(QString("permission type is an unknown (%1)")
|
||||||
|
.arg(jsontype.toString()));
|
||||||
|
return; // There is no more data to get, we don't know what to do
|
||||||
|
}
|
||||||
|
this->type = opt.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PluginPermission::toHtml() const
|
||||||
|
{
|
||||||
|
switch (this->type)
|
||||||
|
{
|
||||||
|
case PluginPermission::Type::FilesystemRead:
|
||||||
|
return "Read files in its data directory";
|
||||||
|
case PluginPermission::Type::FilesystemWrite:
|
||||||
|
return "Write to or create files in its data directory";
|
||||||
|
default:
|
||||||
|
assert(false && "invalid PluginPermission type in toHtml()");
|
||||||
|
return "shut up compiler, this never happens";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
# include <QJsonObject>
|
||||||
|
# include <QString>
|
||||||
|
|
||||||
|
# include <vector>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
struct PluginPermission {
|
||||||
|
explicit PluginPermission(const QJsonObject &obj);
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
FilesystemRead,
|
||||||
|
FilesystemWrite,
|
||||||
|
};
|
||||||
|
Type type;
|
||||||
|
std::vector<QString> errors;
|
||||||
|
|
||||||
|
bool isValid() const
|
||||||
|
{
|
||||||
|
return this->errors.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString toHtml() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
#endif
|
||||||
@@ -74,11 +74,13 @@ ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
|||||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||||
{
|
{
|
||||||
luaL_error(
|
luaL_error(
|
||||||
L, "Called c2.Channel method with a non Channel 'self' argument.");
|
L, "Called c2.Channel method with a non-userdata 'self' argument");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
auto *data = WeakPtrUserData<UserData::Type::Channel, Channel>::from(
|
// luaL_checkudata is no-return if check fails
|
||||||
lua_touserdata(L, lua_gettop(L)));
|
auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel");
|
||||||
|
auto *data =
|
||||||
|
WeakPtrUserData<UserData::Type::Channel, Channel>::from(checked);
|
||||||
if (data == nullptr)
|
if (data == nullptr)
|
||||||
{
|
{
|
||||||
luaL_error(L,
|
luaL_error(L,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/Channel.hpp"
|
# include "common/Channel.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
|
||||||
|
# include <optional>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(readability-identifier-naming)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||||
|
|
||||||
|
# include "Application.hpp"
|
||||||
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "lauxlib.h"
|
||||||
|
# include "lua.h"
|
||||||
|
|
||||||
|
# include <cerrno>
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
|
// Note: Parsing and then serializing the mode ensures we understand it before
|
||||||
|
// passing it to Lua
|
||||||
|
|
||||||
|
struct LuaFileMode {
|
||||||
|
char major = 'r'; // 'r'|'w'|'a'
|
||||||
|
bool update{}; // '+'
|
||||||
|
bool binary{}; // 'b'
|
||||||
|
QString error;
|
||||||
|
|
||||||
|
LuaFileMode() = default;
|
||||||
|
|
||||||
|
LuaFileMode(const QString &smode)
|
||||||
|
{
|
||||||
|
if (smode.isEmpty())
|
||||||
|
{
|
||||||
|
this->error = "Empty mode given, use one matching /[rwa][+]?b?/.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto major = smode.at(0);
|
||||||
|
if (major != 'r' && major != 'w' && major != 'a')
|
||||||
|
{
|
||||||
|
this->error = "Invalid mode, use one matching /[rwa][+]?b?/. "
|
||||||
|
"Parsing failed at 1st character.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->major = major.toLatin1();
|
||||||
|
if (smode.length() > 1)
|
||||||
|
{
|
||||||
|
auto plusOrB = smode.at(1);
|
||||||
|
if (plusOrB == '+')
|
||||||
|
{
|
||||||
|
this->update = true;
|
||||||
|
}
|
||||||
|
else if (plusOrB == 'b')
|
||||||
|
{
|
||||||
|
this->binary = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->error = "Invalid mode, use one matching /[rwa][+]?b?/. "
|
||||||
|
"Parsing failed at 2nd character.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (smode.length() > 2)
|
||||||
|
{
|
||||||
|
auto maybeB = smode.at(2);
|
||||||
|
if (maybeB == 'b')
|
||||||
|
{
|
||||||
|
this->binary = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->error = "Invalid mode, use one matching /[rwa][+]?b?/. "
|
||||||
|
"Parsing failed at 3rd character.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString toString() const
|
||||||
|
{
|
||||||
|
assert(this->major == 'r' || this->major == 'w' || this->major == 'a');
|
||||||
|
QString out;
|
||||||
|
out += this->major;
|
||||||
|
if (this->update)
|
||||||
|
{
|
||||||
|
out += '+';
|
||||||
|
}
|
||||||
|
if (this->binary)
|
||||||
|
{
|
||||||
|
out += 'b';
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int ioError(lua_State *L, const QString &value, int errnoequiv)
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua::push(L, value);
|
||||||
|
lua::push(L, errnoequiv);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOLINTBEGIN(*vararg)
|
||||||
|
int io_open(lua_State *L)
|
||||||
|
{
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
luaL_error(L, "internal error: no plugin");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
LuaFileMode mode;
|
||||||
|
if (lua_gettop(L) == 2)
|
||||||
|
{
|
||||||
|
// we have a mode
|
||||||
|
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));
|
||||||
|
auto abs = file.absoluteFilePath();
|
||||||
|
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||||
|
<< "] Plugin is opening file at " << abs
|
||||||
|
<< " with mode " << mode.toString();
|
||||||
|
bool ok = pl->hasFSPermissionFor(
|
||||||
|
mode.update || mode.major == 'w' || mode.major == 'a', abs);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
return ioError(L,
|
||||||
|
"Plugin does not have permissions to access given file.",
|
||||||
|
EACCES);
|
||||||
|
}
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||||
|
lua_getfield(L, -1, "open");
|
||||||
|
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||||
|
lua::push(L, abs);
|
||||||
|
lua::push(L, mode.toString());
|
||||||
|
lua_call(L, 2, 3);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int io_lines(lua_State *L)
|
||||||
|
{
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
luaL_error(L, "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");
|
||||||
|
}
|
||||||
|
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||||
|
auto abs = file.absoluteFilePath();
|
||||||
|
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||||
|
<< "] Plugin is opening file at " << abs
|
||||||
|
<< " for reading lines";
|
||||||
|
bool ok = pl->hasFSPermissionFor(false, abs);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
return ioError(L,
|
||||||
|
"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);
|
||||||
|
lua_getfield(L, -1, "lines");
|
||||||
|
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||||
|
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 {
|
||||||
|
|
||||||
|
// This is the code for both io.input and io.output
|
||||||
|
int globalFileCommon(lua_State *L, bool output)
|
||||||
|
{
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int io_input(lua_State *L)
|
||||||
|
{
|
||||||
|
return globalFileCommon(L, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
int io_output(lua_State *L)
|
||||||
|
{
|
||||||
|
return globalFileCommon(L, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int io_close(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_gettop(L) > 1)
|
||||||
|
{
|
||||||
|
return luaL_error(
|
||||||
|
L, "Too many arguments for io.close. Expected one or zero.");
|
||||||
|
}
|
||||||
|
if (lua_gettop(L) == 0)
|
||||||
|
{
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||||
|
}
|
||||||
|
lua_getfield(L, -1, "close");
|
||||||
|
lua_pushvalue(L, -2);
|
||||||
|
lua_call(L, 1, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int io_flush(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_gettop(L) > 1)
|
||||||
|
{
|
||||||
|
return luaL_error(
|
||||||
|
L, "Too many arguments for io.flush. Expected one or zero.");
|
||||||
|
}
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||||
|
lua_getfield(L, -1, "flush");
|
||||||
|
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(
|
||||||
|
L, "Too many arguments for io.read. Expected one or zero.");
|
||||||
|
}
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||||
|
lua_getfield(L, -1, "write");
|
||||||
|
lua_insert(L, 1);
|
||||||
|
lua_insert(L, 2);
|
||||||
|
// (input)
|
||||||
|
// (input).read
|
||||||
|
// args
|
||||||
|
lua_call(L, lua_gettop(L) - 1, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int io_popen(lua_State *L)
|
||||||
|
{
|
||||||
|
return luaL_error(L, "io.popen: This function is a stub!");
|
||||||
|
}
|
||||||
|
|
||||||
|
int io_tmpfile(lua_State *L)
|
||||||
|
{
|
||||||
|
return luaL_error(L, "io.tmpfile: This function is a stub!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOLINTEND(*vararg)
|
||||||
|
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
struct lua_State;
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
// 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_C2_IO_NAME = "c2io";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file.
|
||||||
|
* If given a relative path, it will be relative to
|
||||||
|
* c2datadir/Plugins/pluginDir/data/
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.open
|
||||||
|
*
|
||||||
|
* @lua@param filename string
|
||||||
|
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
||||||
|
* @exposed io.open
|
||||||
|
*/
|
||||||
|
int io_open(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
||||||
|
* If given a relative path, it will be relative to
|
||||||
|
* c2datadir/Plugins/pluginDir/data/
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.lines
|
||||||
|
*
|
||||||
|
* @lua@param filename nil|string
|
||||||
|
* @lua@param ...
|
||||||
|
* @exposed io.lines
|
||||||
|
*/
|
||||||
|
int io_lines(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.input
|
||||||
|
*
|
||||||
|
* @lua@param fileorname nil|string|FILE*
|
||||||
|
* @lua@return nil|FILE*
|
||||||
|
* @exposed io.input
|
||||||
|
*/
|
||||||
|
int io_input(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file and sets it as default output or if given no arguments returns the default output
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.output
|
||||||
|
*
|
||||||
|
* @lua@param fileorname nil|string|FILE*
|
||||||
|
* @lua@return nil|FILE*
|
||||||
|
* @exposed io.output
|
||||||
|
*/
|
||||||
|
int io_output(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes given file or io.output() if not given.
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.close
|
||||||
|
*
|
||||||
|
* @lua@param nil|FILE*
|
||||||
|
* @exposed io.close
|
||||||
|
*/
|
||||||
|
int io_close(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes the buffer for given file or io.output() if not given.
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.flush
|
||||||
|
*
|
||||||
|
* @lua@param nil|FILE*
|
||||||
|
* @exposed io.flush
|
||||||
|
*/
|
||||||
|
int io_flush(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads some data from the default input file
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.read
|
||||||
|
*
|
||||||
|
* @lua@param nil|string
|
||||||
|
* @exposed io.read
|
||||||
|
*/
|
||||||
|
int io_read(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes some data to the default output file
|
||||||
|
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.write
|
||||||
|
*
|
||||||
|
* @lua@param nil|string
|
||||||
|
* @exposed io.write
|
||||||
|
*/
|
||||||
|
int io_write(lua_State *L);
|
||||||
|
|
||||||
|
int io_popen(lua_State *L);
|
||||||
|
int io_tmpfile(lua_State *L);
|
||||||
|
|
||||||
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
#endif
|
||||||
@@ -161,6 +161,20 @@ void PluginsPage::rebuildContent()
|
|||||||
}
|
}
|
||||||
pluginEntry->addRow("Commands",
|
pluginEntry->addRow("Commands",
|
||||||
new QLabel(commandsTxt, this->dataFrame_));
|
new QLabel(commandsTxt, this->dataFrame_));
|
||||||
|
if (!plugin->meta.permissions.empty())
|
||||||
|
{
|
||||||
|
QString perms = "<ul>";
|
||||||
|
for (const auto &perm : plugin->meta.permissions)
|
||||||
|
{
|
||||||
|
perms += "<li>" + perm.toHtml() + "</li>";
|
||||||
|
}
|
||||||
|
perms += "</ul>";
|
||||||
|
|
||||||
|
auto *lbl =
|
||||||
|
new QLabel("Required permissions:" + perms, this->dataFrame_);
|
||||||
|
lbl->setTextFormat(Qt::RichText);
|
||||||
|
pluginEntry->addRow(lbl);
|
||||||
|
}
|
||||||
|
|
||||||
if (plugin->meta.isValid())
|
if (plugin->meta.isValid())
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user