Add a new Channel API for experimental plugins feature (#5141)
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
- Minor: Added icons for newer versions of macOS. (#5148)
|
- Minor: Added icons for newer versions of macOS. (#5148)
|
||||||
- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149)
|
- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149)
|
||||||
- Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130)
|
- Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130)
|
||||||
|
- Minor: Add a new Channel API for experimental plugins feature. (#5141)
|
||||||
- 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)
|
||||||
|
|||||||
Vendored
+57
-2
@@ -12,13 +12,68 @@ declare module c2 {
|
|||||||
channel_name: String;
|
channel_name: String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Platform {
|
||||||
|
Twitch,
|
||||||
|
}
|
||||||
|
enum ChannelType {
|
||||||
|
None,
|
||||||
|
Direct,
|
||||||
|
Twitch,
|
||||||
|
TwitchWhispers,
|
||||||
|
TwitchWatching,
|
||||||
|
TwitchMentions,
|
||||||
|
TwitchLive,
|
||||||
|
TwitchAutomod,
|
||||||
|
Irc,
|
||||||
|
Misc,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWeakResource {
|
||||||
|
is_valid(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomModes {
|
||||||
|
unique_chat: boolean;
|
||||||
|
subscriber_only: boolean;
|
||||||
|
emotes_only: boolean;
|
||||||
|
follower_only: null | number;
|
||||||
|
slow_mode: null | number;
|
||||||
|
}
|
||||||
|
class StreamStatus {
|
||||||
|
live: boolean;
|
||||||
|
viewer_count: number;
|
||||||
|
uptime: number;
|
||||||
|
title: string;
|
||||||
|
game_name: string;
|
||||||
|
game_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Channel implements IWeakResource {
|
||||||
|
is_valid(): boolean;
|
||||||
|
get_name(): string;
|
||||||
|
get_type(): ChannelType;
|
||||||
|
get_display_name(): string;
|
||||||
|
send_message(message: string, execute_commands: boolean): void;
|
||||||
|
add_system_message(message: string): void;
|
||||||
|
|
||||||
|
is_twitch_channel(): boolean;
|
||||||
|
|
||||||
|
get_room_modes(): RoomModes;
|
||||||
|
get_stream_status(): StreamStatus;
|
||||||
|
get_twitch_id(): string;
|
||||||
|
is_broadcaster(): boolean;
|
||||||
|
is_mod(): boolean;
|
||||||
|
is_vip(): boolean;
|
||||||
|
|
||||||
|
static by_name(name: string, platform: Platform): null | Channel;
|
||||||
|
static by_twitch_id(id: string): null | Channel;
|
||||||
|
}
|
||||||
|
|
||||||
function log(level: LogLevel, ...data: any[]): void;
|
function log(level: LogLevel, ...data: any[]): void;
|
||||||
function register_command(
|
function register_command(
|
||||||
name: String,
|
name: String,
|
||||||
handler: (ctx: CommandContext) => void
|
handler: (ctx: CommandContext) => void
|
||||||
): boolean;
|
): boolean;
|
||||||
function send_msg(channel: String, text: String): boolean;
|
|
||||||
function system_msg(channel: String, text: String): boolean;
|
|
||||||
|
|
||||||
class CompletionList {
|
class CompletionList {
|
||||||
values: String[];
|
values: String[];
|
||||||
|
|||||||
+145
-16
@@ -6,6 +6,15 @@
|
|||||||
|
|
||||||
c2 = {}
|
c2 = {}
|
||||||
|
|
||||||
|
---@class IWeakResource
|
||||||
|
|
||||||
|
--- Returns true if the channel this object points to is valid.
|
||||||
|
--- If the object expired, returns false
|
||||||
|
--- If given a non-Channel object, it errors.
|
||||||
|
---@return boolean
|
||||||
|
function IWeakResource:is_valid() end
|
||||||
|
|
||||||
|
|
||||||
---@alias LogLevel integer
|
---@alias LogLevel integer
|
||||||
---@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel }
|
---@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel }
|
||||||
c2.LogLevel = {}
|
c2.LogLevel = {}
|
||||||
@@ -20,6 +29,142 @@ c2.EventType = {}
|
|||||||
---@class CompletionList
|
---@class CompletionList
|
||||||
---@field values string[] The completions
|
---@field values string[] The completions
|
||||||
---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
||||||
|
-- Now including data from src/common/Channel.hpp.
|
||||||
|
|
||||||
|
---@alias ChannelType integer
|
||||||
|
---@type { None: ChannelType }
|
||||||
|
ChannelType = {}
|
||||||
|
-- Back to src/controllers/plugins/LuaAPI.hpp.
|
||||||
|
-- Now including data from src/controllers/plugins/api/ChannelRef.hpp.
|
||||||
|
--- This enum describes a platform for the purpose of searching for a channel.
|
||||||
|
--- Currently only Twitch is supported because identifying IRC channels is tricky.
|
||||||
|
|
||||||
|
---@alias Platform integer
|
||||||
|
---@type { Twitch: Platform }
|
||||||
|
Platform = {}
|
||||||
|
---@class Channel: IWeakResource
|
||||||
|
|
||||||
|
--- Returns true if the channel this object points to is valid.
|
||||||
|
--- If the object expired, returns false
|
||||||
|
--- If given a non-Channel object, it errors.
|
||||||
|
---
|
||||||
|
---@return boolean success
|
||||||
|
function Channel:is_valid() end
|
||||||
|
|
||||||
|
--- Gets the channel's name. This is the lowercase login name.
|
||||||
|
---
|
||||||
|
---@return string name
|
||||||
|
function Channel:get_name() end
|
||||||
|
|
||||||
|
--- Gets the channel's type
|
||||||
|
---
|
||||||
|
---@return ChannelType
|
||||||
|
function Channel:get_type() end
|
||||||
|
|
||||||
|
--- Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||||
|
---
|
||||||
|
---@return string name
|
||||||
|
function Channel:get_display_name() end
|
||||||
|
|
||||||
|
--- Sends a message to the target channel.
|
||||||
|
--- Note that this does not execute client-commands.
|
||||||
|
---
|
||||||
|
---@param message string
|
||||||
|
---@param execute_commands boolean Should commands be run on the text?
|
||||||
|
function Channel:send_message(message, execute_commands) end
|
||||||
|
|
||||||
|
--- Adds a system message client-side
|
||||||
|
---
|
||||||
|
---@param message string
|
||||||
|
function Channel:add_system_message(message) end
|
||||||
|
|
||||||
|
--- Returns true for twitch channels.
|
||||||
|
--- Compares the channel Type. Note that enum values aren't guaranteed, just
|
||||||
|
--- that they are equal to the exposed enum.
|
||||||
|
---
|
||||||
|
---@return bool
|
||||||
|
function Channel:is_twitch_channel() end
|
||||||
|
|
||||||
|
--- Twitch Channel specific functions
|
||||||
|
|
||||||
|
--- Returns a copy of the channel mode settings (subscriber only, r9k etc.)
|
||||||
|
---
|
||||||
|
---@return RoomModes
|
||||||
|
function Channel:get_room_modes() end
|
||||||
|
|
||||||
|
--- Returns a copy of the stream status.
|
||||||
|
---
|
||||||
|
---@return StreamStatus
|
||||||
|
function Channel:get_stream_status() end
|
||||||
|
|
||||||
|
--- Returns the Twitch user ID of the owner of the channel.
|
||||||
|
---
|
||||||
|
---@return string
|
||||||
|
function Channel:get_twitch_id() end
|
||||||
|
|
||||||
|
--- Returns true if the channel is a Twitch channel and the user owns it
|
||||||
|
---
|
||||||
|
---@return boolean
|
||||||
|
function Channel:is_broadcaster() end
|
||||||
|
|
||||||
|
--- Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||||
|
--- Returns false for broadcaster.
|
||||||
|
---
|
||||||
|
---@return boolean
|
||||||
|
function Channel:is_mod() end
|
||||||
|
|
||||||
|
--- Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||||
|
--- Returns false for broadcaster.
|
||||||
|
---
|
||||||
|
---@return boolean
|
||||||
|
function Channel:is_vip() end
|
||||||
|
|
||||||
|
--- Misc
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function Channel:__tostring() end
|
||||||
|
|
||||||
|
--- Static functions
|
||||||
|
|
||||||
|
--- Finds a channel by name.
|
||||||
|
---
|
||||||
|
--- Misc channels are marked as Twitch:
|
||||||
|
--- - /whispers
|
||||||
|
--- - /mentions
|
||||||
|
--- - /watching
|
||||||
|
--- - /live
|
||||||
|
--- - /automod
|
||||||
|
---
|
||||||
|
---@param name string Which channel are you looking for?
|
||||||
|
---@param platform Platform Where to search for the channel?
|
||||||
|
---@return Channel?
|
||||||
|
function Channel.by_name(name, platform) end
|
||||||
|
|
||||||
|
--- Finds a channel by the Twitch user ID of its owner.
|
||||||
|
---
|
||||||
|
---@param string id ID of the owner of the channel.
|
||||||
|
---@return Channel?
|
||||||
|
function Channel.by_twitch_id(string) end
|
||||||
|
|
||||||
|
---@class RoomModes
|
||||||
|
---@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||||
|
---@field subscriber_only boolean
|
||||||
|
---@field emotes_only boolean Whether or not text is allowed in messages.
|
||||||
|
|
||||||
|
--- Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||||
|
|
||||||
|
---@field unique_chat number? Time in minutes you need to follow to chat or nil.
|
||||||
|
|
||||||
|
---@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||||
|
|
||||||
|
---@class StreamStatus
|
||||||
|
---@field live boolean
|
||||||
|
---@field viewer_count number
|
||||||
|
---@field uptime number Seconds since the stream started.
|
||||||
|
---@field title string Stream title or last stream title
|
||||||
|
---@field game_name string
|
||||||
|
---@field game_id string
|
||||||
|
-- Back to src/controllers/plugins/LuaAPI.hpp.
|
||||||
|
|
||||||
--- Registers a new command called `name` which when executed will call `handler`.
|
--- Registers a new command called `name` which when executed will call `handler`.
|
||||||
---
|
---
|
||||||
@@ -34,22 +179,6 @@ function c2.register_command(name, handler) end
|
|||||||
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
|
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
|
||||||
function c2.register_callback(type, func) end
|
function c2.register_callback(type, func) end
|
||||||
|
|
||||||
--- Sends a message to `channel` with the specified text. Also executes commands.
|
|
||||||
---
|
|
||||||
--- **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
|
||||||
---
|
|
||||||
---@param channel string The name of the Twitch channel
|
|
||||||
---@param text string The text to be sent
|
|
||||||
---@return boolean ok
|
|
||||||
function c2.send_msg(channel, text) end
|
|
||||||
|
|
||||||
--- Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
|
|
||||||
---
|
|
||||||
---@param channel string
|
|
||||||
---@param text string
|
|
||||||
---@return boolean ok
|
|
||||||
function c2.system_msg(channel, text) end
|
|
||||||
|
|
||||||
--- Writes a message to the Chatterino log.
|
--- Writes a message to the Chatterino log.
|
||||||
---
|
---
|
||||||
---@param level LogLevel The desired level.
|
---@param level LogLevel The desired level.
|
||||||
|
|||||||
+51
-20
@@ -12,6 +12,8 @@ It assumes comments look like:
|
|||||||
- Do not have any useful info on '/**' and '*/' lines.
|
- Do not have any useful info on '/**' and '*/' lines.
|
||||||
- Class members are not allowed to have non-@command lines and commands different from @lua@field
|
- Class members are not allowed to have non-@command lines and commands different from @lua@field
|
||||||
|
|
||||||
|
When this scripts sees "@brief", any further lines of the comment will be ignored
|
||||||
|
|
||||||
Valid commands are:
|
Valid commands are:
|
||||||
1. @exposeenum [dotted.name.in_lua.last_part]
|
1. @exposeenum [dotted.name.in_lua.last_part]
|
||||||
Define a table with keys of the enum. Values behind those keys aren't
|
Define a table with keys of the enum. Values behind those keys aren't
|
||||||
@@ -38,42 +40,54 @@ BOILERPLATE = """
|
|||||||
-- Add the folder this file is in to "Lua.workspace.library".
|
-- Add the folder this file is in to "Lua.workspace.library".
|
||||||
|
|
||||||
c2 = {}
|
c2 = {}
|
||||||
|
|
||||||
|
---@class IWeakResource
|
||||||
|
|
||||||
|
--- Returns true if the channel this object points to is valid.
|
||||||
|
--- If the object expired, returns false
|
||||||
|
--- If given a non-Channel object, it errors.
|
||||||
|
---@return boolean
|
||||||
|
function IWeakResource:is_valid() end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
repo_root = Path(__file__).parent.parent
|
repo_root = Path(__file__).parent.parent
|
||||||
lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp"
|
lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp"
|
||||||
lua_meta = repo_root / "docs" / "plugin-meta.lua"
|
lua_meta = repo_root / "docs" / "plugin-meta.lua"
|
||||||
|
|
||||||
print("Reading from", lua_api_file.relative_to(repo_root))
|
|
||||||
print("Writing to", lua_meta.relative_to(repo_root))
|
print("Writing to", lua_meta.relative_to(repo_root))
|
||||||
with lua_api_file.open("r") as f:
|
|
||||||
lines = f.read().splitlines()
|
|
||||||
|
|
||||||
# Are we in a doc comment?
|
|
||||||
comment: bool = False
|
|
||||||
|
|
||||||
# Last `@lua@param`s seen - for @exposed generation
|
def process_file(target, out):
|
||||||
last_params_names: list[str] = []
|
print("Reading from", target.relative_to(repo_root))
|
||||||
# Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier
|
with target.open("r") as f:
|
||||||
is_class = False
|
lines = f.read().splitlines()
|
||||||
|
|
||||||
# The name of the next enum in lua world
|
# Are we in a doc comment?
|
||||||
expose_next_enum_as: str | None = None
|
comment: bool = False
|
||||||
# Name of the current enum in c++ world, used to generate internal typenames for
|
# This is set when @brief is encountered, making the rest of the comment be
|
||||||
current_enum_name: str | None = None
|
# ignored
|
||||||
|
ignore_this_comment: bool = False
|
||||||
|
|
||||||
with lua_meta.open("w") as out:
|
# Last `@lua@param`s seen - for @exposed generation
|
||||||
out.write(BOILERPLATE[1:]) # skip the newline after triple quote
|
last_params_names: list[str] = []
|
||||||
|
# Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier
|
||||||
|
is_class = False
|
||||||
|
|
||||||
for line in lines:
|
# The name of the next enum in lua world
|
||||||
|
expose_next_enum_as: str | None = None
|
||||||
|
# Name of the current enum in c++ world, used to generate internal typenames for
|
||||||
|
current_enum_name: str | None = None
|
||||||
|
for line_num, line in enumerate(lines):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
|
loc = f'{target.relative_to(repo_root)}:{line_num}'
|
||||||
if line.startswith("enum class "):
|
if line.startswith("enum class "):
|
||||||
line = line.removeprefix("enum class ")
|
line = line.removeprefix("enum class ")
|
||||||
temp = line.split(" ", 2)
|
temp = line.split(" ", 2)
|
||||||
current_enum_name = temp[0]
|
current_enum_name = temp[0]
|
||||||
if not expose_next_enum_as:
|
if not expose_next_enum_as:
|
||||||
print(
|
print(
|
||||||
f"Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
|
f"{loc} Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
|
||||||
)
|
)
|
||||||
current_enum_name = None
|
current_enum_name = None
|
||||||
continue
|
continue
|
||||||
@@ -94,7 +108,7 @@ with lua_meta.open("w") as out:
|
|||||||
out.write(", ")
|
out.write(", ")
|
||||||
out.write(entry + ": " + current_enum_name)
|
out.write(entry + ": " + current_enum_name)
|
||||||
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
|
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
|
||||||
print(f"Wrote enum {expose_next_enum_as} => {current_enum_name}")
|
print(f"{loc} Wrote enum {expose_next_enum_as} => {current_enum_name}")
|
||||||
current_enum_name = None
|
current_enum_name = None
|
||||||
expose_next_enum_as = None
|
expose_next_enum_as = None
|
||||||
continue
|
continue
|
||||||
@@ -104,28 +118,40 @@ with lua_meta.open("w") as out:
|
|||||||
continue
|
continue
|
||||||
elif "*/" in line:
|
elif "*/" in line:
|
||||||
comment = False
|
comment = False
|
||||||
|
ignore_this_comment = False
|
||||||
|
|
||||||
if not is_class:
|
if not is_class:
|
||||||
out.write("\n")
|
out.write("\n")
|
||||||
continue
|
continue
|
||||||
if not comment:
|
if not comment:
|
||||||
continue
|
continue
|
||||||
|
if ignore_this_comment:
|
||||||
|
continue
|
||||||
line = line.replace("*", "", 1).lstrip()
|
line = line.replace("*", "", 1).lstrip()
|
||||||
if line == "":
|
if line == "":
|
||||||
out.write("---\n")
|
out.write("---\n")
|
||||||
|
elif line.startswith('@brief '):
|
||||||
|
# Doxygen comment, on a C++ only method
|
||||||
|
ignore_this_comment = True
|
||||||
elif line.startswith("@exposeenum "):
|
elif line.startswith("@exposeenum "):
|
||||||
expose_next_enum_as = line.split(" ", 1)[1]
|
expose_next_enum_as = line.split(" ", 1)[1]
|
||||||
elif line.startswith("@exposed "):
|
elif line.startswith("@exposed "):
|
||||||
exp = line.replace("@exposed ", "", 1)
|
exp = line.replace("@exposed ", "", 1)
|
||||||
params = ", ".join(last_params_names)
|
params = ", ".join(last_params_names)
|
||||||
out.write(f"function {exp}({params}) end\n")
|
out.write(f"function {exp}({params}) end\n")
|
||||||
print(f"Wrote function {exp}(...)")
|
print(f"{loc} Wrote function {exp}(...)")
|
||||||
last_params_names = []
|
last_params_names = []
|
||||||
|
elif line.startswith("@includefile "):
|
||||||
|
filename = line.replace("@includefile ", "", 1)
|
||||||
|
output.write(f"-- Now including data from src/{filename}.\n")
|
||||||
|
process_file(repo_root / 'src' / filename, output)
|
||||||
|
output.write(f'-- Back to {target.relative_to(repo_root)}.\n')
|
||||||
elif line.startswith("@lua"):
|
elif line.startswith("@lua"):
|
||||||
command = line.replace("@lua", "", 1)
|
command = line.replace("@lua", "", 1)
|
||||||
if command.startswith("@param"):
|
if command.startswith("@param"):
|
||||||
last_params_names.append(command.split(" ", 2)[1])
|
last_params_names.append(command.split(" ", 2)[1])
|
||||||
elif command.startswith("@class"):
|
elif command.startswith("@class"):
|
||||||
print(f"Writing {command}")
|
print(f"{loc} Writing {command}")
|
||||||
if is_class:
|
if is_class:
|
||||||
out.write("\n")
|
out.write("\n")
|
||||||
is_class = True
|
is_class = True
|
||||||
@@ -140,3 +166,8 @@ with lua_meta.open("w") as out:
|
|||||||
|
|
||||||
# note the space difference from the branch above
|
# note the space difference from the branch above
|
||||||
out.write("--- " + line + "\n")
|
out.write("--- " + line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
with lua_meta.open("w") as output:
|
||||||
|
output.write(BOILERPLATE[1:]) # skip the newline after triple quote
|
||||||
|
process_file(lua_api_file, output)
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ set(SOURCE_FILES
|
|||||||
controllers/pings/MutedChannelModel.cpp
|
controllers/pings/MutedChannelModel.cpp
|
||||||
controllers/pings/MutedChannelModel.hpp
|
controllers/pings/MutedChannelModel.hpp
|
||||||
|
|
||||||
|
controllers/plugins/api/ChannelRef.cpp
|
||||||
|
controllers/plugins/api/ChannelRef.hpp
|
||||||
controllers/plugins/LuaAPI.cpp
|
controllers/plugins/LuaAPI.cpp
|
||||||
controllers/plugins/LuaAPI.hpp
|
controllers/plugins/LuaAPI.hpp
|
||||||
controllers/plugins/Plugin.cpp
|
controllers/plugins/Plugin.cpp
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ enum class TimeoutStackStyle : int {
|
|||||||
class Channel : public std::enable_shared_from_this<Channel>
|
class Channel : public std::enable_shared_from_this<Channel>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// This is for Lua. See scripts/make_luals_meta.py
|
||||||
|
/**
|
||||||
|
* @exposeenum ChannelType
|
||||||
|
*/
|
||||||
enum class Type {
|
enum class Type {
|
||||||
None,
|
None,
|
||||||
Direct,
|
Direct,
|
||||||
|
|||||||
@@ -126,97 +126,6 @@ int c2_register_callback(lua_State *L)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = getIApp()->getPlugins()->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 =
|
|
||||||
getIApp()->getCommands()->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 = getIApp()->getPlugins()->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)
|
int c2_log(lua_State *L)
|
||||||
{
|
{
|
||||||
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
# include <lua.h>
|
||||||
# include <QString>
|
# include <QString>
|
||||||
|
|
||||||
|
# include <cassert>
|
||||||
|
# include <memory>
|
||||||
# include <vector>
|
# include <vector>
|
||||||
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
@@ -49,6 +53,11 @@ struct CompletionList {
|
|||||||
bool hideOthers{};
|
bool hideOthers{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @includefile common/Channel.hpp
|
||||||
|
* @includefile controllers/plugins/api/ChannelRef.hpp
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new command called `name` which when executed will call `handler`.
|
* Registers a new command called `name` which when executed will call `handler`.
|
||||||
*
|
*
|
||||||
@@ -68,27 +77,6 @@ int c2_register_command(lua_State *L);
|
|||||||
*/
|
*/
|
||||||
int c2_register_callback(lua_State *L);
|
int c2_register_callback(lua_State *L);
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message to `channel` with the specified text. Also executes commands.
|
|
||||||
*
|
|
||||||
* **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
|
||||||
*
|
|
||||||
* @lua@param channel string The name of the Twitch channel
|
|
||||||
* @lua@param text string The text to be sent
|
|
||||||
* @lua@return boolean ok
|
|
||||||
* @exposed c2.send_msg
|
|
||||||
*/
|
|
||||||
int c2_send_msg(lua_State *L);
|
|
||||||
/**
|
|
||||||
* Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
|
|
||||||
*
|
|
||||||
* @lua@param channel string
|
|
||||||
* @lua@param text string
|
|
||||||
* @lua@return boolean ok
|
|
||||||
* @exposed c2.system_msg
|
|
||||||
*/
|
|
||||||
int c2_system_msg(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a message to the Chatterino log.
|
* Writes a message to the Chatterino log.
|
||||||
*
|
*
|
||||||
@@ -107,6 +95,115 @@ int g_print(lua_State *L);
|
|||||||
int searcherAbsolute(lua_State *L);
|
int searcherAbsolute(lua_State *L);
|
||||||
int searcherRelative(lua_State *L);
|
int searcherRelative(lua_State *L);
|
||||||
|
|
||||||
|
// This is a fat pointer that allows us to type check values given to functions needing a userdata.
|
||||||
|
// Ensure ALL userdata given to Lua are a subclass of this! Otherwise we garbage as a pointer!
|
||||||
|
struct UserData {
|
||||||
|
enum class Type { Channel };
|
||||||
|
Type type;
|
||||||
|
bool isWeak;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <UserData::Type T, typename U>
|
||||||
|
struct WeakPtrUserData : public UserData {
|
||||||
|
std::weak_ptr<U> target;
|
||||||
|
|
||||||
|
WeakPtrUserData(std::weak_ptr<U> t)
|
||||||
|
: UserData()
|
||||||
|
, target(t)
|
||||||
|
{
|
||||||
|
this->type = T;
|
||||||
|
this->isWeak = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static WeakPtrUserData<T, U> *create(lua_State *L, std::weak_ptr<U> target)
|
||||||
|
{
|
||||||
|
void *ptr = lua_newuserdata(L, sizeof(WeakPtrUserData<T, U>));
|
||||||
|
return new (ptr) WeakPtrUserData<T, U>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static WeakPtrUserData<T, U> *from(UserData *target)
|
||||||
|
{
|
||||||
|
if (!target->isWeak)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (target->type != T)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return reinterpret_cast<WeakPtrUserData<T, U> *>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static WeakPtrUserData<T, U> *from(void *target)
|
||||||
|
{
|
||||||
|
return from(reinterpret_cast<UserData *>(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int destroy(lua_State *L)
|
||||||
|
{
|
||||||
|
auto self = WeakPtrUserData<T, U>::from(lua_touserdata(L, -1));
|
||||||
|
// Note it is safe to only check the weakness of the pointer, as
|
||||||
|
// std::weak_ptr seems to have identical representation regardless of
|
||||||
|
// what it points to
|
||||||
|
assert(self->isWeak);
|
||||||
|
|
||||||
|
self->target.reset();
|
||||||
|
lua_pop(L, 1); // Lua deallocates the memory for full user data
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <UserData::Type T, typename U>
|
||||||
|
struct SharedPtrUserData : public UserData {
|
||||||
|
std::shared_ptr<U> target;
|
||||||
|
|
||||||
|
SharedPtrUserData(std::shared_ptr<U> t)
|
||||||
|
: UserData()
|
||||||
|
, target(t)
|
||||||
|
{
|
||||||
|
this->type = T;
|
||||||
|
this->isWeak = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SharedPtrUserData<T, U> *create(lua_State *L,
|
||||||
|
std::shared_ptr<U> target)
|
||||||
|
{
|
||||||
|
void *ptr = lua_newuserdata(L, sizeof(SharedPtrUserData<T, U>));
|
||||||
|
return new (ptr) SharedPtrUserData<T, U>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SharedPtrUserData<T, U> *from(UserData *target)
|
||||||
|
{
|
||||||
|
if (target->isWeak)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (target->type != T)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return reinterpret_cast<SharedPtrUserData<T, U> *>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SharedPtrUserData<T, U> *from(void *target)
|
||||||
|
{
|
||||||
|
return from(reinterpret_cast<UserData *>(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int destroy(lua_State *L)
|
||||||
|
{
|
||||||
|
auto self = SharedPtrUserData<T, U>::from(lua_touserdata(L, -1));
|
||||||
|
// Note it is safe to only check the weakness of the pointer, as
|
||||||
|
// std::shared_ptr seems to have identical representation regardless of
|
||||||
|
// what it points to
|
||||||
|
assert(!self->isWeak);
|
||||||
|
|
||||||
|
self->target.reset();
|
||||||
|
lua_pop(L, 1); // Lua deallocates the memory for full user data
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ public:
|
|||||||
|
|
||||||
/// TEMPLATES
|
/// TEMPLATES
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
StackIdx push(lua_State *L, std::optional<T> val)
|
||||||
|
{
|
||||||
|
if (val.has_value())
|
||||||
|
{
|
||||||
|
return lua::push(L, *val);
|
||||||
|
}
|
||||||
|
lua_pushnil(L);
|
||||||
|
return lua_gettop(L);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
bool peek(lua_State *L, std::optional<T> *out, StackIdx idx = -1)
|
bool peek(lua_State *L, std::optional<T> *out, StackIdx idx = -1)
|
||||||
{
|
{
|
||||||
@@ -262,7 +273,7 @@ StackIdx push(lua_State *L, QList<T> vec)
|
|||||||
*
|
*
|
||||||
* @return Stack index of newly created string.
|
* @return Stack index of newly created string.
|
||||||
*/
|
*/
|
||||||
template <typename T, std::enable_if<std::is_enum_v<T>>>
|
template <typename T, typename std::enable_if_t<std::is_enum_v<T>, bool> = true>
|
||||||
StackIdx push(lua_State *L, T inp)
|
StackIdx push(lua_State *L, T inp)
|
||||||
{
|
{
|
||||||
std::string_view name = magic_enum::enum_name<T>(inp);
|
std::string_view name = magic_enum::enum_name<T>(inp);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandContext.hpp"
|
# include "controllers/commands/CommandContext.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
# include "controllers/commands/CommandController.hpp"
|
||||||
|
# include "controllers/plugins/api/ChannelRef.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"
|
||||||
@@ -143,10 +144,8 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||||
static const luaL_Reg c2Lib[] = {
|
static const luaL_Reg c2Lib[] = {
|
||||||
{"system_msg", lua::api::c2_system_msg},
|
|
||||||
{"register_command", lua::api::c2_register_command},
|
{"register_command", lua::api::c2_register_command},
|
||||||
{"register_callback", lua::api::c2_register_callback},
|
{"register_callback", lua::api::c2_register_callback},
|
||||||
{"send_msg", lua::api::c2_send_msg},
|
|
||||||
{"log", lua::api::c2_log},
|
{"log", lua::api::c2_log},
|
||||||
{nullptr, nullptr},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
@@ -164,6 +163,13 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||||||
lua::pushEnumTable<lua::api::EventType>(L);
|
lua::pushEnumTable<lua::api::EventType>(L);
|
||||||
lua_setfield(L, c2libIdx, "EventType");
|
lua_setfield(L, c2libIdx, "EventType");
|
||||||
|
|
||||||
|
lua::pushEnumTable<lua::api::LPlatform>(L);
|
||||||
|
lua_setfield(L, c2libIdx, "Platform");
|
||||||
|
|
||||||
|
// Initialize metatables for objects
|
||||||
|
lua::api::ChannelRef::createMetatable(L);
|
||||||
|
lua_setfield(L, c2libIdx, "Channel");
|
||||||
|
|
||||||
lua_setfield(L, gtable, "c2");
|
lua_setfield(L, gtable, "c2");
|
||||||
|
|
||||||
// ban functions
|
// ban functions
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
|
||||||
|
# include "common/Channel.hpp"
|
||||||
|
# include "controllers/commands/CommandController.hpp"
|
||||||
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "messages/MessageBuilder.hpp"
|
||||||
|
# include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
|
||||||
|
# include <lauxlib.h>
|
||||||
|
# include <lua.h>
|
||||||
|
|
||||||
|
# include <cassert>
|
||||||
|
# include <memory>
|
||||||
|
# include <optional>
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
// NOLINTBEGIN(*vararg)
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||||
|
static const luaL_Reg CHANNEL_REF_METHODS[] = {
|
||||||
|
{"is_valid", &ChannelRef::is_valid},
|
||||||
|
{"get_name", &ChannelRef::get_name},
|
||||||
|
{"get_type", &ChannelRef::get_type},
|
||||||
|
{"get_display_name", &ChannelRef::get_display_name},
|
||||||
|
{"send_message", &ChannelRef::send_message},
|
||||||
|
{"add_system_message", &ChannelRef::add_system_message},
|
||||||
|
{"is_twitch_channel", &ChannelRef::is_twitch_channel},
|
||||||
|
|
||||||
|
// Twitch
|
||||||
|
{"get_room_modes", &ChannelRef::get_room_modes},
|
||||||
|
{"get_stream_status", &ChannelRef::get_stream_status},
|
||||||
|
{"get_twitch_id", &ChannelRef::get_twitch_id},
|
||||||
|
{"is_broadcaster", &ChannelRef::is_broadcaster},
|
||||||
|
{"is_mod", &ChannelRef::is_mod},
|
||||||
|
{"is_vip", &ChannelRef::is_vip},
|
||||||
|
|
||||||
|
// misc
|
||||||
|
{"__tostring", &ChannelRef::to_string},
|
||||||
|
|
||||||
|
// static
|
||||||
|
{"by_name", &ChannelRef::get_by_name},
|
||||||
|
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
||||||
|
{nullptr, nullptr},
|
||||||
|
};
|
||||||
|
|
||||||
|
void ChannelRef::createMetatable(lua_State *L)
|
||||||
|
{
|
||||||
|
lua::StackGuard guard(L, 1);
|
||||||
|
|
||||||
|
luaL_newmetatable(L, "c2.Channel");
|
||||||
|
lua_pushstring(L, "__index");
|
||||||
|
lua_pushvalue(L, -2); // clone metatable
|
||||||
|
lua_settable(L, -3); // metatable.__index = metatable
|
||||||
|
|
||||||
|
// Generic IWeakResource stuff
|
||||||
|
lua_pushstring(L, "__gc");
|
||||||
|
lua_pushcfunction(
|
||||||
|
L, (&WeakPtrUserData<UserData::Type::Channel, ChannelRef>::destroy));
|
||||||
|
lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy
|
||||||
|
|
||||||
|
luaL_setfuncs(L, CHANNEL_REF_METHODS, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
||||||
|
{
|
||||||
|
if (lua_gettop(L) < 1)
|
||||||
|
{
|
||||||
|
luaL_error(L, "Called c2.Channel method without a channel object");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||||
|
{
|
||||||
|
luaL_error(
|
||||||
|
L, "Called c2.Channel method with a non Channel 'self' argument.");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto *data = WeakPtrUserData<UserData::Type::Channel, Channel>::from(
|
||||||
|
lua_touserdata(L, lua_gettop(L)));
|
||||||
|
if (data == nullptr)
|
||||||
|
{
|
||||||
|
luaL_error(L,
|
||||||
|
"Called c2.Channel method with an invalid channel pointer");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (data->target.expired())
|
||||||
|
{
|
||||||
|
if (!expiredOk)
|
||||||
|
{
|
||||||
|
luaL_error(L,
|
||||||
|
"Usage of expired c2.Channel object. Underlying "
|
||||||
|
"resource was freed. Use Channel:is_valid() to check");
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return data->target.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<TwitchChannel> ChannelRef::getTwitchOrError(lua_State *L)
|
||||||
|
{
|
||||||
|
auto ref = ChannelRef::getOrError(L);
|
||||||
|
auto ptr = dynamic_pointer_cast<TwitchChannel>(ref);
|
||||||
|
if (ptr == nullptr)
|
||||||
|
{
|
||||||
|
luaL_error(L,
|
||||||
|
"c2.Channel Twitch-only operation on non-Twitch channel.");
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::is_valid(lua_State *L)
|
||||||
|
{
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||||
|
lua::push(L, that != nullptr);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_name(lua_State *L)
|
||||||
|
{
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L);
|
||||||
|
lua::push(L, that->getName());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_type(lua_State *L)
|
||||||
|
{
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L);
|
||||||
|
lua::push(L, that->getType());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_display_name(lua_State *L)
|
||||||
|
{
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L);
|
||||||
|
lua::push(L, that->getDisplayName());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::send_message(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_gettop(L) != 2 && lua_gettop(L) != 3)
|
||||||
|
{
|
||||||
|
luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message "
|
||||||
|
"text and optionally execute_commands flag)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bool execcmds = false;
|
||||||
|
if (lua_gettop(L) == 3)
|
||||||
|
{
|
||||||
|
if (!lua::pop(L, &execcmds))
|
||||||
|
{
|
||||||
|
luaL_error(L, "cannot get execute_commands (2nd argument of "
|
||||||
|
"Channel:send_message)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString text;
|
||||||
|
if (!lua::pop(L, &text))
|
||||||
|
{
|
||||||
|
luaL_error(L, "cannot get text (1st argument of Channel:send_message)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L);
|
||||||
|
|
||||||
|
text = text.replace('\n', ' ');
|
||||||
|
if (execcmds)
|
||||||
|
{
|
||||||
|
text = getIApp()->getCommands()->execCommand(text, that, false);
|
||||||
|
}
|
||||||
|
that->sendMessage(text);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::add_system_message(lua_State *L)
|
||||||
|
{
|
||||||
|
// needs to account for the hidden self argument
|
||||||
|
if (lua_gettop(L) != 2)
|
||||||
|
{
|
||||||
|
luaL_error(
|
||||||
|
L, "Channel:add_system_message needs exactly 1 argument (message "
|
||||||
|
"text)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString text;
|
||||||
|
if (!lua::pop(L, &text))
|
||||||
|
{
|
||||||
|
luaL_error(
|
||||||
|
L, "cannot get text (1st argument of Channel:add_system_message)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L);
|
||||||
|
text = text.replace('\n', ' ');
|
||||||
|
that->addMessage(makeSystemMessage(text));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::is_twitch_channel(lua_State *L)
|
||||||
|
{
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L);
|
||||||
|
lua::push(L, that->isTwitchChannel());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_room_modes(lua_State *L)
|
||||||
|
{
|
||||||
|
auto tc = ChannelRef::getTwitchOrError(L);
|
||||||
|
const auto m = tc->accessRoomModes();
|
||||||
|
const auto modes = LuaRoomModes{
|
||||||
|
.unique_chat = m->r9k,
|
||||||
|
.subscriber_only = m->submode,
|
||||||
|
.emotes_only = m->emoteOnly,
|
||||||
|
.follower_only = (m->followerOnly == -1)
|
||||||
|
? std::nullopt
|
||||||
|
: std::optional(m->followerOnly),
|
||||||
|
.slow_mode =
|
||||||
|
(m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode),
|
||||||
|
|
||||||
|
};
|
||||||
|
lua::push(L, modes);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_stream_status(lua_State *L)
|
||||||
|
{
|
||||||
|
auto tc = ChannelRef::getTwitchOrError(L);
|
||||||
|
const auto s = tc->accessStreamStatus();
|
||||||
|
const auto status = LuaStreamStatus{
|
||||||
|
.live = s->live,
|
||||||
|
.viewer_count = static_cast<int>(s->viewerCount),
|
||||||
|
.uptime = s->uptimeSeconds,
|
||||||
|
.title = s->title,
|
||||||
|
.game_name = s->game,
|
||||||
|
.game_id = s->gameId,
|
||||||
|
};
|
||||||
|
lua::push(L, status);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_twitch_id(lua_State *L)
|
||||||
|
{
|
||||||
|
auto tc = ChannelRef::getTwitchOrError(L);
|
||||||
|
lua::push(L, tc->roomId());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::is_broadcaster(lua_State *L)
|
||||||
|
{
|
||||||
|
auto tc = ChannelRef::getTwitchOrError(L);
|
||||||
|
lua::push(L, tc->isBroadcaster());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::is_mod(lua_State *L)
|
||||||
|
{
|
||||||
|
auto tc = ChannelRef::getTwitchOrError(L);
|
||||||
|
lua::push(L, tc->isMod());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::is_vip(lua_State *L)
|
||||||
|
{
|
||||||
|
auto tc = ChannelRef::getTwitchOrError(L);
|
||||||
|
lua::push(L, tc->isVip());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_by_name(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_gettop(L) != 2)
|
||||||
|
{
|
||||||
|
luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel "
|
||||||
|
"name and platform)");
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
LPlatform platform{};
|
||||||
|
if (!lua::pop(L, &platform))
|
||||||
|
{
|
||||||
|
luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, "
|
||||||
|
"expected a string)");
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QString name;
|
||||||
|
if (!lua::pop(L, &name))
|
||||||
|
{
|
||||||
|
luaL_error(L,
|
||||||
|
"cannot get channel name (1st argument of Channel.by_name, "
|
||||||
|
"expected a string)");
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto chn = getApp()->twitch->getChannelOrEmpty(name);
|
||||||
|
if (chn->isEmpty())
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// pushes onto stack
|
||||||
|
WeakPtrUserData<UserData::Type::Channel, Channel>::create(
|
||||||
|
L, chn->weak_from_this());
|
||||||
|
luaL_getmetatable(L, "c2.Channel");
|
||||||
|
lua_setmetatable(L, -2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::get_by_twitch_id(lua_State *L)
|
||||||
|
{
|
||||||
|
if (lua_gettop(L) != 1)
|
||||||
|
{
|
||||||
|
luaL_error(
|
||||||
|
L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner "
|
||||||
|
"id)");
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QString id;
|
||||||
|
if (!lua::pop(L, &id))
|
||||||
|
{
|
||||||
|
luaL_error(L,
|
||||||
|
"cannot get channel name (1st argument of Channel.by_name, "
|
||||||
|
"expected a string)");
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto chn = getApp()->twitch->getChannelOrEmptyByID(id);
|
||||||
|
if (chn->isEmpty())
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// pushes onto stack
|
||||||
|
WeakPtrUserData<UserData::Type::Channel, Channel>::create(
|
||||||
|
L, chn->weak_from_this());
|
||||||
|
luaL_getmetatable(L, "c2.Channel");
|
||||||
|
lua_setmetatable(L, -2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChannelRef::to_string(lua_State *L)
|
||||||
|
{
|
||||||
|
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||||
|
if (that == nullptr)
|
||||||
|
{
|
||||||
|
lua_pushstring(L, "<c2.Channel expired>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QString formated = QString("<c2.Channel %1>").arg(that->getName());
|
||||||
|
lua::push(L, formated);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
// NOLINTEND(*vararg)
|
||||||
|
//
|
||||||
|
namespace chatterino::lua {
|
||||||
|
StackIdx push(lua_State *L, const api::LuaRoomModes &modes)
|
||||||
|
{
|
||||||
|
auto out = lua::pushEmptyTable(L, 6);
|
||||||
|
# define PUSH(field) \
|
||||||
|
lua::push(L, modes.field); \
|
||||||
|
lua_setfield(L, out, #field)
|
||||||
|
PUSH(unique_chat);
|
||||||
|
PUSH(subscriber_only);
|
||||||
|
PUSH(emotes_only);
|
||||||
|
PUSH(follower_only);
|
||||||
|
PUSH(slow_mode);
|
||||||
|
# undef PUSH
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
StackIdx push(lua_State *L, const api::LuaStreamStatus &status)
|
||||||
|
{
|
||||||
|
auto out = lua::pushEmptyTable(L, 6);
|
||||||
|
# define PUSH(field) \
|
||||||
|
lua::push(L, status.field); \
|
||||||
|
lua_setfield(L, out, #field)
|
||||||
|
PUSH(live);
|
||||||
|
PUSH(viewer_count);
|
||||||
|
PUSH(uptime);
|
||||||
|
PUSH(title);
|
||||||
|
PUSH(game_name);
|
||||||
|
PUSH(game_id);
|
||||||
|
# undef PUSH
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "common/Channel.hpp"
|
||||||
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This enum describes a platform for the purpose of searching for a channel.
|
||||||
|
* Currently only Twitch is supported because identifying IRC channels is tricky.
|
||||||
|
* @exposeenum Platform
|
||||||
|
*/
|
||||||
|
enum class LPlatform {
|
||||||
|
Twitch,
|
||||||
|
//IRC,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@class Channel: IWeakResource
|
||||||
|
*/
|
||||||
|
struct ChannelRef {
|
||||||
|
static void createMetatable(lua_State *L);
|
||||||
|
friend class chatterino::PluginController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr.
|
||||||
|
* If the object given is not a userdatum or the pointer inside that
|
||||||
|
* userdatum doesn't point to a Channel, a lua error is thrown.
|
||||||
|
*
|
||||||
|
* @param expiredOk Should an expired return nullptr instead of erroring
|
||||||
|
*/
|
||||||
|
static ChannelPtr getOrError(lua_State *L, bool expiredOk = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Casts the result of getOrError to std::shared_ptr<TwitchChannel>
|
||||||
|
* if that fails thows a lua error.
|
||||||
|
*/
|
||||||
|
static std::shared_ptr<TwitchChannel> getTwitchOrError(lua_State *L);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Returns true if the channel this object points to is valid.
|
||||||
|
* If the object expired, returns false
|
||||||
|
* If given a non-Channel object, it errors.
|
||||||
|
*
|
||||||
|
* @lua@return boolean success
|
||||||
|
* @exposed Channel:is_valid
|
||||||
|
*/
|
||||||
|
static int is_valid(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the channel's name. This is the lowercase login name.
|
||||||
|
*
|
||||||
|
* @lua@return string name
|
||||||
|
* @exposed Channel:get_name
|
||||||
|
*/
|
||||||
|
static int get_name(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the channel's type
|
||||||
|
*
|
||||||
|
* @lua@return ChannelType
|
||||||
|
* @exposed Channel:get_type
|
||||||
|
*/
|
||||||
|
static int get_type(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||||
|
*
|
||||||
|
* @lua@return string name
|
||||||
|
* @exposed Channel:get_display_name
|
||||||
|
*/
|
||||||
|
static int get_display_name(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the target channel.
|
||||||
|
* Note that this does not execute client-commands.
|
||||||
|
*
|
||||||
|
* @lua@param message string
|
||||||
|
* @lua@param execute_commands boolean Should commands be run on the text?
|
||||||
|
* @exposed Channel:send_message
|
||||||
|
*/
|
||||||
|
static int send_message(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a system message client-side
|
||||||
|
*
|
||||||
|
* @lua@param message string
|
||||||
|
* @exposed Channel:add_system_message
|
||||||
|
*/
|
||||||
|
static int add_system_message(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true for twitch channels.
|
||||||
|
* Compares the channel Type. Note that enum values aren't guaranteed, just
|
||||||
|
* that they are equal to the exposed enum.
|
||||||
|
*
|
||||||
|
* @lua@return bool
|
||||||
|
* @exposed Channel:is_twitch_channel
|
||||||
|
*/
|
||||||
|
static int is_twitch_channel(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Twitch Channel specific functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the channel mode settings (subscriber only, r9k etc.)
|
||||||
|
*
|
||||||
|
* @lua@return RoomModes
|
||||||
|
* @exposed Channel:get_room_modes
|
||||||
|
*/
|
||||||
|
static int get_room_modes(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the stream status.
|
||||||
|
*
|
||||||
|
* @lua@return StreamStatus
|
||||||
|
* @exposed Channel:get_stream_status
|
||||||
|
*/
|
||||||
|
static int get_stream_status(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Twitch user ID of the owner of the channel.
|
||||||
|
*
|
||||||
|
* @lua@return string
|
||||||
|
* @exposed Channel:get_twitch_id
|
||||||
|
*/
|
||||||
|
static int get_twitch_id(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the channel is a Twitch channel and the user owns it
|
||||||
|
*
|
||||||
|
* @lua@return boolean
|
||||||
|
* @exposed Channel:is_broadcaster
|
||||||
|
*/
|
||||||
|
static int is_broadcaster(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||||
|
* Returns false for broadcaster.
|
||||||
|
*
|
||||||
|
* @lua@return boolean
|
||||||
|
* @exposed Channel:is_mod
|
||||||
|
*/
|
||||||
|
static int is_mod(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||||
|
* Returns false for broadcaster.
|
||||||
|
*
|
||||||
|
* @lua@return boolean
|
||||||
|
* @exposed Channel:is_vip
|
||||||
|
*/
|
||||||
|
static int is_vip(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misc
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@return string
|
||||||
|
* @exposed Channel:__tostring
|
||||||
|
*/
|
||||||
|
static int to_string(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a channel by name.
|
||||||
|
*
|
||||||
|
* Misc channels are marked as Twitch:
|
||||||
|
* - /whispers
|
||||||
|
* - /mentions
|
||||||
|
* - /watching
|
||||||
|
* - /live
|
||||||
|
* - /automod
|
||||||
|
*
|
||||||
|
* @lua@param name string Which channel are you looking for?
|
||||||
|
* @lua@param platform Platform Where to search for the channel?
|
||||||
|
* @lua@return Channel?
|
||||||
|
* @exposed Channel.by_name
|
||||||
|
*/
|
||||||
|
static int get_by_name(lua_State *L);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a channel by the Twitch user ID of its owner.
|
||||||
|
*
|
||||||
|
* @lua@param string id ID of the owner of the channel.
|
||||||
|
* @lua@return Channel?
|
||||||
|
* @exposed Channel.by_twitch_id
|
||||||
|
*/
|
||||||
|
static int get_by_twitch_id(lua_State *L);
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals
|
||||||
|
/**
|
||||||
|
* @lua@class RoomModes
|
||||||
|
*/
|
||||||
|
struct LuaRoomModes {
|
||||||
|
/**
|
||||||
|
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||||
|
*/
|
||||||
|
bool unique_chat = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field subscriber_only boolean
|
||||||
|
*/
|
||||||
|
bool subscriber_only = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field emotes_only boolean Whether or not text is allowed in messages.
|
||||||
|
* Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||||
|
*/
|
||||||
|
bool emotes_only = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field unique_chat number? Time in minutes you need to follow to chat or nil.
|
||||||
|
*/
|
||||||
|
std::optional<int> follower_only;
|
||||||
|
/**
|
||||||
|
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||||
|
*/
|
||||||
|
std::optional<int> slow_mode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@class StreamStatus
|
||||||
|
*/
|
||||||
|
struct LuaStreamStatus {
|
||||||
|
/**
|
||||||
|
* @lua@field live boolean
|
||||||
|
*/
|
||||||
|
bool live = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field viewer_count number
|
||||||
|
*/
|
||||||
|
int viewer_count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field uptime number Seconds since the stream started.
|
||||||
|
*/
|
||||||
|
int uptime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field title string Stream title or last stream title
|
||||||
|
*/
|
||||||
|
QString title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field game_name string
|
||||||
|
*/
|
||||||
|
QString game_name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@field game_id string
|
||||||
|
*/
|
||||||
|
QString game_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
namespace chatterino::lua {
|
||||||
|
StackIdx push(lua_State *L, const api::LuaRoomModes &modes);
|
||||||
|
StackIdx push(lua_State *L, const api::LuaStreamStatus &status);
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
#endif
|
||||||
@@ -467,6 +467,7 @@ void TwitchChannel::updateStreamStatus(
|
|||||||
auto diff = since.secsTo(QDateTime::currentDateTime());
|
auto diff = since.secsTo(QDateTime::currentDateTime());
|
||||||
status->uptime = QString::number(diff / 3600) + "h " +
|
status->uptime = QString::number(diff / 3600) + "h " +
|
||||||
QString::number(diff % 3600 / 60) + "m";
|
QString::number(diff % 3600 / 60) + "m";
|
||||||
|
status->uptimeSeconds = diff;
|
||||||
|
|
||||||
status->rerun = false;
|
status->rerun = false;
|
||||||
status->streamType = stream.type;
|
status->streamType = stream.type;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ public:
|
|||||||
QString game;
|
QString game;
|
||||||
QString gameId;
|
QString gameId;
|
||||||
QString uptime;
|
QString uptime;
|
||||||
|
int uptimeSeconds = 0;
|
||||||
QString streamType;
|
QString streamType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user