defmodule GelotvBot do
@moduledoc """
Multi-platform livestream chat bot dispatch library.
`GelotvBot` coordinates supervised bot instances that can send the same
command to multiple livestream chats on platforms such as Twitch, YouTube,
and Kick. Platform-specific code is isolated behind `GelotvBot.Adapter` so
applications can use official APIs, credentials, and policy-compliant send
behavior for each service.
"""
alias GelotvBot.{Bot, Command, Dispatcher, Message, Target}
@type send_result :: Dispatcher.send_result()
@type message_input :: Message.t() | Command.t() | String.t()
@type message_input_or_many :: message_input() | [message_input()]
@type target_input_or_many :: Target.t() | [Target.t()]
@doc """
Starts a supervised bot instance.
{:ok, _pid} =
GelotvBot.start_bot(:alerts,
targets: [
%GelotvBot.Target{
platform: :twitch,
channel: "gelotv",
adapter: MyApp.TwitchAdapter
}
]
)
"""
@spec start_bot(term(), keyword()) :: DynamicSupervisor.on_start_child()
def start_bot(name, opts \\ []) do
child = {Bot, Keyword.put(opts, :name, name)}
DynamicSupervisor.start_child(GelotvBot.BotSupervisor, child)
end
@doc """
Stops a supervised bot instance by name.
"""
@spec stop_bot(term()) :: :ok | {:error, :not_found}
def stop_bot(name) do
case Registry.lookup(GelotvBot.Registry, name) do
[{pid, _value}] -> DynamicSupervisor.terminate_child(GelotvBot.BotSupervisor, pid)
[] -> {:error, :not_found}
end
end
@doc """
Lists currently running supervised bot instance names.
"""
@spec list_bots() :: [term()]
def list_bots do
Registry.select(GelotvBot.Registry, [{{:"$1", :_, :_}, [], [:"$1"]}])
end
@doc """
Broadcasts a message through a named supervised bot instance.
"""
@spec send(term(), Message.t() | Command.t() | String.t(), keyword()) :: [send_result()]
def send(bot_name, message, opts \\ []) do
Bot.send_all(Bot.via(bot_name), message, opts)
end
@doc """
Broadcasts multiple messages/commands through a named supervised bot instance.
"""
@spec send_many(term(), [Message.t() | Command.t() | String.t()], keyword()) :: [send_result()]
def send_many(bot_name, messages, opts \\ []) do
Bot.send_many(Bot.via(bot_name), messages, opts)
end
@doc """
Replaces the target list for a named supervised bot instance.
"""
@spec put_targets(term(), [Target.t()]) :: :ok | {:error, term()}
def put_targets(bot_name, targets) do
Bot.put_targets(Bot.via(bot_name), targets)
end
@doc """
Returns the target list for a named supervised bot instance.
"""
@spec targets(term()) :: [Target.t()]
def targets(bot_name) do
Bot.targets(Bot.via(bot_name))
end
@doc """
Sends a message directly to a list of targets without starting a named bot.
"""
@spec broadcast([Target.t()], Message.t() | Command.t() | String.t(), keyword()) :: [
send_result()
]
def broadcast(targets, message, opts \\ [])
def broadcast(targets, %Message{} = message, opts) do
Dispatcher.send_all(targets, message, opts)
end
def broadcast(targets, %Command{} = command, opts) do
Dispatcher.send_all(targets, Command.to_message(command), opts)
end
def broadcast(targets, body, opts) when is_binary(body) do
Dispatcher.send_all(targets, Message.new(body), opts)
end
@doc """
Sends multiple messages/commands directly to all targets.
"""
@spec broadcast_many([Target.t()], [Message.t() | Command.t() | String.t()], keyword()) :: [
send_result()
]
def broadcast_many(targets, messages, opts \\ []) do
Dispatcher.send_many(targets, messages, opts)
end
@doc """
Dispatches one or many messages to one or many targets with one direct call.
This is the most compact path for applications that do not want to manage a
named bot process. A single target/message and a list of targets/messages use
the same function.
"""
@spec dispatch(target_input_or_many(), message_input_or_many(), keyword()) :: [send_result()]
def dispatch(target_or_targets, message_or_messages, opts \\ []) do
targets = List.wrap(target_or_targets)
messages = List.wrap(message_or_messages)
Dispatcher.send_many(targets, messages, opts)
end
@doc """
Discovers active live targets for one or more platform specs.
"""
@spec discover_live_targets([GelotvBot.LiveDiscovery.spec()], keyword()) ::
{:ok, [Target.t()]} | {:error, term()}
def discover_live_targets(specs, opts \\ []) do
GelotvBot.LiveDiscovery.discover(specs, opts)
end
@doc """
Discovers active live targets and sends multiple messages to all of them.
"""
@spec broadcast_live(
[GelotvBot.LiveDiscovery.spec()],
message_input_or_many(),
keyword()
) ::
{:ok, [send_result()]} | {:error, term()}
def broadcast_live(specs, messages, opts \\ []) do
with {:ok, targets} <- discover_live_targets(specs, opts) do
{:ok, dispatch(targets, messages, opts)}
end
end
@doc """
Alias for `broadcast_live/3` for callers that prefer send-oriented naming.
"""
@spec send_live([GelotvBot.LiveDiscovery.spec()], message_input_or_many(), keyword()) ::
{:ok, [send_result()]} | {:error, term()}
def send_live(specs, messages, opts \\ []) do
broadcast_live(specs, messages, opts)
end
@doc """
Sends a raw request to a platform API using one unified function.
GelotvBot.api_request(:twitch, :get, "/streams", credentials,
params: [user_login: "gelotv"]
)
Use this when you need an endpoint that does not have a typed helper yet.
"""
@spec api_request(:twitch | :youtube | :kick, atom(), String.t(), map(), keyword()) ::
GelotvBot.HTTPClient.response()
def api_request(platform, method, path, credentials, opts \\ [])
def api_request(:twitch, method, path, credentials, opts) do
GelotvBot.APIs.Twitch.request(method, path, credentials, opts)
end
def api_request(:youtube, method, path, credentials, opts) do
GelotvBot.APIs.YouTube.request(method, path, credentials, opts)
end
def api_request(:kick, method, path, credentials, opts) do
GelotvBot.APIs.Kick.request(method, path, credentials, opts)
end
@doc """
Sends a platform API request and returns the decoded JSON body.
The response keeps raw `:status`, `:headers`, and `:body` fields and adds
`:decoded_body` for successful JSON responses.
"""
@spec api_request_decoded(:twitch | :youtube | :kick, atom(), String.t(), map(), keyword()) ::
{:ok, map()} | {:error, term()}
def api_request_decoded(platform, method, path, credentials, opts \\ [])
def api_request_decoded(:twitch, method, path, credentials, opts) do
GelotvBot.APIs.Twitch.request_decoded(method, path, credentials, opts)
end
def api_request_decoded(:youtube, method, path, credentials, opts) do
GelotvBot.APIs.YouTube.request_decoded(method, path, credentials, opts)
end
def api_request_decoded(:kick, method, path, credentials, opts) do
GelotvBot.APIs.Kick.request_decoded(method, path, credentials, opts)
end
@doc """
Collects paginated API responses using one function for all platforms.
Twitch and YouTube use built-in cursor/page-token extractors. Kick and custom
endpoints can pass `next: fn decoded_body -> next_params_or_nil end`.
"""
@spec api_paginate(:twitch | :youtube | :kick, String.t(), map(), keyword()) ::
{:ok, [map()]} | {:error, term()}
def api_paginate(platform, path, credentials, opts \\ [])
def api_paginate(:twitch, path, credentials, opts) do
GelotvBot.APIs.Twitch.paginate(path, credentials, opts)
end
def api_paginate(:youtube, path, credentials, opts) do
GelotvBot.APIs.YouTube.paginate(path, credentials, opts)
end
def api_paginate(:kick, path, credentials, opts) do
GelotvBot.APIs.Kick.paginate(path, credentials, opts)
end
@doc """
Requests an OAuth token using one function for supported platform helpers.
"""
@spec token(:twitch | :youtube | :kick, :client_credentials | :refresh, map(), keyword()) ::
{:ok, map()} | {:error, term()}
def token(platform, grant, credentials, opts \\ [])
def token(:twitch, :client_credentials, credentials, opts) do
GelotvBot.APIs.Twitch.client_credentials_token(credentials, opts)
end
def token(:twitch, :refresh, credentials, opts) do
GelotvBot.APIs.Twitch.refresh_token(credentials, opts)
end
def token(:youtube, :refresh, credentials, opts) do
GelotvBot.APIs.YouTube.refresh_token(credentials, opts)
end
def token(:kick, :client_credentials, credentials, opts) do
GelotvBot.APIs.Kick.client_credentials_token(credentials, opts)
end
def token(:kick, :refresh, credentials, opts) do
GelotvBot.APIs.Kick.refresh_token(credentials, opts)
end
end