diff --git a/config/runtime.exs b/config/runtime.exs index f935fb3..aebc49a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,7 +1,6 @@ import Config config :plantid_discord_bot, - start_time: DateTime.utc_now(), 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", diff --git a/lib/application.ex b/lib/application.ex index 4d061d0..ca6cc2a 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -5,6 +5,10 @@ defmodule PlantIdDiscordBot.Application do @impl true def start(_type, _args) do + # Set the start time for the application + start_time = DateTime.utc_now() + Application.put_env(:plantid_discord_bot, :start_time, start_time) + children = [ PlantIdDiscordBot.Consumer, PlantIdDiscordBot.RateLimiter, diff --git a/lib/plantid_discord_bot/cogs/plantnet.ex b/lib/plantid_discord_bot/cogs/plantnet.ex index fb8f263..bb4da64 100644 --- a/lib/plantid_discord_bot/cogs/plantnet.ex +++ b/lib/plantid_discord_bot/cogs/plantnet.ex @@ -1,9 +1,14 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do use Nostrum.Consumer + # alias PlantIdDiscordBot.Cog.PlantNet alias PlantIdDiscordBot.RateLimiter + alias PlantIdDiscordBot.PlantNet.Parser @api Application.compile_env(:plantid_discord_bot, :api) + # TEMP: Mock data + @plantnet_raw_response "{\"query\":{\"project\":\"all\",\"images\":[\"https://upload.wikimedia.org/wikipedia/commons/f/ff/Prunus_cerasifera_A.jpg\",\"https://le-jardin-de-pascal.com/2195113-large_default/prunus-cerasifera-atropurpurea-prunier-myrobolan-nigra.jpg\"],\"organs\":[\"auto\",\"auto\"],\"includeRelatedImages\":false,\"noReject\":false},\"language\":\"en\",\"preferedReferential\":\"k-world-flora\",\"bestMatch\":\"Prunus cerasifera Ehrh.\",\"results\":[{\"score\":0.87871,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus cerasifera\",\"scientificNameAuthorship\":\"Ehrh.\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Cherry plum, myrobalan\",\"Cherry Plum\",\"Purple-leaf Plum\"],\"scientificName\":\"Prunus cerasifera Ehrh.\"},\"gbif\":{\"id\":\"3021730\"},\"powo\":{\"id\":\"729568-1\"},\"iucn\":{\"id\":\"172162\",\"category\":\"DD\"}},{\"score\":0.31668,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus × cistena\",\"scientificNameAuthorship\":\"N.E.Hansen ex Koehne\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Dwarf red-leaf plum\",\"Purple-leaf sand cherry\",\"Purple-leaved sand cherry\"],\"scientificName\":\"Prunus × cistena N.E.Hansen ex Koehne\"},\"gbif\":{\"id\":\"3022465\"},\"powo\":{\"id\":\"2959315-4\"}},{\"score\":0.01801,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus sargentii\",\"scientificNameAuthorship\":\"Rehder\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Sargent's cherry\",\"Northern Japanese hill cherry\",\"Sargent’s cherry\"],\"scientificName\":\"Prunus sargentii Rehder\"},\"gbif\":{\"id\":\"3020955\"},\"powo\":{\"id\":\"730239-1\"},\"iucn\":{\"id\":\"64127603\",\"category\":\"LC\"}},{\"score\":0.00896,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus × yedoensis\",\"scientificNameAuthorship\":\"Matsum.\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Yoshino cherry\",\"Hybrid cherry\",\"Korean flowering cherry\"],\"scientificName\":\"Prunus × yedoensis Matsum.\"},\"gbif\":{\"id\":\"3021335\"},\"powo\":{\"id\":\"30119904-2\"}},{\"score\":0.00518,\"species\":{\"scientificNameWithoutAuthor\":\"Prunus serrulata\",\"scientificNameAuthorship\":\"Lindl.\",\"genus\":{\"scientificNameWithoutAuthor\":\"Prunus\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Prunus\"},\"family\":{\"scientificNameWithoutAuthor\":\"Rosaceae\",\"scientificNameAuthorship\":\"\",\"scientificName\":\"Rosaceae\"},\"commonNames\":[\"Japanese flowering cherry\",\"Japanese flowering cherry Kwanzan\",\"Tibetan Cherry\"],\"scientificName\":\"Prunus serrulata Lindl.\"},\"gbif\":{\"id\":\"3022609\"},\"powo\":{\"id\":\"730268-1\"},\"iucn\":{\"id\":\"217170511\",\"category\":\"LC\"}}],\"version\":\"2024-11-19 (7.3)\",\"remainingIdentificationRequests\":488}" + @doc """ ID a plant from up to 5 images of organs. """ @@ -30,9 +35,12 @@ defmodule PlantIdDiscordBot.Cog.PlantNet do attachment_urls = get_attachment_urls(interaction) original_images = get_original_images(attachment_urls) + # temp data + response_message = Parser.parse(@plantnet_raw_response) + @api.create_interaction_response(interaction, %{ type: 4, - data: %{content: "This command is not yet implemented." <> "\n#{original_images}"} + data: %{content: response_message <> "\n#{original_images}"} }) end diff --git a/lib/plantid_discord_bot/plantnet/parser.ex b/lib/plantid_discord_bot/plantnet/parser.ex index b1ae9b0..990ed57 100644 --- a/lib/plantid_discord_bot/plantnet/parser.ex +++ b/lib/plantid_discord_bot/plantnet/parser.ex @@ -1,6 +1,28 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do @moduledoc """ Parses the response from the PlantNet API. + + # Examples + iex> response = ~S({ + ...> "results": [ + ...> { + ...> "score": 0.87871, + ...> "species": { + ...> "scientificNameWithoutAuthor": "Prunus cerasifera", + ...> "commonNames": ["Cherry plum", "myrobalan", "Cherry Plum", "Purple-leaf Plum"] + ...> }, + ...> "gbif": {"id": "3021730"}, + ...> "powo": {"id": "729568-1"}, + ...> "iucn": {"category": "DD"} + ...> } + ...> ] + ...> }) + iex> PlantIdDiscordBot.PlantNet.Parser.parse(response) + + "My best guess is **Prunus cerasifera** with a confidence of **88%**. Common names include **Cherry plum, myrobalan, Cherry Plum, Purple-leaf Plum**. + + [GBIF]() | [PFAF]() | [POWO]() + Threat status: DD" """ @gbif_base_url "https://www.gbif.org/species" @@ -12,7 +34,16 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do Parses the response from the PlantNet API into a map. """ @spec parse(String.t()) :: map() - def parse(response), do: Jason.decode!(response) + def parse(response) do + response + |> 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) @doc """ Filters the data by score. Data is a parsed response from the PlantNet API. @@ -40,6 +71,24 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do Map.put(data, "results", updated_results) end + @doc """ + Generates a response message from the data. Data is a parsed response from the PlantNet API. + """ + @spec generate_response_message(map()) :: String.t() + def generate_response_message(data) do + best_result = hd(data["results"]) + other_results = tl(data["results"]) + best_guess_name = best_result["species"]["scientificNameWithoutAuthor"] + score = round(best_result["score"] * 100) |> Integer.to_string() + + """ + My best guess is **#{best_guess_name}** with a confidence of **#{score}%**. Common names include **#{Enum.join(best_result["species"]["commonNames"], ", ")}**. + + [GBIF](<#{best_result["gbif_url"]}>) | [PFAF](<#{best_result["pfaf_url"]}>) | [POWO](<#{best_result["powo_url"]}>) + Threat status: #{best_result["iucn"]["category"]} + """ + end + @spec generate_gbif_url(map()) :: map() defp generate_gbif_url(data) do Enum.map(data, fn result -> @@ -66,4 +115,14 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do if powo_id, do: Map.put(result, "powo_url", "#{@powo_base_url}/#{powo_id}"), else: result end) end + + defp get_alternatives(data) do + if length(data) === 0 do + "No alternatives found." + else + alternatives = + Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"]) + |> Enum.join(", ") + end + end end diff --git a/lib/plantid_discord_bot/utils.ex b/lib/plantid_discord_bot/utils.ex index b01e1d2..75ed6bc 100644 --- a/lib/plantid_discord_bot/utils.ex +++ b/lib/plantid_discord_bot/utils.ex @@ -1,5 +1,5 @@ defmodule PlantIdDiscordBot.Utils do - @start_time Application.compile_env(:plantid_discord_bot, :start_time) + @start_time Application.get_env(:plantid_discord_bot, :start_time) def get_uptime() do DateTime.diff(DateTime.utc_now(), @start_time) diff --git a/test/plantnet/parser_test.exs b/test/plantnet/parser_test.exs index 0399991..b0ece34 100644 --- a/test/plantnet/parser_test.exs +++ b/test/plantnet/parser_test.exs @@ -4,10 +4,10 @@ defmodule PlantIdDiscordBotTest.PlantNet.Parser do doctest PlantIdDiscordBot.PlantNet.Parser - test "parse" do + test "to_map" do result = PlantNetFixtures.raw_response() - |> Parser.parse() + |> Parser.to_map() assert result == PlantNetFixtures.parsed_response() end