From 9c34447f718a098f502ac49c5b1cbf1d0a416a56 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 15:18:05 +0100 Subject: [PATCH 01/33] Add configuration for asdf --- .gitignore | 4 +++- .tool-versions | 2 ++ README.md | 17 +++++++---------- 3 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 .tool-versions diff --git a/.gitignore b/.gitignore index 0d78984..4c096da 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ cassian-*.tar /.elixir_ls # For certificates -/priv \ No newline at end of file +/priv + +.asdf-vars \ No newline at end of file diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..546395a --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.16.0 +erlang 26.2.1 diff --git a/README.md b/README.md index 4a5ddc9..ecac908 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ A Discord bot for music written in Elixir. ## Table of contents - [About](#about) -- [Note regarding Nostrum](#note-regarding-nostrum) - [Note regarding the web part](#note-regarding-the-web-part) - [System requirements](#system-requirements) - [Configuration](#configuration) @@ -23,10 +22,6 @@ The bot is written in Elixir, a functional programming language based upon Erlan The Discord library is [Nostrum](https://github.com/Kraigie/nostrum), written in Elixir and supporting everything from events to voice channels. -## Note regarding Nostrum - -As of now the Hex release of Nostrum `v0.4.6` is a bit buggy so the current version of this bot uses the nightly version of Nostrum. - ## Note regarding the web part This is deprecated. It will be removed once a web app has been created because I don't want to add web functionality to the bot. @@ -39,18 +34,20 @@ This bot needs `youtube-dl` and `ffmpeg` in order to play music. The path of the ### General bot requirements -Of course as this is written in Elixir, you will need Elixir installed. This can be easily done on Debian-based distros: +Of course as this is written in Elixir, you will need Elixir installed. This can be easily done with [asdf](https://asdf-vm.com/). After following the asdf [instructions](https://asdf-vm.com/guide/getting-started.html) one can start adding the required plugins for the runtimes: ```bash -sudo apt-get install elixir +asdf plugin add elixir +asdf plugin add erlang +asdf install ``` -After installing Elixir you should be able to see the version: +After that you should be able to run `elixir -v` ```bash -Erlang/OTP 23 [erts-11.1.7] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] +Erlang/OTP 26 [erts-14.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] -Elixir 1.11.2 (compiled with Erlang/OTP 23) +Elixir 1.16.0 (compiled with Erlang/OTP 24) ``` Try to keep the Elixir version `1.11.2` and up. From 74dce9c00960a08d28dfe63de5a31450d4518b88 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 16:14:34 +0100 Subject: [PATCH 02/33] Rewrite play file Using with statement for play command to reduce space --- lib/cassian/commands/playback/play.ex | 53 ++++++--------------------- lib/cassian/utils.ex | 8 ++-- lib/cassian/utils/voice.ex | 8 ++-- 3 files changed, 20 insertions(+), 49 deletions(-) diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index 84a8ef1..3952ec3 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -17,53 +17,24 @@ defmodule Cassian.Commands.Playback.Play do sends an embed message that the user isn't connected to channel. """ def handle_request(message, args) do - case VoiceUtils.get_sender_voice_id(message) do - {:ok, {_guild_id, voice_id}} -> - handle_connect_possibility(message, voice_id, args) + with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(message), + {:voice_connect, true} <- {:voice_connect, VoiceUtils.can_connect?(message.guild_id, voice_id)} do + {:ok, metadata} <- song_metadata(Enum.fetch!(args, 0)) - # THe user is not in a voice channel... - {:error, :noop} -> + VoiceUtils.join_or_switch_voice(message.guild_id, voice_id) + PlayManager.insert!(message.guild_id, message.channel_id, metadata) + PlayManager.play_if_needed(message.guild_id) + MessageManager.disable_embed(message) + else + {:error, :not_in_voice} -> no_channel_error(message) - end - end - - @doc """ - Determine whether you have the possiblity to connect. Continues - with the rest of the logic pipe or sends an embed message - that the bot doesn't have permission to connect. - """ - def handle_connect_possibility(message, voice_id, args) do - if VoiceUtils.can_connect?(message.guild_id, voice_id), - do: handle_metadata(message, voice_id, args), - else: no_permissions_error(message) - end - - @doc """ - It is determined that the caller user is in a voice channel and that the bot has permissions - to connect. Awesome. Now check if the link metadata is correct. If it is correct, continue with the - logic pipe or send an embed that the link is not correct. - """ - def handle_metadata(message, voice_id, args) do - case song_metadata(Enum.fetch!(args, 0)) do - {true, metadata} -> - handle_connect(message, voice_id, metadata) - - {false, :noop} -> + {:voice_connect, false} -> + no_permissions_error(message) + {:error, :no_metadata} -> invalid_link_error(message) end end - @doc """ - Everything is A-ok. You can connect and the link is valid. Connect to the voice channel and - cache the voice state. Continue wth the pipeline. - """ - def handle_connect(message, voice_id, metadata) do - VoiceUtils.join_or_switch_voice(message.guild_id, voice_id) - PlayManager.insert!(message.guild_id, message.channel_id, metadata) - PlayManager.play_if_needed(message.guild_id) - MessageManager.disable_embed(message) - end - # Error handlers @doc """ diff --git a/lib/cassian/utils.ex b/lib/cassian/utils.ex index 89b6f4b..6238250 100644 --- a/lib/cassian/utils.ex +++ b/lib/cassian/utils.ex @@ -16,20 +16,20 @@ defmodule Cassian.Utils do @doc """ Check whether a link is a YouTube one. """ - @spec song_metadata(link :: String.t()) :: {true, metadata :: Hash} | {false, :noop} + @spec song_metadata(link :: String.t()) :: {:ok, metadata :: Hash} | {:error, :no_metadata} def song_metadata(link) do case YoutubeService.oembed_song_data(link) do {:ok, metadata} -> - {true, metadata} + {:ok, metadata} _ -> case SoundCloudService.oembed_song_data(link) do {:ok, metadata} -> - {true, metadata} + {:ok, metadata} _ -> - {false, :noop} + {:error, :no_metadata} end end end diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index ecf6a62..15d664c 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -88,9 +88,9 @@ defmodule Cassian.Utils.Voice do @doc """ Safely the current voice id in which the user is. Also returns the guild id. """ - @spec get_sender_voice_id(message :: Nostrum.Struct.Channel) :: - {:ok, {guild_id :: String.t(), channel_id :: String.t()}} | {:error, :noop} - def get_sender_voice_id(message) do + @spec sender_voice_id(message :: Nostrum.Struct.Channel) :: + {:ok, {guild_id :: String.t(), channel_id :: String.t()}} | {:error, :not_in_voice} + def sender_voice_id(message) do voice_id = GuildCache.get!(message.guild_id) |> Map.fetch!(:voice_states) @@ -101,7 +101,7 @@ defmodule Cassian.Utils.Voice do if voice_id do {:ok, {message.guild_id, voice_id}} else - {:error, :noop} + {:error, :not_in_voice} end end From 71a296b7419e79b3d100ccfe2c908ddcb743c34b Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 16:25:52 +0100 Subject: [PATCH 03/33] Update dependencies to newest versions It lives! It's starting at the very least! --- lib/cassian/commands/playback/play.ex | 4 +-- lib/cassian/consumer.ex | 2 -- mix.exs | 5 +-- mix.lock | 47 ++++++++++++++------------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index 3952ec3..ded71de 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -18,8 +18,8 @@ defmodule Cassian.Commands.Playback.Play do """ def handle_request(message, args) do with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(message), - {:voice_connect, true} <- {:voice_connect, VoiceUtils.can_connect?(message.guild_id, voice_id)} do - {:ok, metadata} <- song_metadata(Enum.fetch!(args, 0)) + {:voice_connect, true} <- {:voice_connect, VoiceUtils.can_connect?(message.guild_id, voice_id)}, + {:ok, metadata} <- song_metadata(Enum.fetch!(args, 0)) do VoiceUtils.join_or_switch_voice(message.guild_id, voice_id) PlayManager.insert!(message.guild_id, message.channel_id, metadata) diff --git a/lib/cassian/consumer.ex b/lib/cassian/consumer.ex index db16950..37e8eb5 100644 --- a/lib/cassian/consumer.ex +++ b/lib/cassian/consumer.ex @@ -18,8 +18,6 @@ defmodule Cassian.Consumer do do: Command.handle_message(message) end - @dialyzer {:no_return, {:update_status, 3}} - @doc false def handle_event({:READY, _, _}) do Nostrum.Api.update_status("", "music 🎶", 2) diff --git a/mix.exs b/mix.exs index e03e393..04d78ed 100644 --- a/mix.exs +++ b/mix.exs @@ -22,10 +22,11 @@ defmodule Cassian.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:nostrum, git: "https://github.com/Kraigie/nostrum.git"}, + {:nostrum, "~> 0.8.0"}, + {:poison, "~> 5.0"}, + {:httpoison, "~> 2.0"}, {:cowlib, "~> 2.9.1", override: true}, {:plug_cowboy, "~> 2.4"}, - {:poison, "~> 4.0", override: true}, {:floki, "~> 0.31.0"} ] end diff --git a/mix.lock b/mix.lock index 01a61f3..43a3231 100644 --- a/mix.lock +++ b/mix.lock @@ -1,34 +1,35 @@ %{ - "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, - "chacha20": {:hex, :chacha20, "1.0.2", "73c5e96eba5e94917603a43c5c7c6b049436a5d71d9d3182781c345d87d28c8b", [:mix], [], "hexpm", "549b89314cbffa0893ef923d999d625c227cab5f1301133598793225f02a3963"}, - "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, - "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"}, + "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, - "curve25519": {:hex, :curve25519, "1.0.4", "e570561b832c29b3ce4fd8b9fcd9c9546916188860568f1c1fc9428d7cb00894", [:mix], [], "hexpm", "1a068bf9646e7067bf6aa5bf802b404002734e09bb5300f098109df03e31f9f5"}, - "ed25519": {:hex, :ed25519, "1.3.2", "e3a2d4badf57f0799279cf09925bd761ec38df6df3696e266585626280b5c0ad", [:mix], [], "hexpm", "2290e46e0e23717adbe20632c6dd29aa71a46ca6e153ef7ba41fe1204f66f859"}, - "equivalex": {:hex, :equivalex, "1.0.2", "b9a9aaf79f2556288f514218653beaddb15afa2af407bfec37c5c4906e39f514", [:mix], [], "hexpm", "f7f8127c59be715ee6288f8c59fa8fc40e6428fb5c9bd2a001de2c9b1ff3f1c2"}, + "curve25519": {:hex, :curve25519, "1.0.5", "f801179424e4012049fcfcfcda74ac04f65d0ffceeb80e7ef1d3352deb09f5bb", [:mix], [], "hexpm", "0fba3ad55bf1154d4d5fc3ae5fb91b912b77b13f0def6ccb3a5d58168ff4192d"}, + "ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"}, + "equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"}, "floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"}, "gen_stage": {:hex, :gen_stage, "1.1.0", "dd0c0f8d2f3b993fdbd3d58e94abbe65380f4e78bdee3fa93d5618d7d14abe60", [:mix], [], "hexpm", "7f2b36a6d02f7ef2ba410733b540ec423af65ec9c99f3d1083da508aca3b9305"}, - "gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"}, - "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, + "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, + "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "kcl": {:hex, :kcl, "1.3.2", "40ca3e6c6421781b6db8ba2e5354e64c975f19a3073aed62f632e6edcb714148", [: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", "f66d34ef4c9be59fa439e8ca861daed074e6542306a81dabe8e3ab2be9dc78fd"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "kcl": {:hex, :kcl, "1.4.2", "8b73a55a14899dc172fcb05a13a754ac171c8165c14f65043382d567922f44ab", [: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", "9f083dd3844d902df6834b258564a82b21a15eb9f6acdc98e8df0c10feeabf05"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nostrum": {:git, "https://github.com/Kraigie/nostrum.git", "4f05b3582117ac61861125da68482662649b7a02", []}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.1", "5c854427528bf61d159855cedddffc0625e2228b5f30eff76d5a4de42d896ef4", [:mix], [], "hexpm", "6961c0e17febd9d0bfa89632d391d2545d2e0eb73768f5f50305a23961d8782c"}, - "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, - "poly1305": {:hex, :poly1305, "1.0.2", "c6bb74cbe79747cc12aa1580791c2bd8e0f062bc8faaf117b756e675cfaea03d", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "d0a0f8be324e7bfdd61e8e52215024a025816f3a7f665c274ad5bea154480c2b"}, + "nostrum": {:hex, :nostrum, "0.8.0", "36f5a08e99c3df3020523be9e1c650ad926a63becc5318562abfe782d586e078", [:mix], [{:certifi, "~> 2.8", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "ce6861391ff346089d32a243fa71c0cb8bff79ab86ad53e8bf72808267899aee"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, + "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, + "poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"}, "porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, - "salsa20": {:hex, :salsa20, "1.0.2", "558fddb4169b1f90b1748d42e9221ba8d1354c414a03361308cc7bc0fd2f45c7", [:mix], [], "hexpm", "8d0d394c67da8d44073cbf2c030c4e0e04e7bed92967761be60419d8d46df18d"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } From e355282464138c3d2761d49c127566c758e8d308 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 16:45:32 +0100 Subject: [PATCH 04/33] Remove porcelain I don't know why this was here, maybe because nostrum needed it at some point but everything seems to work fine* without it. * It wasn't to start, I can't even get the status to change so this commit might have to be reversed --- README.md | 4 ---- config/config.exs | 3 --- mix.lock | 1 - 3 files changed, 8 deletions(-) diff --git a/README.md b/README.md index ecac908..b8775ba 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,6 @@ To set the bot up-and-running you will need to set a couple of variables. Here's the basic configuration: ```elixir -use Mix.Config -config :porcelain, - driver: Porcelain.Driver.Basic - config :nostrum, ffmpeg: System.get_env("FFMPEG_PATH") || "/usr/bin/ffmpeg", youtubedl: System.get_env("YTDL_PATH") || "/usr/bin/youtube-dl", diff --git a/config/config.exs b/config/config.exs index e306779..f3a870a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,8 +1,5 @@ use Mix.Config -config :porcelain, - driver: Porcelain.Driver.Basic - config :nostrum, ffmpeg: System.get_env("FFMPEG_PATH") || "/usr/bin/ffmpeg", youtubedl: System.get_env("YTDL_PATH") || "/usr/bin/youtube-dl", diff --git a/mix.lock b/mix.lock index 43a3231..15dd46e 100644 --- a/mix.lock +++ b/mix.lock @@ -26,7 +26,6 @@ "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"}, - "porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, From 1d93c75440c0e0562b33ea887e3be9cf784ce7f7 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 17:00:11 +0100 Subject: [PATCH 05/33] Update children Apparently nostrum does injection of consumer now so need to make a hash and touple for it. --- lib/cassian/application.ex | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/cassian/application.ex b/lib/cassian/application.ex index 2c68d5f..ce158c8 100644 --- a/lib/cassian/application.ex +++ b/lib/cassian/application.ex @@ -16,15 +16,12 @@ defmodule Cassian.Application do children = [ - %{ - id: Consumer, - start: {Consumer, :start_link, []} - }, + Cassian.Consumer, Cassian.Servers.SoundCloudToken ] ++ web_child!() children - |> Enum.each(fn child -> DynamicSupervisor.start_child(Cassian.Supervisor, child) end) + |> Enum.each(fn child -> DynamicSupervisor.start_child(Cassian.Supervisor, child) |> IO.inspect(label: "Start child") end) end @doc false From 8f6e12ef611781771cac1fd41dfca6f6518ba906 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 17:01:02 +0100 Subject: [PATCH 06/33] Remove inspect code Left it in here by accident --- lib/cassian/application.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cassian/application.ex b/lib/cassian/application.ex index ce158c8..3c238a1 100644 --- a/lib/cassian/application.ex +++ b/lib/cassian/application.ex @@ -21,7 +21,7 @@ defmodule Cassian.Application do ] ++ web_child!() children - |> Enum.each(fn child -> DynamicSupervisor.start_child(Cassian.Supervisor, child) |> IO.inspect(label: "Start child") end) + |> Enum.each(fn child -> DynamicSupervisor.start_child(Cassian.Supervisor, child) end) end @doc false From fd3c9390a87c71e97702501aa0584d1922716e58 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 19:30:48 +0100 Subject: [PATCH 07/33] Rewrite SoundCloud client_id token acquisition Look at all of the script elements to get the `client_id`. Now it loops through the scripts in reverse in hopes of finding the SoundClound token. --- lib/cassian/servers/sound_cloud_token.ex | 60 +++++++++++++++++------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/lib/cassian/servers/sound_cloud_token.ex b/lib/cassian/servers/sound_cloud_token.ex index 4fc66c4..ace6f94 100644 --- a/lib/cassian/servers/sound_cloud_token.ex +++ b/lib/cassian/servers/sound_cloud_token.ex @@ -7,7 +7,7 @@ defmodule Cassian.Servers.SoundCloudToken do require Logger # 15 minutes~ - @timeout 900_000 + @timeout 5_000 # API @@ -28,9 +28,8 @@ defmodule Cassian.Servers.SoundCloudToken do @impl true def handle_info(:timeout, state) do - Logger.debug("Got timeout with state #{state}") - state = acquire_new_client_id!() - Logger.debug("New client id is: #{state}") + Logger.debug("Got timeout!") + state = acquire_new_client_id() {:noreply, state, @timeout} end @@ -40,19 +39,46 @@ defmodule Cassian.Servers.SoundCloudToken do @impl true def init(_), - do: {:ok, acquire_new_client_id!(), @timeout} + do: {:ok, acquire_new_client_id(), @timeout} - defp acquire_new_client_id!() do - scripts = - HTTPoison.get!("https://soundcloud.com/discover") - |> Map.get(:body) - |> Floki.parse_document!() - |> Floki.find("script") - - {"script", [_head | [{"src", source}]], _} = Enum.at(scripts, length(scripts) - 2) - - body = HTTPoison.get!(source).body - - Regex.run(~r/(?<=,client_id:\")(.*)(?=\",env:)/, body) |> List.first() + defp acquire_new_client_id() do + with {:ok, response = %HTTPoison.Response{status_code: 200}} <- HTTPoison.get("https://soundcloud.com/discover"), + {:ok, value} <- generate_new_client_id(response) do + Logger.debug("Acquired new soundcloud token.") + value + else + _ -> + Logger.warning ("Failed to retrieve token!") + nil + end + end + + defp generate_new_client_id(response) do + data = + response + |> Map.get(:body) + |> Floki.parse_document!() + |> Floki.find("script[src][crossorigin]") + |> Stream.map(&filter_scripts/1) + |> Stream.reject(&is_nil/1) + |> Enum.reverse() # Generally client_id is near the end script from experience + |> Enum.find_value(fn script -> + with {:ok, response = %HTTPoison.Response{status_code: 200}} <- HTTPoison.get(script), + regex_code <- Regex.run(~r/client_id:\"(.*)\",env:/, response.body), + false <- is_nil(regex_code) do + {:ok, List.last(regex_code)} + else + _ -> + nil + end + end) + end + + defp filter_scripts({"script", [_, {"src", script}], _}) do + script + end + + defp filter_scripts(_) do + nil end end From 753381f26e311ca4f797728dfad018de750562b8 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 20:40:58 +0100 Subject: [PATCH 08/33] Add start for interactions Add basic interaction for help --- lib/cassian/behaviours/command.ex | 8 +++++-- lib/cassian/commands/bot/help.ex | 12 +++++++--- lib/cassian/commands/bot/ping.ex | 38 ------------------------------- lib/cassian/consumer.ex | 13 +++++++---- lib/cassian/consumers/command.ex | 32 ++++++++------------------ 5 files changed, 33 insertions(+), 70 deletions(-) delete mode 100644 lib/cassian/commands/bot/ping.ex diff --git a/lib/cassian/behaviours/command.ex b/lib/cassian/behaviours/command.ex index eb2beae..4b6829b 100644 --- a/lib/cassian/behaviours/command.ex +++ b/lib/cassian/behaviours/command.ex @@ -7,11 +7,15 @@ defmodule Cassian.Behaviours.Command do @doc """ The function which is called that does all of the stuff needed in the command. """ - @callback execute(message :: Nostrum.Struct.Message.t(), args :: List.t()) :: :ok + @callback execute(interaction :: Nostrum.Struct.Interaction.t()) :: :ok + + @doc """ + The definition as a Discord application command. + """ + @callback application_command_definition() :: %{} defmacro __using__(_) do quote do - defmacro handle_command(message, args), do: execute(message, args) @behaviour Cassian.Behaviours.Command end end diff --git a/lib/cassian/commands/bot/help.ex b/lib/cassian/commands/bot/help.ex index dc9af39..0f3caa0 100644 --- a/lib/cassian/commands/bot/help.ex +++ b/lib/cassian/commands/bot/help.ex @@ -6,13 +6,19 @@ defmodule Cassian.Commands.Bot.Help do @moduledoc """ The help command. Shows a menu of commands. """ + + def application_command_definition() do + %{ + name: "help", + description: "Get the list of all commands from Cassian" + } + end @doc false - def execute(message, _args) do + def execute(interaction) do generate_help_embed!() - |> MessageManager.send_embed(message.channel_id) - :ok + %{type: 4, data: %{embeds: [generate_help_embed!()]}} end import Cassian.Utils.Embed diff --git a/lib/cassian/commands/bot/ping.ex b/lib/cassian/commands/bot/ping.ex deleted file mode 100644 index d8e5441..0000000 --- a/lib/cassian/commands/bot/ping.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule Cassian.Commands.Bot.Ping do - use Cassian.Behaviours.Command - - alias Cassian.Managers.MessageManager - - @moduledoc """ - The ping command. Shows how many ms was needed to perform the request. - """ - - def call, do: "ping" - - @doc false - def execute(message, _args) do - {:ok, request_time, _} = - message.timestamp - |> DateTime.from_iso8601() - - recieved_time = DateTime.utc_now() - - request_recieved_diff = - recieved_time - |> DateTime.diff(request_time, :millisecond) - - generate_ping_embed!(request_recieved_diff) - |> MessageManager.send_embed(message.channel_id) - - :ok - end - - import Cassian.Utils.Embed - alias Nostrum.Struct.Embed - - def generate_ping_embed!(diff) do - create_empty_embed!() - |> Embed.put_title("Pong!") - |> Embed.put_description("Command took `#{diff}ms` to execute!") - end -end diff --git a/lib/cassian/consumer.ex b/lib/cassian/consumer.ex index 37e8eb5..d87028d 100644 --- a/lib/cassian/consumer.ex +++ b/lib/cassian/consumer.ex @@ -13,13 +13,13 @@ defmodule Cassian.Consumer do end @doc false - def handle_event({:MESSAGE_CREATE, message, _ws_state}) do - if !message.author.bot and is_cassian_command?(message), - do: Command.handle_message(message) + def handle_event({:INTERACTION_CREATE, interaction, _ws_state}) when is_nil(interaction.user.bot) do + Command.handle_interaction(interaction) end @doc false - def handle_event({:READY, _, _}) do + def handle_event({:READY, user_data, _}) do + Enum.each(user_data.guilds, &generate_commands/1) Nostrum.Api.update_status("", "music 🎶", 2) end @@ -48,4 +48,9 @@ defmodule Cassian.Consumer do |> String.downcase() |> String.starts_with?(Cassian.command_prefix!()) end + + defp generate_commands(%Nostrum.Struct.Guild.UnavailableGuild{id: guild_id}) do + Nostrum.Api.create_guild_application_command(guild_id, Cassian.Commands.Bot.Help.application_command_definition()) + end + end diff --git a/lib/cassian/consumers/command.ex b/lib/cassian/consumers/command.ex index 8842e2e..0c9630f 100644 --- a/lib/cassian/consumers/command.ex +++ b/lib/cassian/consumers/command.ex @@ -2,37 +2,26 @@ defmodule Cassian.Consumers.Command do @moduledoc """ Main consumer for the command event of the bot. Redirects it to other commands. """ + + alias Nostrum.Api @doc """ - Handle the mssage. A message has been filtered which is for the bot. - Dynamically find which module should be used for the command and continue on with that. + Handle the user interaction. """ - @spec handle_message(message :: Nostrum.Struct.Message) :: :ok | :noop - def handle_message(message) do - {command, args} = - message.content - |> String.trim_leading() - |> String.split(" ") - |> List.pop_at(0) - |> filter_command() - - case associated_module(command) do + @spec handle_interaction(interaction :: Nostrum.Struct.Interaction) :: :ok | :noop + def handle_interaction(interaction) do + case associated_module(interaction.data.name) do nil -> :noop module -> - module.execute(message, args) + interaction + |> module.execute() + |> (&Api.create_interaction_response(interaction, &1)).() :ok end end - # Filter the prefix from the command in the tuple. - @spec filter_command({command :: String.t(), args :: list(String.t())}) :: - {command :: String.t(), args :: list(String.t())} - defp filter_command({command, args}), - do: - {String.replace_leading(command, Cassian.command_prefix!(), "") |> String.downcase(), args} - defp associated_module(command) do alias Cassian.Commands.{Bot, Playback} @@ -40,9 +29,6 @@ defmodule Cassian.Consumers.Command do "help" -> Bot.Help - "ping" -> - Bot.Ping - "backward" -> Playback.Backward From 53d9f28a44fa2b233e0c87ed9b8cc38a4a44d13d Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 20:41:31 +0100 Subject: [PATCH 09/33] Put old value back The old value was changed by accident during testing --- lib/cassian/servers/sound_cloud_token.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cassian/servers/sound_cloud_token.ex b/lib/cassian/servers/sound_cloud_token.ex index ace6f94..2fef298 100644 --- a/lib/cassian/servers/sound_cloud_token.ex +++ b/lib/cassian/servers/sound_cloud_token.ex @@ -7,7 +7,7 @@ defmodule Cassian.Servers.SoundCloudToken do require Logger # 15 minutes~ - @timeout 5_000 + @timeout 900_000 # API From 6d6ad7cb1f6c58b832bb2807a69f579d3d72f8e6 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 21:19:18 +0100 Subject: [PATCH 10/33] Disappearing help command Add the correct flags to make the command be disappearing. --- lib/cassian/behaviours/command.ex | 1 + lib/cassian/commands/bot/help.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/cassian/behaviours/command.ex b/lib/cassian/behaviours/command.ex index 4b6829b..7c40937 100644 --- a/lib/cassian/behaviours/command.ex +++ b/lib/cassian/behaviours/command.ex @@ -17,6 +17,7 @@ defmodule Cassian.Behaviours.Command do defmacro __using__(_) do quote do @behaviour Cassian.Behaviours.Command + import Bitwise end end end diff --git a/lib/cassian/commands/bot/help.ex b/lib/cassian/commands/bot/help.ex index 0f3caa0..d41e698 100644 --- a/lib/cassian/commands/bot/help.ex +++ b/lib/cassian/commands/bot/help.ex @@ -18,7 +18,7 @@ defmodule Cassian.Commands.Bot.Help do def execute(interaction) do generate_help_embed!() - %{type: 4, data: %{embeds: [generate_help_embed!()]}} + %{type: 4, data: %{embeds: [generate_help_embed!()], flags: 1 <<< 6}} end import Cassian.Utils.Embed From 9befcf55b86ac97a79a6446acdc5df7f6d7f0d35 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Wed, 10 Jan 2024 21:43:58 +0100 Subject: [PATCH 11/33] Working forward and backward commands Working commands for forward and backward. --- lib/cassian/behaviours/command.ex | 1 + lib/cassian/commands/playback/backward.ex | 13 ++++++-- lib/cassian/commands/playback/forward.ex | 13 ++++++-- lib/cassian/consumer.ex | 7 +--- lib/cassian/consumers/command.ex | 12 +++++++ lib/cassian/managers/play_manager.ex | 40 ++++++++++++++--------- 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/lib/cassian/behaviours/command.ex b/lib/cassian/behaviours/command.ex index 7c40937..f19c576 100644 --- a/lib/cassian/behaviours/command.ex +++ b/lib/cassian/behaviours/command.ex @@ -18,6 +18,7 @@ defmodule Cassian.Behaviours.Command do quote do @behaviour Cassian.Behaviours.Command import Bitwise + alias Cassian.Utils.Embed, as: EmbedUtils end end end diff --git a/lib/cassian/commands/playback/backward.ex b/lib/cassian/commands/playback/backward.ex index cf64611..143823c 100644 --- a/lib/cassian/commands/playback/backward.ex +++ b/lib/cassian/commands/playback/backward.ex @@ -6,8 +6,17 @@ defmodule Cassian.Commands.Playback.Backward do @doc """ Play the playlist backwards. """ + + def application_command_definition() do + %{ + name: "backward", + description: "Play the playlist backwards." + } + end - def execute(message, _args) do - PlayManager.change_direction_with_notification(message, true) + def execute(interaction) do + {embed, flags} = PlayManager.change_direction_with_notification(interaction, true) + + %{type: 4, data: %{embeds: [embed], flags: flags}} end end diff --git a/lib/cassian/commands/playback/forward.ex b/lib/cassian/commands/playback/forward.ex index 0cff4d3..9ace1b5 100644 --- a/lib/cassian/commands/playback/forward.ex +++ b/lib/cassian/commands/playback/forward.ex @@ -6,8 +6,17 @@ defmodule Cassian.Commands.Playback.Forward do @doc """ Play the playlist forward. """ + + def application_command_definition() do + %{ + name: "forward", + description: "Play the playlist forward." + } + end - def execute(message, _args) do - PlayManager.change_direction_with_notification(message, false) + def execute(interaction) do + {embed, flags} = PlayManager.change_direction_with_notification(interaction, false) + + %{type: 4, data: %{embeds: [embed], flags: flags}} end end diff --git a/lib/cassian/consumer.ex b/lib/cassian/consumer.ex index d87028d..867e88d 100644 --- a/lib/cassian/consumer.ex +++ b/lib/cassian/consumer.ex @@ -19,7 +19,7 @@ defmodule Cassian.Consumer do @doc false def handle_event({:READY, user_data, _}) do - Enum.each(user_data.guilds, &generate_commands/1) + Enum.each(user_data.guilds, &Command.generate_commands/1) Nostrum.Api.update_status("", "music 🎶", 2) end @@ -48,9 +48,4 @@ defmodule Cassian.Consumer do |> String.downcase() |> String.starts_with?(Cassian.command_prefix!()) end - - defp generate_commands(%Nostrum.Struct.Guild.UnavailableGuild{id: guild_id}) do - Nostrum.Api.create_guild_application_command(guild_id, Cassian.Commands.Bot.Help.application_command_definition()) - end - end diff --git a/lib/cassian/consumers/command.ex b/lib/cassian/consumers/command.ex index 0c9630f..2517816 100644 --- a/lib/cassian/consumers/command.ex +++ b/lib/cassian/consumers/command.ex @@ -4,6 +4,8 @@ defmodule Cassian.Consumers.Command do """ alias Nostrum.Api + + alias Cassian.Commands.{Bot, Playback} @doc """ Handle the user interaction. @@ -21,6 +23,16 @@ defmodule Cassian.Consumers.Command do :ok end end + + @doc """ + Generate the Discord interaction commands for each guild. + """ + @spec generate_commands(Nostrum.Struct.Guild.UnavailableGuild.t()) :: :ok + def generate_commands(%Nostrum.Struct.Guild.UnavailableGuild{id: guild_id}) do + Nostrum.Api.create_guild_application_command(guild_id, Bot.Help.application_command_definition()) + Nostrum.Api.create_guild_application_command(guild_id, Playback.Backward.application_command_definition()) + Nostrum.Api.create_guild_application_command(guild_id, Playback.Forward.application_command_definition()) + end defp associated_module(command) do alias Cassian.Commands.{Bot, Playback} diff --git a/lib/cassian/managers/play_manager.ex b/lib/cassian/managers/play_manager.ex index 9a3c885..49d794d 100644 --- a/lib/cassian/managers/play_manager.ex +++ b/lib/cassian/managers/play_manager.ex @@ -8,6 +8,8 @@ defmodule Cassian.Managers.PlayManager do alias Cassian.Utils.Embed, as: EmbedUtils alias Cassian.Managers.MessageManager alias Nostrum.Struct.{Message, Embed} + + import Bitwise @doc """ Add a song to the playlist. @@ -198,24 +200,30 @@ defmodule Cassian.Managers.PlayManager do @doc """ Chaange the direction of the playlist and send a notification. """ - @spec change_direction_with_notification(message :: %Message{}, reverse :: boolean()) :: - :ok | :noop - def change_direction_with_notification(message, reverse) do + @spec change_direction_with_notification(interaction :: Interaction.t(), reverse :: boolean()) :: + {embed :: Embed.t(), flags :: integer()} + def change_direction_with_notification(interaction, reverse) do title_part = if reverse, do: "in reverse", else: "normally" - case change_direction(message.guild_id, reverse) do - :ok -> - EmbedUtils.create_empty_embed!() - |> Embed.put_title("Playing #{title_part}.") - |> Embed.put_description("The current playlist will play #{title_part}.") - - :noop -> - EmbedUtils.generate_error_embed( - "There is no playlist.", - "I can't change the direction of the playlist if it doesn't exist." - ) - end - |> MessageManager.send_dissapearing_embed(message.channel_id) + {embed, flags} = + case change_direction(interaction.guild_id, true) do + :ok -> + { + EmbedUtils.create_empty_embed!() + |> Embed.put_title("Playing in reverse.") + |> Embed.put_description("The current playlist will play #{title_part}."), + 0 + } + + :noop -> + { + EmbedUtils.generate_error_embed( + "There is no playlist.", + "I can't change the direction of the playlist if it doesn't exist." + ), + 1 <<< 6 + } + end end @doc """ From 0182e5213bd21492eb6893ae997bca8ecb1d1808 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 00:01:20 +0100 Subject: [PATCH 12/33] Change nostrum version to git Discord messed something up so I need to use the git version of Nostrum for it to work. --- mix.exs | 2 +- mix.lock | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 04d78ed..7201200 100644 --- a/mix.exs +++ b/mix.exs @@ -22,7 +22,7 @@ defmodule Cassian.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:nostrum, "~> 0.8.0"}, + {:nostrum, github: "Kraigie/nostrum"}, {:poison, "~> 5.0"}, {:httpoison, "~> 2.0"}, {:cowlib, "~> 2.9.1", override: true}, diff --git a/mix.lock b/mix.lock index 15dd46e..4b2b25c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "castle": {:hex, :castle, "0.3.0", "47b1a550b2348a6d7e60e43ded1df19dca601ed21ef6f267c3dbb1b3a301fbf5", [:mix], [{:forecastle, "~> 0.1.0", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "dbdc1c171520c4591101938a3d342dec70d36b7f5b102a5c138098581e35fcef"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, @@ -8,6 +9,7 @@ "ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"}, "equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"}, "floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"}, + "forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"}, "gen_stage": {:hex, :gen_stage, "1.1.0", "dd0c0f8d2f3b993fdbd3d58e94abbe65380f4e78bdee3fa93d5618d7d14abe60", [:mix], [], "hexpm", "7f2b36a6d02f7ef2ba410733b540ec423af65ec9c99f3d1083da508aca3b9305"}, "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, @@ -19,7 +21,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nostrum": {:hex, :nostrum, "0.8.0", "36f5a08e99c3df3020523be9e1c650ad926a63becc5318562abfe782d586e078", [:mix], [{:certifi, "~> 2.8", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "ce6861391ff346089d32a243fa71c0cb8bff79ab86ad53e8bf72808267899aee"}, + "nostrum": {:git, "https://github.com/Kraigie/nostrum.git", "d2daf4941927bc4452a4e79acbef4a574ce32f57", []}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, From 1dc9f3874ba71ceb48e8dd45d0d715dbc386402b Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 00:42:05 +0100 Subject: [PATCH 13/33] Barely functioning play command --- lib/cassian/commands/playback/play.ex | 115 +++++++++++++++++--------- lib/cassian/consumers/command.ex | 1 + lib/cassian/managers/play_manager.ex | 28 +++++-- lib/cassian/utils/voice.ex | 26 ++---- 4 files changed, 103 insertions(+), 67 deletions(-) diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index ded71de..3f84f53 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -1,37 +1,68 @@ defmodule Cassian.Commands.Playback.Play do + alias Nostrum.Struct.Embed use Cassian.Behaviours.Command import Cassian.Utils - alias Cassian.Utils.Embed, as: EmbedUtils alias Cassian.Utils.Voice, as: VoiceUtils alias Cassian.Managers.{MessageManager, PlayManager} # Main logic pipe - - def execute(message, args) do - handle_request(message, args) + + def application_command_definition() do + %{ + name: "play", + description: "Play a song or queue it.", + options: [ + %{ + type: 3, + name: "query", + required: true, + description: "Name of the song of URL for it." + + } + ] + } end - @doc """ - Handle the request. Continues with the rest of the logic pipe or - sends an embed message that the user isn't connected to channel. - """ - def handle_request(message, args) do - with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(message), - {:voice_connect, true} <- {:voice_connect, VoiceUtils.can_connect?(message.guild_id, voice_id)}, - {:ok, metadata} <- song_metadata(Enum.fetch!(args, 0)) do - - VoiceUtils.join_or_switch_voice(message.guild_id, voice_id) - PlayManager.insert!(message.guild_id, message.channel_id, metadata) - PlayManager.play_if_needed(message.guild_id) - MessageManager.disable_embed(message) - else - {:error, :not_in_voice} -> - no_channel_error(message) - {:voice_connect, false} -> - no_permissions_error(message) - {:error, :no_metadata} -> - invalid_link_error(message) + def execute(interaction) do + {embed, flags} = + with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(interaction), + {:ok, query} <- fetch_query(interaction.data.options), + {:ok, metadata} <- song_metadata(query), + :ok <- VoiceUtils.join_or_switch_voice(interaction.guild_id, voice_id), + :ok <- PlayManager.insert!(interaction.guild_id, interaction.channel_id, metadata), + {:ok, :will_play} <- PlayManager.play_if_needed(interaction.guild_id) do + { + EmbedUtils.create_empty_embed!() + |> Embed.put_title("Enqueued the song") + |> Embed.put_description("It'll start playing soon..."), + 1 <<< 6 + } + else + {:error, :not_in_voice} -> + no_channel_error(interaction) + {:voice_connect, false} -> + no_permissions_error(interaction) + {:error, :no_metadata} -> + invalid_link_error(interaction) + {:error, :wont_play} -> + no_permissions_error(interaction) + end + + %{type: 4, data: %{embeds: [embed], flags: flags}} + end + + defp fetch_query(options) do + option = + options + |> Enum.find(fn option -> String.equivalent?(option.name, "query") end) + + case option do + nil -> + {:error, :no_metadata} + + _ -> + {:ok, option.value} end end @@ -41,11 +72,13 @@ defmodule Cassian.Commands.Playback.Play do Generate and send the embed for when a user isn't in a voice channel. """ def no_channel_error(message) do - EmbedUtils.generate_error_embed( - "Hey you... You're not in a voice channel.", - "I can't play any music if you're not a voice channel. Join one first." - ) - |> MessageManager.send_dissapearing_embed(message.channel_id) + { + EmbedUtils.generate_error_embed( + "Hey you... You're not in a voice channel.", + "I can't play any music if you're not a voice channel. Join one first." + ), + 1 <<< 6 + } end @doc """ @@ -53,21 +86,25 @@ defmodule Cassian.Commands.Playback.Play do speak in a channel. """ def no_permissions_error(message) do - EmbedUtils.generate_error_embed( - "And how do you think that's possible?", - "I don't have the permissions to play music there... Fix it up first." - ) - |> MessageManager.send_dissapearing_embed(message.channel_id) + { + EmbedUtils.generate_error_embed( + "And how do you think that's possible?", + "I don't have the permissions to play music there... Fix it up first." + ), + 1 <<< 6 + } end @doc """ Tell the user that the link is not valid. """ def invalid_link_error(message) do - EmbedUtils.generate_error_embed( - "Yeah, that won't work.", - "The link you tried to provide me isn't working. Recheck it." - ) - |> MessageManager.send_dissapearing_embed(message.channel_id) + { + EmbedUtils.generate_error_embed( + "Yeah, that won't work.", + "The link you tried to provide me isn't working. Recheck it." + ), + 1 <<< 6 + } end end diff --git a/lib/cassian/consumers/command.ex b/lib/cassian/consumers/command.ex index 2517816..f566264 100644 --- a/lib/cassian/consumers/command.ex +++ b/lib/cassian/consumers/command.ex @@ -32,6 +32,7 @@ defmodule Cassian.Consumers.Command do Nostrum.Api.create_guild_application_command(guild_id, Bot.Help.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Backward.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Forward.application_command_definition()) + Nostrum.Api.create_guild_application_command(guild_id, Playback.Play.application_command_definition()) |> IO.inspect() end defp associated_module(command) do diff --git a/lib/cassian/managers/play_manager.ex b/lib/cassian/managers/play_manager.ex index 49d794d..3ccaa80 100644 --- a/lib/cassian/managers/play_manager.ex +++ b/lib/cassian/managers/play_manager.ex @@ -75,6 +75,8 @@ defmodule Cassian.Managers.PlayManager do """ def play_if_needed(guild_id) do state = VoiceState.get!(guild_id) + + playing_state = false if state.status == :noop and Playlist.exists?(guild_id) do case Playlist.show(guild_id) do @@ -96,20 +98,30 @@ defmodule Cassian.Managers.PlayManager do index = keep_in_bounds(index, ordered) metadata = Enum.at(ordered, index) - - Voice.play_when_ready!(metadata, guild_id) - - notifiy_playing(state.channel_id, metadata) - - state - |> Map.put(:status, :playing) - |> VoiceState.put() + + IO.inspect("I mean I got here, no?") + + case Voice.play_when_ready(metadata, guild_id, 20) do + {:ok, data} -> + notifiy_playing(state.channel_id, metadata) + + state + |> Map.put(:status, :playing) + |> VoiceState.put() + + playing_state = true + + _ -> + nil + end end _ -> nil end end + + if playing_state, do: {:ok, :will_play}, else: {:error, :wont_play} end @doc """ diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 15d664c..79f0c56 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -52,7 +52,7 @@ defmodule Cassian.Utils.Voice do end def play_when_ready(_, _, _) do - {:ok, :failed_max} + {:error, :failed_max} end defp stream_url!(metadata) do @@ -71,35 +71,21 @@ defmodule Cassian.Utils.Voice do end end - @doc """ - Play the music without a max retry. This in theory can infinitely loop if the bot is never ready to play music. - - See `play_when_ready/3` as a safe way to play this. - """ - def play_when_ready!(metadata, guild_id) do - if Nostrum.Voice.ready?(guild_id) do - Nostrum.Voice.play(guild_id, stream_url!(metadata), metadata.stream_method) - else - :timer.sleep(100) - play_when_ready!(metadata, guild_id) - end - end - @doc """ Safely the current voice id in which the user is. Also returns the guild id. """ - @spec sender_voice_id(message :: Nostrum.Struct.Channel) :: + @spec sender_voice_id(interaction :: Nostrum.Struct.Interaction.t()) :: {:ok, {guild_id :: String.t(), channel_id :: String.t()}} | {:error, :not_in_voice} - def sender_voice_id(message) do + def sender_voice_id(interaction) do voice_id = - GuildCache.get!(message.guild_id) + GuildCache.get!(interaction.guild_id) |> Map.fetch!(:voice_states) - |> Enum.filter(fn state -> state.user_id == message.author.id end) + |> Enum.filter(fn state -> state.user_id == interaction.user.id end) |> List.first() |> extract_id() if voice_id do - {:ok, {message.guild_id, voice_id}} + {:ok, {interaction.guild_id, voice_id}} else {:error, :not_in_voice} end From 5df066b55d0ef3af3f16d3239f7291dccd04ae5b Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 14:19:56 +0100 Subject: [PATCH 14/33] Optimizations and syntax fixes Using better conditionals and such. --- lib/cassian/application.ex | 2 - lib/cassian/consumer.ex | 19 +--- lib/cassian/consumers/command.ex | 4 +- lib/cassian/managers/play_manager.ex | 4 +- lib/cassian/servers/sound_cloud_token.ex | 40 ++++---- lib/cassian/services/sound_cloud_service.ex | 100 ++++++++++++-------- lib/cassian/utils.ex | 22 ++--- lib/cassian/utils/embed.ex | 4 +- lib/cassian/utils/voice.ex | 13 ++- 9 files changed, 105 insertions(+), 103 deletions(-) diff --git a/lib/cassian/application.ex b/lib/cassian/application.ex index 3c238a1..0824a39 100644 --- a/lib/cassian/application.ex +++ b/lib/cassian/application.ex @@ -12,8 +12,6 @@ defmodule Cassian.Application do @doc false def add_children() do - alias Cassian.Consumer - children = [ Cassian.Consumer, diff --git a/lib/cassian/consumer.ex b/lib/cassian/consumer.ex index 867e88d..2bff41d 100644 --- a/lib/cassian/consumer.ex +++ b/lib/cassian/consumer.ex @@ -8,19 +8,17 @@ defmodule Cassian.Consumer do alias Cassian.Consumers.{Command, VoiceEvent} - def start_link do - Consumer.start_link(__MODULE__) - end - @doc false def handle_event({:INTERACTION_CREATE, interaction, _ws_state}) when is_nil(interaction.user.bot) do Command.handle_interaction(interaction) + :ok end @doc false def handle_event({:READY, user_data, _}) do Enum.each(user_data.guilds, &Command.generate_commands/1) - Nostrum.Api.update_status("", "music 🎶", 2) + Nostrum.Api.update_status(:online, "music 🎶", 2) + :ok end @doc false @@ -37,15 +35,4 @@ defmodule Cassian.Consumer do def handle_event(_) do :noop end - - @doc """ - Checks whether the command is for this bot. Returns a boolean. - """ - @spec is_cassian_command?(message :: Nostrum.Struct.Message) :: boolean() - def is_cassian_command?(message) do - message.content - |> String.trim_leading() - |> String.downcase() - |> String.starts_with?(Cassian.command_prefix!()) - end end diff --git a/lib/cassian/consumers/command.ex b/lib/cassian/consumers/command.ex index f566264..5b4bc53 100644 --- a/lib/cassian/consumers/command.ex +++ b/lib/cassian/consumers/command.ex @@ -10,7 +10,7 @@ defmodule Cassian.Consumers.Command do @doc """ Handle the user interaction. """ - @spec handle_interaction(interaction :: Nostrum.Struct.Interaction) :: :ok | :noop + @spec handle_interaction(interaction :: Nostrum.Struct.Interaction.t()) :: :ok | :noop def handle_interaction(interaction) do case associated_module(interaction.data.name) do nil -> @@ -32,7 +32,7 @@ defmodule Cassian.Consumers.Command do Nostrum.Api.create_guild_application_command(guild_id, Bot.Help.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Backward.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Forward.application_command_definition()) - Nostrum.Api.create_guild_application_command(guild_id, Playback.Play.application_command_definition()) |> IO.inspect() + Nostrum.Api.create_guild_application_command(guild_id, Playback.Play.application_command_definition()) end defp associated_module(command) do diff --git a/lib/cassian/managers/play_manager.ex b/lib/cassian/managers/play_manager.ex index 3ccaa80..d14bbc6 100644 --- a/lib/cassian/managers/play_manager.ex +++ b/lib/cassian/managers/play_manager.ex @@ -98,10 +98,8 @@ defmodule Cassian.Managers.PlayManager do index = keep_in_bounds(index, ordered) metadata = Enum.at(ordered, index) - - IO.inspect("I mean I got here, no?") - case Voice.play_when_ready(metadata, guild_id, 20) do + case Voice.play_when_ready(metadata, guild_id, 10) do {:ok, data} -> notifiy_playing(state.channel_id, metadata) diff --git a/lib/cassian/servers/sound_cloud_token.ex b/lib/cassian/servers/sound_cloud_token.ex index 2fef298..bb1fc21 100644 --- a/lib/cassian/servers/sound_cloud_token.ex +++ b/lib/cassian/servers/sound_cloud_token.ex @@ -42,8 +42,8 @@ defmodule Cassian.Servers.SoundCloudToken do do: {:ok, acquire_new_client_id(), @timeout} defp acquire_new_client_id() do - with {:ok, response = %HTTPoison.Response{status_code: 200}} <- HTTPoison.get("https://soundcloud.com/discover"), - {:ok, value} <- generate_new_client_id(response) do + with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get("https://soundcloud.com/discover"), + {:ok, value} <- generate_new_client_id(body) do Logger.debug("Acquired new soundcloud token.") value else @@ -53,25 +53,23 @@ defmodule Cassian.Servers.SoundCloudToken do end end - defp generate_new_client_id(response) do - data = - response - |> Map.get(:body) - |> Floki.parse_document!() - |> Floki.find("script[src][crossorigin]") - |> Stream.map(&filter_scripts/1) - |> Stream.reject(&is_nil/1) - |> Enum.reverse() # Generally client_id is near the end script from experience - |> Enum.find_value(fn script -> - with {:ok, response = %HTTPoison.Response{status_code: 200}} <- HTTPoison.get(script), - regex_code <- Regex.run(~r/client_id:\"(.*)\",env:/, response.body), - false <- is_nil(regex_code) do - {:ok, List.last(regex_code)} - else - _ -> - nil - end - end) + defp generate_new_client_id(body) do + body + |> Floki.parse_document!() + |> Floki.find("script[src][crossorigin]") + |> Stream.map(&filter_scripts/1) + |> Stream.reject(&is_nil/1) + |> Enum.reverse() # Generally client_id is near the end script from experience + |> Enum.find_value(fn script -> + with {:ok, response = %HTTPoison.Response{status_code: 200}} <- HTTPoison.get(script), + regex_code <- Regex.run(~r/client_id:\"(.*)\",env:/, response.body), + false <- is_nil(regex_code) do + {:ok, List.last(regex_code)} + else + _ -> + nil + end + end) end defp filter_scripts({"script", [_, {"src", script}], _}) do diff --git a/lib/cassian/services/sound_cloud_service.ex b/lib/cassian/services/sound_cloud_service.ex index 41604a1..a323ef6 100644 --- a/lib/cassian/services/sound_cloud_service.ex +++ b/lib/cassian/services/sound_cloud_service.ex @@ -4,6 +4,8 @@ defmodule Cassian.Services.SoundCloudService do """ alias Cassian.Structs.Metadata + + require Logger defdelegate client_id(), to: Cassian.Servers.SoundCloudToken @@ -24,25 +26,25 @@ defmodule Cassian.Services.SoundCloudService do format: :json } - case HTTPoison.get(link, headers, params: params) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - body = - body - |> Poison.decode!(keys: :atoms) - - body = %Metadata{ - title: body.title, - author: body.author_name, - provider: "soundcloud", - link: url, - color: "ff9033", - thumbnail_url: body.thumbnail_url, - stream_link: nil, - stream_method: :url - } - - {:ok, body} - + with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(link, headers, params: params), + {:ok, body} <- Poison.decode(body, %{keys: :atoms}) do + metadata = %Metadata{ + title: body.title, + author: body.author_name, + provider: "soundcloud", + link: url, + color: "ff9033", + thumbnail_url: body.thumbnail_url, + stream_link: nil, + stream_method: :url + } + + {:ok, metadata} + + else + {_, %HTTPoison.Response{status_code: code}} -> + {:error, code} + {_, %HTTPoison.Response{status_code: code}} -> {:error, code} end @@ -53,6 +55,7 @@ defmodule Cassian.Services.SoundCloudService do """ @spec stream_from_url(url :: String.t()) :: {:ok, String.t()} | {:error, any()} def stream_from_url(url) do + Logger.debug("Stream from url called with: #{inspect(url)}") case acquire_track_id(url) do {:ok, track_id} -> case get_progressive_link(track_id) do @@ -75,23 +78,26 @@ defmodule Cassian.Services.SoundCloudService do def acquire_track_id(url) do headers = [ # I honestly have no clue what is the content type... - {"Content-Type", "*/*"} + {"Content-Type", "application/json"} ] params = %{ url: url, format: "json" } - - case HTTPoison.get("https://soundcloud.com/oembed", headers, - params: params, - follow_redirect: true - ) do + + response = + HTTPoison.get("https://soundcloud.com/oembed", headers, + params: params, + follow_redirect: true + ) + + case response do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> { :ok, # Woohoo Regex magic - Regex.run(~r/(?<=tracks%2F)(.*)(?=&show_artwork)/, body) |> List.first() + Regex.run(~r/tracks%2F(.*)&show_artwork/, body) |> List.last() } _ -> @@ -108,21 +114,29 @@ defmodule Cassian.Services.SoundCloudService do } # Some information is here - case HTTPoison.get(url, %{}, params: params) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - # Doesn't look safe but as long as SoundCloud IS giving an OK response the JSON is okay... - url = - Poison.decode!(body, %{keys: :atoms}).media.transcodings - |> Enum.filter(fn transcoding -> transcoding.format.protocol == "progressive" end) - |> List.first() - |> Map.get(:url) - + with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, %{}, params: params), + {:ok, decoded} <- Poison.decode(body, %{keys: :atoms}), + {:ok, url} <- acquire_link_from_body(decoded) do {:ok, url} - + else _ -> {:error, nil} end end + + defp acquire_link_from_body(body) do + transcoding = + body.media.transcodings + |> Enum.find(fn transcoding -> transcoding.format.protocol == "progressive" end) + |> Map.get(:url) + + case transcoding do + nil -> + {:error, nil} + value -> + {:ok, value} + end + end @doc """ Get the stream URL from the transcoded-progressive url. @@ -133,13 +147,15 @@ defmodule Cassian.Services.SoundCloudService do params = %{ client_id: client_id() } - - case HTTPoison.get(progressive_transcoding_url, %{}, params: params) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - {:ok, body |> Poison.decode!(keys: :atoms) |> Map.get(:url)} - + + with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(progressive_transcoding_url, %{}, params: params), + {:ok, decoded} <- Poison.decode(body, %{keys: :atoms}), + value <- Map.get(decoded, :url), + false <- is_nil(value) do + {:ok, value} + else _ -> - {:error, nil} + {:error, nil} end end end diff --git a/lib/cassian/utils.ex b/lib/cassian/utils.ex index 6238250..0f53abf 100644 --- a/lib/cassian/utils.ex +++ b/lib/cassian/utils.ex @@ -14,23 +14,21 @@ defmodule Cassian.Utils do end @doc """ - Check whether a link is a YouTube one. + Get the song metadata if it's from a valid provider """ @spec song_metadata(link :: String.t()) :: {:ok, metadata :: Hash} | {:error, :no_metadata} def song_metadata(link) do - - case YoutubeService.oembed_song_data(link) do - {:ok, metadata} -> - {:ok, metadata} + [YoutubeService, SoundCloudService] + |> Enum.find_value({:error, :no_metadata}, &check_for_data(&1, link)) + end + + defp check_for_data(module, link) do + case module.oembed_song_data(link) do + {:ok, data} -> + {:ok, data} _ -> - case SoundCloudService.oembed_song_data(link) do - {:ok, metadata} -> - {:ok, metadata} - - _ -> - {:error, :no_metadata} - end + nil end end end diff --git a/lib/cassian/utils/embed.ex b/lib/cassian/utils/embed.ex index aa38ad6..1c4a59b 100644 --- a/lib/cassian/utils/embed.ex +++ b/lib/cassian/utils/embed.ex @@ -7,7 +7,7 @@ defmodule Cassian.Utils.Embed do Add a color on an embed. The `color` params ia a hex string value of the color. It will be automaically converted to something Discord can use. """ - @spec put_color_on_embed(embed :: Embed, color :: String.t()) :: Embed + @spec put_color_on_embed(embed :: Embed.t(), color :: String.t()) :: Embed.t() def put_color_on_embed(embed, color \\ "#6996ff") do {color, _} = color @@ -24,7 +24,7 @@ defmodule Cassian.Utils.Embed do @doc """ Create an empty embed. It has the default color of the bot. """ - @spec create_empty_embed!() :: Embed + @spec create_empty_embed!() :: Embed.t() def create_empty_embed!() do %Nostrum.Struct.Embed{} |> put_color_on_embed() diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 79f0c56..ee97350 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -3,6 +3,8 @@ defmodule Cassian.Utils.Voice do alias Cassian.Structs.VoicePermissions alias Nostrum.Cache.GuildCache alias Cassian.Structs.Metadata + + require Logger @doc """ Join or switch from the voice channel. Set the channel to nil to @@ -37,16 +39,21 @@ defmodule Cassian.Utils.Voice do @doc """ Play the music with a max retry amount. If the retry amount is less than zero it will just fail automatically. - Every retry approx lasts for approx. `100ms`. + Every retry approx lasts for approx one second. """ @spec play_when_ready(metadata :: %Metadata{}, guild_id :: Snowflake.t(), max_retries :: integer()) :: {:ok, :ok | any()} | {:error, :failed_max} def play_when_ready(metadata, guild_id, max_retries) when is_integer(max_retries) and max_retries > 0 do + + Logger.debug("Trying to play song from metadata: #{inspect(metadata)} in guild id: #{guild_id}. Remaining retries: #{max_retries}.") + if Nostrum.Voice.ready?(guild_id) do - {:ok, Nostrum.Voice.play(guild_id, stream_url!(metadata), metadata.stream_method)} + stream_source = stream_url!(metadata) + Logger.debug("Voice is ready. Streaming audiosource: #{inspect(stream_source)}") + {:ok, Nostrum.Voice.play(guild_id, stream_source, metadata.stream_method)} else - :timer.sleep(100) + :timer.sleep(1000) play_when_ready(metadata, guild_id, max_retries - 1) end end From 60027b1932eb0bfebf0b4e83cb9e65d69eebcd2a Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 14:23:41 +0100 Subject: [PATCH 15/33] Better error handling Better error handling for poision and httpoison --- lib/cassian/services/sound_cloud_service.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cassian/services/sound_cloud_service.ex b/lib/cassian/services/sound_cloud_service.ex index a323ef6..d8e7651 100644 --- a/lib/cassian/services/sound_cloud_service.ex +++ b/lib/cassian/services/sound_cloud_service.ex @@ -45,8 +45,11 @@ defmodule Cassian.Services.SoundCloudService do {_, %HTTPoison.Response{status_code: code}} -> {:error, code} - {_, %HTTPoison.Response{status_code: code}} -> - {:error, code} + {_, %HTTPoison.Error{}} -> + {:error, -1} + + {_, _} -> + {:error, -2} end end From b61cad5b9dd14629a085d8327a8fc43a788b31af Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 20:54:07 +0100 Subject: [PATCH 16/33] Add OCI support Add support for OCI. I have no idea why `podman-compose` doesn't work, for some reason it goes straight into `/bin/sh` instead of the defined `CMD`. --- Dockerfile | 78 ++++++++++++++++++++++++++++++++++++++++++++++ config/runtime.exs | 12 +++++++ docker-compose.yml | 7 +++++ 3 files changed, 97 insertions(+) create mode 100644 Dockerfile create mode 100644 config/runtime.exs create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aa2969a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +# Stage to build the correct version of dependecies: + +# * Elixir +# * Erlang + +FROM alpine:3.19.0 AS builder + +RUN apk update && \ + apk upgrade && \ + apk add curl git unzip \ + gcc autoconf automake ncurses-dev \ + bash make g++ openssl-dev + +RUN adduser -gD cassian --disabled-password + +# Building Erlang/Elixir + +WORKDIR /build + +COPY .tool-versions . + +RUN chown -R cassian:cassian /build + +USER cassian + +RUN git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.13.1; + +RUN /bin/bash -c 'echo -e "\n\n## Configure ASDF \n. $HOME/.asdf/asdf.sh" >> ~/.bashrc' + +RUN /bin/bash -c 'echo -e "\n\n## ASDF Bash Completion: \n. $HOME/.asdf/completions/asdf.bash" >> ~/.bashrc'; + +ENV PATH="$PATH:/home/cassian/.asdf/bin:/home/cassian/.asdf/shims" + +RUN asdf plugin add elixir + +RUN asdf plugin add erlang + +RUN asdf install + +# Building the application + +RUN mix local.hex --force + +COPY mix.exs . + +COPY mix.lock . + +COPY config ./config + +COPY lib ./lib + +COPY .formatter.exs . + +RUN mix deps.get + +RUN mix deps.compile + +RUN mix release + +# Actual app stage + +FROM alpine:3.19.0 AS cassian-app + +RUN adduser -D cassian --disabled-password + +RUN apk update +# +RUN apk upgrade +# +RUN apk add ffmpeg youtube-dl + +USER cassian + +WORKDIR /app + +COPY --from=builder /build/_build/dev/rel/cassian /app/cassian + +CMD ["/app/cassian/bin/cassian", "start"] \ No newline at end of file diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..1027e37 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,12 @@ +import Config + +config :nostrum, + ffmpeg: "/usr/bin/ffmpeg", + youtubedl: "/usr/bin/youtube-dl", + token: System.fetch_env!("DISCORD_BOT_TOKEN"), + num_shards: :auto + +config :cassian, + web_enabled: System.fetch_env!("WEB_ENABLED"), + port: "4000", + force_ssl: System.fetch_env!("FORCE_SSL") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..29fd6f9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.0' + +services: + bot: + build: + context: . + dockerfile: Dockerfile From 80071d4c105f63009dedfc24950cc32cf823b454 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 21:01:18 +0100 Subject: [PATCH 17/33] Remove accidental comments --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index aa2969a..1ab0937 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,9 +64,9 @@ FROM alpine:3.19.0 AS cassian-app RUN adduser -D cassian --disabled-password RUN apk update -# + RUN apk upgrade -# + RUN apk add ffmpeg youtube-dl USER cassian From 8abf2cf8cccaa337b4e465973ea3036dd0e06325 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 21:05:20 +0100 Subject: [PATCH 18/33] Update the docker-compose.yml file Add environment configuration for the docker-compose.yml file. --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 29fd6f9..9619cc7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,8 @@ services: build: context: . dockerfile: Dockerfile + environment: + DISCORD_BOT_TOKEN: $DISCORD_BOT_TOKEN + WEB_ENABLED: false + FORCE_SSL: false + From 692263147367fa62aee6ec1cd42bd8c62e2bb785 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Thu, 11 Jan 2024 21:06:20 +0100 Subject: [PATCH 19/33] Add name for container Add container name to the default compose file. --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 9619cc7..ee2c577 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: build: context: . dockerfile: Dockerfile + name: cassian-bot environment: DISCORD_BOT_TOKEN: $DISCORD_BOT_TOKEN WEB_ENABLED: false From e8219dad750b0bf9159271d914fe03c1e1308aa1 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 00:16:52 +0100 Subject: [PATCH 20/33] Fix specs --- lib/cassian/behaviours/command.ex | 4 ++-- lib/cassian/commands/playback/play.ex | 26 ++++++++++++-------------- lib/cassian/managers/play_manager.ex | 2 ++ lib/cassian/utils.ex | 3 ++- lib/cassian/utils/voice.ex | 5 +++-- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/cassian/behaviours/command.ex b/lib/cassian/behaviours/command.ex index f19c576..0f97e7f 100644 --- a/lib/cassian/behaviours/command.ex +++ b/lib/cassian/behaviours/command.ex @@ -7,12 +7,12 @@ defmodule Cassian.Behaviours.Command do @doc """ The function which is called that does all of the stuff needed in the command. """ - @callback execute(interaction :: Nostrum.Struct.Interaction.t()) :: :ok + @callback execute(interaction :: Nostrum.Struct.Interaction.t()) :: map() @doc """ The definition as a Discord application command. """ - @callback application_command_definition() :: %{} + @callback application_command_definition() :: map() defmacro __using__(_) do quote do diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index 3f84f53..1e4867b 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -4,7 +4,7 @@ defmodule Cassian.Commands.Playback.Play do import Cassian.Utils alias Cassian.Utils.Voice, as: VoiceUtils - alias Cassian.Managers.{MessageManager, PlayManager} + alias Cassian.Managers.PlayManager # Main logic pipe @@ -27,11 +27,11 @@ defmodule Cassian.Commands.Playback.Play do def execute(interaction) do {embed, flags} = with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(interaction), - {:ok, query} <- fetch_query(interaction.data.options), - {:ok, metadata} <- song_metadata(query), - :ok <- VoiceUtils.join_or_switch_voice(interaction.guild_id, voice_id), - :ok <- PlayManager.insert!(interaction.guild_id, interaction.channel_id, metadata), - {:ok, :will_play} <- PlayManager.play_if_needed(interaction.guild_id) do + {:ok, query} <- fetch_query(interaction.data.options), + {:ok, metadata} <- song_metadata(query), + :ok <- VoiceUtils.join_or_switch_voice(interaction.guild_id, voice_id), # just to continue with the flow + :ok <- PlayManager.insert!(interaction.guild_id, interaction.channel_id, metadata), + {:ok, :will_play} <- PlayManager.play_if_needed(interaction.guild_id) do { EmbedUtils.create_empty_embed!() |> Embed.put_title("Enqueued the song") @@ -40,13 +40,11 @@ defmodule Cassian.Commands.Playback.Play do } else {:error, :not_in_voice} -> - no_channel_error(interaction) - {:voice_connect, false} -> - no_permissions_error(interaction) + no_channel_error() {:error, :no_metadata} -> - invalid_link_error(interaction) + invalid_link_error() {:error, :wont_play} -> - no_permissions_error(interaction) + no_permissions_error() end %{type: 4, data: %{embeds: [embed], flags: flags}} @@ -71,7 +69,7 @@ defmodule Cassian.Commands.Playback.Play do @doc """ Generate and send the embed for when a user isn't in a voice channel. """ - def no_channel_error(message) do + def no_channel_error() do { EmbedUtils.generate_error_embed( "Hey you... You're not in a voice channel.", @@ -85,7 +83,7 @@ defmodule Cassian.Commands.Playback.Play do Generate and send the embed for when the bot doesn't have permissions to view, connect or speak in a channel. """ - def no_permissions_error(message) do + def no_permissions_error() do { EmbedUtils.generate_error_embed( "And how do you think that's possible?", @@ -98,7 +96,7 @@ defmodule Cassian.Commands.Playback.Play do @doc """ Tell the user that the link is not valid. """ - def invalid_link_error(message) do + def invalid_link_error() do { EmbedUtils.generate_error_embed( "Yeah, that won't work.", diff --git a/lib/cassian/managers/play_manager.ex b/lib/cassian/managers/play_manager.ex index d14bbc6..04a1dbd 100644 --- a/lib/cassian/managers/play_manager.ex +++ b/lib/cassian/managers/play_manager.ex @@ -3,6 +3,7 @@ defmodule Cassian.Managers.PlayManager do Manager for queues. """ + alias Nostrum.Snowflake alias Cassian.Structs.{VoiceState, Playlist} alias Cassian.Utils.Voice alias Cassian.Utils.Embed, as: EmbedUtils @@ -14,6 +15,7 @@ defmodule Cassian.Managers.PlayManager do @doc """ Add a song to the playlist. """ + @spec insert!(guild_id :: Snowflake.t(), channel_id :: Snowflake.t(), metadata :: Metadata.t()) :: :ok def insert!(guild_id, channel_id, metadata) do Playlist.insert!(guild_id, metadata) diff --git a/lib/cassian/utils.ex b/lib/cassian/utils.ex index 0f53abf..6154d66 100644 --- a/lib/cassian/utils.ex +++ b/lib/cassian/utils.ex @@ -3,6 +3,7 @@ defmodule Cassian.Utils do Module for general utils... """ + alias Cassian.Structs.Metadata alias Cassian.Services.{YoutubeService, SoundCloudService} @doc """ @@ -16,7 +17,7 @@ defmodule Cassian.Utils do @doc """ Get the song metadata if it's from a valid provider """ - @spec song_metadata(link :: String.t()) :: {:ok, metadata :: Hash} | {:error, :no_metadata} + @spec song_metadata(link :: String.t()) :: {:ok, metadata :: Metadata.t()} | {:error, :no_metadata} def song_metadata(link) do [YoutubeService, SoundCloudService] |> Enum.find_value({:error, :no_metadata}, &check_for_data(&1, link)) diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index ee97350..63fb447 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -1,6 +1,6 @@ defmodule Cassian.Utils.Voice do + alias Nostrum.Snowflake alias Nostrum.Api - alias Cassian.Structs.VoicePermissions alias Nostrum.Cache.GuildCache alias Cassian.Structs.Metadata @@ -14,6 +14,7 @@ defmodule Cassian.Utils.Voice do def join_or_switch_voice(guild_id, channel_id) do guild_id |> Api.update_voice_state(channel_id, false, true) + :ok end @doc """ @@ -82,7 +83,7 @@ defmodule Cassian.Utils.Voice do Safely the current voice id in which the user is. Also returns the guild id. """ @spec sender_voice_id(interaction :: Nostrum.Struct.Interaction.t()) :: - {:ok, {guild_id :: String.t(), channel_id :: String.t()}} | {:error, :not_in_voice} + {:ok, {guild_id :: Snowflake.t(), channel_id :: Snowflake.t()}} | {:error, :not_in_voice} def sender_voice_id(interaction) do voice_id = GuildCache.get!(interaction.guild_id) From c2e23c69535e21a2c203bf663158dbfb50b9b452 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 00:17:36 +0100 Subject: [PATCH 21/33] Fully remove permission checks --- lib/cassian/structs/voice_permissions.ex | 77 ---------------- lib/cassian/utils/permissions.ex | 109 ----------------------- lib/cassian/utils/voice.ex | 10 --- 3 files changed, 196 deletions(-) delete mode 100644 lib/cassian/structs/voice_permissions.ex delete mode 100644 lib/cassian/utils/permissions.ex diff --git a/lib/cassian/structs/voice_permissions.ex b/lib/cassian/structs/voice_permissions.ex deleted file mode 100644 index 57980b5..0000000 --- a/lib/cassian/structs/voice_permissions.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Cassian.Structs.VoicePermissions do - @moduledoc """ - Struct for the permissions of the bot. Generally this should be - used per channel... - """ - - defstruct [ - :administrator, - :view_channel, - :connect, - :speak - ] - - @type t() :: %__MODULE__{ - administrator: boolean(), - view_channel: boolean(), - connect: boolean(), - speak: boolean() - } - - @typedoc "Permission whether the bot is an admin." - @type administrator :: boolean() - - @typedoc "Permission whether the bot can view the channel." - @type view_channel :: boolean() - - @typedoc "Permission whether the bot can connect to the channel." - @type connect :: boolean() - - @typedoc "Permission whether the bot can speak in the channel" - @type speak :: boolean() - - alias Cassian.Utils.Permissions, as: Util - - @doc """ - Generate the struct from the perm number. - """ - @spec from_number(value :: integer()) :: __MODULE__.t() - def from_number(value) do - %__MODULE__{ - administrator: Util.admin?(value), - view_channel: Util.view_channel?(value), - connect: Util.connect?(value), - speak: Util.speak?(value) - } - end - - @doc """ - Get the base server permissions. - """ - @spec base_server_permissions(guild_id :: Snowflake.t()) :: __MODULE__.t() - def base_server_permissions(guild_id) do - Util.server_permissions(guild_id) - |> from_number() - end - - @doc """ - Get the permission for a channel in a guild. - """ - @spec channel_permission( - user :: Nostrum.Struct.User, - guild_id :: Snowflake.t(), - channel_id :: Snowflake.t() - ) :: __MODULE__.t() - def channel_permission(user, guild_id, channel_id) do - Util.channel_permissions(user, guild_id, channel_id) - |> from_number() - end - - @doc """ - Get the permission for a channel in a guild for this bot. - """ - @spec my_channel_permissions(guild_id :: Snowflake.t(), channel_id :: Snowflake.t()) :: - __MODULE__.t() - def my_channel_permissions(guild_id, channel_id), - do: channel_permission(Nostrum.Cache.Me.get(), guild_id, channel_id) -end diff --git a/lib/cassian/utils/permissions.ex b/lib/cassian/utils/permissions.ex deleted file mode 100644 index 6eaf48c..0000000 --- a/lib/cassian/utils/permissions.ex +++ /dev/null @@ -1,109 +0,0 @@ -defmodule Cassian.Utils.Permissions do - alias Nostrum.Cache.{GuildCache, ChannelCache} - use Bitwise - - @moduledoc """ - Util module for building permissions for the bot. - """ - - @doc """ - Get the channel permission for the bot. Doesn't give a struct but rather a flag-number - which can be later used to extract the permissions bits from (is a bit really because it's hexadecimal but okay) - """ - @spec channel_permissions( - user :: Nostrum.Struct.User, - guild_id :: Snowflake.t(), - channel_id :: Snowflake.t() - ) :: integer() - def channel_permissions(user, guild_id, channel_id) do - user_id = user.id - roles = GuildCache.get!(guild_id).members[user_id].roles ++ [guild_role(guild_id).id] - - overs = - ChannelCache.get!(channel_id) - |> Map.get(:permission_overwrites) - |> Enum.filter(&connected?(&1, roles, user_id)) - |> Enum.reduce(%{allows: [], denies: []}, &accumulate_overs/2) - - perms = - Enum.reduce(overs.denies, server_permissions(guild_id), fn deny, perms -> - perms &&& ~~~deny - end) - - Enum.reduce(overs.allows, perms, fn allow, perms -> perms ||| allow end) - end - - defp accumulate_overs(over, acc) do - %{allows: acc.allows ++ [over.allow], denies: acc.denies ++ [over.deny]} - end - - defp connected?(overwrite, roles, user_id) do - case overwrite.type do - :role -> - overwrite.id in roles - - :member -> - overwrite.id == user_id - end - end - - @doc """ - Get the guild role based upon the name. - """ - @spec guild_role(guild_id :: Snowflake.t(), name :: String.t()) :: Nostrum.Struct.Guild.Role.t() - def guild_role(guild_id, name \\ "@everyone") do - GuildCache.get!(guild_id).roles - |> Enum.reduce([], fn {_, role}, acc -> acc ++ [role] end) - |> Enum.filter(fn role -> role.name == name end) - |> List.first() - end - - @doc """ - Get the permissoin of the server for the role name - """ - @spec server_permissions(guild_id :: Snowflake.t(), name :: String.t()) :: integer() - def server_permissions(guild_id, name \\ "@everyone") do - guild_role(guild_id, name) - |> Map.get(:permissions) - end - - @doc """ - Check whether the integer has a permission. - """ - @spec has_perm(value :: integer(), perm :: integer()) :: boolean() - def has_perm(value, perm) do - (value &&& perm) == perm - end - - @doc """ - Check whether the value has the `connect` permission. - """ - @spec connect?(value :: integer()) :: boolean() - def connect?(value) do - has_perm(value, 0x00100000) - end - - @doc """ - Check whether the value has the `admin` permission. - """ - @spec admin?(value :: integer()) :: boolean() - def admin?(value) do - has_perm(value, 0x00000008) - end - - @doc """ - Check whether the value has the `view_channel` permission. - """ - @spec view_channel?(value :: integer()) :: boolean() - def view_channel?(value) do - has_perm(value, 0x00000400) - end - - @doc """ - Check whether the value has the `view_channel` permission. - """ - @spec view_channel?(value :: integer()) :: boolean() - def speak?(value) do - has_perm(value, 0x00200000) - end -end diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 63fb447..9ebd44c 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -26,16 +26,6 @@ defmodule Cassian.Utils.Voice do |> Api.update_voice_state(nil) end - @doc """ - Check if the bot can connect to a specific voice channel. - """ - @spec can_connect?(guild_id :: Snowflake.t(), voice_id :: Snowflake.t()) :: boolean() - def can_connect?(guild_id, voice_id) do - perms = VoicePermissions.my_channel_permissions(guild_id, voice_id) - - perms.administrator || perms.connect - end - defguard positive_integer(value) when is_integer(value) and value > 0 @doc """ From 96318b1b2b8466e9eb94b85771543650ed2577b2 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 00:18:53 +0100 Subject: [PATCH 22/33] Fix warning on sound_cloud_token.ex Put `_` for unused value and better information on the Logger.debug/1 call --- lib/cassian/servers/sound_cloud_token.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cassian/servers/sound_cloud_token.ex b/lib/cassian/servers/sound_cloud_token.ex index bb1fc21..059814c 100644 --- a/lib/cassian/servers/sound_cloud_token.ex +++ b/lib/cassian/servers/sound_cloud_token.ex @@ -27,8 +27,8 @@ defmodule Cassian.Servers.SoundCloudToken do end @impl true - def handle_info(:timeout, state) do - Logger.debug("Got timeout!") + def handle_info(:timeout, _) do + Logger.debug("Got timeout on SoundCloud, acquiring new token!") state = acquire_new_client_id() {:noreply, state, @timeout} end From 7caa59b0c8b90fe20037b47858de05c2fa8243c7 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 18:47:42 +0100 Subject: [PATCH 23/33] Handle voice state of the bot correctly --- lib/cassian/commands/playback/play.ex | 12 ++++++------ lib/cassian/servers/playlist.ex | 2 ++ lib/cassian/utils/voice.ex | 24 +++++++++++++++++++----- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index 1e4867b..4a20d56 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -1,4 +1,5 @@ defmodule Cassian.Commands.Playback.Play do + require Logger alias Nostrum.Struct.Embed use Cassian.Behaviours.Command @@ -18,7 +19,6 @@ defmodule Cassian.Commands.Playback.Play do name: "query", required: true, description: "Name of the song of URL for it." - } ] } @@ -29,9 +29,9 @@ defmodule Cassian.Commands.Playback.Play do with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(interaction), {:ok, query} <- fetch_query(interaction.data.options), {:ok, metadata} <- song_metadata(query), - :ok <- VoiceUtils.join_or_switch_voice(interaction.guild_id, voice_id), # just to continue with the flow - :ok <- PlayManager.insert!(interaction.guild_id, interaction.channel_id, metadata), - {:ok, :will_play} <- PlayManager.play_if_needed(interaction.guild_id) do + {:ok, _} <- VoiceUtils.join_or_switch_voice(interaction.guild_id, voice_id), + :ok <- PlayManager.insert!(interaction.guild_id, interaction.channel_id, metadata) do + PlayManager.play_if_needed(interaction.guild_id) { EmbedUtils.create_empty_embed!() |> Embed.put_title("Enqueued the song") @@ -43,7 +43,7 @@ defmodule Cassian.Commands.Playback.Play do no_channel_error() {:error, :no_metadata} -> invalid_link_error() - {:error, :wont_play} -> + {:error, :failed_to_join} -> no_permissions_error() end @@ -87,7 +87,7 @@ defmodule Cassian.Commands.Playback.Play do { EmbedUtils.generate_error_embed( "And how do you think that's possible?", - "I don't have the permissions to play music there... Fix it up first." + "I don't have the permissions to play music there or something else really messed me up." ), 1 <<< 6 } diff --git a/lib/cassian/servers/playlist.ex b/lib/cassian/servers/playlist.ex index 26c3edb..c46c373 100644 --- a/lib/cassian/servers/playlist.ex +++ b/lib/cassian/servers/playlist.ex @@ -5,6 +5,7 @@ defmodule Cassian.Servers.Playlist do use GenServer + require Logger alias Cassian.Structs.{Metadata, Playlist} @doc """ @@ -42,6 +43,7 @@ defmodule Cassian.Servers.Playlist do """ @spec delete(guild_id :: Snowflake.t()) :: :ok def delete(guild_id) do + Logger.debug("Disconnected from voice chat. Deleting playlist for guild_id: #{inspect(guild_id)}") if exists?(guild_id), do: GenServer.stop(from_guild_id(guild_id)) :ok end diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 9ebd44c..80397fa 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -1,6 +1,5 @@ defmodule Cassian.Utils.Voice do - alias Nostrum.Snowflake - alias Nostrum.Api + alias Nostrum.{Snowflake, Api, ConsumerGroup} alias Nostrum.Cache.GuildCache alias Cassian.Structs.Metadata @@ -8,13 +7,28 @@ defmodule Cassian.Utils.Voice do @doc """ Join or switch from the voice channel. Set the channel to nil to - leave it. + leave it. It can take up to one second to get the correct event from discord + or fail with `{:error, :failed_to_join}` tuple. """ - @spec join_or_switch_voice(guild_id :: Snowflake.t(), channel_id :: Snowflake.t()) :: :ok + @spec join_or_switch_voice(guild_id :: Snowflake.t(), channel_id :: Snowflake.t()) :: {:ok, :joined} | {:ok, :present} | {:error, :failed_to_join} def join_or_switch_voice(guild_id, channel_id) do guild_id |> Api.update_voice_state(channel_id, false, true) - :ok + + if Nostrum.Voice.ready?(guild_id) do + {:ok, :present} + else + ConsumerGroup.join() + receive do + {:event, {:VOICE_STATE_UPDATE, %Nostrum.Struct.Event.VoiceState{}, _socket}} -> + Logger.debug("Got event for voice state update, joined without and issue #{guild_id}.") + {:ok, :joined} + after + 1_000 -> + Logger.debug("Failed to join on guild: #{guild_id}.") + {:error, :failed_to_join} + end + end end @doc """ From f64cdd9dd809bd2d2829bc1df7be4d1bc6f88260 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 19:19:54 +0100 Subject: [PATCH 24/33] Better logging and error handling --- lib/cassian/commands/playback/play.ex | 2 ++ lib/cassian/managers/message_manager.ex | 4 +-- lib/cassian/servers/playlist.ex | 2 +- lib/cassian/servers/sound_cloud_token.ex | 8 +++--- lib/cassian/servers/voice_state.ex | 10 ++++--- lib/cassian/services/sound_cloud_service.ex | 29 ++++++++++++--------- lib/cassian/utils/voice.ex | 22 +++++++++++----- 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index 4a20d56..abf3aa3 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -45,6 +45,8 @@ defmodule Cassian.Commands.Playback.Play do invalid_link_error() {:error, :failed_to_join} -> no_permissions_error() + {:error, :failed_to_get_stream} + invalid_link_error() end %{type: 4, data: %{embeds: [embed], flags: flags}} diff --git a/lib/cassian/managers/message_manager.ex b/lib/cassian/managers/message_manager.ex index 1c49e1c..52c29aa 100644 --- a/lib/cassian/managers/message_manager.ex +++ b/lib/cassian/managers/message_manager.ex @@ -20,8 +20,8 @@ defmodule Cassian.Managers.MessageManager do :ok {:error, error} -> - Logger.warn(error |> Poison.encode!()) - :nnop + Logger.warning("Failed to edit message: #{inspect(message)} with error: #{inspect(error)}!") + :noop end end diff --git a/lib/cassian/servers/playlist.ex b/lib/cassian/servers/playlist.ex index c46c373..51f047c 100644 --- a/lib/cassian/servers/playlist.ex +++ b/lib/cassian/servers/playlist.ex @@ -43,7 +43,7 @@ defmodule Cassian.Servers.Playlist do """ @spec delete(guild_id :: Snowflake.t()) :: :ok def delete(guild_id) do - Logger.debug("Disconnected from voice chat. Deleting playlist for guild_id: #{inspect(guild_id)}") + Logger.info("Disconnected from voice chat. Deleting playlist for guild_id: #{inspect(guild_id)}") if exists?(guild_id), do: GenServer.stop(from_guild_id(guild_id)) :ok end diff --git a/lib/cassian/servers/sound_cloud_token.ex b/lib/cassian/servers/sound_cloud_token.ex index 059814c..c60e6a9 100644 --- a/lib/cassian/servers/sound_cloud_token.ex +++ b/lib/cassian/servers/sound_cloud_token.ex @@ -28,8 +28,9 @@ defmodule Cassian.Servers.SoundCloudToken do @impl true def handle_info(:timeout, _) do - Logger.debug("Got timeout on SoundCloud, acquiring new token!") + Logger.info("Got timeout on SoundCloud, acquiring new token!") state = acquire_new_client_id() + Logger.debug("New token is: #{inspect(state)}") {:noreply, state, @timeout} end @@ -44,11 +45,12 @@ defmodule Cassian.Servers.SoundCloudToken do defp acquire_new_client_id() do with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get("https://soundcloud.com/discover"), {:ok, value} <- generate_new_client_id(body) do - Logger.debug("Acquired new soundcloud token.") + Logger.info("Acquired new soundcloud token.") + Logger.debug("New token is: #{inspect(value)}.") value else _ -> - Logger.warning ("Failed to retrieve token!") + Logger.error("Failed to retrieve token!") nil end end diff --git a/lib/cassian/servers/voice_state.ex b/lib/cassian/servers/voice_state.ex index 9d57dd4..b0ba29f 100644 --- a/lib/cassian/servers/voice_state.ex +++ b/lib/cassian/servers/voice_state.ex @@ -71,19 +71,20 @@ defmodule Cassian.Servers.VoiceState do @doc false def handle_cast({:put, new}, _state) do - Logger.debug("Putting new value #{Poison.encode!(new)}") + Logger.debug("Putting new voice state: #{inspect(new)}") {:noreply, new, @timeout} end @doc false def handle_info(:timeout, %VoiceState{status: :noop} = state) do - Logger.debug("Matched #{Poison.encode!(state)}") + Logger.info("Matched noop voice state.") + Logger.info("Noop voice state on timeout: #{inspect(state)}") {:stop, {:shutdown, :afk}, state} end @doc false def handle_info(:timeout, state) do - Logger.debug("Not matched #{Poison.encode!(state)}") + Logger.debug("Did not match noop voice state: #{inspect(state)}") {:noreply, state, @timeout} end @@ -93,7 +94,8 @@ defmodule Cassian.Servers.VoiceState do end def terminate({:shutdown, :afk}, state) do - Logger.debug("Clossing #{Poison.encode!(state)}") + Logger.info("Shutdown state due to afk on guild_id: #{state.guild_id}.") + Logger.debug("Clossing voice state due to afk: #{inspect(state)}.") Cassian.Utils.Voice.join_or_switch_voice(state.guild_id, nil) {:shutdown, :afk} end diff --git a/lib/cassian/services/sound_cloud_service.ex b/lib/cassian/services/sound_cloud_service.ex index d8e7651..9fc86b7 100644 --- a/lib/cassian/services/sound_cloud_service.ex +++ b/lib/cassian/services/sound_cloud_service.ex @@ -59,16 +59,10 @@ defmodule Cassian.Services.SoundCloudService do @spec stream_from_url(url :: String.t()) :: {:ok, String.t()} | {:error, any()} def stream_from_url(url) do Logger.debug("Stream from url called with: #{inspect(url)}") - case acquire_track_id(url) do - {:ok, track_id} -> - case get_progressive_link(track_id) do - {:ok, progressive_transcoding} -> - stream_url(progressive_transcoding) - - {:error, _} -> - nil - end - + with {:ok, track_id} <- acquire_track_id(url), + {:ok, transcoding} <- get_progressive_link(track_id) do + stream_url(transcoding) + else _ -> {:error, nil} end @@ -94,13 +88,21 @@ defmodule Cassian.Services.SoundCloudService do params: params, follow_redirect: true ) + + Logger.debug("Got SoundCloud acquire_track_id/1 response: #{inspect(response)}") case response do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + track_id = + Regex.run(~r/tracks%2F(.*)&show_artwork/, body) + |> List.last() + + Logger.debug("SoundCloud track_id is: #{inspect(track_id)}.") + { :ok, # Woohoo Regex magic - Regex.run(~r/tracks%2F(.*)&show_artwork/, body) |> List.last() + track_id } _ -> @@ -115,14 +117,17 @@ defmodule Cassian.Services.SoundCloudService do params = %{ client_id: client_id() } + + response = HTTPoison.get(url, %{}, params: params) # Some information is here - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, %{}, params: params), + with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- response, {:ok, decoded} <- Poison.decode(body, %{keys: :atoms}), {:ok, url} <- acquire_link_from_body(decoded) do {:ok, url} else _ -> + Logger.error("Failed to get correct response, it's: #{inspect(response)}") {:error, nil} end end diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 80397fa..7fa5d1c 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -21,11 +21,11 @@ defmodule Cassian.Utils.Voice do ConsumerGroup.join() receive do {:event, {:VOICE_STATE_UPDATE, %Nostrum.Struct.Event.VoiceState{}, _socket}} -> - Logger.debug("Got event for voice state update, joined without and issue #{guild_id}.") + Logger.info("Joined voice chat on guild_id: #{inspect(guild_id)}") {:ok, :joined} after 1_000 -> - Logger.debug("Failed to join on guild: #{guild_id}.") + Logger.error("Failed to join voice chat on guild: #{guild_id}.") {:error, :failed_to_join} end end @@ -47,23 +47,31 @@ defmodule Cassian.Utils.Voice do Every retry approx lasts for approx one second. """ @spec play_when_ready(metadata :: %Metadata{}, guild_id :: Snowflake.t(), max_retries :: integer()) :: - {:ok, :ok | any()} | {:error, :failed_max} + {:ok, :ok | any()} | {:error, :failed_max} | {:error, :failed_to_get_stream} def play_when_ready(metadata, guild_id, max_retries) when is_integer(max_retries) and max_retries > 0 do + Logger.info("play_when_ready/3 on guild_id: #{inspect(guild_id)} failed. Trying #{inspect(max_retries)} more tries.") Logger.debug("Trying to play song from metadata: #{inspect(metadata)} in guild id: #{guild_id}. Remaining retries: #{max_retries}.") if Nostrum.Voice.ready?(guild_id) do - stream_source = stream_url!(metadata) - Logger.debug("Voice is ready. Streaming audiosource: #{inspect(stream_source)}") - {:ok, Nostrum.Voice.play(guild_id, stream_source, metadata.stream_method)} + case stream_url!(metadata) do + nil -> + Logger.error("Failed to get stream source. Erroring out safely!") + {:error, :failed_to_get_stream} + stream_source -> + Logger.info("Joined voice chat on guild_id: #{inspect(guild_id)}.") + Logger.debug("Voice is ready. Streaming audiosource: #{inspect(stream_source)}") + {:ok, Nostrum.Voice.play(guild_id, stream_source, metadata.stream_method)} + end else :timer.sleep(1000) play_when_ready(metadata, guild_id, max_retries - 1) end end - def play_when_ready(_, _, _) do + def play_when_ready(_, guild_id, _) do + Logger.error("Failed to join voice chat on guild_id: #{inspect(guild_id)}.") {:error, :failed_max} end From 4ca448ff51bdb60c5772d1a737dd90ee9e19b1e1 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 19:37:48 +0100 Subject: [PATCH 25/33] Fixed play_manager and voice specs/namings Added correct specs and change some functions to be private as they're not designed ot be used outside of the current module. --- lib/cassian/managers/play_manager.ex | 14 ++++++++++---- lib/cassian/utils/voice.ex | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/cassian/managers/play_manager.ex b/lib/cassian/managers/play_manager.ex index 04a1dbd..519b7ff 100644 --- a/lib/cassian/managers/play_manager.ex +++ b/lib/cassian/managers/play_manager.ex @@ -25,7 +25,13 @@ defmodule Cassian.Managers.PlayManager do |> VoiceState.put() end + @spec alter_index(any()) :: nil | no_return() @doc """ + Alter the new index in the playlist. It will either increment or decrement it. + + * `no repeat`, keep the new index + * `one`, repeat the song + * `all`, alter the index but loop it around (keep it in bounds) """ def alter_index(guild_id) do case Playlist.show(guild_id) do @@ -41,7 +47,7 @@ defmodule Cassian.Managers.PlayManager do playlist.index :all -> - keep_in_bounds(index, playlist.elements) + keep_in_bounds!(index, playlist.elements) end playlist @@ -53,12 +59,12 @@ defmodule Cassian.Managers.PlayManager do end end - def in_bound?(index, ordered) do + defp in_bound?(index, ordered) do index >= 0 and index < length(ordered) end # Keep the index in bounds, loops around. - def keep_in_bounds(index, ordered) do + defp keep_in_bounds!(index, ordered) do size = length(ordered) index = if index >= size, do: 0, else: index if index < 0, do: size - 1, else: index @@ -97,7 +103,7 @@ defmodule Cassian.Managers.PlayManager do index = old_index if should_play? do - index = keep_in_bounds(index, ordered) + index = keep_in_bounds!(index, ordered) metadata = Enum.at(ordered, index) diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 7fa5d1c..fcf4d76 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -10,7 +10,7 @@ defmodule Cassian.Utils.Voice do leave it. It can take up to one second to get the correct event from discord or fail with `{:error, :failed_to_join}` tuple. """ - @spec join_or_switch_voice(guild_id :: Snowflake.t(), channel_id :: Snowflake.t()) :: {:ok, :joined} | {:ok, :present} | {:error, :failed_to_join} + @spec join_or_switch_voice(guild_id :: Snowflake.t(), channel_id :: Snowflake.t() | nil) :: {:ok, :joined} | {:ok, :present} | {:error, :failed_to_join} def join_or_switch_voice(guild_id, channel_id) do guild_id |> Api.update_voice_state(channel_id, false, true) From 98086cbda1de564eed9eaea0b102ab460b19f9cb Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 20:00:18 +0100 Subject: [PATCH 26/33] Using import Config `use Mix.Config` is deprecated. --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index f3a870a..bd5246d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :nostrum, ffmpeg: System.get_env("FFMPEG_PATH") || "/usr/bin/ffmpeg", From d0e2f15a7e726bb356e0241936e4451847492db8 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 21:09:57 +0100 Subject: [PATCH 27/33] More specific builder/runtime for Dockerfile Rather than naming the user cassian, it's named builder and runner respectively for their function. The main functionality isn't changed though. --- Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1ab0937..3377e97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apk update && \ gcc autoconf automake ncurses-dev \ bash make g++ openssl-dev -RUN adduser -gD cassian --disabled-password +RUN adduser -gD builder --disabled-password # Building Erlang/Elixir @@ -19,9 +19,9 @@ WORKDIR /build COPY .tool-versions . -RUN chown -R cassian:cassian /build +RUN chown -R builder:builder /build -USER cassian +USER builder RUN git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.13.1; @@ -29,7 +29,7 @@ RUN /bin/bash -c 'echo -e "\n\n## Configure ASDF \n. $HOME/.asdf/asdf.sh" >> ~/. RUN /bin/bash -c 'echo -e "\n\n## ASDF Bash Completion: \n. $HOME/.asdf/completions/asdf.bash" >> ~/.bashrc'; -ENV PATH="$PATH:/home/cassian/.asdf/bin:/home/cassian/.asdf/shims" +ENV PATH="$PATH:/home/builder/.asdf/bin:/home/builder/.asdf/shims" RUN asdf plugin add elixir @@ -59,9 +59,9 @@ RUN mix release # Actual app stage -FROM alpine:3.19.0 AS cassian-app +FROM alpine:3.19.0 AS runtime -RUN adduser -D cassian --disabled-password +RUN adduser -D runner --disabled-password RUN apk update @@ -69,7 +69,7 @@ RUN apk upgrade RUN apk add ffmpeg youtube-dl -USER cassian +USER runner WORKDIR /app From fa9ef041626c3e5712fef548190c24d7a99495cb Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 22:46:08 +0100 Subject: [PATCH 28/33] Rely of youtube-dl Youtube-dl already has youtube and soundcloud setup. So in order to not invent hot water it was opted out to not use a genserver hack with floki (TODO: Remove Floki, possibly HTTPoison as well) and it's just made that youtube-dl from path is used. --- config/config.exs | 2 - config/runtime.exs | 2 - lib/cassian.ex | 14 -- lib/cassian/application.ex | 1 - lib/cassian/behaviours/source_service.ex | 17 ++ lib/cassian/servers/sound_cloud_token.ex | 84 ---------- lib/cassian/services/sound_cloud.ex | 39 +++++ lib/cassian/services/sound_cloud_service.ex | 169 -------------------- lib/cassian/services/youtube.ex | 42 +++++ lib/cassian/services/youtube_service.ex | 126 --------------- lib/cassian/utils.ex | 6 +- lib/cassian/utils/voice.ex | 28 +--- 12 files changed, 104 insertions(+), 426 deletions(-) create mode 100644 lib/cassian/behaviours/source_service.ex delete mode 100644 lib/cassian/servers/sound_cloud_token.ex create mode 100644 lib/cassian/services/sound_cloud.ex delete mode 100644 lib/cassian/services/sound_cloud_service.ex create mode 100644 lib/cassian/services/youtube.ex delete mode 100644 lib/cassian/services/youtube_service.ex diff --git a/config/config.exs b/config/config.exs index bd5246d..8609780 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,8 +1,6 @@ import Config config :nostrum, - ffmpeg: System.get_env("FFMPEG_PATH") || "/usr/bin/ffmpeg", - youtubedl: System.get_env("YTDL_PATH") || "/usr/bin/youtube-dl", token: System.get_env("DISCORD_BOT_TOKEN"), num_shards: :auto diff --git a/config/runtime.exs b/config/runtime.exs index 1027e37..df059f7 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,8 +1,6 @@ import Config config :nostrum, - ffmpeg: "/usr/bin/ffmpeg", - youtubedl: "/usr/bin/youtube-dl", token: System.fetch_env!("DISCORD_BOT_TOKEN"), num_shards: :auto diff --git a/lib/cassian.ex b/lib/cassian.ex index f67dfe0..0f4e0cb 100644 --- a/lib/cassian.ex +++ b/lib/cassian.ex @@ -6,8 +6,6 @@ defmodule Cassian do alias Nostrum.Cache.Me - @default_command_prefix Application.get_env(:cassian, :prefix) - @doc """ Get the avatar of the bot itself. """ @@ -15,18 +13,6 @@ defmodule Cassian do def get_own_avatar, do: Cassian.Utils.user_avatar(Me.get()) - @doc """ - Get the command prefix of the bot from a server. if no server - is specific then the default prefix is taken. - - Currently this works only with the preset one as database has not yet - been added. - """ - @spec command_prefix!(server_id :: Nostrum.Struct.Guild.id() | nil) :: binary - def command_prefix!(_server_id \\ nil) do - @default_command_prefix - end - @doc """ Get the user name of bot account. """ diff --git a/lib/cassian/application.ex b/lib/cassian/application.ex index 0824a39..4dab79e 100644 --- a/lib/cassian/application.ex +++ b/lib/cassian/application.ex @@ -15,7 +15,6 @@ defmodule Cassian.Application do children = [ Cassian.Consumer, - Cassian.Servers.SoundCloudToken ] ++ web_child!() children diff --git a/lib/cassian/behaviours/source_service.ex b/lib/cassian/behaviours/source_service.ex new file mode 100644 index 0000000..1c3148d --- /dev/null +++ b/lib/cassian/behaviours/source_service.ex @@ -0,0 +1,17 @@ +defmodule Cassian.Behaviours.SourceService do + @moduledoc """ + The general service for a source of audio. + """ + + @doc """ + Try to get metadata for a specific song if it's supported. + """ + @callback song_metadata(url :: String.t()) :: {:ok, %Cassian.Structs.Metadata{}} | {:error, :not_found} + + defmacro __using__(_) do + quote do + @behaviour Cassian.Behaviours.SourceService + alias Cassian.Structs.Metadata + end + end +end diff --git a/lib/cassian/servers/sound_cloud_token.ex b/lib/cassian/servers/sound_cloud_token.ex deleted file mode 100644 index c60e6a9..0000000 --- a/lib/cassian/servers/sound_cloud_token.ex +++ /dev/null @@ -1,84 +0,0 @@ -defmodule Cassian.Servers.SoundCloudToken do - @moduledoc """ - Server which stores the SoundCloud `client_id`, automatically refreshes it every 15 minutes. - """ - - use GenServer - require Logger - - # 15 minutes~ - @timeout 900_000 - - # API - - @doc """ - Get the current SoundCloud client ID. - """ - @spec client_id() :: Sting.t() - def client_id do - GenServer.call(__MODULE__, :client_id) - end - - # Server-side - - @impl true - def handle_call(:client_id, _from, state) do - {:reply, state, state} - end - - @impl true - def handle_info(:timeout, _) do - Logger.info("Got timeout on SoundCloud, acquiring new token!") - state = acquire_new_client_id() - Logger.debug("New token is: #{inspect(state)}") - {:noreply, state, @timeout} - end - - def start_link(_) do - GenServer.start_link(__MODULE__, [nil], name: __MODULE__) - end - - @impl true - def init(_), - do: {:ok, acquire_new_client_id(), @timeout} - - defp acquire_new_client_id() do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get("https://soundcloud.com/discover"), - {:ok, value} <- generate_new_client_id(body) do - Logger.info("Acquired new soundcloud token.") - Logger.debug("New token is: #{inspect(value)}.") - value - else - _ -> - Logger.error("Failed to retrieve token!") - nil - end - end - - defp generate_new_client_id(body) do - body - |> Floki.parse_document!() - |> Floki.find("script[src][crossorigin]") - |> Stream.map(&filter_scripts/1) - |> Stream.reject(&is_nil/1) - |> Enum.reverse() # Generally client_id is near the end script from experience - |> Enum.find_value(fn script -> - with {:ok, response = %HTTPoison.Response{status_code: 200}} <- HTTPoison.get(script), - regex_code <- Regex.run(~r/client_id:\"(.*)\",env:/, response.body), - false <- is_nil(regex_code) do - {:ok, List.last(regex_code)} - else - _ -> - nil - end - end) - end - - defp filter_scripts({"script", [_, {"src", script}], _}) do - script - end - - defp filter_scripts(_) do - nil - end -end diff --git a/lib/cassian/services/sound_cloud.ex b/lib/cassian/services/sound_cloud.ex new file mode 100644 index 0000000..06641c2 --- /dev/null +++ b/lib/cassian/services/sound_cloud.ex @@ -0,0 +1,39 @@ +defmodule Cassian.Services.SoundCloud do + @moduledoc """ + Service module for SoundCloud songs. + """ + + use Cassian.Behaviours.SourceService + + require Logger + + def song_metadata(url) do + with {json_body, 0} <- System.cmd("youtube-dl", [url, "--dump-json"]), + {:ok, body = %{"extractor" => "soundcloud"}} <- Poison.decode(json_body) do + Logger.debug(inspect(body)) + + metadata = + %Metadata{ + title: body["title"], + author: body["uploader"], + provider: "soundcloud", + link: body["webpage_url"], + color: "ff9033", + thumbnail_url: body["thumbnail"], + stream_link: url, + stream_method: :ytdl + } + + {:ok, metadata} + else + {output, code} when is_integer(code) -> + Logger.critical("youtube-dl failed with code: #{inspect(code)} and output: #{inspect(output)}!") + {:error, :no_youtube_dl} + {:error, _} -> + Logger.critical("SoundCloud body failed to decode.") + {:error, :json_decode} + {:ok, _} -> + Logger.debug("URL is not a soundcloud one: #{inspect(url)}") + end + end +end diff --git a/lib/cassian/services/sound_cloud_service.ex b/lib/cassian/services/sound_cloud_service.ex deleted file mode 100644 index 9fc86b7..0000000 --- a/lib/cassian/services/sound_cloud_service.ex +++ /dev/null @@ -1,169 +0,0 @@ -defmodule Cassian.Services.SoundCloudService do - @moduledoc """ - Service module which does most of the calls for the SoundCloud API. - """ - - alias Cassian.Structs.Metadata - - require Logger - - defdelegate client_id(), to: Cassian.Servers.SoundCloudToken - - @doc """ - Get the metadata for the song from the oembed... embed... - """ - @spec oembed_song_data(url :: String.t()) :: {:ok, %Metadata{}} | {:error, any()} - def oembed_song_data(url) do - link = "https://soundcloud.com/oembed" - - headers = [ - "User-agent": "#{Cassian.username!()} #{Cassian.version!()}", - Accept: "Application/json; Charset=utf-8" - ] - - params = %{ - url: url, - format: :json - } - - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(link, headers, params: params), - {:ok, body} <- Poison.decode(body, %{keys: :atoms}) do - metadata = %Metadata{ - title: body.title, - author: body.author_name, - provider: "soundcloud", - link: url, - color: "ff9033", - thumbnail_url: body.thumbnail_url, - stream_link: nil, - stream_method: :url - } - - {:ok, metadata} - - else - {_, %HTTPoison.Response{status_code: code}} -> - {:error, code} - - {_, %HTTPoison.Error{}} -> - {:error, -1} - - {_, _} -> - {:error, -2} - end - end - - @doc """ - Get the SoundCloud raw stream from a SoundCloud url. - """ - @spec stream_from_url(url :: String.t()) :: {:ok, String.t()} | {:error, any()} - def stream_from_url(url) do - Logger.debug("Stream from url called with: #{inspect(url)}") - with {:ok, track_id} <- acquire_track_id(url), - {:ok, transcoding} <- get_progressive_link(track_id) do - stream_url(transcoding) - else - _ -> - {:error, nil} - end - end - - @doc """ - Get the SoundCloud track id from a SoundCloud url. - """ - @spec acquire_track_id(url :: String.t()) :: {:ok, String.t()} | {:error, any()} - def acquire_track_id(url) do - headers = [ - # I honestly have no clue what is the content type... - {"Content-Type", "application/json"} - ] - - params = %{ - url: url, - format: "json" - } - - response = - HTTPoison.get("https://soundcloud.com/oembed", headers, - params: params, - follow_redirect: true - ) - - Logger.debug("Got SoundCloud acquire_track_id/1 response: #{inspect(response)}") - - case response do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - track_id = - Regex.run(~r/tracks%2F(.*)&show_artwork/, body) - |> List.last() - - Logger.debug("SoundCloud track_id is: #{inspect(track_id)}.") - - { - :ok, - # Woohoo Regex magic - track_id - } - - _ -> - {:error, nil} - end - end - - @spec get_progressive_link(track_id :: String.t()) :: {:ok, String.t()} | {:error, any()} - def get_progressive_link(track_id) do - url = "https://api-v2.soundcloud.com/tracks/#{track_id}" - - params = %{ - client_id: client_id() - } - - response = HTTPoison.get(url, %{}, params: params) - - # Some information is here - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- response, - {:ok, decoded} <- Poison.decode(body, %{keys: :atoms}), - {:ok, url} <- acquire_link_from_body(decoded) do - {:ok, url} - else - _ -> - Logger.error("Failed to get correct response, it's: #{inspect(response)}") - {:error, nil} - end - end - - defp acquire_link_from_body(body) do - transcoding = - body.media.transcodings - |> Enum.find(fn transcoding -> transcoding.format.protocol == "progressive" end) - |> Map.get(:url) - - case transcoding do - nil -> - {:error, nil} - value -> - {:ok, value} - end - end - - @doc """ - Get the stream URL from the transcoded-progressive url. - """ - @spec stream_url(progressive_transcoding_url :: String.t()) :: - {:ok, String.t()} | {:error, any()} - def stream_url(progressive_transcoding_url) do - params = %{ - client_id: client_id() - } - - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(progressive_transcoding_url, %{}, params: params), - {:ok, decoded} <- Poison.decode(body, %{keys: :atoms}), - value <- Map.get(decoded, :url), - false <- is_nil(value) do - {:ok, value} - else - _ -> - {:error, nil} - end - end -end diff --git a/lib/cassian/services/youtube.ex b/lib/cassian/services/youtube.ex new file mode 100644 index 0000000..eb8679c --- /dev/null +++ b/lib/cassian/services/youtube.ex @@ -0,0 +1,42 @@ +defmodule Cassian.Services.Youtube do + @moduledoc """ + Service module for Youtube songs. + """ + + use Cassian.Behaviours.SourceService + + require Logger + + def song_metadata(url) do + with {json_body, 0} <- System.cmd("youtube-dl", [url, "--dump-json"]), + {:ok, body = %{"extractor" => "youtube"}} <- Poison.decode(json_body) do + + Logger.debug("Got Youtube JSON from youtube-dl: #{inspect(body)}.") + + metadata = + %Metadata{ + title: body["title"], + author: body["uploader"], + provider: "youtube", + link: body["webpage_url"], + color: "ff3333", + thumbnail_url: body["thumbnail"], + stream_link: url, + stream_method: :ytdl + } + + Logger.debug("Giving metadata: #{inspect(metadata)}.") + + {:ok, metadata} + else + {output, code} when is_integer(code) -> + Logger.critical("youtube-dl failed with code: #{inspect(code)} and output: #{inspect(output)}!") + {:error, :no_youtube_dl} + {:error, _} -> + Logger.critical("Youtube body failed to decode.") + {:error, :json_decode} + {:ok, _} -> + Logger.debug("URL is not a youtube one: #{inspect(url)}") + end + end +end diff --git a/lib/cassian/services/youtube_service.ex b/lib/cassian/services/youtube_service.ex deleted file mode 100644 index 4f4bd60..0000000 --- a/lib/cassian/services/youtube_service.ex +++ /dev/null @@ -1,126 +0,0 @@ -defmodule Cassian.Services.YoutubeService do - @moduledoc """ - Service module which corresponds to YouTube calls. - """ - - alias Cassian.Structs.Metadata - - @doc """ - Create metadata from a youtube song. - """ - @spec oembed_song_data(url :: String.t()) :: {:ok, %Metadata{}} | {:error, Integer.t()} - def oembed_song_data(url) do - link = "https://www.youtube.com/oembed" - - headers = [ - "User-agent": "#{Cassian.username!()} #{Cassian.version!()}", - Accept: "Application/json; Charset=utf-8" - ] - - params = %{ - url: url, - format: :json - } - - case HTTPoison.get(link, headers, params: params) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - body = - body - |> Poison.decode!(keys: :atoms) - - # Doing some sanitization for the link so that we get a singleton song... - url = "https://www.youtube.com/watch?v=#{ - Regex.run(~r/(?<=\/vi\/)(.*)(?=\/)/, body.thumbnail_url) - }" - - body = %Metadata{ - title: body.title, - author: body.author_name, - provider: "youtube", - link: url, - color: "ff3333", - thumbnail_url: body.thumbnail_url, - stream_link: url, - stream_method: :ytdl - } - - {:ok, body} - - {_, %HTTPoison.Response{status_code: code}} -> - {:error, code} - end - end - - @doc """ - Get a list of metadatas from a youtube playlist. - """ - @spec playlist_information(link :: String.t()) :: - {:ok, list(%Metadata{})} - | {:error, :invalid} - | {:error, :broken} - | {:error, :no_playlist} - def playlist_information(link) do - %URI{query: query} = URI.parse(link) - - case URI.decode_query(query || "") |> Map.get("list") do - nil -> - {:error, :invalid} - - list -> - url = "https://www.youtube.com/embed/videoseries" - - params = %{ - list: list - } - - headers = [ - "User-agent": "#{Cassian.username!()} #{Cassian.version!()}" - ] - - case HTTPoison.get(url, headers, params: params) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - response = - Regex.scan(~r/(?<=ytcfg\.set\().*?(?=\);)/, body) - |> Poison.decode!(keys: :atoms) - |> Map.get(:PLAYER_VARS) - |> Map.get(:embedded_player_response) - - case response |> Poison.decode() do - {:ok, json} -> - songs = - json - |> Map.get("embedPreview") - |> Map.get("thumbnailPreviewRenderer") - |> Map.get("playlist") - |> Map.get("playlistPanelRenderer") - |> Map.get("contents") - |> Enum.map(&playlist_element_to_meta/1) - - {:ok, songs} - - {:error, _} -> - {:error, :no_playlist} - end - - _ -> - {:error, :broken} - end - end - end - - # Transform to meta... - defp playlist_element_to_meta(element) do - element = element["playlistPanelVideoRenderer"] - - %Metadata{ - title: element["title"]["runs"] |> List.first() |> Map.get("text"), - author: element["longBylineText"]["runs"] |> List.first() |> Map.get("text"), - provider: "youtube", - link: "https://www.youtube.com/watch?v=#{element["videoId"]}", - color: "ff3333", - thumbnail_url: element["thumbnail"]["thumbnails"] |> List.last() |> Map.get("url"), - stream_link: "https://www.youtube.com/watch?v=#{element["videoId"]}", - stream_method: :ytdl - } - end -end diff --git a/lib/cassian/utils.ex b/lib/cassian/utils.ex index 6154d66..5eb9e0f 100644 --- a/lib/cassian/utils.ex +++ b/lib/cassian/utils.ex @@ -4,7 +4,7 @@ defmodule Cassian.Utils do """ alias Cassian.Structs.Metadata - alias Cassian.Services.{YoutubeService, SoundCloudService} + alias Cassian.Services.{Youtube, SoundCloud} @doc """ Get the user avatar url. @@ -19,12 +19,12 @@ defmodule Cassian.Utils do """ @spec song_metadata(link :: String.t()) :: {:ok, metadata :: Metadata.t()} | {:error, :no_metadata} def song_metadata(link) do - [YoutubeService, SoundCloudService] + [Youtube, SoundCloud] |> Enum.find_value({:error, :no_metadata}, &check_for_data(&1, link)) end defp check_for_data(module, link) do - case module.oembed_song_data(link) do + case module.song_metadata(link) do {:ok, data} -> {:ok, data} diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index fcf4d76..8c6639b 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -55,15 +55,9 @@ defmodule Cassian.Utils.Voice do Logger.debug("Trying to play song from metadata: #{inspect(metadata)} in guild id: #{guild_id}. Remaining retries: #{max_retries}.") if Nostrum.Voice.ready?(guild_id) do - case stream_url!(metadata) do - nil -> - Logger.error("Failed to get stream source. Erroring out safely!") - {:error, :failed_to_get_stream} - stream_source -> - Logger.info("Joined voice chat on guild_id: #{inspect(guild_id)}.") - Logger.debug("Voice is ready. Streaming audiosource: #{inspect(stream_source)}") - {:ok, Nostrum.Voice.play(guild_id, stream_source, metadata.stream_method)} - end + Logger.info("Joined voice chat on guild_id: #{inspect(guild_id)}.") + Logger.debug("Voice is ready. Streaming audiosource: #{inspect(metadata.stream_link)}") + {:ok, Nostrum.Voice.play(guild_id, metadata.stream_link, metadata.stream_method)} else :timer.sleep(1000) play_when_ready(metadata, guild_id, max_retries - 1) @@ -75,22 +69,6 @@ defmodule Cassian.Utils.Voice do {:error, :failed_max} end - defp stream_url!(metadata) do - case metadata.provider do - "soundcloud" -> - case Cassian.Services.SoundCloudService.stream_from_url(metadata.link) do - {:ok, stream} -> - stream - - _ -> - nil - end - - _ -> - metadata.stream_link - end - end - @doc """ Safely the current voice id in which the user is. Also returns the guild id. """ From d6a1d6d996b0091ccecc45ca28178d55a4773271 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 22:49:15 +0100 Subject: [PATCH 29/33] Remove unused dependencies --- mix.exs | 4 +--- mix.lock | 11 ----------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/mix.exs b/mix.exs index 7201200..9ed7f4d 100644 --- a/mix.exs +++ b/mix.exs @@ -24,10 +24,8 @@ defmodule Cassian.MixProject do [ {:nostrum, github: "Kraigie/nostrum"}, {:poison, "~> 5.0"}, - {:httpoison, "~> 2.0"}, {:cowlib, "~> 2.9.1", override: true}, - {:plug_cowboy, "~> 2.4"}, - {:floki, "~> 0.31.0"} + {:plug_cowboy, "~> 2.4"} ] end end diff --git a/mix.lock b/mix.lock index 4b2b25c..ff9e4a2 100644 --- a/mix.lock +++ b/mix.lock @@ -8,21 +8,12 @@ "curve25519": {:hex, :curve25519, "1.0.5", "f801179424e4012049fcfcfcda74ac04f65d0ffceeb80e7ef1d3352deb09f5bb", [:mix], [], "hexpm", "0fba3ad55bf1154d4d5fc3ae5fb91b912b77b13f0def6ccb3a5d58168ff4192d"}, "ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"}, "equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"}, - "floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"}, "forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"}, - "gen_stage": {:hex, :gen_stage, "1.1.0", "dd0c0f8d2f3b993fdbd3d58e94abbe65380f4e78bdee3fa93d5618d7d14abe60", [:mix], [], "hexpm", "7f2b36a6d02f7ef2ba410733b540ec423af65ec9c99f3d1083da508aca3b9305"}, "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "kcl": {:hex, :kcl, "1.4.2", "8b73a55a14899dc172fcb05a13a754ac171c8165c14f65043382d567922f44ab", [: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", "9f083dd3844d902df6834b258564a82b21a15eb9f6acdc98e8df0c10feeabf05"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nostrum": {:git, "https://github.com/Kraigie/nostrum.git", "d2daf4941927bc4452a4e79acbef4a574ce32f57", []}, - "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, @@ -30,7 +21,5 @@ "poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } From c6882309b663524317cd6a2509e19f0573e3ca72 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Fri, 12 Jan 2024 22:57:25 +0100 Subject: [PATCH 30/33] Minimize help function --- lib/cassian/commands/bot/help.ex | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/cassian/commands/bot/help.ex b/lib/cassian/commands/bot/help.ex index d41e698..df81866 100644 --- a/lib/cassian/commands/bot/help.ex +++ b/lib/cassian/commands/bot/help.ex @@ -1,8 +1,6 @@ defmodule Cassian.Commands.Bot.Help do use Cassian.Behaviours.Command - alias Cassian.Managers.MessageManager - @moduledoc """ The help command. Shows a menu of commands. """ @@ -14,24 +12,19 @@ defmodule Cassian.Commands.Bot.Help do } end - @doc false - def execute(interaction) do - generate_help_embed!() - + def execute(_interaction) do %{type: 4, data: %{embeds: [generate_help_embed!()], flags: 1 <<< 6}} end import Cassian.Utils.Embed alias Nostrum.Struct.Embed - def generate_help_embed!() do + defp generate_help_embed!() do create_empty_embed!() |> Embed.put_title("Yo! Thanks for adding me to the server!") |> Embed.put_description( "I currently don't have a propper help command as I'm a WIP. Though once it has been created it will be linked here so " <> - "you can see all of my commands. Although you can start playing music with `#{ - Cassian.command_prefix!() - }play`." + "you can see all of my commands. Although you can start playing music with `/play`." ) |> Embed.put_thumbnail(Cassian.get_own_avatar()) end From c65219c4c4fe3630a42369320711ee17b1c50343 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Mon, 15 Jan 2024 17:30:16 +0100 Subject: [PATCH 31/33] Allow longer durations of messages Sometimes the metadata might take a few seconds. This fixes the issue by not allowing playlists and by sending an initial wait message and there won't be an issue as long it responds within 15 minutes. --- lib/cassian/commands/playback/play.ex | 27 ++++++++++----------- lib/cassian/consumers/command.ex | 34 +++++++++++++++++++++++---- lib/cassian/services/youtube.ex | 6 ++--- lib/cassian/utils.ex | 9 +++++++ lib/cassian/utils/voice.ex | 28 ++++++++++------------ 5 files changed, 66 insertions(+), 38 deletions(-) diff --git a/lib/cassian/commands/playback/play.ex b/lib/cassian/commands/playback/play.ex index abf3aa3..7f2346f 100644 --- a/lib/cassian/commands/playback/play.ex +++ b/lib/cassian/commands/playback/play.ex @@ -1,6 +1,7 @@ defmodule Cassian.Commands.Playback.Play do require Logger - alias Nostrum.Struct.Embed + alias Cassian.Utils + alias Nostrum.Struct.{Embed, ApplicationCommandInteractionDataOption} use Cassian.Behaviours.Command import Cassian.Utils @@ -25,6 +26,8 @@ defmodule Cassian.Commands.Playback.Play do end def execute(interaction) do + Utils.notify_longer_response(interaction) + {embed, flags} = with {:ok, {_guild_id, voice_id}} <- VoiceUtils.sender_voice_id(interaction), {:ok, query} <- fetch_query(interaction.data.options), @@ -49,22 +52,18 @@ defmodule Cassian.Commands.Playback.Play do invalid_link_error() end - %{type: 4, data: %{embeds: [embed], flags: flags}} + %{type: 4, data: %{embeds: [embed], flags: flags}, edit: true} end - defp fetch_query(options) do - option = - options - |> Enum.find(fn option -> String.equivalent?(option.name, "query") end) - - case option do - nil -> - {:error, :no_metadata} - - _ -> - {:ok, option.value} - end + # Pattern-matches per-element if the head doesn't have this and it'll try to find the first + # element which has "query" as the name. + defp fetch_query([%ApplicationCommandInteractionDataOption{name: "query", value: value} | _]) do + {:ok, value} end + + defp fetch_query([_ | tail]), do: fetch_query(tail) + + defp fetch_query(_), do: {:error, :no_metadata} # Error handlers diff --git a/lib/cassian/consumers/command.ex b/lib/cassian/consumers/command.ex index 5b4bc53..8cba51d 100644 --- a/lib/cassian/consumers/command.ex +++ b/lib/cassian/consumers/command.ex @@ -3,6 +3,7 @@ defmodule Cassian.Consumers.Command do Main consumer for the command event of the bot. Redirects it to other commands. """ + require Logger alias Nostrum.Api alias Cassian.Commands.{Bot, Playback} @@ -17,17 +18,40 @@ defmodule Cassian.Consumers.Command do :noop module -> - interaction - |> module.execute() - |> (&Api.create_interaction_response(interaction, &1)).() - :ok + api_response = + interaction + |> module.execute() + |> (&send_response(interaction, &1)).() + + Logger.debug("API's response is: #{inspect(api_response)}.") end end + # I know it looks ugly when called in the pipeline *but* it's consitent with + # the library that itneraction should be first and then the response should be + # something additional. + defp send_response(interaction, response = %{edit: true}) do + new_response = %{ + embeds: response.data.embeds, + type: response.type, + flags: response.data.flags + } + + Logger.debug("Sending edit response: #{inspect(new_response)}") + + Api.edit_interaction_response(interaction, new_response) + end + + defp send_response(interaction, response) do + Logger.debug("Sending standard response: #{inspect(response)}") + + Api.create_interaction_response(interaction, response) + end + @doc """ Generate the Discord interaction commands for each guild. """ - @spec generate_commands(Nostrum.Struct.Guild.UnavailableGuild.t()) :: :ok + @spec generate_commands(Nostrum.Struct.Guild.UnavailableGuild.t()) :: no_return() def generate_commands(%Nostrum.Struct.Guild.UnavailableGuild{id: guild_id}) do Nostrum.Api.create_guild_application_command(guild_id, Bot.Help.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Backward.application_command_definition()) diff --git a/lib/cassian/services/youtube.ex b/lib/cassian/services/youtube.ex index eb8679c..31f0c1e 100644 --- a/lib/cassian/services/youtube.ex +++ b/lib/cassian/services/youtube.ex @@ -8,7 +8,7 @@ defmodule Cassian.Services.Youtube do require Logger def song_metadata(url) do - with {json_body, 0} <- System.cmd("youtube-dl", [url, "--dump-json"]), + with {json_body, 0} <- System.cmd("youtube-dl", [url, "--dump-json", "--no-playlist", "--quiet"]), {:ok, body = %{"extractor" => "youtube"}} <- Poison.decode(json_body) do Logger.debug("Got Youtube JSON from youtube-dl: #{inspect(body)}.") @@ -25,7 +25,7 @@ defmodule Cassian.Services.Youtube do stream_method: :ytdl } - Logger.debug("Giving metadata: #{inspect(metadata)}.") + Logger.debug("Giving metadata: #{inspect(metadata)}.") {:ok, metadata} else @@ -35,8 +35,6 @@ defmodule Cassian.Services.Youtube do {:error, _} -> Logger.critical("Youtube body failed to decode.") {:error, :json_decode} - {:ok, _} -> - Logger.debug("URL is not a youtube one: #{inspect(url)}") end end end diff --git a/lib/cassian/utils.ex b/lib/cassian/utils.ex index 5eb9e0f..7a2ded7 100644 --- a/lib/cassian/utils.ex +++ b/lib/cassian/utils.ex @@ -23,6 +23,15 @@ defmodule Cassian.Utils do |> Enum.find_value({:error, :no_metadata}, &check_for_data(&1, link)) end + @doc """ + Create an interaction response for events which can take a little bit longer. This will keep it like that + for roughly 15 minutes. + """ + @spec notify_longer_response(interaction :: Nostrum.Struct.Interaction.t(), flags :: integer()) :: no_return() + def notify_longer_response(interaction, flags \\ 64) do + Nostrum.Api.create_interaction_response(interaction, %{type: 5, data: %{flags: flags}}) + end + defp check_for_data(module, link) do case module.song_metadata(link) do {:ok, data} -> diff --git a/lib/cassian/utils/voice.ex b/lib/cassian/utils/voice.ex index 8c6639b..a236602 100644 --- a/lib/cassian/utils/voice.ex +++ b/lib/cassian/utils/voice.ex @@ -2,6 +2,7 @@ defmodule Cassian.Utils.Voice do alias Nostrum.{Snowflake, Api, ConsumerGroup} alias Nostrum.Cache.GuildCache alias Cassian.Structs.Metadata + alias Nostrum.Struct.Guild require Logger @@ -24,7 +25,7 @@ defmodule Cassian.Utils.Voice do Logger.info("Joined voice chat on guild_id: #{inspect(guild_id)}") {:ok, :joined} after - 1_000 -> + 5_000 -> Logger.error("Failed to join voice chat on guild: #{guild_id}.") {:error, :failed_to_join} end @@ -74,22 +75,19 @@ defmodule Cassian.Utils.Voice do """ @spec sender_voice_id(interaction :: Nostrum.Struct.Interaction.t()) :: {:ok, {guild_id :: Snowflake.t(), channel_id :: Snowflake.t()}} | {:error, :not_in_voice} - def sender_voice_id(interaction) do - voice_id = - GuildCache.get!(interaction.guild_id) - |> Map.fetch!(:voice_states) - |> Enum.filter(fn state -> state.user_id == interaction.user.id end) - |> List.first() - |> extract_id() + def sender_voice_id(interaction = %Nostrum.Struct.Interaction{user: user, guild_id: guild_id}) do + Logger.debug("Checking if user is in voice: #{inspect(interaction)}") - if voice_id do - {:ok, {interaction.guild_id, voice_id}} + with {:ok, %Guild{voice_states: voice_states}} <- GuildCache.get(guild_id), + new_filter <- Enum.filter(voice_states, fn state -> state.user_id == user.id end), + voice_channel <- List.first(new_filter), + false <- is_nil(voice_channel) do + Logger.debug("User is present in voice chat: #{inspect(voice_channel)}") + {:ok, {guild_id, voice_channel.channel_id}} else - {:error, :not_in_voice} + _ -> + Logger.debug("User is not present in voice channel!") + {:error, :not_in_voice} end end - - defp extract_id(channel) do - channel[:channel_id] - end end From 0d72380332c8fed6b5875ce89a0b4243509b7f25 Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Mon, 15 Jan 2024 22:32:15 +0100 Subject: [PATCH 32/33] So-so working playlist controls Managed to make `playlist`, `next` and `previous` work with interactions. Need to see how to implement disapearring messages but it's mostly working. --- lib/cassian/commands/playback/next.ex | 23 +++++++++++++++++-- lib/cassian/commands/playback/playlist.ex | 27 ++++++++++++++--------- lib/cassian/commands/playback/previous.ex | 24 +++++++++++++++++--- lib/cassian/consumers/command.ex | 3 +++ lib/cassian/managers/play_manager.ex | 11 ++++----- lib/cassian/services/youtube.ex | 3 +++ 6 files changed, 71 insertions(+), 20 deletions(-) diff --git a/lib/cassian/commands/playback/next.ex b/lib/cassian/commands/playback/next.ex index 27ca971..40ac5a6 100644 --- a/lib/cassian/commands/playback/next.ex +++ b/lib/cassian/commands/playback/next.ex @@ -2,12 +2,31 @@ defmodule Cassian.Commands.Playback.Next do use Cassian.Behaviours.Command alias Cassian.Managers.PlayManager + + alias Nostrum.Struct.Embed + + def application_command_definition() do + %{ + name: "next", + description: "Play the next song." + } + end @doc """ Play the next song in the playlist. """ - def execute(message, _args) do - PlayManager.switch_song_with_notification(message, true) + def execute(interaction) do + PlayManager.switch_song_with_notification(interaction, true) + + {embed, flags} = + { + EmbedUtils.create_empty_embed!() + |> Embed.put_title("Changing to next song.") + |> Embed.put_description("It'll start playing soon."), + 1 <<< 6 + } + + %{type: 4, data: %{embeds: [embed], flags: flags}} end end diff --git a/lib/cassian/commands/playback/playlist.ex b/lib/cassian/commands/playback/playlist.ex index e2fe1c0..4d9b91a 100644 --- a/lib/cassian/commands/playback/playlist.ex +++ b/lib/cassian/commands/playback/playlist.ex @@ -3,31 +3,39 @@ defmodule Cassian.Commands.Playback.Playlist do alias Cassian.Structs.Playlist alias Cassian.Utils.Embed, as: EmbedUtils alias Nostrum.Struct.Embed - alias Cassian.Managers.MessageManager + + def application_command_definition() do + %{ + name: "playlist", + description: "Show the playlist." + } + end @doc """ Show info about the playlist. """ - def execute(message, _args) do - case Playlist.show(message.guild_id) do + def execute(interaction) do + {embed, flags} = + case Playlist.show(interaction.guild_id) do {:ok, playlist} -> - send_metadata(message, playlist) + {create_embed(playlist), 0} {:error, :noop} -> - send_not_playing(message) + {send_not_playing(), 0} end + + %{type: 4, data: %{embeds: [embed], flags: flags}} end - def send_not_playing(message) do + def send_not_playing() do EmbedUtils.create_empty_embed!() |> EmbedUtils.put_error_color_on_embed() |> Embed.put_title("I don't have any songs in the playlist.") |> Embed.put_description("I just don't have them. Give me songs first.") - |> MessageManager.send_dissapearing_embed(message.channel_id) end - def send_metadata(message, playlist) do + def create_embed(playlist) do {index, sorted} = playlist |> Playlist.order_playlist() @@ -61,10 +69,9 @@ defmodule Cassian.Commands.Playback.Playlist do |> Enum.with_index() |> Enum.reduce([], &filter(&1, &2, index)) |> Enum.join("\n") - + embed |> Embed.put_description(description) - |> MessageManager.send_embed(message.channel_id) end defp filter({metadata, current_index}, acc, index) do diff --git a/lib/cassian/commands/playback/previous.ex b/lib/cassian/commands/playback/previous.ex index 18ef569..b10002c 100644 --- a/lib/cassian/commands/playback/previous.ex +++ b/lib/cassian/commands/playback/previous.ex @@ -1,13 +1,31 @@ defmodule Cassian.Commands.Playback.Previous do use Cassian.Behaviours.Command - + alias Cassian.Managers.PlayManager + alias Nostrum.Struct.Embed\ + + def application_command_definition() do + %{ + name: "previous", + description: "Play the previous song." + } + end @doc """ Play the previous song. """ - def execute(message, _args) do - PlayManager.switch_song_with_notification(message, false) + def execute(interaction) do + PlayManager.switch_song_with_notification(interaction, false) + + {embed, flags} = + { + EmbedUtils.create_empty_embed!() + |> Embed.put_title("Changing to previous song.") + |> Embed.put_description("It'll start playing soon."), + 1 <<< 6 + } + + %{type: 4, data: %{embeds: [embed], flags: flags}} end end diff --git a/lib/cassian/consumers/command.ex b/lib/cassian/consumers/command.ex index 8cba51d..0750e08 100644 --- a/lib/cassian/consumers/command.ex +++ b/lib/cassian/consumers/command.ex @@ -57,6 +57,9 @@ defmodule Cassian.Consumers.Command do Nostrum.Api.create_guild_application_command(guild_id, Playback.Backward.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Forward.application_command_definition()) Nostrum.Api.create_guild_application_command(guild_id, Playback.Play.application_command_definition()) + Nostrum.Api.create_guild_application_command(guild_id, Playback.Previous.application_command_definition()) + Nostrum.Api.create_guild_application_command(guild_id, Playback.Next.application_command_definition()) + Nostrum.Api.create_guild_application_command(guild_id, Playback.Playlist.application_command_definition()) end defp associated_module(command) do diff --git a/lib/cassian/managers/play_manager.ex b/lib/cassian/managers/play_manager.ex index 519b7ff..c083063 100644 --- a/lib/cassian/managers/play_manager.ex +++ b/lib/cassian/managers/play_manager.ex @@ -3,6 +3,7 @@ defmodule Cassian.Managers.PlayManager do Manager for queues. """ + alias Nostrum.Struct.Interaction alias Nostrum.Snowflake alias Cassian.Structs.{VoiceState, Playlist} alias Cassian.Utils.Voice @@ -323,22 +324,22 @@ defmodule Cassian.Managers.PlayManager do @doc """ Switch to the next or previous song. Also send notification to the channel. """ - @spec switch_song_with_notification(message :: %Message{}, next :: boolean()) :: :ok | :noop - def switch_song_with_notification(message, next) do + @spec switch_song_with_notification(message :: Interaction.t(), next :: boolean()) :: :ok | :noop + def switch_song_with_notification(interaction, next) do title_part = if next, do: "next", else: "previous" - case switch_song(message.guild_id, next) do + case switch_song(interaction.guild_id, next) do :ok -> EmbedUtils.create_empty_embed!() |> Embed.put_title("Playing #{title_part} song.") - :noop -> + _ -> EmbedUtils.generate_error_embed( "There is no playlist.", "You can't change songs if there is no playlist." ) end - |> MessageManager.send_dissapearing_embed(message.channel_id) + |> MessageManager.send_dissapearing_embed(interaction.channel_id) end @doc """ diff --git a/lib/cassian/services/youtube.ex b/lib/cassian/services/youtube.ex index 31f0c1e..65e0984 100644 --- a/lib/cassian/services/youtube.ex +++ b/lib/cassian/services/youtube.ex @@ -29,6 +29,9 @@ defmodule Cassian.Services.Youtube do {:ok, metadata} else + {:ok, json} when is_map(json) -> + Logger.debug("The link is not a youtube one: #{inspect(json)}!") + {:error, :no_youtube_dl} {output, code} when is_integer(code) -> Logger.critical("youtube-dl failed with code: #{inspect(code)} and output: #{inspect(output)}!") {:error, :no_youtube_dl} From c4d9db6c807db1bd7b4c923f5246eb7b160b713d Mon Sep 17 00:00:00 2001 From: zastrixarundell Date: Tue, 16 Jan 2024 04:23:52 +0100 Subject: [PATCH 33/33] Add restart=always policy Basically I want for my bot to be restarted on every boot and such. --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index ee2c577..1bc48c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: context: . dockerfile: Dockerfile name: cassian-bot + restart: always environment: DISCORD_BOT_TOKEN: $DISCORD_BOT_TOKEN WEB_ENABLED: false