request limiter

This commit is contained in:
Owen
2024-12-15 22:34:59 +01:00
parent 7cf2e79c3e
commit 81c95c28b2
12 changed files with 177 additions and 48 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import Config
config :plantid_discord_bot,
start_time: DateTime.utc_now()
start_time: DateTime.utc_now(),
image_folder: "priv/static"
import_config "#{config_env()}.exs"
+1 -1
View File
@@ -1,10 +1,10 @@
import Config
config :plantid_discord_bot,
guild_request_limit_per_day: 20,
guild_ids: [1_002_507_312_159_797_318],
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
image_folder: "priv/static",
fileserver_url: "http://localhost:4321",
port: 4321,
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
+1 -1
View File
@@ -1,10 +1,10 @@
import Config
config :plantid_discord_bot,
guild_request_limit_per_day: 20,
guild_ids: [],
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
image_folder: "priv/static",
fileserver_url: "http://localhost:4321",
port: 4321,
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
-1
View File
@@ -4,7 +4,6 @@ config :plantid_discord_bot,
guild_ids: [1_002_507_312_159_797_318],
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
image_folder: "priv/static",
fileserver_url: "http://localhost:4321",
port: 4321,
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
+1 -1
View File
@@ -1,10 +1,10 @@
import Config
config :plantid_discord_bot,
guild_request_limit_per_day: 20,
guild_ids: [1_002_507_312_159_797_318],
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
image_folder: "priv/static",
fileserver_url: "http://localhost:4321",
port: 4321,
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
+2 -1
View File
@@ -6,7 +6,8 @@ defmodule PlantIdDiscordBot.Application do
@impl true
def start(_type, _args) do
children = [
PlantIdDiscordBot
PlantIdDiscordBot.Consumer,
PlantIdDiscordBot.RateLimiter
]
opts = [strategy: :one_for_one, name: PlantIdDiscordBot.Supervisor]
+13 -35
View File
@@ -1,4 +1,4 @@
defmodule PlantIdDiscordBot do
defmodule PlantIdDiscordBot.Consumer do
@moduledoc """
A Discord bot that identifies plants from photos of their organs.
"""
@@ -30,39 +30,17 @@ defmodule PlantIdDiscordBot do
Api.create_guild_application_command(1_002_507_312_159_797_318, @global_application_commands)
end
# Handle /source command
def handle_event({:INTERACTION_CREATE, %{data: %{name: "source"}} = interaction, _ws_state}) do
Cog.Info.source(interaction)
# Handle application commands
def handle_event({:INTERACTION_CREATE, %{data: %{name: command}} = interaction, _ws_state}) do
case command do
"source" -> Cog.Info.source(interaction)
"invite" -> Cog.Info.invite(interaction)
"help" -> Cog.Info.help(interaction)
"info" -> Cog.Info.info(interaction)
"stats" -> Cog.Info.stats(interaction)
"status" -> Cog.Info.status(interaction)
"servers" -> Cog.Info.servers(interaction)
"id" -> Cog.PlantNet.id(interaction)
end
end
# Handle /invite command
def handle_event({:INTERACTION_CREATE, %{data: %{name: "invite"}} = interaction, _ws_state}) do
Cog.Info.invite(interaction)
end
# Handle /help command
def handle_event({:INTERACTION_CREATE, %{data: %{name: "help"}} = interaction, _ws_state}) do
Cog.Info.help(interaction)
end
# Handle /info command
def handle_event({:INTERACTION_CREATE, %{data: %{name: "info"}} = interaction, _ws_state}) do
Cog.Info.info(interaction)
end
# def handle_event({:INTERACTION_CREATE, %{data: %{name: "stats"}} = interaction, _ws_state}) do
# Cog.Info.info(interaction)
# end
def handle_event({:INTERACTION_CREATE, %{data: %{name: "status"}} = interaction, _ws_state}) do
Cog.Info.status(interaction)
end
# def handle_event({:INTERACTION_CREATE, %{data: %{name: "servers"}} = interaction, _ws_state}) do
# Cog.Info.info(interaction)
# end
# def handle_event({:INTERACTION_CREATE, %{data: %{name: "id"}} = interaction, _ws_state}) do
# Cog.Info.info(interaction)
# end
end
+46 -7
View File
@@ -63,15 +63,16 @@ defmodule PlantIdDiscordBot.Cog.Info do
embed =
%Nostrum.Struct.Embed{}
|> put_title(app_info.name)
|> put_description("Stats etc.")
|> put_color(0x1AAAE5)
|> put_field("Server Count", app_info.approximate_guild_count, true)
|> put_field("Uptime", Utils.get_uptime(), true)
|> put_field("Latency", Utils.get_shard_latency(), true)
|> put_description("This bot is designed to help you identify plants")
|> put_color(0x41C03F)
|> put_field("/info", "Show this message", true)
|> put_field("/help", "Gives detailed help on the plant ID bot", true)
|> put_field("/stats", "Bot stats", true)
|> put_field("/invite", "Invite the bot to your server", true)
|> put_field("/source", "Link to the source code", true)
|> put_footer("Made by #{app_info.owner.username}")
response = %{type: 4, data: %{embeds: [embed]}}
@api.create_interaction_response(interaction, response)
@api.create_interaction_response(interaction, %{type: 4, data: %{embeds: [embed]}})
end
@doc """
@@ -89,4 +90,42 @@ defmodule PlantIdDiscordBot.Cog.Info do
@api.create_interaction_response(interaction, %{type: 4, data: %{content: message}})
end
@doc """
Bot stats.
"""
def stats(interaction) do
{:ok, app_info} = @api.get_application_information()
embed =
%Nostrum.Struct.Embed{}
|> put_title(app_info.name)
|> put_description("Stats etc.")
|> put_color(0x1AAAE5)
|> put_field("Server Count", app_info.approximate_guild_count, true)
|> put_field("Uptime", Utils.get_uptime(), true)
|> put_field("Latency", Utils.get_shard_latency(), true)
|> put_footer("Made by #{app_info.owner.username}")
@api.create_interaction_response(interaction, %{type: 4, data: %{embeds: [embed]}})
end
@doc """
List all servers the bot is connected to.
"""
def servers(interaction) do
{:ok, %{owner: owner}} = @api.get_application_information()
guilds = PlantIdDiscordBot.Utils.get_guilds_names()
guilds_string = Enum.join(guilds, "\n")
embed =
%Nostrum.Struct.Embed{}
|> put_title("Connected on #{length(guilds)} server(s):")
|> put_description(guilds_string)
|> put_color(0x1AAAE5)
|> put_footer("Made by #{owner.username}")
@api.create_interaction_response(interaction, %{type: 4, data: %{embeds: [embed]}})
end
end
+46
View File
@@ -1,3 +1,49 @@
defmodule PlantIdDiscordBot.Cog.PlantNet do
use Nostrum.Consumer
alias PlantIdDiscordBot.RateLimiter
@api Application.compile_env(:plantid_discord_bot, :api)
@doc """
ID a plant from up to 5 images of organs.
"""
def id(interaction) do
guild_id = interaction.guild_id
case RateLimiter.check_limit(guild_id) do
{:limit_exceeded, value} ->
@api.create_interaction_response(interaction, %{
type: 4,
data: %{
content:
"This server has exceeded its allowed requests in 24 hours. Please try again tomorrow."
}
})
{:ok, _} ->
do_identification(interaction)
end
# TODO increase on success (failed id or otherwise)
# RateLimiter.increase_counter(guild_id)
end
defp do_identification(interaction) do
attachment_urls = get_attachment_urls(interaction)
original_images = get_original_images(attachment_urls)
@api.create_interaction_response(interaction, %{
type: 4,
data: %{content: "This command is not yet implemented." <> "\n#{original_images}"}
})
end
defp get_attachment_urls(interaction) do
interaction.data.resolved.attachments
|> Map.values()
|> Enum.map(& &1.url)
end
@spec get_original_images([String.t()]) :: String.t()
defp get_original_images(attachment_urls), do: Enum.join(attachment_urls, "\n")
end
+52
View File
@@ -0,0 +1,52 @@
defmodule PlantIdDiscordBot.RateLimiter do
use GenServer
@request_limit Application.compile_env(:plantid_discord_bot, :guild_request_limit_per_day)
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
@impl GenServer
def init(_) do
:ets.new(__MODULE__, [:named_table, :public, write_concurrency: true])
{:ok, nil}
end
defp put(key, value) do
:ets.insert(__MODULE__, {key, value})
end
defp get(key) do
case :ets.lookup(__MODULE__, key) do
[{^key, value}] -> {:ok, value}
[] -> {:error, nil}
end
end
@doc """
Check if the number of requests for a guild exceeds the limit.
"""
@spec check_limit(String.t()) :: {:ok, integer()} | {:limit_exceeded, integer()}
def check_limit(guild_id) do
case get(guild_id) do
{:ok, value} ->
if value >= @request_limit do
{:limit_exceeded, value}
else
{:ok, value}
end
{:error, _} ->
put(guild_id, 0)
{:ok, 0}
end
end
@doc """
Increase the counter for the number of requests for a guild by one.
"""
def increase_counter(guild_id) do
:ets.update_counter(__MODULE__, guild_id, 1)
end
end
+4
View File
@@ -12,4 +12,8 @@ defmodule PlantIdDiscordBot.Utils do
|> to_string()
|> Kernel.<>("ms")
end
def get_guilds_names() do
Nostrum.Cache.GuildCache.fold([], fn %{name: name}, acc -> [name | acc] end)
end
end
+9
View File
@@ -0,0 +1,9 @@
defmodule PlantIdDiscordBotTest.Cog.Stats do
use ExUnit.Case
test "/stats" do
{:ok, response} = PlantIdDiscordBot.Cog.Info.info(%{data: %{name: "stats"}})
[embed] = response[:data][:embeds]
assert match?(%Nostrum.Struct.Embed{}, embed)
end
end