Feat/error logs (#16)

* added list of tasks to achieve

* update deps

* update elixir version in CI/CD pipeline

* update todo with branch push checks

* underscores separation of large numbers

* update elixir version and deps

* add basic module  docs

* remove IO.inspect call

* check if list is empty rather than traversing list for length

* revert empty list code

* duration tests

* add mime type tests

* use logger instread of inspect, remove todos

* replace list length check with Enum.empty

* replace list length check with !Enum.empty?

* numbers readability fix by using underscores on large numbers

* iucn parser replaced case with multiclause function and added test

* replace map and join with map_join

* fix multiple spec issues where map() must be inside a list
This commit is contained in:
2025-12-06 23:41:59 +01:00
committed by GitHub
parent 3d3059e77f
commit 02372f2076
27 changed files with 241 additions and 45 deletions
+1
View File
@@ -1,4 +1,5 @@
defmodule PlantIdDiscordBot.Application do
@moduledoc false
use Application
@impl true
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Consumer.Commands do
@moduledoc """
Application commands.
"""
def global_application_commands do
[
%{
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Metrics.Message do
@moduledoc """
Metrics messaging.
"""
def send() do
PlantIdDiscordBot.Metrics.requests()
|> format_message()
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Metrics.Requests do
@moduledoc """
Basic metrics with usage count per guild.
"""
use Agent
alias PlantIdDiscordBot.Metrics.Requests
alias PlantIdDiscordBot.ProcessRegistry
+1 -1
View File
@@ -6,7 +6,7 @@ defmodule PlantIdDiscordBotTest.Mocks.Nostrum.Api do
# def create_message(_channel_id, content), do: {:ok, content}
def create_message(_channel_id, content) do
send(self(), {:create_message, 123456, content})
send(self(), {:create_message, 123_456, content})
{:ok, content}
end
+7 -2
View File
@@ -26,10 +26,15 @@ defmodule PlantIdDiscordBot.Consumer do
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
if !Enum.empty?(attachments) do
Api.start_typing!(message.channel_id)
Cog.PlantNetMessage.id(message)
end
# 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
+5
View File
@@ -1,4 +1,9 @@
defmodule PlantIdDiscordBot.Cog.Info do
@moduledoc """
Functions for using the /info application command.
Returns a message embed of application commands available to the user.
"""
use Nostrum.Consumer
import Nostrum.Struct.Embed
alias PlantIdDiscordBot.Utils
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
@moduledoc """
Functions for sending and receiving data from PLantNet
"""
require Logger
use Nostrum.Consumer
@@ -49,7 +52,6 @@ defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
nil
end
# TODO improve composition
if saved_images do
try do
prepare_images(saved_images)
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.FileServer do
@moduledoc """
File server for temporary storage of images until a response is made.
"""
use Plug.Builder
plug(Plug.Logger)
+3 -3
View File
@@ -2,6 +2,8 @@ defmodule PlantIdDiscordBot.FileServer.File do
@moduledoc """
File utilities
"""
require Logger
alias PlantIdDiscordBot.FileServer.ImageConverter
@image_path Application.compile_env(:plantid_discord_bot, :image_path)
@@ -56,7 +58,6 @@ defmodule PlantIdDiscordBot.FileServer.File do
|> File.read()
end
# TODO make into a task for async deletion and deal with errors
@spec delete_files!([String.t()]) :: :ok
def delete_files!(filenames) do
tasks =
@@ -65,8 +66,7 @@ defmodule PlantIdDiscordBot.FileServer.File do
try do
File.rm!(Path.join(@image_path, filename))
rescue
# TODO Logger
e -> IO.inspect(e)
e -> Logger.error(e)
end
end)
end)
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Guild do
@moduledoc """
Guild related functions.
"""
alias Nostrum.Cache.GuildCache
def get_guild_name!(guild_id) do
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.ProcessRegistry do
@moduledoc """
Process registry.
"""
def start_link do
Registry.start_link(keys: :unique, name: __MODULE__)
end
+6
View File
@@ -1,4 +1,10 @@
defmodule PlantIdDiscordBot.RateLimiter do
@moduledoc """
Basic rate limiter to prevent one guild from calling too many requests in 24 hours.
Custom limits have been set for some guilds that are trusted.
"""
use GenServer
require Logger
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Utils do
@moduledoc """
Bot related utility functions.
"""
def get_uptime() do
start_time = Application.get_env(:plantid_discord_bot, :start_time)
+32 -21
View File
@@ -33,7 +33,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
@doc """
Parses the response from the PlantNet API into a map.
"""
@spec parse(String.t()) :: map()
@spec parse(String.t()) :: String.t()
def parse(response) do
response
|> to_map!()
@@ -85,7 +85,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
"My best guess is **#{best_guess_name}** with a confidence of **#{score}%**.#{get_common_names(best_result)}\n\nSpecies info from plant databases:\n[GBIF](<#{best_result["gbif_url"]}>) | [PFAF](<#{best_result["pfaf_url"]}>) | [POWO](<#{best_result["powo_url"]}>)#{if best_result_iucn_category, do: "\n\nConservation status: #{iucn_parser(best_result_iucn_category)}"}#{get_alternatives(other_results)}"
end
@spec generate_gbif_url(map()) :: map()
@spec generate_gbif_url([map()]) :: [map()]
defp generate_gbif_url(data) do
Enum.map(data, fn result ->
gbif_id = result["gbif"]["id"]
@@ -93,7 +93,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
end)
end
@spec generate_pfaf_url(map()) :: map()
@spec generate_pfaf_url([map()]) :: [map()]
defp generate_pfaf_url(data) do
Enum.map(data, fn result ->
pfaf_slug = String.replace(result["species"]["scientificNameWithoutAuthor"], " ", "+")
@@ -104,7 +104,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
end)
end
@spec generate_powo_url(map()) :: map()
@spec generate_powo_url([map()]) :: [map()]
defp generate_powo_url(data) do
Enum.map(data, fn result ->
powo_id = result["powo"]["id"]
@@ -123,29 +123,40 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
end
@spec iucn_parser(String.t()) :: String.t()
defp iucn_parser(abbreviation) do
case abbreviation do
"DD" -> "Data Deficient"
"LC" -> "Least Concern"
"NT" -> "Near Threatened"
"VU" -> "Vulnerable"
"EN" -> "Endangered"
"CR" -> "Critically Endangered"
"EW" -> "Extinct in the Wild"
"EX" -> "Extinct"
"NE" -> "Not Evaluated"
_ -> "Unknown"
end
end
def iucn_parser("DD"), do: "Data Deficient"
def iucn_parser("LC"), do: "Least Concern"
def iucn_parser("NT"), do: "Near Threatened"
def iucn_parser("VU"), do: "Vulnerable"
def iucn_parser("EN"), do: "Endangered"
def iucn_parser("CR"), do: "Critically Endangered"
def iucn_parser("EW"), do: "Extinct in the Wild"
def iucn_parser("EX"), do: "Extinct"
def iucn_parser("NE"), do: "Not Evaluated"
def iucn_parser(_), do: "Unknown"
@spec get_alternatives(map()) :: String.t()
defp get_alternatives(data) do
if length(data) > 0 do
if !Enum.empty?(data) do
alternatives =
Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"])
|> Enum.join(", ")
Enum.map_join(data, ", ", & &1["species"]["scientificNameWithoutAuthor"])
"\n\nAlternatives include **#{alternatives}**."
end
# if !Enum.empty?(data) do
# alternatives =
# Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"])
# |> Enum.join(", ")
# "\n\nAlternatives include **#{alternatives}**."
# end
# if length(data) > 0 do
# alternatives =
# Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"])
# |> Enum.join(", ")
# "\n\nAlternatives include **#{alternatives}**."
# end
end
end
+3
View File
@@ -1,3 +1,6 @@
defmodule PlantIdDiscordBot.Scheduler do
@moduledoc """
CRON job scheduler.
"""
use Quantum, otp_app: :plantid_discord_bot
end
+11 -1
View File
@@ -1,11 +1,21 @@
# https://rosettacode.org/wiki/Convert_seconds_to_compound_duration
defmodule PlantIdDiscordBot.Utils.Duration do
@moduledoc """
Duration conversion functions.
"""
@minute 60
@hour @minute * 60
@day @hour * 24
@week @day * 7
@divisor [@week, @day, @hour, @minute, 1]
@doc """
Convert seconds to a time string.
# https://rosettacode.org/wiki/Convert_seconds_to_compound_duration
## Examples
iex> PlantIdDiscordBot.Utils.Duration.sec_to_str(65)
iex> "1m 5s"
"""
def sec_to_str(sec) do
{_, [s, m, h, d, w]} =
Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} ->