From 4099d13514583bb0b9aa5daa2bcc2f8837734e8c Mon Sep 17 00:00:00 2001 From: Dysrhythmic Date: Tue, 10 May 2022 19:50:15 -0500 Subject: [PATCH] Initial commit --- .dockerignore | 8 +++++ .formatter.exs | 4 +++ .gitignore | 8 +++++ Dockerfile | 19 +++++++++++ LICENSE | 21 ++++++++++++ README.md | 23 +++++++++++++ config/runtime.exs | 7 ++++ lib/remedy.ex | 81 ++++++++++++++++++++++++++++++++++++++++++++++ mix.exs | 29 +++++++++++++++++ mix.lock | 17 ++++++++++ 10 files changed, 217 insertions(+) create mode 100644 .dockerignore create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/runtime.exs create mode 100644 lib/remedy.ex create mode 100644 mix.exs create mode 100644 mix.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..38a1855 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.gitignore +README.md +LICENSE +/_build +/deps +.elixir_ls +.formatter.exs +mix.lock diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc8c1e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.elixir_ls +/_build +/deps + +.env + +todo.txt +stream_urls.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ad4b47d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM elixir:1.13.4 + +WORKDIR /app + +RUN apt-get -y update && apt-get install -y \ + ffmpeg \ + python3-pip &&\ + pip install --upgrade streamlink &&\ + ln -s /usr/bin/python3 /usr/bin/python &&\ + curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/bin/youtube-dl &&\ + chmod a+rx /usr/bin/youtube-dl &&\ + mix local.hex --force &&\ + mix local.rebar --force + +COPY ./mix.exs ./ +RUN mix deps.get +COPY ./ ./ + +CMD ["mix", "run", "--no-halt"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b38d071 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Dysrhythmic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..21ebcc4 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Remedy +An Elixir Discord bot made with [Nostrum](https://github.com/Kraigie/nostrum) for playing audio from various sources in voice channels. + +## Setup +The app expects a bot token to be provided in a `.env` file in the following format: `DISCORD_BOT_TOKEN="TOKEN_HERE"`. + +After creating the `.env` file all you need to do is build the image from the Dockerfile and run it. + +If the bot isn't responding to message commands, ensure privileged gateway intents are enabled for the application in your Discord developer portal. + +## Usage +The bot expects commands to be given via message events rather than slash commands. Commands it listens for are: +* `!ping` - Responds with "pong". +* `!summon` - Will find the voice channel the user sending the command is in and joins the channel. +* `!leave` - Will leave the voice channel it is connected to. +* `!play URL_HERE` - Will attempt to play the audio from the given URL. The URL can be [any that ffmpeg can read](https://www.ffmpeg.org/ffmpeg-protocols.html). E.g. `!play music/my favorite song.mp3` will play the file `my favorite song.mp3` if it exists at that path, and `!play http://streams.ilovemusic.de/iloveradio10.mp3` will attempt to play the audio from that HTTP stream. +* `!stream URL_HERE` - Will attempt to stream the audio from the given livestream URL using [streamlink](https://streamlink.github.io/index.html). E.g. `!stream twitch.tv/day9tv` will attempt to stream the audio from [Day9tv's](https://www.twitch.tv/day9tv) Twitch channel. +* `!stop` - Will stop playback. +* `!pause` - Will pause the playback. +* `!resume` - Will resume the playback. + +## License +[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..b94e004 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,7 @@ +import Config + +DotenvParser.load_file(".env") + +config :nostrum, + gateway_intents: :all, + token: System.get_env("DISCORD_BOT_TOKEN") diff --git a/lib/remedy.ex b/lib/remedy.ex new file mode 100644 index 0000000..716be1b --- /dev/null +++ b/lib/remedy.ex @@ -0,0 +1,81 @@ +defmodule RemedySupervisor do + use Application + + def start(_type, _args) do + children = [Remedy] + + Supervisor.start_link(children, strategy: :one_for_one) + end +end + +defmodule Remedy do + use Nostrum.Consumer + + alias Nostrum.Api + alias Nostrum.Struct.Message + alias Nostrum.Voice + alias Nostrum.Cache.GuildCache + + def start_link, do: Consumer.start_link(__MODULE__) + + def handle_event({:MESSAGE_CREATE, msg, _ws_state}), do: handle_msg(msg) + + def handle_event(_event), do: :noop + + defp handle_msg(%Message{content: "!ping", channel_id: channel_id}), + do: Api.create_message(channel_id, "pong") + + defp handle_msg(%Message{content: "!summon"} = msg) do + case get_voice_channel_from_msg(msg) do + nil -> + Api.create_message(msg.channel_id, "You must be in a voice channel to summon") + + voice_channel_id -> + Voice.join_channel(msg.guild_id, voice_channel_id) + end + end + + defp handle_msg(%Message{content: "!leave", guild_id: guild_id}), + do: Voice.leave_channel(guild_id) + + defp handle_msg(%Message{content: "!pause", guild_id: guild_id}), do: Voice.pause(guild_id) + + defp handle_msg(%Message{content: "!resume", guild_id: guild_id}), do: Voice.resume(guild_id) + + defp handle_msg(%Message{content: "!stop", guild_id: guild_id}), do: Voice.stop(guild_id) + + defp handle_msg(%Message{content: "!play " <> url} = msg) do + if String.contains?(url, "youtu.be/") or String.contains?(url, "youtube.com/") do + play_if_voice_ready(msg, url, :ytdl) + else + play_if_voice_ready(msg, url, :url) + end + end + + defp handle_msg(%Message{content: "!stream " <> url} = msg), + do: play_if_voice_ready(msg, url, :stream) + + defp handle_msg(_msg), do: :ignore + + defp get_voice_channel_from_msg(msg) do + msg.guild_id + |> GuildCache.get!() + |> Map.get(:voice_states) + |> Enum.find(%{}, fn voice_state -> voice_state.user_id == msg.author.id end) + |> Map.get(:channel_id) + end + + defp play_if_voice_ready(msg, url, playback_type) do + if Voice.ready?(msg.guild_id) do + play_audio(msg.guild_id, url, playback_type) + else + Api.create_message(msg.channel_id, "I must be in a voice channel to do that") + end + end + + defp play_audio(guild_id, url, playback_type) do + if Voice.playing?(guild_id), do: Voice.stop(guild_id) + + Voice.play(guild_id, url, playback_type) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..867aba9 --- /dev/null +++ b/mix.exs @@ -0,0 +1,29 @@ +defmodule Remedy.MixProject do + use Mix.Project + + def project do + [ + app: :remedy, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {RemedySupervisor, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + def deps do + [ + {:dotenv_parser, "~> 2.0"}, + {:nostrum, github: "Kraigie/nostrum"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..cd75725 --- /dev/null +++ b/mix.lock @@ -0,0 +1,17 @@ +%{ + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "chacha20": {:hex, :chacha20, "1.0.3", "26372176993172260968b36b4e7bc2e007e6b2b397ae08083e4836df67cdf03c", [:mix], [], "hexpm", "27f23b680e63f04b5bced77a9d8867033b381c091f92d544de0ad93accfb7cec"}, + "cowlib": {:hex, :remedy_cowlib, "2.11.1", "7abb4d0779a7d1c655f7642dc0bd0af754951e95005dfa01b500c68fe35a5961", [:rebar3], [], "hexpm", "0b613dc308e080cb6134285f1b1b55c3873e101652e70c70010fc6651c91b130"}, + "curve25519": {:hex, :curve25519, "1.0.4", "e570561b832c29b3ce4fd8b9fcd9c9546916188860568f1c1fc9428d7cb00894", [:mix], [], "hexpm", "1a068bf9646e7067bf6aa5bf802b404002734e09bb5300f098109df03e31f9f5"}, + "dotenv_parser": {:hex, :dotenv_parser, "2.0.0", "0f999196857e4ee18cbba1413018d5e4980ab16b397e3a2f8d0cf541fe683181", [:mix], [], "hexpm", "e769bde2dbff5b0cd0d9d877a9ccfd2c6dd84772dfb405d5a43cceb4f93616c5"}, + "ed25519": {:hex, :ed25519, "1.4.0", "3eee373a77c8230ac25ab1d557f436eb6ec584946d7b76ca311f9fa18db84b55", [:mix], [], "hexpm", "420b52c68e2eda14a822c1d15df6c3e386f7b604d4351d6621a2f6baa68ed6dd"}, + "equivalex": {:hex, :equivalex, "1.0.2", "b9a9aaf79f2556288f514218653beaddb15afa2af407bfec37c5c4906e39f514", [:mix], [], "hexpm", "f7f8127c59be715ee6288f8c59fa8fc40e6428fb5c9bd2a001de2c9b1ff3f1c2"}, + "gen_stage": {:hex, :gen_stage, "1.1.2", "b1656cd4ba431ed02c5656fe10cb5423820847113a07218da68eae5d6a260c23", [:mix], [], "hexpm", "9e39af23140f704e2b07a3e29d8f05fd21c2aaf4088ff43cb82be4b9e3148d02"}, + "gun": {:hex, :remedy_gun, "2.0.1", "0f0caed812ed9e4da4f144df2d5bf73b0a99481d395ecde990a3791decf321c6", [:rebar3], [{:cowlib, "~> 2.11.1", [hex: :remedy_cowlib, repo: "hexpm", optional: false]}], "hexpm", "b6685a85fbd12b757f86809be1b3d88fcef365b77605cd5aa34db003294c446e"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "kcl": {:hex, :kcl, "1.4.1", "9ae313d567c990371ae7a4f7d98e4adae512ca4f315dc03b8995244cc01a5c2f", [:mix], [{:curve25519, ">= 1.0.4", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 1.0", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 1.0", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "1c9a7604d55e933c6536f30a8523e5d1ce58bf595ca16fbd9d8d1a2519e91a01"}, + "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, + "nostrum": {:git, "https://github.com/Kraigie/nostrum.git", "b1d3d118edb0fda4f9e66d6688674f9734034a36", []}, + "poly1305": {:hex, :poly1305, "1.0.3", "9ec6b5b3b96d96b15d4f2b8a3629aad2a2f5fe82f4ea0f13e5a3512737af4ee7", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "fbe549d59e74e7cde680e7ae6baf7fe2b5f9053a84c1b5c866f703d0651d6b22"}, + "salsa20": {:hex, :salsa20, "1.0.3", "fb900fc9b26b713a98618f3c6d6b6c35a5514477c6047caca8d03f3a70175ab0", [:mix], [], "hexpm", "91cbfa537f369d074a79f926f36a6c2ac24fba12cbadcb23aa04a759282887fe"}, +}