Discord notifs #14

Merged
Simponic merged 4 commits from discord_notifs into main 2023-02-01 16:57:14 -05:00
11 changed files with 166 additions and 11 deletions
Showing only changes of commit b09dd7d90f - Show all commits

View File

@ -26,3 +26,7 @@ REACT_APP_SSH_PORT=42069
REDIS_HOST=localhost REDIS_HOST=localhost
REDIS_PORT=6379 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/

View File

@ -14,14 +14,19 @@ config :chessh, RateLimits,
player_session_message_burst_rate: 8, player_session_message_burst_rate: 8,
player_public_keys: 15, player_public_keys: 15,
create_game_ms: 60 * 1000, 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, config :chessh, Web,
discord_oauth_login_url: "https://discord.com/api/oauth2/token", discord_oauth_login_url: "https://discord.com/api/oauth2/token",
discord_user_api_url: "https://discord.com/api/users/@me", discord_user_api_url: "https://discord.com/api/users/@me",
discord_scope: "identify" discord_scope: "identify"
config :chessh, DiscordNotifications, looking_for_games_mention: "<@&1070084105796075550>" 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" config :joken, default_signer: "secret"

View File

@ -3,6 +3,11 @@ import Config
config :chessh, config :chessh,
ssh_port: String.to_integer(System.get_env("SSH_PORT", "34355")) 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, config :chessh, Web,
discord_client_id: System.get_env("DISCORD_CLIENT_ID"), discord_client_id: System.get_env("DISCORD_CLIENT_ID"),
discord_client_secret: System.get_env("DISCORD_CLIENT_SECRET"), discord_client_secret: System.get_env("DISCORD_CLIENT_SECRET"),

View File

@ -18,6 +18,7 @@ defmodule Chessh.Application do
children = [ children = [
Chessh.Repo, Chessh.Repo,
Chessh.SSH.Daemon, Chessh.SSH.Daemon,
Chessh.DiscordNotifier,
Plug.Cowboy.child_spec( Plug.Cowboy.child_spec(
scheme: :http, scheme: :http,
plug: Chessh.Web.Endpoint, plug: Chessh.Web.Endpoint,

View File

@ -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

View File

@ -77,7 +77,7 @@ defmodule Chessh.SSH.Client.Game do
) do ) do
{:allow, _count} -> {:allow, _count} ->
# Starting a new game # Starting a new game
{:ok, %Game{} = game} = {:ok, %Game{id: game_id} = game} =
Game.changeset( Game.changeset(
%Game{}, %Game{},
Map.merge( Map.merge(
@ -92,6 +92,12 @@ defmodule Chessh.SSH.Client.Game do
) )
|> Repo.insert() |> Repo.insert()
GenServer.cast(
:discord_notifier,
{:schedule_notification, {:game_created, game_id},
Application.get_env(:chessh, DiscordNotifications)[:game_created_notif_delay_ms]}
)
init([ init([
%State{ %State{
state state
@ -403,6 +409,12 @@ defmodule Chessh.SSH.Client.Game do
:syn.publish(:games, {:game, game_id}, {:new_move, attempted_move}) :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 nil
end end

View File

@ -2,11 +2,9 @@ defmodule Chessh.Repo.Migrations.CreatePlayer do
use Ecto.Migration use Ecto.Migration
def change do def change do
execute("CREATE EXTENSION IF NOT EXISTS citext", "")
create table(:players) do create table(:players) do
add(:discord_id, :string, null: false) add(:discord_id, :string, null: false)
add(:username, :citext, null: false) add(:username, :string, null: false)
add(:hashed_password, :string, null: true) add(:hashed_password, :string, null: true)
timestamps() timestamps()
end end

View File

@ -2,7 +2,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
use ExUnit.Case use ExUnit.Case
alias Chessh.{Player, Repo} alias Chessh.{Player, Repo}
@valid_user %{username: "lizzy", password: "password", discord_id: "1"} @valid_user %{username: "lizzy#0003", password: "password", discord_id: "1"}
setup_all do setup_all do
Ecto.Adapters.SQL.Sandbox.checkout(Repo) Ecto.Adapters.SQL.Sandbox.checkout(Repo)
@ -26,7 +26,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
end end
test "Password can authenticate a user instance" do test "Password can authenticate a user instance" do
player = Repo.get_by(Player, username: "lizzy") player = Repo.get_by(Player, username: "lizzy#0003")
assert Chessh.Auth.PasswordAuthenticator.authenticate( assert Chessh.Auth.PasswordAuthenticator.authenticate(
player, player,

View File

@ -2,7 +2,7 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
use ExUnit.Case use ExUnit.Case
alias Chessh.{Key, Repo, Player} alias Chessh.{Key, Repo, Player}
@valid_user %{username: "lizzy", password: "password", discord_id: "2"} @valid_user %{username: "lizzy#0003", password: "password", discord_id: "2"}
@valid_key %{ @valid_key %{
name: "The Gamer Machine", name: "The Gamer Machine",
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44" key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"

View File

@ -5,7 +5,8 @@ defmodule Chessh.Auth.UserRegistrationTest do
@valid_user %{username: "lizzy#0003", 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_username %{username: "a", password: "password", discord_id: "7"}
@invalid_password %{username: "lizzy#0003", password: "pass", discord_id: "6"} @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 test "Password must be at least 8 characters and username must be at least 2" do
refute Player.registration_changeset(%Player{}, @invalid_password).valid? refute Player.registration_changeset(%Player{}, @invalid_password).valid?

View File

@ -5,7 +5,7 @@ defmodule Chessh.SSH.AuthTest do
@localhost '127.0.0.1' @localhost '127.0.0.1'
@localhost_inet {{127, 0, 0, 1}, 1} @localhost_inet {{127, 0, 0, 1}, 1}
@key_name "The Gamer Machine" @key_name "The Gamer Machine"
@valid_user %{username: "lizzy", 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_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
@client_pub_key 'id_ed25519.pub' @client_pub_key 'id_ed25519.pub'