GelotvBot

gelotv_bot is an Elixir/OTP library for sending the same chat command or alert to multiple livestream chats through supervised bot instances.

The library is platform-neutral. Twitch, YouTube, Kick, or private chat integrations are implemented as adapters behind GelotvBot.Adapter; the core library handles message normalization, concurrent fan-out, shared rate-limit coordination, supervision, and signed metadata helpers.

Installation

When published, add the package to your dependencies:

def deps do
[
{:gelotv_bot, "~> 0.1"}
]
end

Core Concepts

Example

targets = [
%GelotvBot.Target{
platform: :twitch,
channel: "gelotv",
adapter: MyApp.TwitchAdapter,
rate_limit: [limit: 20, interval: 30_000, burst: 5]
},
%GelotvBot.Target{
platform: :youtube,
channel: "gelotv-live",
adapter: MyApp.YouTubeAdapter,
rate_limit: [limit: 60, interval: 60_000, burst: 10]
},
%GelotvBot.Target{
platform: :kick,
channel: "gelotv",
adapter: MyApp.KickAdapter
}
]
{:ok, _pid} = GelotvBot.start_bot(:donation_alerts, targets: targets)
GelotvBot.send(:donation_alerts, "Thanks Ana for the donation!")

Retries can be configured per bot or per send:

GelotvBot.start_bot(:donation_alerts,
targets: targets,
retry: [max_attempts: 3, base_backoff: 250, max_backoff: 5_000]
)

Multiple commands can be dispatched in one call:

GelotvBot.send_many(:donation_alerts, [
GelotvBot.Command.new(:donation, "Thanks Ana!", %{amount: 10}),
GelotvBot.Command.new(:follow, "Welcome Bruno!")
])

For the direct no-bot-process path, use one function for one or many targets and one or many messages:

GelotvBot.dispatch(targets, [
"First live message",
GelotvBot.Command.new(:follow, "Welcome Bruno!")
])

Bot instances are independent supervised processes, but the default GelotvBot.RateLimiter is shared by the application, so two instances sending to the same target coordinate against the same bucket.

Targets can be replaced while a bot is running:

GelotvBot.put_targets(:donation_alerts, updated_targets)

Running bot instances can be listed and stopped:

GelotvBot.list_bots()
GelotvBot.stop_bot(:donation_alerts)

For direct one-call bot sends without managing a named bot process, discover active live chats and broadcast to all of them:

specs = [
%{
platform: :twitch,
channels: ["gelotv"],
credentials: %{access_token: twitch_token, client_id: twitch_client_id, sender_id: twitch_sender_id}
},
%{
platform: :youtube,
credentials: %{access_token: youtube_token}
},
%{
platform: :kick,
channels: ["gelotv"],
credentials: %{access_token: kick_token}
}
]
GelotvBot.send_live(specs, [
GelotvBot.Command.new(:donation, "Thanks Ana!"),
GelotvBot.Command.new(:follow, "Welcome Bruno!")
])

send_live/3 and broadcast_live/3 discover active lives, convert them into Targets, and use the same direct dispatch path as every other multi-live send. They accept either a single message or a list of messages.

Token helpers cover the OAuth flows commonly needed before bot sends:

{:ok, twitch_token} =
GelotvBot.token(:twitch, :client_credentials, %{
client_id: client_id,
client_secret: client_secret
})
{:ok, youtube_token} =
GelotvBot.token(:youtube, :refresh, %{
client_id: client_id,
client_secret: client_secret,
refresh_token: refresh_token
})
{:ok, kick_token} =
GelotvBot.token(:kick, :refresh, %{
client_id: client_id,
client_secret: client_secret,
refresh_token: refresh_token
})

Built-In Platform Adapters

The package includes HTTP adapters for current chat-send APIs:

The application is still responsible for OAuth flows and token refresh. Credentials are passed on the target:

%GelotvBot.Target{
platform: :twitch,
channel: "gelotv",
adapter: GelotvBot.Adapters.Twitch,
credentials: %{
access_token: "...",
client_id: "...",
broadcaster_id: "...",
sender_id: "..."
}
}

The built-in adapters validate messages before making HTTP calls. Blank messages are rejected, and Twitch/Kick messages are capped at 500 characters to match their chat-send APIs.

Typed helpers cover common bot-live API calls while the generic request layer remains available for the rest of each platform:

GelotvBot.APIs.Twitch.streams(twitch_credentials, params: [user_login: "gelotv"])
GelotvBot.APIs.Twitch.chat_settings(twitch_credentials, broadcaster_id, moderator_id)
GelotvBot.APIs.YouTube.live_broadcasts(youtube_credentials,
params: [broadcastStatus: "active", mine: true]
)
GelotvBot.APIs.YouTube.live_chat_messages(youtube_credentials, live_chat_id)
GelotvBot.APIs.Kick.channels(kick_credentials, params: [slug: ["gelotv"]])

Full Platform API Access

The high-level adapters are intentionally small, but the library also exposes generic dependency-free API clients for Twitch, YouTube, and Kick:

These clients build authenticated requests for their platform base APIs and can call any current or future endpoint by path, method, params, headers, and body. Responses are returned as raw HTTP status, headers, and body so callers can work with the complete platform surface without waiting for typed wrappers.

