diff --git a/config/config.exs b/config/config.exs index ac806c5..da98668 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ import Config config :plantid_discord_bot, - image_folder: "priv/static" + image_path: "priv/static" import_config "#{config_env()}.exs" diff --git a/lib/plantid_discord_bot/cogs/plantnet.ex b/lib/plantid_discord_bot/cogs/plantnet.ex index bb4da64..6c036d8 100644 --- a/lib/plantid_discord_bot/cogs/plantnet.ex +++ b/lib/plantid_discord_bot/cogs/plantnet.ex @@ -1,8 +1,8 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do use Nostrum.Consumer - # alias PlantIdDiscordBot.Cog.PlantNet alias PlantIdDiscordBot.RateLimiter alias PlantIdDiscordBot.PlantNet.Parser + alias PlantIdDiscordBot.FileServer.File @api Application.compile_env(:plantid_discord_bot, :api) @@ -26,18 +26,40 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do {: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) + try do + Enum.each(attachment_urls, fn url -> + binary = File.download_file!(url) + File.save_file!(binary) + end) + rescue + e in RuntimeError -> + # Logger.error("An error occurred while processing the images.") + + @api.create_interaction_response(interaction, %{ + type: 4, + data: %{ + content: + "An error occurred while processing the images." <> "\n\nReason:\n#{e.message}" + } + }) + end + + # TODO call PlantNet API and get response, with error handling + + # TODO pass this response to the parser as below, with error handling + # temp data response_message = Parser.parse(@plantnet_raw_response) + # TODO increase on success (failed id or otherwise) + # RateLimiter.increase_counter(guild_id) + @api.create_interaction_response(interaction, %{ type: 4, data: %{content: response_message <> "\n#{original_images}"} diff --git a/lib/plantid_discord_bot/file_server/file.ex b/lib/plantid_discord_bot/file_server/file.ex new file mode 100644 index 0000000..db9950e --- /dev/null +++ b/lib/plantid_discord_bot/file_server/file.ex @@ -0,0 +1,36 @@ +defmodule PlantIdDiscordBot.FileServer.File do + @moduledoc """ + File utilities + """ + alias PlantIdDiscordBot.FileServer.ImageConverter + + @image_path Application.compile_env(:plantid_discord_bot, :image_path) + + @doc """ + Downloads a file from the given URL. Throws an error if unsuccessful. + """ + @spec download_file!(String.t()) :: binary + def download_file!(url) do + HTTPoison.get!(url) + |> Map.get(:body) + end + + @spec save_file!(binary) :: :ok + def save_file!(binary) do + File.mkdir_p!(@image_path) + + jpg_binary = ImageConverter.to_jpg!(binary) + + generate_unique_filename("jpg") + |> File.write!(binary) + end + + @doc """ + Generates a unique filename with the given file extension. + """ + @spec generate_unique_filename(String.t()) :: String.t() + def generate_unique_filename(file_extension) do + filename = :crypto.strong_rand_bytes(8) |> Base.encode16() + Path.join(@image_path, "#{filename}.#{file_extension}") + end +end diff --git a/lib/plantid_discord_bot/file_server/image_converter.ex b/lib/plantid_discord_bot/file_server/image_converter.ex new file mode 100644 index 0000000..edf7984 --- /dev/null +++ b/lib/plantid_discord_bot/file_server/image_converter.ex @@ -0,0 +1,19 @@ +defmodule PlantIdDiscordBot.FileServer.ImageConverter do + @moduledoc """ + Image conversion utilities + """ + alias PlantIdDiscordBot.FileServer.MimeTypes + + def to_jpg!(binary) do + case MimeTypes.detect_mime_type(binary) do + {:error, message} -> raise RuntimeError, message + "image/jpeg" -> binary + _ -> do_to_jpg(binary) + end + end + + defp do_to_jpg(binary) do + Image.from_binary!(binary) + |> Image.write!(:memory, suffix: ".jpg") + end +end diff --git a/lib/plantid_discord_bot/file_server/mime_types.ex b/lib/plantid_discord_bot/file_server/mime_types.ex new file mode 100644 index 0000000..bc3d7d0 --- /dev/null +++ b/lib/plantid_discord_bot/file_server/mime_types.ex @@ -0,0 +1,17 @@ +defmodule PlantIdDiscordBot.FileServer.MimeTypes do + @moduledoc """ + Mime type utilities + """ + + @doc """ + Detect the mime type of a binary + """ + @spec detect_mime_type(binary) :: String.t() + def detect_mime_type(<<0x52, 0x49, 0x46, 0x46, _::binary>>), do: "image/webp" + def detect_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>), do: "image/jpeg" + def detect_mime_type(<<0x89, 0x50, 0x4E, 0x47, _::binary>>), do: "image/png" + def detect_mime_type(<<0x47, 0x49, 0x46, 0x38, _::binary>>), do: "image/gif" + def detect_mime_type(<<0x42, 0x4D, _::binary>>), do: "image/bmp" + def detect_mime_type(<<0x49, 0x49, 0x2A, 0x00, _::binary>>), do: "image/tiff" + def detect_mime_type(_), do: {:error, "Unsupported file type"} +end diff --git a/lib/plantid_discord_bot/plantnet/parser.ex b/lib/plantid_discord_bot/plantnet/parser.ex index 990ed57..2271992 100644 --- a/lib/plantid_discord_bot/plantnet/parser.ex +++ b/lib/plantid_discord_bot/plantnet/parser.ex @@ -36,14 +36,14 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do @spec parse(String.t()) :: map() def parse(response) do response - |> to_map() + |> to_map!() |> filter_by_score() |> add_external_urls() |> generate_response_message() end - @spec to_map(String.t()) :: map() - def to_map(response), do: Jason.decode!(response) + @spec to_map!(String.t()) :: map() + def to_map!(response), do: Jason.decode!(response) @doc """ Filters the data by score. Data is a parsed response from the PlantNet API. diff --git a/priv/static/1A35143CA68E4051.jpg b/priv/static/1A35143CA68E4051.jpg new file mode 100644 index 0000000..11eb818 Binary files /dev/null and b/priv/static/1A35143CA68E4051.jpg differ diff --git a/priv/static/59E1C41C70906CE8.jpg b/priv/static/59E1C41C70906CE8.jpg new file mode 100644 index 0000000..c4a5884 Binary files /dev/null and b/priv/static/59E1C41C70906CE8.jpg differ diff --git a/priv/static/6375C61A2E725A57.jpg b/priv/static/6375C61A2E725A57.jpg new file mode 100644 index 0000000..8987dc1 Binary files /dev/null and b/priv/static/6375C61A2E725A57.jpg differ diff --git a/priv/static/7D43973D2332545A.jpg b/priv/static/7D43973D2332545A.jpg new file mode 100644 index 0000000..8987dc1 Binary files /dev/null and b/priv/static/7D43973D2332545A.jpg differ diff --git a/priv/static/E8632956C78C83CC.jpg b/priv/static/E8632956C78C83CC.jpg new file mode 100644 index 0000000..9b95fef Binary files /dev/null and b/priv/static/E8632956C78C83CC.jpg differ diff --git a/test/file_server/file_test.exs b/test/file_server/file_test.exs new file mode 100644 index 0000000..fbd1790 --- /dev/null +++ b/test/file_server/file_test.exs @@ -0,0 +1,13 @@ +defmodule PlantIdDiscordBotTest.FileServer.File do + use ExUnit.Case + doctest PlantIdDiscordBot.FileServer.File + + @image_path Application.compile_env(:plantid_discord_bot, :image_path) + + test "generate_unique_filename" do + file_path = PlantIdDiscordBot.FileServer.File.generate_unique_filename("jpg") + file_name = Path.basename(file_path) + assert Regex.match?(~r/^[0-9A-F]{16}\.jpg$/, file_name) + assert file_path == Path.join(@image_path, file_name) + end +end diff --git a/test/plantnet/parser_test.exs b/test/plantnet/parser_test.exs index b0ece34..dadaebb 100644 --- a/test/plantnet/parser_test.exs +++ b/test/plantnet/parser_test.exs @@ -7,7 +7,7 @@ defmodule PlantIdDiscordBotTest.PlantNet.Parser do test "to_map" do result = PlantNetFixtures.raw_response() - |> Parser.to_map() + |> Parser.to_map!() assert result == PlantNetFixtures.parsed_response() end