summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimponic <loganhunt@simponic.xyz>2023-02-01 14:57:14 -0700
committerGitHub <noreply@github.com>2023-02-01 14:57:14 -0700
commitfe5f5b77fcc3ef24516866561f9b54ac07663ad6 (patch)
treeba63998ec306983d87594195d60ecb0abb6ea5d1
parent324d041d5c5cbcdf0083dcd802144a57443789f6 (diff)
downloadchessh-fe5f5b77fcc3ef24516866561f9b54ac07663ad6.tar.gz
chessh-fe5f5b77fcc3ef24516866561f9b54ac07663ad6.zip
Discord notifs (#14)
* Add role id to config * Add discord notifications for games * Fix discord discriminant tests
-rw-r--r--.env.example4
-rw-r--r--config/config.exs9
-rw-r--r--config/runtime.exs5
-rw-r--r--lib/chessh/application.ex1
-rw-r--r--lib/chessh/discord/notifier.ex129
-rw-r--r--lib/chessh/ssh/client/game/game.ex14
-rw-r--r--priv/repo/migrations/20221219082326_create_player.exs4
-rw-r--r--test/auth/password_test.exs4
-rw-r--r--test/auth/pubkey_test.exs2
-rw-r--r--test/schema/key_test.exs6
-rw-r--r--test/schema/register_test.exs8
-rw-r--r--test/ssh/ssh_auth_test.exs2
12 files changed, 172 insertions, 16 deletions
diff --git a/.env.example b/.env.example
index f27a541..51168fc 100644
--- a/.env.example
+++ b/.env.example
@@ -26,3 +26,7 @@ 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
diff --git a/config/config.exs b/config/config.exs
index c19937a..e732049 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -14,13 +14,20 @@ config :chessh, RateLimits,
player_session_message_burst_rate: 8,
player_public_keys: 15,
create_game_ms: 60 * 1000,
- create_game_rate: 3
+ create_game_rate: 3,
+ discord_notification_rate: 3,
+ discord_notification_rate_ms: 1000
config :chessh, Web,
discord_oauth_login_url: "https://discord.com/api/oauth2/token",
discord_user_api_url: "https://discord.com/api/users/@me",
discord_scope: "identify"
+config :chessh, DiscordNotifications,
+ game_move_notif_delay_ms: 3 * 60 * 1000,
+ game_created_notif_delay_ms: 30 * 1000,
+ reschedule_delay: 5 * 1000
+
config :joken, default_signer: "secret"
import_config "#{config_env()}.exs"
diff --git a/config/runtime.exs b/config/runtime.exs
index 5e03614..684e48e 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -3,6 +3,11 @@ import Config
config :chessh,
ssh_port: String.to_integer(System.get_env("SSH_PORT", "34355"))
+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")
+
config :chessh, Web,
discord_client_id: System.get_env("DISCORD_CLIENT_ID"),
discord_client_secret: System.get_env("DISCORD_CLIENT_SECRET"),
diff --git a/lib/chessh/application.ex b/lib/chessh/application.ex
index 59926cc..f92707f 100644
--- a/lib/chessh/application.ex
+++ b/lib/chessh/application.ex
@@ -18,6 +18,7 @@ defmodule Chessh.Application do
children = [
Chessh.Repo,
Chessh.SSH.Daemon,
+ Chessh.DiscordNotifier,
Plug.Cowboy.child_spec(
scheme: :http,
plug: Chessh.Web.Endpoint,
diff --git a/lib/chessh/discord/notifier.ex b/lib/chessh/discord/notifier.ex
new file mode 100644
index 0000000..09c4ec0
--- /dev/null
+++ b/lib/chessh/discord/notifier.ex
@@ -0,0 +1,129 @@
+defmodule Chessh.DiscordNotifier do
+ use GenServer
+
+ @name :discord_notifier
+
+ alias Chessh.{Game, Player, Repo}
+
+ def start_link(state \\ []) do
+ GenServer.start_link(__MODULE__, state, name: @name)
+ end
+
+ @impl true
+ def init(state) do
+ {:ok, state}
+ end
+
+ @impl true
+ def handle_cast(x, state), do: handle_info(x, state)
+
+ @impl true
+ def handle_info({:attempt_notification, notification} = body, state) do
+ [discord_notification_rate, discord_notification_rate_ms] =
+ Application.get_env(:chessh, RateLimits)
+ |> Keyword.take([:discord_notification_rate, :discord_notification_rate_ms])
+ |> Keyword.values()
+
+ reschedule_delay = Application.get_env(:chessh, RateLimits)[:reschedule_delay]
+
+ case Hammer.check_rate_inc(
+ :redis,
+ "discord-webhook-message-rate",
+ discord_notification_rate_ms,
+ discord_notification_rate,
+ 1
+ ) do
+ {:allow, _count} ->
+ send_notification(notification)
+
+ {:deny, _limit} ->
+ Process.send_after(self(), body, reschedule_delay)
+ end
+
+ {:noreply, state}
+ end
+
+ @impl true
+ def handle_info({:schedule_notification, notification, delay}, state) do
+ Process.send_after(self(), {:attempt_notification, notification}, delay)
+ {:noreply, state}
+ end
+
+ defp send_notification({:move_reminder, game_id}) do
+ [min_delta_t, discord_game_move_notif_webhook] =
+ Application.get_env(:chessh, DiscordNotifications)
+ |> Keyword.take([:game_move_notif_delay_ms, :discord_game_move_notif_webhook])
+ |> Keyword.values()
+
+ case Repo.get(Game, game_id) do
+ nil ->
+ nil
+
+ game ->
+ %Game{
+ dark_player: %Player{discord_id: dark_player_discord_id},
+ light_player: %Player{discord_id: light_player_discord_id},
+ turn: turn,
+ updated_at: last_updated,
+ moves: move_count
+ } = Repo.preload(game, [:dark_player, :light_player])
+
+ delta_t = NaiveDateTime.diff(NaiveDateTime.utc_now(), last_updated, :millisecond)
+
+ if delta_t >= min_delta_t do
+ post_discord(
+ discord_game_move_notif_webhook,
+ "<@#{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
+ end
+ end
+
+ defp send_notification({:game_created, game_id}) do
+ [pingable_mention, discord_game_created_notif_webhook] =
+ Application.get_env(:chessh, DiscordNotifications)
+ |> Keyword.take([:looking_for_games_role_mention, :discord_new_game_notif_webhook])
+ |> Keyword.values()
+
+ case Repo.get(Game, game_id) do
+ nil ->
+ nil
+
+ game ->
+ %Game{
+ dark_player: dark_player,
+ light_player: light_player
+ } = Repo.preload(game, [:dark_player, :light_player])
+
+ message =
+ case {is_nil(light_player), is_nil(dark_player)} do
+ {true, false} ->
+ "#{pingable_mention}, <@#{dark_player.discord_id}> is looking for an opponent to play as light in Game #{game_id}"
+
+ {false, true} ->
+ "#{pingable_mention}, <@#{light_player.discord_id}> is looking for an opponent to play as dark in Game #{game_id}"
+
+ _ ->
+ false
+ end
+
+ if message do
+ post_discord(discord_game_created_notif_webhook, message)
+ end
+ end
+ end
+
+ defp post_discord(webhook, message) do
+ :httpc.request(
+ :post,
+ {
+ String.to_charlist(webhook),
+ [],
+ 'application/json',
+ %{content: message} |> Jason.encode!() |> String.to_charlist()
+ },
+ [],
+ []
+ )
+ end
+end
diff --git a/lib/chessh/ssh/client/game/game.ex b/lib/chessh/ssh/client/game/game.ex
index 4a79d05..fc48d6f 100644
--- a/lib/chessh/ssh/client/game/game.ex
+++ b/lib/chessh/ssh/client/game/game.ex
@@ -77,7 +77,7 @@ defmodule Chessh.SSH.Client.Game do
) do
{:allow, _count} ->
# Starting a new game
- {:ok, %Game{} = game} =
+ {:ok, %Game{id: game_id} = game} =
Game.changeset(
%Game{},
Map.merge(
@@ -92,6 +92,12 @@ defmodule Chessh.SSH.Client.Game do
)
|> Repo.insert()
+ GenServer.cast(
+ :discord_notifier,
+ {:schedule_notification, {:game_created, game_id},
+ Application.get_env(:chessh, DiscordNotifications)[:game_created_notif_delay_ms]}
+ )
+
init([
%State{
state
@@ -403,6 +409,12 @@ 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]}
+ )
+
_ ->
nil
end
diff --git a/priv/repo/migrations/20221219082326_create_player.exs b/priv/repo/migrations/20221219082326_create_player.exs
index 0e605c9..2b7d2b6 100644
--- a/priv/repo/migrations/20221219082326_create_player.exs
+++ b/priv/repo/migrations/20221219082326_create_player.exs
@@ -2,11 +2,9 @@ defmodule Chessh.Repo.Migrations.CreatePlayer do
use Ecto.Migration
def change do
- execute("CREATE EXTENSION IF NOT EXISTS citext", "")
-
create table(:players) do
add(:discord_id, :string, null: false)
- add(:username, :citext, null: false)
+ add(:username, :string, null: false)
add(:hashed_password, :string, null: true)
timestamps()
end
diff --git a/test/auth/password_test.exs b/test/auth/password_test.exs
index 4072293..86c3758 100644
--- a/test/auth/password_test.exs
+++ b/test/auth/password_test.exs
@@ -2,7 +2,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
use ExUnit.Case
alias Chessh.{Player, Repo}
- @valid_user %{username: "logan", password: "password", discord_id: "1"}
+ @valid_user %{username: "lizzy#0003", password: "password", discord_id: "1"}
setup_all do
Ecto.Adapters.SQL.Sandbox.checkout(Repo)
@@ -26,7 +26,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
end
test "Password can authenticate a user instance" do
- player = Repo.get_by(Player, username: "logan")
+ player = Repo.get_by(Player, username: "lizzy#0003")
assert Chessh.Auth.PasswordAuthenticator.authenticate(
player,
diff --git a/test/auth/pubkey_test.exs b/test/auth/pubkey_test.exs
index 690dfdf..97a1695 100644
--- a/test/auth/pubkey_test.exs
+++ b/test/auth/pubkey_test.exs
@@ -2,7 +2,7 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
use ExUnit.Case
alias Chessh.{Key, Repo, Player}
- @valid_user %{username: "logan", password: "password", discord_id: "2"}
+ @valid_user %{username: "lizzy#0003", password: "password", discord_id: "2"}
@valid_key %{
name: "The Gamer Machine",
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"
diff --git a/test/schema/key_test.exs b/test/schema/key_test.exs
index 6dbb574..4b62d7f 100644
--- a/test/schema/key_test.exs
+++ b/test/schema/key_test.exs
@@ -4,17 +4,17 @@ defmodule Chessh.Schema.KeyTest do
alias Chessh.Key
@valid_attrs %{
- name: "Logan's Key",
+ name: "Lizzy's Key",
key:
{{{:ECPoint,
<<159, 246, 44, 226, 70, 24, 71, 127, 118, 17, 96, 71, 18, 121, 48, 203, 244, 140, 156,
56, 179, 138, 64, 242, 169, 140, 109, 156, 174, 148, 222, 56>>},
- {:namedCurve, {1, 3, 101, 112}}}, [comment: 'logan@yagami']}
+ {:namedCurve, {1, 3, 101, 112}}}, [comment: 'lizzy@yagami']}
}
@valid_key_attrs %{
name: "asdf key",
key:
- "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC7Mpf2QIL32MmKxcrXAoZM3l7/hBy+8d+WqTRMun+tC/XYNiXSIDuZv01an3D1d22fmSpZiprFQzjB4yEz23qw= logan@yagami"
+ "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC7Mpf2QIL32MmKxcrXAoZM3l7/hBy+8d+WqTRMun+tC/XYNiXSIDuZv01an3D1d22fmSpZiprFQzjB4yEz23qw= lizzy@yagami"
}
@invalid_key_attrs %{
name: "An Invalid Key",
diff --git a/test/schema/register_test.exs b/test/schema/register_test.exs
index 6d32769..40dc210 100644
--- a/test/schema/register_test.exs
+++ b/test/schema/register_test.exs
@@ -3,10 +3,10 @@ defmodule Chessh.Auth.UserRegistrationTest do
use ExUnit.Case
alias Chessh.{Player, Repo}
- @valid_user %{username: "logan", password: "password", discord_id: "4"}
+ @valid_user %{username: "lizzy#0003", password: "password", discord_id: "4"}
@invalid_username %{username: "a", password: "password", discord_id: "7"}
- @invalid_password %{username: "aasdf", password: "pass", discord_id: "6"}
- @repeated_username %{username: "LoGan", password: "password", discord_id: "5"}
+ @invalid_password %{username: "bruh#0003", password: "pass", discord_id: "6"}
+ @repeated_username %{username: "lizzy#0003", password: "password", discord_id: "6"}
test "Password must be at least 8 characters and username must be at least 2" do
refute Player.registration_changeset(%Player{}, @invalid_password).valid?
@@ -40,7 +40,7 @@ defmodule Chessh.Auth.UserRegistrationTest do
refute changeset.changes.hashed_password == @valid_user.password
end
- test "Username is uniquely case insensitive" do
+ test "Username is uniquely case sensitive" do
assert Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
assert {:error,
diff --git a/test/ssh/ssh_auth_test.exs b/test/ssh/ssh_auth_test.exs
index 024a54f..ab6f827 100644
--- a/test/ssh/ssh_auth_test.exs
+++ b/test/ssh/ssh_auth_test.exs
@@ -5,7 +5,7 @@ defmodule Chessh.SSH.AuthTest do
@localhost '127.0.0.1'
@localhost_inet {{127, 0, 0, 1}, 1}
@key_name "The Gamer Machine"
- @valid_user %{username: "logan", password: "password", discord_id: "3"}
+ @valid_user %{username: "lizzy#0003", password: "password", discord_id: "3"}
@client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
@client_pub_key 'id_ed25519.pub'