From fcabcdb5cb463f15d036169317f77bd7b2043ac8 Mon Sep 17 00:00:00 2001 From: Logan Hunt Date: Wed, 1 Feb 2023 18:23:23 -0700 Subject: [PATCH 1/2] Add initial support for discord threads --- .env.example | 3 +- config/config.exs | 2 +- config/runtime.exs | 5 +- lib/chessh/discord/notifier.ex | 73 ++++++++++++++++--- lib/chessh/schema/game.ex | 5 +- .../20230202004927_add_discord_thread_id.exs | 7 ++ 6 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 priv/repo/migrations/20230202004927_add_discord_thread_id.exs diff --git a/.env.example b/.env.example index 51168fc..11ca42e 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 @@ -29,4 +30,4 @@ 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 +REMIND_MOVE_CHANNEL_ID= diff --git a/config/config.exs b/config/config.exs index e732049..2c08a17 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, 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..c19495c 100644 --- a/lib/chessh/discord/notifier.ex +++ b/lib/chessh/discord/notifier.ex @@ -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,22 @@ defmodule Chessh.DiscordNotifier do turn: turn, updated_at: last_updated, moves: move_count, - status: :continue - } -> + status: :continue, + discord_thread_id: discord_thread_id + } = 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() + end + delta_t = NaiveDateTime.diff(NaiveDateTime.utc_now(), last_updated, :millisecond) 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 @@ -79,9 +88,9 @@ defmodule Chessh.DiscordNotifier do 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 @@ -107,16 +116,16 @@ defmodule Chessh.DiscordNotifier do end if message do - post_discord(discord_game_created_notif_webhook, message) + post_discord(new_game_channel_id, message) end end end - defp post_discord(webhook, message) do + defp post_discord(channel_id, message) do :httpc.request( :post, { - String.to_charlist(webhook), + 'https://discord.com/api/channels/#{channel_id}/messages', [], 'application/json', %{content: message} |> Jason.encode!() |> String.to_charlist() @@ -125,4 +134,48 @@ defmodule Chessh.DiscordNotifier do [] ) end + + defp make_private_discord_thread_id(channel_id, %Game{ + dark_player: %Player{discord_id: dark_player_discord_id}, + light_player: %Player{discord_id: light_player_discord_id} + }) do + %{"id" => thread_id} = + :httpc.request( + :post, + { + 'https://discord.com/api/channels/#{channel_id}/threads', + [ + make_authorization_header() + ], + 'application/json', + %{ + # Private thread + type: 12 + } + |> Jason.encode!() + |> String.to_charlist() + }, + [], + [] + ) + |> Jason.decode!() + + [light_player_discord_id, dark_player_discord_id] + |> Enum.map(fn id -> + :httpc.request( + :post, + { + 'https://discord.com/api/channels/#{thread_id}/thread-members/#{id}', + [] + } + ) + end) + + thread_id + end + + 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/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..fadb8f1 --- /dev/null +++ b/priv/repo/migrations/20230202004927_add_discord_thread_id.exs @@ -0,0 +1,7 @@ +defmodule Chessh.Repo.Migrations.AddDiscordThreadId do + use Ecto.Migration + + def change do + + end +end -- 2.45.2 From 329291a0760f1ee5e342f5fef72f07ee33c10df8 Mon Sep 17 00:00:00 2001 From: Simponic Date: Wed, 1 Feb 2023 22:50:59 -0700 Subject: [PATCH 2/2] Finish thread creation --- .env.example | 6 +- config/config.exs | 4 +- lib/chessh/discord/notifier.ex | 139 ++++++++++++------ lib/chessh/ssh/client/game/game.ex | 19 ++- .../20230202004927_add_discord_thread_id.exs | 4 +- 5 files changed, 111 insertions(+), 61 deletions(-) diff --git a/.env.example b/.env.example index 11ca42e..6636fb9 100644 --- a/.env.example +++ b/.env.example @@ -28,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_ID= +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 2c08a17..5ada6e6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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/lib/chessh/discord/notifier.ex b/lib/chessh/discord/notifier.ex index c19495c..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 @@ -65,16 +65,21 @@ defmodule Chessh.DiscordNotifier do status: :continue, discord_thread_id: discord_thread_id } = 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() - end - 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( game.discord_thread_id, @@ -87,6 +92,26 @@ 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, new_game_channel_id] = Application.get_env(:chessh, DiscordNotifications) @@ -94,9 +119,6 @@ defmodule Chessh.DiscordNotifier do |> Keyword.values() case Repo.get(Game, game_id) do - nil -> - nil - game -> %Game{ dark_player: dark_player, @@ -118,61 +140,80 @@ defmodule Chessh.DiscordNotifier do if message do post_discord(new_game_channel_id, message) end + + nil -> + nil + end + end + + 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 - :httpc.request( - :post, - { - 'https://discord.com/api/channels/#{channel_id}/messages', - [], - 'application/json', - %{content: message} |> Jason.encode!() |> String.to_charlist() - }, - [], - [] - ) + make_discord_api_call(:post, "channels/#{channel_id}/messages", %{content: message}) end - defp make_private_discord_thread_id(channel_id, %Game{ - dark_player: %Player{discord_id: dark_player_discord_id}, - light_player: %Player{discord_id: light_player_discord_id} - }) do - %{"id" => thread_id} = + 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( - :post, + method, { - 'https://discord.com/api/channels/#{channel_id}/threads', + 'https://discord.com/api/#{route}', + [ + make_authorization_header() + ] + }, + [], + [] + ) + + defp make_discord_api_call(method, route, body), + do: + :httpc.request( + method, + { + 'https://discord.com/api/#{route}', [ make_authorization_header() ], 'application/json', - %{ - # Private thread - type: 12 - } + body |> Jason.encode!() |> String.to_charlist() }, [], [] ) - |> Jason.decode!() - - [light_player_discord_id, dark_player_discord_id] - |> Enum.map(fn id -> - :httpc.request( - :post, - { - 'https://discord.com/api/channels/#{thread_id}/thread-members/#{id}', - [] - } - ) - end) - - thread_id - end defp make_authorization_header() do bot_token = Application.get_env(:chessh, DiscordNotifications)[:discord_bot_token] 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 index fadb8f1..8c7c83a 100644 --- a/priv/repo/migrations/20230202004927_add_discord_thread_id.exs +++ b/priv/repo/migrations/20230202004927_add_discord_thread_id.exs @@ -2,6 +2,8 @@ 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 -- 2.45.2