diff --git a/config/config.exs b/config/config.exs index 8ffa7f1..1c441ed 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,7 +11,8 @@ config :plantid_discord_bot, "https://discord.com/api/oauth2/authorize?client_id=948227126094598204&permissions=19520&scope=bot" config :nostrum, - ffmpeg: nil + ffmpeg: nil, + gateway_intents: [:guilds, :guild_messages, :message_content] config :plantid_discord_bot, PlantIdDiscordBot.Scheduler, jobs: [ diff --git a/lib/plantid_discord_bot.ex b/lib/plantid_discord_bot.ex index 6d86545..b38b484 100644 --- a/lib/plantid_discord_bot.ex +++ b/lib/plantid_discord_bot.ex @@ -26,4 +26,12 @@ defmodule PlantIdDiscordBot.Consumer do "id" -> Cog.PlantNet.id(interaction) end end + + def handle_event({:MESSAGE_CREATE, %{attachments: attachments} = message, _ws_state}) do + if length(attachments) > 0 do + # deprecated -> Nostrum.Api.Channel.start_typing/1 in v1.0 + Api.start_typing!(message.channel_id) + Cog.PlantNetMessage.id(message) + end + end end diff --git a/lib/plantid_discord_bot/cogs/plantnet_message.ex b/lib/plantid_discord_bot/cogs/plantnet_message.ex new file mode 100644 index 0000000..3e6c838 --- /dev/null +++ b/lib/plantid_discord_bot/cogs/plantnet_message.ex @@ -0,0 +1,141 @@ +defmodule PlantIdDiscordBot.Cog.PlantNetMessage do + require Logger + + use Nostrum.Consumer + alias Nostrum.Api + alias PlantIdDiscordBot.RateLimiter + alias PlantIdDiscordBot.FileServer.File + alias PlantIdDiscordBot.PlantNet.Parser + alias Nostrum.Cache.GuildCache + alias PlantIdDiscordBot.Metrics + + # @api Application.compile_env(:plantid_discord_bot, :api) + @plantnet_api_base_url Application.compile_env(:plantid_discord_bot, :plantnet_api_base_url) + @max_results Application.compile_env(:plantid_discord_bot, :max_results) + + def id(message) do + IO.inspect(message) + + case RateLimiter.check_limit(message.guild_id) do + {:limit_exceeded, _} -> + Api.create_message(message.channel_id, + content: + "This server has exceeded its allowed requests in 24 hours. Please try again tomorrow.", + message_reference: %{message_id: message.id} + ) + + {:ok, _} -> + do_identification(message) + end + end + + def do_identification(message) do + saved_images = + try do + Enum.map(message.attachments, fn attachment -> attachment.url end) + |> File.download_and_save_files!() + rescue + e in ArgumentError -> + Api.create_message(message.channel_id, content: e.message) + nil + end + + if saved_images do + try do + prepare_images(saved_images) + |> build_query_uri() + |> get_response(message) + rescue + e -> + Logger.error(Exception.format(:error, e, __STACKTRACE__)) + Api.create_message(message.channel_id, "An error occurred. This error has been logged.") + after + cleanup_saved_images(saved_images) + end + end + end + + defp prepare_images(saved_images) do + case Application.get_env(:plantid_discord_bot, :environment) do + :test -> + PlantIdDiscordBotTest.Mocks.PlantNet.Images.images() + + :dev -> + PlantIdDiscordBotTest.Mocks.PlantNet.Images.images() + + _ -> + Enum.map(saved_images, fn {:ok, filename} -> + "#{Application.get_env(:plantid_discord_bot, :fileserver_url)}/#{filename}" + end) + end + end + + defp get_response(query_uri, message) do + guild_id = message.guild_id + {:ok, %{name: guild_name}} = GuildCache.get(guild_id) + + case HTTPoison.get(query_uri) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + response_message = Parser.parse(body) + + RateLimiter.increase_counter(guild_id) + Metrics.put(guild_id, guild_name) + + # Nostrum.Api.create_message/2 is deprecated but the new function is not available in v0.10 of the library + # Nostrum.Api.message/2 will be the new function + Api.create_message(message.channel_id, + content: response_message, + message_reference: %{message_id: message.id} + ) + + {:ok, %HTTPoison.Response{status_code: 401, body: body}} -> + Logger.critical("Unauthorized request to PlantNet API: #{body}") + + Api.create_message(message.channel_id, + content: "Unauthorized request to PlantNet API.", + message_reference: %{message_id: message.id} + ) + + {:ok, %HTTPoison.Response{status_code: 404}} -> + RateLimiter.increase_counter(guild_id) + Metrics.put(guild_id, guild_name) + + Api.create_message(message.application_id, + content: "Species Not Found", + message_reference: %{message_id: message.id} + ) + + {:ok, %HTTPoison.Response{status_code: 429}} -> + Logger.warning("Request limit exceeded for the PlantNet API") + + Api.create_message(message.application_id, + content: "Too Many Requests", + message_reference: %{message_id: message.id} + ) + + {_, _} -> + Logger.error("Internal server error when contacting the PlantNet API") + + Api.create_message(message.application_id, + content: "Internal Server Error", + message_reference: %{message_id: message.id} + ) + end + end + + @spec build_query_uri([String.t()]) :: String.t() + defp build_query_uri(image_filenames) do + URI.parse( + "#{@plantnet_api_base_url}/identify/all?api-key=#{Application.get_env(:plantid_discord_bot, :plantnet_api_key)}" + ) + |> URI.append_query("images=#{Enum.join(image_filenames, "&images=")}") + |> URI.append_query("nb-results=#{@max_results}") + |> URI.append_query("type=kt") + |> URI.to_string() + end + + defp cleanup_saved_images(saved_images) do + Enum.map(saved_images, fn {:ok, filename} -> filename end) + |> File.delete_files!() + end +end