mirror of
https://github.com/TheRealOwenRees/plantid-discord-bot.git
synced 2026-07-23 04:26:57 +00:00
request limiter
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :plantid_discord_bot,
|
config :plantid_discord_bot,
|
||||||
start_time: DateTime.utc_now()
|
start_time: DateTime.utc_now(),
|
||||||
|
image_folder: "priv/static"
|
||||||
|
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :plantid_discord_bot,
|
config :plantid_discord_bot,
|
||||||
|
guild_request_limit_per_day: 20,
|
||||||
guild_ids: [1_002_507_312_159_797_318],
|
guild_ids: [1_002_507_312_159_797_318],
|
||||||
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
||||||
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
||||||
image_folder: "priv/static",
|
|
||||||
fileserver_url: "http://localhost:4321",
|
fileserver_url: "http://localhost:4321",
|
||||||
port: 4321,
|
port: 4321,
|
||||||
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :plantid_discord_bot,
|
config :plantid_discord_bot,
|
||||||
|
guild_request_limit_per_day: 20,
|
||||||
guild_ids: [],
|
guild_ids: [],
|
||||||
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
||||||
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
||||||
image_folder: "priv/static",
|
|
||||||
fileserver_url: "http://localhost:4321",
|
fileserver_url: "http://localhost:4321",
|
||||||
port: 4321,
|
port: 4321,
|
||||||
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
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],
|
guild_ids: [1_002_507_312_159_797_318],
|
||||||
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
||||||
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
||||||
image_folder: "priv/static",
|
|
||||||
fileserver_url: "http://localhost:4321",
|
fileserver_url: "http://localhost:4321",
|
||||||
port: 4321,
|
port: 4321,
|
||||||
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :plantid_discord_bot,
|
config :plantid_discord_bot,
|
||||||
|
guild_request_limit_per_day: 20,
|
||||||
guild_ids: [1_002_507_312_159_797_318],
|
guild_ids: [1_002_507_312_159_797_318],
|
||||||
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
|
||||||
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
plantnet_api_base_url: "https://my-api.plantnet.org/v2",
|
||||||
image_folder: "priv/static",
|
|
||||||
fileserver_url: "http://localhost:4321",
|
fileserver_url: "http://localhost:4321",
|
||||||
port: 4321,
|
port: 4321,
|
||||||
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
source: "https://github.com/TheRealOwenRees/plantid-discord-bot",
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@ defmodule PlantIdDiscordBot.Application do
|
|||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children = [
|
||||||
PlantIdDiscordBot
|
PlantIdDiscordBot.Consumer,
|
||||||
|
PlantIdDiscordBot.RateLimiter
|
||||||
]
|
]
|
||||||
|
|
||||||
opts = [strategy: :one_for_one, name: PlantIdDiscordBot.Supervisor]
|
opts = [strategy: :one_for_one, name: PlantIdDiscordBot.Supervisor]
|
||||||
|
|||||||
+13
-35
@@ -1,4 +1,4 @@
|
|||||||
defmodule PlantIdDiscordBot do
|
defmodule PlantIdDiscordBot.Consumer do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A Discord bot that identifies plants from photos of their organs.
|
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)
|
Api.create_guild_application_command(1_002_507_312_159_797_318, @global_application_commands)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle /source command
|
# Handle application commands
|
||||||
def handle_event({:INTERACTION_CREATE, %{data: %{name: "source"}} = interaction, _ws_state}) do
|
def handle_event({:INTERACTION_CREATE, %{data: %{name: command}} = interaction, _ws_state}) do
|
||||||
Cog.Info.source(interaction)
|
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
|
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
|
end
|
||||||
|
|||||||
@@ -63,15 +63,16 @@ defmodule PlantIdDiscordBot.Cog.Info do
|
|||||||
embed =
|
embed =
|
||||||
%Nostrum.Struct.Embed{}
|
%Nostrum.Struct.Embed{}
|
||||||
|> put_title(app_info.name)
|
|> put_title(app_info.name)
|
||||||
|> put_description("Stats etc.")
|
|> put_description("This bot is designed to help you identify plants")
|
||||||
|> put_color(0x1AAAE5)
|
|> put_color(0x41C03F)
|
||||||
|> put_field("Server Count", app_info.approximate_guild_count, true)
|
|> put_field("/info", "Show this message", true)
|
||||||
|> put_field("Uptime", Utils.get_uptime(), true)
|
|> put_field("/help", "Gives detailed help on the plant ID bot", true)
|
||||||
|> put_field("Latency", Utils.get_shard_latency(), 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}")
|
|> put_footer("Made by #{app_info.owner.username}")
|
||||||
|
|
||||||
response = %{type: 4, data: %{embeds: [embed]}}
|
@api.create_interaction_response(interaction, %{type: 4, data: %{embeds: [embed]}})
|
||||||
@api.create_interaction_response(interaction, response)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -89,4 +90,42 @@ defmodule PlantIdDiscordBot.Cog.Info do
|
|||||||
|
|
||||||
@api.create_interaction_response(interaction, %{type: 4, data: %{content: message}})
|
@api.create_interaction_response(interaction, %{type: 4, data: %{content: message}})
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -1,3 +1,49 @@
|
|||||||
defmodule PlantIdDiscordBot.Cog.PlantNet do
|
defmodule PlantIdDiscordBot.Cog.PlantNet do
|
||||||
use Nostrum.Consumer
|
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
|
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()
|
|> to_string()
|
||||||
|> Kernel.<>("ms")
|
|> Kernel.<>("ms")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_guilds_names() do
|
||||||
|
Nostrum.Cache.GuildCache.fold([], fn %{name: name}, acc -> [name | acc] end)
|
||||||
|
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