From faec21b934eef733577aa566ff0cf9df0a735d44 Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Fri, 19 Jan 2024 10:19:30 -0400 Subject: [PATCH] Update event_sub opts and README --- README.md | 251 +++++++++++++++++++++++------------- lib/tmi/event_sub/socket.ex | 16 ++- lib/tmi/supervisor.ex | 9 +- 3 files changed, 182 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 25e47b3..4993bc2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Hex.pm](https://img.shields.io/hexpm/dt/tmi)](https://hex.pm/packages/tmi) [![Hex.pm](https://img.shields.io/hexpm/l/tmi)](https://github.com/ryanwinchester/tmi.ex/blob/main/LICENSE) -Connect to Twitch chat with Elixir. +Connect to Twitch chat and EventSub with Elixir. ## Installation @@ -22,133 +22,210 @@ Documentation can be found at [https://hexdocs.pm/tmi/readme.html](https://hexdo ## Usage +### Chat You can use your own Twitch username, but it is recommended to make a new twitch account just for your bot. You'll also need an OAuth token for the password. -The simplest method to get an OAuth token (while logged in to the account your bot will use), use the [Twitch Chat OAuth Password Generator](https://twitchapps.com/tmi/). + * The simplest method to get an OAuth token (while logged in to the account your bot will use), use + the [Twitch Chat OAuth Password Generator](https://twitchapps.com/tmi/). -Create a bot module to deal with chat messages or events: - -```elixir -defmodule MyBot do - use TMI - - alias TMI.Events.Message - - @impl true - def handle_event(%Message{message: "!" <> cmd} = event) do - cmd(cmd, event) - end - - # ... TODO: match on other you're interested in ... +#### Config options (Chat/TMI) - ## Helpers - - defp cmd("roll", %Message{channel: channel, display_name: user}), - do: say(channel, "@#{user} rolled a #{Enum.random(1..6)}!") - - defp cmd("echo " <> rest, %Message{channel: channel}), - do: say(channel, rest) + * `:bot` - A module that `use`s `TMI` and implements the `TMI.Handler` behaviour. + * `:user` - Twitch username of your bot user (lowercase). + * `:pass` - OAuth token to use as a password, prefixed with `oauth:`. + * `:channels` - The list of channels to join (lowercase). + * `:mod_channels` - The list of channels where your bot is a moderator + (this effects the message and command rate limits). - defp cmd("dance", %Message{channel: channel, display_name: user}), - do: me(channel, "dances for @#{user}") +```elixir +# config/runtime.exs - defp cmd(_command, msg), - do: say(msg.channel, "unrecognized command") -end +config :my_app, + bots: [ + [ + bot: MyApp.MyBot, + user: "myappbot", + pass: System.fetch_env!("TWITCH_OATH_TOKEN"), # "oauth:myappbotpassword" + channels: ["mychannel", "foo"], + mod_channels: ["mychannel"], + debug: false # defaults to false + ] + ] ``` -## EventSub +### EventSub -https://twitchapps.com/tokengen + * You need to create an app on the [Twitch Developer Console](https://dev.twitch.tv/console/apps/create) + to get the `client_id`. Also, add the redirect URL from the instructions in the token generator + linked below if you use that. + * To get an OAuth token for EventSub, it's easiest of you are logged in as the broadcaster of the + channel you want to use the bot for and then you can use the [Twitch OAuth Token Generator](https://twitchapps.com/tokengen/) + with the `client_id` of the app you created. -#### Available handler callbacks: +For scopes, I just use all the `read` scopes except for `whisper` and `stream_key`. If you want to +do the same, just paste the below into the `scopes` field on the token generator page: - handle_connected(server, port) - handle_logged_in() - handle_login_failed(reason) - handle_disconnected() - handle_join(chat) - handle_join(chat, user) - handle_part(chat) - handle_part(chat, user) - handle_kick(chat, kicker) - handle_kick(chat, user, kicker) - handle_whisper(message, sender) - handle_whisper(message, sender, tags) - handle_message(message, sender, chat) - handle_message(message, sender, chat, tags) - handle_mention(message, sender, chat) - handle_action(message, sender, chat) - handle_unrecognized(msg) - handle_unrecognized(msg, tags) +``` +analytics:read:extensions analytics:read:games bits:read channel:read:ads channel:read:charity channel:read:goals channel:read:guest_star channel:read:hype_train channel:read:polls channel:read:predictions channel:read:redemptions channel:read:subscriptions channel:read:vips moderation:read moderator:read:automod_settings moderator:read:blocked_terms moderator:read:chat_settings moderator:read:chatters moderator:read:followers moderator:read:guest_star moderator:read:shield_mode moderator:read:shoutouts user:read:blocked_users user:read:broadcast user:read:email user:read:follows user:read:subscriptions channel:bot chat:read user:bot user:read:chat +``` -### Starting +If you want to do moderation things with this token, then you can add the required scopes for +your actions found here [https://dev.twitch.tv/docs/authentication/scopes](https://dev.twitch.tv/docs/authentication/scopes/). -First we need to go over the config options. +#### Config options (EventSub) -#### Config options + * `:user_id` - The twitch user ID of the broadcaster. + * `:handler` - A module that `use`s `TMI` and implements the `TMI.Handler` behaviour. + * `:client_id` - The client ID of the application you used for the token. + * `:access_token` - The OAuth token you generated with the correct scopes for your subscriptions. + * `:keepalive_timeout` - Optional. The keepalive timeout in seconds. Specifying an invalid, + but numeric value will return the nearest acceptable value. Defaults to `10`. + * `:start?` - Optional. A boolean value of whether or not to start the eventsub socket. + Defaults to `false` if there are no `event_sub` config options. + * `:subscriptions` - Optional. The list of subscriptions to create. See below for more info. + Defaults to: - * `:bot` - The module that `use`s `TMI` and implements the `TMI.Handler` behaviour. - * `:user` - Twitch username of your bot user (lowercase). - * `:pass` - OAuth token to use as a password, prefixed with `oauth:`. - * `:channels` - The list of channels to join (lowercase). - * `:mod_channels` - The list of channels where your bot is a moderator - (this effects the message and command rate limits). - -#### Example config +```elixir +# Default subscriptions. +~w[ + channel.ad_break.begin channel.cheer channel.follow channel.subscription.end + channel.channel_points_custom_reward_redemption.add + channel.channel_points_custom_reward_redemption.update + channel.charity_campaign.donate channel.charity_campaign.progress + channel.goal.begin channel.goal.progress channel.goal.end + channel.hype_train.begin channel.hype_train.progress channel.hype_train.end + channel.shoutout.create channel.shoutout.receive + stream.online stream.offline +] +``` ```elixir # config/runtime.exs +# Add to the existing bot config. config :my_app, bots: [ [ - bot: MyApp.Bot, + # Example existing Bot config. + bot: MyApp.MyBot, user: "myappbot", pass: System.fetch_env!("TWITCH_OATH_TOKEN"), # "oauth:myappbotpassword" channels: ["mychannel", "foo"], mod_channels: ["mychannel"], - debug: false # defaults to false + debug: false, # defaults to false + # Adding here ===>: + # Adding event_sub config options will start the eventsub socket. + event_sub: [ + user_id: "123456", + handler: MyApp.MyBot, + client_id: System.get_env("TWITCH_CLIENT_ID"), + access_token: System.get_env("TWITCH_ACCESS_TOKEN") + ] ] ] ``` -### Add to your supervision tree +### Bot module -##### Single bot example: +Create a bot module to deal with chat messages or events: ```elixir -# lib/my_app/application.ex +defmodule MyBot do + use TMI -[bot_config] = Application.fetch_env!(:my_app, :bots) + alias TMI.Events.Follow + alias TMI.Events.Message -children = [ - # If you have existing children, e.g.: - Existing.Worker, - {Another.Existing.Supervisor, []}, - # Add the bot. - {TMI.Supervisor, bot_config} -] + @impl true + def handle_event(%Message{message: "!" <> cmd} = event) do + dispatch(cmd, event) + end -Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) + def handle_event(%Follow{} = event) do + say(event.broadcaster_user_login, "Thanks for the follow, @#{event.user_name}") + end + + ## Helpers + + defp dispatch("roll", %{channel: channel, display_name: user}), + do: say(channel, "@#{user} rolled a #{Enum.random(1..6)}!") + + defp dispatch(_command, _msg), + do: :noop +end +``` + +#### Available handler callbacks: + +```elixir +## Receives `Event` structs +handle_event(event) + +## IRC-related callbacks +handle_connected(server, port) +handle_disconnected() +handle_join(channel) +handle_join(channel, user) +handle_kick(channel, kicker) +handle_kick(channel, user, kicker) +handle_logged_in() +handle_login_failed(reason) +handle_part(channel) +handle_part(channel, user) +handle_unrecognized(msg) +handle_unrecognized(msg, tags) +``` + +### Starting + +Examples of adding it to your application's supervision tree below. + +##### Single bot example: + +```elixir +# lib/my_app/application.ex in `start/2` function: +defmodule MyApp.Application do + # ... + @impl true + def start(_type, _args) do + [bot_config] = Application.fetch_env!(:my_app, :bots) + + children = [ + # ... existing stuff ... + # Add the bot. + {TMI.Supervisor, bot_config} + ] + + # ... + end + # ... +end ``` ##### Multiple bots example: ```elixir -bots = Application.fetch_env!(:my_app, :bots) -bot_children = for bot_config <- bots, do: {TMI.Supervisor, bot_config} - -children = [ - # If you have existing children, e.g.: - Existing.Worker, - {Another.Existing.Supervisor, []} - # Add the bot children. - | bot_children -] +# lib/my_app/application.ex in `start/2` function: +defmodule MyApp.Application do + # ... + @impl true + def start(_type, _args) do + bots = Application.fetch_env!(:my_app, :bots) + bot_children = for bot_config <- bots, do: {TMI.Supervisor, bot_config} + + children = [ + # If you have existing children, e.g.: + Existing.Worker, + {Another.Existing.Supervisor, []} + # Add the bot children. + | bot_children + ] -Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) + # ... + end + # ... +end ``` ### To get your bot verified: @@ -170,7 +247,3 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -## For the memes - -![roxcar76](https://user-images.githubusercontent.com/2897340/163022131-e86af85b-6b2f-4ee8-b44b-486f267ac7bd.png) diff --git a/lib/tmi/event_sub/socket.ex b/lib/tmi/event_sub/socket.ex index 9dc4176..6717a63 100644 --- a/lib/tmi/event_sub/socket.ex +++ b/lib/tmi/event_sub/socket.ex @@ -38,9 +38,19 @@ defmodule TMI.EventSub.Socket do * `:url` - A websocket URL to connect to. `Defaults to "wss://eventsub.wss.twitch.tv/ws"`. * `:keepalive_timeout` - The keepalive timeout in seconds. Specifying an invalid, but numeric value will return the nearest acceptable value. Defaults to `10`. + * `:start?` - A boolean value of whether or not to start the eventsub socket. """ + @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts) do + if Keyword.get(opts, :start?, true) do + do_start(opts) + else + :ignore + end + end + + defp do_start(opts) do Logger.info("[TMI.EventSub.Socket] connecting...") if not Enum.all?(@required_opts, &Keyword.has_key?(opts, &1)) do @@ -212,7 +222,11 @@ defmodule TMI.EventSub.Socket do # } # } # - defp handle_message(%{"message_type" => "notification", "subscription_type" => type}, %{"event" => payload}, state) do + defp handle_message( + %{"message_type" => "notification", "subscription_type" => type}, + %{"event" => payload}, + state + ) do Logger.debug("[TMI.EventSub.Socket] got notification: " <> inspect(payload, pretty: true)) type diff --git a/lib/tmi/supervisor.ex b/lib/tmi/supervisor.ex index 39b8c22..8e63d3c 100644 --- a/lib/tmi/supervisor.ex +++ b/lib/tmi/supervisor.ex @@ -18,23 +18,24 @@ defmodule TMI.Supervisor do @impl true def init({bot, opts}) do + {eventsub_opts, opts} = Keyword.pop(opts, :event_sub, start?: false) + # IRC Bot config. {is_verified, opts} = Keyword.pop(opts, :is_verified, false) {mod_channels, opts} = Keyword.pop(opts, :mod_channels, []) + {:ok, client} = TMI.IRC.Client.start_link(Keyword.take(opts, [:debug])) + conn = build_irc_conn(client, opts) dynamic_supervisor = TMI.IRC.MessageServer.supervisor_name(bot) - # EventSub config. - eventsub_config = Application.fetch_env!(:abesai_bot, TMI.EventSub.Socket) - children = [ {DynamicSupervisor, strategy: :one_for_one, name: dynamic_supervisor}, {TMI.IRC.ChannelServer, {bot, conn, is_verified, mod_channels}}, {TMI.IRC.ConnectionServer, {bot, conn}}, {TMI.IRC.WhisperServer, {bot, conn}}, {bot, conn}, - {TMI.EventSub.Socket, eventsub_config} + {TMI.EventSub.Socket, eventsub_opts} ] Supervisor.init(children, strategy: :one_for_one)