Skip to main content

README.md

# 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:

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

## Core Concepts

- `GelotvBot.Target` describes one platform/channel destination.
- `GelotvBot.Message` stores the visible chat body plus structured metadata.
- `GelotvBot.Adapter` is the behaviour for platform-specific send code.
- `GelotvBot.Bot` is a supervised named bot instance.
- `GelotvBot.RateLimiter` coordinates per-target token buckets across bot
  instances.
- `GelotvBot.RetryPolicy` retries transient adapter errors such as platform
  rate-limit responses with bounded backoff.

## Example

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
GelotvBot.put_targets(:donation_alerts, updated_targets)
```

Running bot instances can be listed and stopped:

```elixir
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:

```elixir
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
`Target`s, 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:

```elixir
{: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:

- `GelotvBot.Adapters.Twitch`: `POST https://api.twitch.tv/helix/chat/messages`
- `GelotvBot.Adapters.YouTube`: `POST https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet`
- `GelotvBot.Adapters.Kick`: `POST https://api.kick.com/public/v1/chat`

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

```elixir
%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:

```elixir
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:

- `GelotvBot.APIs.Twitch`
- `GelotvBot.APIs.YouTube`
- `GelotvBot.APIs.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.

```elixir
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`:

```elixir
{: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:

```elixir
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`:

```elixir
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`:

```elixir
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:

```elixir
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:

```elixir
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

```elixir
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:

```elixir
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:

```elixir
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:

- `U+200B ZERO WIDTH SPACE` represents bit `0`.
- `U+200C ZERO WIDTH NON-JOINER` represents bit `1`.
- Payload bytes are encoded most-significant bit first.

```elixir
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:

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

## Legal and Privacy Notice

`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:

- complying with Twitch, YouTube, Kick, and any other platform terms and API
  rules;
- obtaining any required permissions from streamers, moderators, viewers, and
  message recipients;
- choosing whether metadata is sent out-of-band, visibly in-band, or through
  the zero-width codec;
- protecting access tokens, secrets, and chat logs;
- complying with privacy, consumer-protection, advertising, donation, and other
  laws that apply to their own use case and jurisdiction.

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

```bash
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:

```bash
mix hex.build
mix hex.publish
```

License: Apache-2.0.