GelotvBot.APIs.Twitch.get(
"/streams",
%{access_token: token, client_id: client_id},
params: [user_login: "gelotv"]
)
GelotvBot.APIs.YouTube.get(
"/videos",
%{access_token: token, api_key: api_key},
params: [part: "snippet", id: video_id]
)
GelotvBot.APIs.Kick.post(
"/chat",
%{access_token: token},
%{content: "Hello chat"}
)

If you want parsed JSON without giving up raw response data, use the decoded variants. They preserve :status, :headers, and :body, then add :decoded_body. Successful empty responses such as 204 No Content return decoded_body: nil:

{:ok, response} =
GelotvBot.APIs.Twitch.get_decoded(
"/streams",
%{access_token: token, client_id: client_id},
params: [user_login: "gelotv"]
)
response.decoded_body["data"]

Absolute URLs are also accepted for platform-adjacent endpoints such as OAuth:

GelotvBot.APIs.Twitch.post(
"https://id.twitch.tv/oauth2/token",
%{},
%{client_id: client_id, client_secret: client_secret, grant_type: "client_credentials"},
body_format: :form
)

All of this uses the built-in :httpc client by default and does not require a runtime HTTP or JSON dependency. Applications can inject another module that implements GelotvBot.HTTPClient.

For APIs that need upload or custom content bodies, use body_format: :raw and set content_type:

GelotvBot.APIs.YouTube.post(
"https://www.googleapis.com/upload/youtube/v3/videos",
%{access_token: token},
raw_video_bytes,
params: [uploadType: "media", part: "snippet"],
body_format: :raw,
content_type: "video/mp4"
)

If you want one function for all platforms, use GelotvBot.api_request/5:

GelotvBot.api_request(:twitch, :get, "/streams", credentials,
params: [user_login: "gelotv"]
)
GelotvBot.api_request(:youtube, :get, "/videos", credentials,
params: [part: "snippet", id: video_id]
)
GelotvBot.api_request(:kick, :post, "/chat", credentials,
body: %{content: "Hello chat"}
)

Use GelotvBot.api_request_decoded/5 for the same cross-platform request path with JSON decoding:

GelotvBot.api_request_decoded(:twitch, :get, "/streams", credentials,
params: [user_login: "gelotv"]
)

For paginated endpoints, use paginate/3 on a platform module or GelotvBot.api_paginate/4. Twitch cursor pagination and YouTube page-token pagination are built in; Kick or custom endpoints can pass a next callback:

GelotvBot.api_paginate(:twitch, "/streams", credentials,
params: [first: 100]
)
GelotvBot.api_paginate(:youtube, "/videos", credentials,
params: [part: "snippet"]
)
GelotvBot.api_paginate(:kick, "/channels", credentials,
next: fn
%{"next" => next} when is_binary(next) -> [page: next]
_ -> nil
end
)

Custom Adapter Contract

defmodule MyApp.TwitchAdapter do
@behaviour GelotvBot.Adapter
@impl true
def send_message(target, message, _opts) do
# Use official platform APIs and authenticated credentials here.
# Return :ok, {:ok, response}, or {:error, reason}.
# For platform throttles, return {:error, {:rate_limited, retry_after_ms}}.
end
end

Metadata

Use out-of-band metadata for the cleanest chat UX. The chat body remains exactly what viewers see, while routing/audit fields stay on the message struct:

message =
GelotvBot.Message.new("Thanks Ana!", %{
event: "donation",
source: "gelotv-bot"
})

Dispatch results include the original message, so applications can persist metadata next to platform responses or returned message IDs without adding hidden characters to public chat text.

If metadata must travel in the chat message, GelotvBot.Metadata.attach/2 creates a visible, signed token:

message =
message
|> GelotvBot.Metadata.attach(secret: System.fetch_env!("GELOTV_BOT_SECRET"))

For integrations that require an invisible carrier, encode_invisible/2 returns a signed payload encoded with the zero-width codec:

encoded = GelotvBot.Metadata.encode_invisible(%{event: "donation"}, secret)
{:ok, decoded} = GelotvBot.Metadata.decode_invisible(encoded, secret)

The same standardized codec can carry arbitrary binary strings:

encoded = GelotvBot.Metadata.encode_zero_width("hello")
{:ok, "hello"} = GelotvBot.Metadata.decode_zero_width(encoded)

gelotv_bot is a software library. Installing or using the package does not create any hosted service relationship with GeloTV, and the library does not send chat messages, credentials, metadata, tokens, analytics, logs, or telemetry to GeloTV by itself.

Applications using this library decide which platforms receive messages, which credentials are used, what metadata is attached, and how sent-message records are stored. Operators of those applications are responsible for:

The names Twitch, YouTube, and Kick refer to third-party platforms. This package is not endorsed by, sponsored by, or affiliated with those platforms. GeloTV is not responsible for messages, metadata, automations, accounts, credentials, moderation decisions, bans, platform enforcement, or legal consequences caused by applications built with this library. Review the Apache-2.0 license and NOTICE file before distributing software based on this package.

Development

mix deps.get
mix test
mix format
MIX_ENV=docs mix docs
mix hex.build

Publishing Notes

The Mix project includes package metadata for Hex. Before publishing, update the repository URL and maintainer information in mix.exs, then run:

mix hex.build
mix hex.publish

License: Apache-2.0.