Discord notifs (#14)
* Add role id to config * Add discord notifications for games * Fix discord discriminant tests
This commit is contained in:
parent
324d041d5c
commit
fe5f5b77fc
@ -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/
|
@ -14,13 +14,20 @@ 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,
|
||||||
|
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"
|
||||||
|
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
@ -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"),
|
||||||
|
@ -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,
|
||||||
|
129
lib/chessh/discord/notifier.ex
Normal file
129
lib/chessh/discord/notifier.ex
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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: "logan", 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: "logan")
|
player = Repo.get_by(Player, username: "lizzy#0003")
|
||||||
|
|
||||||
assert Chessh.Auth.PasswordAuthenticator.authenticate(
|
assert Chessh.Auth.PasswordAuthenticator.authenticate(
|
||||||
player,
|
player,
|
||||||
|
@ -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: "logan", 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"
|
||||||
|
@ -4,17 +4,17 @@ defmodule Chessh.Schema.KeyTest do
|
|||||||
alias Chessh.Key
|
alias Chessh.Key
|
||||||
|
|
||||||
@valid_attrs %{
|
@valid_attrs %{
|
||||||
name: "Logan's Key",
|
name: "Lizzy's Key",
|
||||||
key:
|
key:
|
||||||
{{{:ECPoint,
|
{{{:ECPoint,
|
||||||
<<159, 246, 44, 226, 70, 24, 71, 127, 118, 17, 96, 71, 18, 121, 48, 203, 244, 140, 156,
|
<<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>>},
|
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 %{
|
@valid_key_attrs %{
|
||||||
name: "asdf key",
|
name: "asdf key",
|
||||||
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 %{
|
@invalid_key_attrs %{
|
||||||
name: "An Invalid Key",
|
name: "An Invalid Key",
|
||||||
|
@ -3,10 +3,10 @@ defmodule Chessh.Auth.UserRegistrationTest do
|
|||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
alias Chessh.{Player, Repo}
|
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_username %{username: "a", password: "password", discord_id: "7"}
|
||||||
@invalid_password %{username: "aasdf", password: "pass", discord_id: "6"}
|
@invalid_password %{username: "bruh#0003", password: "pass", discord_id: "6"}
|
||||||
@repeated_username %{username: "LoGan", password: "password", discord_id: "5"}
|
@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?
|
||||||
@ -40,7 +40,7 @@ defmodule Chessh.Auth.UserRegistrationTest do
|
|||||||
refute changeset.changes.hashed_password == @valid_user.password
|
refute changeset.changes.hashed_password == @valid_user.password
|
||||||
end
|
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 Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
|
||||||
|
|
||||||
assert {:error,
|
assert {:error,
|
||||||
|
@ -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: "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_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'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user