mirror of
https://github.com/TheRealOwenRees/plantid-discord-bot.git
synced 2026-07-22 20:16:57 +00:00
request limiter
This commit is contained in:
+2
-1
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
+12
-34
@@ -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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user