From 47d3f28f16bdd1eb7ad99b91f7963fd2c4b8f9bf Mon Sep 17 00:00:00 2001 From: Simponic Date: Wed, 1 Feb 2023 22:55:06 -0700 Subject: [PATCH] Discord threads (#16) * Add initial support for discord threads * Finish thread creation --- .env.example | 7 +- config/config.exs | 6 +- config/runtime.exs | 5 +- lib/chessh/discord/notifier.ex | 140 +++++++++++++++--- lib/chessh/schema/game.ex | 5 +- lib/chessh/ssh/client/game/game.ex | 19 ++- .../20230202004927_add_discord_thread_id.exs | 9 ++ 7 files changed, 153 insertions(+), 38 deletions(-) create mode 100644 priv/repo/migrations/20230202004927_add_discord_thread_id.exs diff --git a/.env.example b/.env.example index 51168fc..6636fb9 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ SERVER_REDIRECT_URI=http://127.0.0.1:3000/api/oauth/redirect DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= DISCORD_USER_AGENT= +DISCORD_BOT_TOKEN= JWT_SECRET=aVerySecretJwtSigningSecret @@ -27,6 +28,6 @@ REACT_APP_SSH_PORT=42069 REDIS_HOST=localhost REDIS_PORT=6379 -NEW_GAME_PINGABLE_ROLE_ID=1123232 -NEW_GAME_CHANNEL_WEBHOOK=https://discordapp.com/api/webhooks/ -REMIND_MOVE_CHANNEL_WEBHOOK=https://discordapp.com/api/webhooks/ \ No newline at end of file +NEW_GAME_PINGABLE_ROLE_ID=10 +REMIND_MOVE_CHANNEL_ID=91 +NEW_GAME_CHANNEL_ID=91 diff --git a/config/config.exs b/config/config.exs index e732049..5ada6e6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -15,7 +15,7 @@ config :chessh, RateLimits, player_public_keys: 15, create_game_ms: 60 * 1000, create_game_rate: 3, - discord_notification_rate: 3, + discord_notification_rate: 30, discord_notification_rate_ms: 1000 config :chessh, Web, @@ -24,8 +24,8 @@ config :chessh, Web, discord_scope: "identify" config :chessh, DiscordNotifications, - game_move_notif_delay_ms: 3 * 60 * 1000, - game_created_notif_delay_ms: 30 * 1000, + game_move_notif_delay_ms: 10 * 1000, + game_created_notif_delay_ms: 10 * 1000, reschedule_delay: 5 * 1000 config :joken, default_signer: "secret" diff --git a/config/runtime.exs b/config/runtime.exs index 684e48e..5da6d47 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -5,8 +5,9 @@ config :chessh, config :chessh, DiscordNotifications, looking_for_games_role_mention: "<@&#{System.get_env("NEW_GAME_PINGABLE_ROLE_ID")}>", - discord_game_move_notif_webhook: System.get_env("REMIND_MOVE_CHANNEL_WEBHOOK"), - discord_new_game_notif_webhook: System.get_env("NEW_GAME_CHANNEL_WEBHOOK") + remind_move_channel_id: System.get_env("REMIND_MOVE_CHANNEL_ID"), + discord_bot_token: System.get_env("DISCORD_BOT_TOKEN"), + new_game_channel_id: System.get_env("NEW_GAME_CHANNEL_ID") config :chessh, Web, discord_client_id: System.get_env("DISCORD_CLIENT_ID"), diff --git a/lib/chessh/discord/notifier.ex b/lib/chessh/discord/notifier.ex index 6d7bd46..02b7c5f 100644 --- a/lib/chessh/discord/notifier.ex +++ b/lib/chessh/discord/notifier.ex @@ -28,7 +28,7 @@ defmodule Chessh.DiscordNotifier do case Hammer.check_rate_inc( :redis, - "discord-webhook-message-rate", + "discord-rate", discord_notification_rate_ms, discord_notification_rate, 1 @@ -50,9 +50,9 @@ defmodule Chessh.DiscordNotifier do end defp send_notification({:move_reminder, game_id}) do - [min_delta_t, discord_game_move_notif_webhook] = + [min_delta_t, remind_move_channel_id] = Application.get_env(:chessh, DiscordNotifications) - |> Keyword.take([:game_move_notif_delay_ms, :discord_game_move_notif_webhook]) + |> Keyword.take([:game_move_notif_delay_ms, :remind_move_channel_id]) |> Keyword.values() case Repo.get(Game, game_id) |> Repo.preload([:dark_player, :light_player]) do @@ -62,13 +62,27 @@ defmodule Chessh.DiscordNotifier do turn: turn, updated_at: last_updated, moves: move_count, - status: :continue - } -> + status: :continue, + discord_thread_id: discord_thread_id + } = game -> delta_t = NaiveDateTime.diff(NaiveDateTime.utc_now(), last_updated, :millisecond) + game = + if is_nil(discord_thread_id) do + {:ok, game} = + Game.changeset(game, %{ + discord_thread_id: make_private_discord_thread_id(remind_move_channel_id, game) + }) + |> Repo.update() + + game + else + game + end + if delta_t >= min_delta_t do post_discord( - discord_game_move_notif_webhook, + game.discord_thread_id, "<@#{if turn == :light, do: light_player_discord_id, else: dark_player_discord_id}> it is your move in Game #{game_id} (move #{move_count})." ) end @@ -78,16 +92,33 @@ defmodule Chessh.DiscordNotifier do end end + defp send_notification({:cleanup_thread, game_id}) do + case Repo.get(Game, game_id) |> Repo.preload([:dark_player, :light_player]) do + %Game{ + discord_thread_id: discord_thread_id, + status: status + } = game -> + if !is_nil(discord_thread_id) && status != :continue do + destroy_channel(discord_thread_id) + + Game.changeset(game, %{ + discord_thread_id: nil + }) + |> Repo.update() + end + + _ -> + nil + end + end + defp send_notification({:game_created, game_id}) do - [pingable_mention, discord_game_created_notif_webhook] = + [pingable_mention, new_game_channel_id] = Application.get_env(:chessh, DiscordNotifications) - |> Keyword.take([:looking_for_games_role_mention, :discord_new_game_notif_webhook]) + |> Keyword.take([:looking_for_games_role_mention, :new_game_channel_id]) |> Keyword.values() case Repo.get(Game, game_id) do - nil -> - nil - game -> %Game{ dark_player: dark_player, @@ -107,22 +138,85 @@ defmodule Chessh.DiscordNotifier do end if message do - post_discord(discord_game_created_notif_webhook, message) + post_discord(new_game_channel_id, message) end + + nil -> + nil end end - defp post_discord(webhook, message) do - :httpc.request( - :post, - { - String.to_charlist(webhook), + defp make_private_discord_thread_id(channel_id, %Game{ + id: game_id, + dark_player: %Player{discord_id: dark_player_discord_id, username: dark_username}, + light_player: %Player{discord_id: light_player_discord_id, username: light_username} + }) do + case make_discord_api_call( + :post, + "channels/#{channel_id}/threads", + %{ + # Private thread + type: 12, + name: "Game #{game_id} - #{light_username} V #{dark_username}" + } + ) do + {:ok, {_, _, body}} -> + %{"id" => thread_id} = Jason.decode!(body) + + [light_player_discord_id, dark_player_discord_id] + |> Enum.map(fn id -> + make_discord_api_call(:put, 'channels/#{thread_id}/thread-members/#{id}') + end) + + thread_id + + _ -> + nil + end + end + + defp post_discord(channel_id, message) do + make_discord_api_call(:post, "channels/#{channel_id}/messages", %{content: message}) + end + + defp destroy_channel(channel_id) do + make_discord_api_call(:delete, "channels/#{channel_id}") + end + + defp make_discord_api_call(method, route), + do: + :httpc.request( + method, + { + 'https://discord.com/api/#{route}', + [ + make_authorization_header() + ] + }, [], - 'application/json', - %{content: message} |> Jason.encode!() |> String.to_charlist() - }, - [], - [] - ) + [] + ) + + defp make_discord_api_call(method, route, body), + do: + :httpc.request( + method, + { + 'https://discord.com/api/#{route}', + [ + make_authorization_header() + ], + 'application/json', + body + |> Jason.encode!() + |> String.to_charlist() + }, + [], + [] + ) + + defp make_authorization_header() do + bot_token = Application.get_env(:chessh, DiscordNotifications)[:discord_bot_token] + {'Authorization', 'Bot #{bot_token}'} end end diff --git a/lib/chessh/schema/game.ex b/lib/chessh/schema/game.ex index b7893f1..55b9ea4 100644 --- a/lib/chessh/schema/game.ex +++ b/lib/chessh/schema/game.ex @@ -15,6 +15,8 @@ defmodule Chessh.Game do belongs_to(:light_player, Player, foreign_key: :light_player_id) belongs_to(:dark_player, Player, foreign_key: :dark_player_id) + field(:discord_thread_id, :string) + timestamps() end @@ -28,7 +30,8 @@ defmodule Chessh.Game do :status, :last_move, :light_player_id, - :dark_player_id + :dark_player_id, + :discord_thread_id ]) end end diff --git a/lib/chessh/ssh/client/game/game.ex b/lib/chessh/ssh/client/game/game.ex index d6b0b5b..6b2ef60 100644 --- a/lib/chessh/ssh/client/game/game.ex +++ b/lib/chessh/ssh/client/game/game.ex @@ -396,7 +396,7 @@ defmodule Chessh.SSH.Client.Game do {:ok, status} -> {:ok, fen} = :binbo.get_fen(binbo_pid) - {:ok, _new_game} = + {:ok, %Game{status: after_move_status}} = game |> Game.changeset( Map.merge( @@ -413,11 +413,18 @@ defmodule Chessh.SSH.Client.Game do :syn.publish(:games, {:game, game_id}, {:new_move, attempted_move}) - GenServer.cast( - :discord_notifier, - {:schedule_notification, {:move_reminder, game_id}, - Application.get_env(:chessh, DiscordNotifications)[:game_move_notif_delay_ms]} - ) + if after_move_status == :continue do + GenServer.cast( + :discord_notifier, + {:schedule_notification, {:move_reminder, game_id}, + Application.get_env(:chessh, DiscordNotifications)[:game_move_notif_delay_ms]} + ) + else + GenServer.cast( + :discord_notifier, + {:schedule_notification, {:cleanup_thread, game_id}, 0} + ) + end _ -> nil diff --git a/priv/repo/migrations/20230202004927_add_discord_thread_id.exs b/priv/repo/migrations/20230202004927_add_discord_thread_id.exs new file mode 100644 index 0000000..8c7c83a --- /dev/null +++ b/priv/repo/migrations/20230202004927_add_discord_thread_id.exs @@ -0,0 +1,9 @@ +defmodule Chessh.Repo.Migrations.AddDiscordThreadId do + use Ecto.Migration + + def change do + alter table(:games) do + add(:discord_thread_id, :string, null: true) + end + end +end