mirror of
https://github.com/TheRealOwenRees/plantid-discord-bot.git
synced 2026-07-23 04:26:57 +00:00
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:
@@ -15,8 +15,8 @@ jobs:
|
|||||||
- name: Setup BEAM / Elixir
|
- name: Setup BEAM / Elixir
|
||||||
uses: erlef/setup-beam@v1
|
uses: erlef/setup-beam@v1
|
||||||
with:
|
with:
|
||||||
otp-version: 26
|
otp-version: 28
|
||||||
elixir-version: 1.16.2
|
elixir-version: 1.18.4
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
@@ -15,19 +15,20 @@ If you wish to invite this bot to your server, use [this link.](https://discord.
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
This application has been built and tested with Elixir 1.16 / OTP 26.
|
This application has been built and tested with Elixir 1.18 / OTP 28.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# mix.exs
|
# mix.exs
|
||||||
|
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
|
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
|
||||||
{:nostrum, "~> 0.10"},
|
{:nostrum, "~> 0.10"},
|
||||||
{:httpoison, "~> 2.2"},
|
{:httpoison, "~> 2.2"},
|
||||||
{:image, "~> 0.55"},
|
{:image, "~> 0.55"},
|
||||||
{:jason, "~> 1.4"},
|
{:jason, "~> 1.4"},
|
||||||
{:logger_webhook_backend, "~> 0.0.2"},
|
{:logger_webhook_backend, "~> 0.0.4"},
|
||||||
{:plug, "~> 1.12"},
|
{:plug, "~> 1.12"},
|
||||||
{:plug_cowboy, "~> 2.7"},
|
{:plug_cowboy, "~> 2.7"},
|
||||||
{:quantum, "~> 3.5"}
|
{:quantum, "~> 3.5"}
|
||||||
@@ -40,9 +41,12 @@ defp deps do
|
|||||||
Below are the environment variables that you need to set for the program to function:
|
Below are the environment variables that you need to set for the program to function:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# needed in development and production
|
||||||
PLANTID_DISCORD_BOT_TOKEN= client secret from the bot's application
|
PLANTID_DISCORD_BOT_TOKEN= client secret from the bot's application
|
||||||
PLANTID_LOGS_DISCORD_WEBHOOK_URL= webhook url for the log channel
|
PLANTID_LOGS_DISCORD_WEBHOOK_URL= webhook url for the log channel
|
||||||
PLANTNET_API_KEY= API key for the PlantNet service
|
PLANTNET_API_KEY= API key for the PlantNet service
|
||||||
|
|
||||||
|
# needed in production
|
||||||
PLANTID_FILESERVER_URL URL and port(if needed) for the http file server
|
PLANTID_FILESERVER_URL URL and port(if needed) for the http file server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 📝 Project TODOs
|
||||||
|
|
||||||
|
## 🚀 High Priority
|
||||||
|
|
||||||
|
- [x] Update deps and run on Elixir 1.18 / otp 28
|
||||||
|
- [ ] Run static analysis tools and fix code
|
||||||
|
- [ ] Increase test coverage
|
||||||
|
- [ ] Fix spec errors
|
||||||
|
- [ ] Improve error return message
|
||||||
|
|
||||||
|
## 📦 Medium Priority
|
||||||
|
|
||||||
|
- [ ] - add credo, dialyxir and test to GH actions
|
||||||
|
- [ ] - use Req instead of HTTPoison
|
||||||
|
|
||||||
|
## 🛠️ Low Priority
|
||||||
|
|
||||||
|
- [ ] add checks on branch push / PR creation
|
||||||
|
- [ ] add specs where missing
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
✅ Keep this file updated as tasks are completed. Use `git diff` to track progress.
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
defmodule PlantIdDiscordBot.Application do
|
defmodule PlantIdDiscordBot.Application do
|
||||||
|
@moduledoc false
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.Consumer.Commands do
|
defmodule PlantIdDiscordBot.Consumer.Commands do
|
||||||
|
@moduledoc """
|
||||||
|
Application commands.
|
||||||
|
"""
|
||||||
def global_application_commands do
|
def global_application_commands do
|
||||||
[
|
[
|
||||||
%{
|
%{
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.Metrics.Message do
|
defmodule PlantIdDiscordBot.Metrics.Message do
|
||||||
|
@moduledoc """
|
||||||
|
Metrics messaging.
|
||||||
|
"""
|
||||||
def send() do
|
def send() do
|
||||||
PlantIdDiscordBot.Metrics.requests()
|
PlantIdDiscordBot.Metrics.requests()
|
||||||
|> format_message()
|
|> format_message()
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.Metrics.Requests do
|
defmodule PlantIdDiscordBot.Metrics.Requests do
|
||||||
|
@moduledoc """
|
||||||
|
Basic metrics with usage count per guild.
|
||||||
|
"""
|
||||||
use Agent
|
use Agent
|
||||||
alias PlantIdDiscordBot.Metrics.Requests
|
alias PlantIdDiscordBot.Metrics.Requests
|
||||||
alias PlantIdDiscordBot.ProcessRegistry
|
alias PlantIdDiscordBot.ProcessRegistry
|
||||||
|
|||||||
@@ -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: {:ok, content}
|
||||||
|
|
||||||
def create_message(_channel_id, content) do
|
def create_message(_channel_id, content) do
|
||||||
send(self(), {:create_message, 123456, content})
|
send(self(), {:create_message, 123_456, content})
|
||||||
{:ok, content}
|
{:ok, content}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,15 @@ defmodule PlantIdDiscordBot.Consumer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_event({:MESSAGE_CREATE, %{attachments: attachments} = message, _ws_state}) do
|
def handle_event({:MESSAGE_CREATE, %{attachments: attachments} = message, _ws_state}) do
|
||||||
if length(attachments) > 0 do
|
if !Enum.empty?(attachments) do
|
||||||
# deprecated -> Nostrum.Api.Channel.start_typing/1 in v1.0
|
|
||||||
Api.start_typing!(message.channel_id)
|
Api.start_typing!(message.channel_id)
|
||||||
Cog.PlantNetMessage.id(message)
|
Cog.PlantNetMessage.id(message)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
defmodule PlantIdDiscordBot.Cog.Info do
|
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
|
use Nostrum.Consumer
|
||||||
import Nostrum.Struct.Embed
|
import Nostrum.Struct.Embed
|
||||||
alias PlantIdDiscordBot.Utils
|
alias PlantIdDiscordBot.Utils
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
|
defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
|
||||||
|
@moduledoc """
|
||||||
|
Functions for sending and receiving data from PLantNet
|
||||||
|
"""
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
use Nostrum.Consumer
|
use Nostrum.Consumer
|
||||||
@@ -49,7 +52,6 @@ defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO improve composition
|
|
||||||
if saved_images do
|
if saved_images do
|
||||||
try do
|
try do
|
||||||
prepare_images(saved_images)
|
prepare_images(saved_images)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.FileServer do
|
defmodule PlantIdDiscordBot.FileServer do
|
||||||
|
@moduledoc """
|
||||||
|
File server for temporary storage of images until a response is made.
|
||||||
|
"""
|
||||||
use Plug.Builder
|
use Plug.Builder
|
||||||
|
|
||||||
plug(Plug.Logger)
|
plug(Plug.Logger)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ defmodule PlantIdDiscordBot.FileServer.File do
|
|||||||
@moduledoc """
|
@moduledoc """
|
||||||
File utilities
|
File utilities
|
||||||
"""
|
"""
|
||||||
|
require Logger
|
||||||
|
|
||||||
alias PlantIdDiscordBot.FileServer.ImageConverter
|
alias PlantIdDiscordBot.FileServer.ImageConverter
|
||||||
|
|
||||||
@image_path Application.compile_env(:plantid_discord_bot, :image_path)
|
@image_path Application.compile_env(:plantid_discord_bot, :image_path)
|
||||||
@@ -56,7 +58,6 @@ defmodule PlantIdDiscordBot.FileServer.File do
|
|||||||
|> File.read()
|
|> File.read()
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO make into a task for async deletion and deal with errors
|
|
||||||
@spec delete_files!([String.t()]) :: :ok
|
@spec delete_files!([String.t()]) :: :ok
|
||||||
def delete_files!(filenames) do
|
def delete_files!(filenames) do
|
||||||
tasks =
|
tasks =
|
||||||
@@ -65,8 +66,7 @@ defmodule PlantIdDiscordBot.FileServer.File do
|
|||||||
try do
|
try do
|
||||||
File.rm!(Path.join(@image_path, filename))
|
File.rm!(Path.join(@image_path, filename))
|
||||||
rescue
|
rescue
|
||||||
# TODO Logger
|
e -> Logger.error(e)
|
||||||
e -> IO.inspect(e)
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.Guild do
|
defmodule PlantIdDiscordBot.Guild do
|
||||||
|
@moduledoc """
|
||||||
|
Guild related functions.
|
||||||
|
"""
|
||||||
alias Nostrum.Cache.GuildCache
|
alias Nostrum.Cache.GuildCache
|
||||||
|
|
||||||
def get_guild_name!(guild_id) do
|
def get_guild_name!(guild_id) do
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.ProcessRegistry do
|
defmodule PlantIdDiscordBot.ProcessRegistry do
|
||||||
|
@moduledoc """
|
||||||
|
Process registry.
|
||||||
|
"""
|
||||||
def start_link do
|
def start_link do
|
||||||
Registry.start_link(keys: :unique, name: __MODULE__)
|
Registry.start_link(keys: :unique, name: __MODULE__)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
defmodule PlantIdDiscordBot.RateLimiter do
|
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
|
use GenServer
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
defmodule PlantIdDiscordBot.Utils do
|
defmodule PlantIdDiscordBot.Utils do
|
||||||
|
@moduledoc """
|
||||||
|
Bot related utility functions.
|
||||||
|
"""
|
||||||
def get_uptime() do
|
def get_uptime() do
|
||||||
start_time = Application.get_env(:plantid_discord_bot, :start_time)
|
start_time = Application.get_env(:plantid_discord_bot, :start_time)
|
||||||
|
|
||||||
|
|||||||
+32
-21
@@ -33,7 +33,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
|
|||||||
@doc """
|
@doc """
|
||||||
Parses the response from the PlantNet API into a map.
|
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
|
def parse(response) do
|
||||||
response
|
response
|
||||||
|> to_map!()
|
|> 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)}"
|
"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
|
end
|
||||||
|
|
||||||
@spec generate_gbif_url(map()) :: map()
|
@spec generate_gbif_url([map()]) :: [map()]
|
||||||
defp generate_gbif_url(data) do
|
defp generate_gbif_url(data) do
|
||||||
Enum.map(data, fn result ->
|
Enum.map(data, fn result ->
|
||||||
gbif_id = result["gbif"]["id"]
|
gbif_id = result["gbif"]["id"]
|
||||||
@@ -93,7 +93,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec generate_pfaf_url(map()) :: map()
|
@spec generate_pfaf_url([map()]) :: [map()]
|
||||||
defp generate_pfaf_url(data) do
|
defp generate_pfaf_url(data) do
|
||||||
Enum.map(data, fn result ->
|
Enum.map(data, fn result ->
|
||||||
pfaf_slug = String.replace(result["species"]["scientificNameWithoutAuthor"], " ", "+")
|
pfaf_slug = String.replace(result["species"]["scientificNameWithoutAuthor"], " ", "+")
|
||||||
@@ -104,7 +104,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec generate_powo_url(map()) :: map()
|
@spec generate_powo_url([map()]) :: [map()]
|
||||||
defp generate_powo_url(data) do
|
defp generate_powo_url(data) do
|
||||||
Enum.map(data, fn result ->
|
Enum.map(data, fn result ->
|
||||||
powo_id = result["powo"]["id"]
|
powo_id = result["powo"]["id"]
|
||||||
@@ -123,29 +123,40 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec iucn_parser(String.t()) :: String.t()
|
@spec iucn_parser(String.t()) :: String.t()
|
||||||
defp iucn_parser(abbreviation) do
|
def iucn_parser("DD"), do: "Data Deficient"
|
||||||
case abbreviation do
|
def iucn_parser("LC"), do: "Least Concern"
|
||||||
"DD" -> "Data Deficient"
|
def iucn_parser("NT"), do: "Near Threatened"
|
||||||
"LC" -> "Least Concern"
|
def iucn_parser("VU"), do: "Vulnerable"
|
||||||
"NT" -> "Near Threatened"
|
def iucn_parser("EN"), do: "Endangered"
|
||||||
"VU" -> "Vulnerable"
|
def iucn_parser("CR"), do: "Critically Endangered"
|
||||||
"EN" -> "Endangered"
|
def iucn_parser("EW"), do: "Extinct in the Wild"
|
||||||
"CR" -> "Critically Endangered"
|
def iucn_parser("EX"), do: "Extinct"
|
||||||
"EW" -> "Extinct in the Wild"
|
def iucn_parser("NE"), do: "Not Evaluated"
|
||||||
"EX" -> "Extinct"
|
def iucn_parser(_), do: "Unknown"
|
||||||
"NE" -> "Not Evaluated"
|
|
||||||
_ -> "Unknown"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_alternatives(map()) :: String.t()
|
@spec get_alternatives(map()) :: String.t()
|
||||||
defp get_alternatives(data) do
|
defp get_alternatives(data) do
|
||||||
if length(data) > 0 do
|
if !Enum.empty?(data) do
|
||||||
alternatives =
|
alternatives =
|
||||||
Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"])
|
Enum.map_join(data, ", ", & &1["species"]["scientificNameWithoutAuthor"])
|
||||||
|> Enum.join(", ")
|
|
||||||
|
|
||||||
"\n\nAlternatives include **#{alternatives}**."
|
"\n\nAlternatives include **#{alternatives}**."
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
defmodule PlantIdDiscordBot.Scheduler do
|
defmodule PlantIdDiscordBot.Scheduler do
|
||||||
|
@moduledoc """
|
||||||
|
CRON job scheduler.
|
||||||
|
"""
|
||||||
use Quantum, otp_app: :plantid_discord_bot
|
use Quantum, otp_app: :plantid_discord_bot
|
||||||
end
|
end
|
||||||
|
|||||||
+11
-1
@@ -1,11 +1,21 @@
|
|||||||
# https://rosettacode.org/wiki/Convert_seconds_to_compound_duration
|
|
||||||
defmodule PlantIdDiscordBot.Utils.Duration do
|
defmodule PlantIdDiscordBot.Utils.Duration do
|
||||||
|
@moduledoc """
|
||||||
|
Duration conversion functions.
|
||||||
|
"""
|
||||||
@minute 60
|
@minute 60
|
||||||
@hour @minute * 60
|
@hour @minute * 60
|
||||||
@day @hour * 24
|
@day @hour * 24
|
||||||
@week @day * 7
|
@week @day * 7
|
||||||
@divisor [@week, @day, @hour, @minute, 1]
|
@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
|
def sec_to_str(sec) do
|
||||||
{_, [s, m, h, d, w]} =
|
{_, [s, m, h, d, w]} =
|
||||||
Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} ->
|
Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} ->
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ defmodule PlantidDiscordBot.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :plantid_discord_bot,
|
app: :plantid_discord_bot,
|
||||||
version: "0.2.0",
|
version: "0.2.1",
|
||||||
elixir: "~> 1.16",
|
elixir: "~> 1.8",
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps()
|
deps: deps()
|
||||||
]
|
]
|
||||||
@@ -24,6 +24,7 @@ defmodule PlantidDiscordBot.MixProject do
|
|||||||
# Run "mix help deps" to learn about dependencies.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
|
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
|
||||||
{:nostrum, "~> 0.10"},
|
{:nostrum, "~> 0.10"},
|
||||||
{:httpoison, "~> 2.2"},
|
{:httpoison, "~> 2.2"},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
%{
|
%{
|
||||||
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"castle": {:hex, :castle, "0.3.0", "47b1a550b2348a6d7e60e43ded1df19dca601ed21ef6f267c3dbb1b3a301fbf5", [:mix], [{:forecastle, "~> 0.1.0", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "dbdc1c171520c4591101938a3d342dec70d36b7f5b102a5c138098581e35fcef"},
|
"castle": {:hex, :castle, "0.3.0", "47b1a550b2348a6d7e60e43ded1df19dca601ed21ef6f267c3dbb1b3a301fbf5", [:mix], [{:forecastle, "~> 0.1.0", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "dbdc1c171520c4591101938a3d342dec70d36b7f5b102a5c138098581e35fcef"},
|
||||||
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
|
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||||
@@ -6,10 +7,12 @@
|
|||||||
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
|
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
|
||||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
|
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
|
||||||
|
"credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"},
|
||||||
"crontab": {:hex, :crontab, "1.1.14", "233fcfdc2c74510cabdbcb800626babef414e7cb13cea11ddf62e10e16e2bf76", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "4e3b9950bc22ae8d0395ffb5f4b127a140005cba95745abf5ff9ee7e8203c6fa"},
|
"crontab": {:hex, :crontab, "1.1.14", "233fcfdc2c74510cabdbcb800626babef414e7cb13cea11ddf62e10e16e2bf76", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "4e3b9950bc22ae8d0395ffb5f4b127a140005cba95745abf5ff9ee7e8203c6fa"},
|
||||||
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
|
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||||
|
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||||
"forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"},
|
"forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"},
|
||||||
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
|
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
|
||||||
"gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
|
"gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ defmodule PlantIdDiscordBot.ErrorHandlingTest do
|
|||||||
test "do_identification/1 returns invokes logger on error" do
|
test "do_identification/1 returns invokes logger on error" do
|
||||||
message = PlantNetFixtures.Message.message()
|
message = PlantNetFixtures.Message.message()
|
||||||
|
|
||||||
log = capture_log(fn ->
|
log =
|
||||||
|
capture_log(fn ->
|
||||||
PlantIdDiscordBot.Cog.PlantNetMessage.do_identification(message)
|
PlantIdDiscordBot.Cog.PlantNetMessage.do_identification(message)
|
||||||
|
|
||||||
assert_received {:create_message, 123456, content: "An error has occured. Please try again later."}
|
assert_received {:create_message, 123_456,
|
||||||
|
content: "An error has occured. Please try again later."}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
IO.inspect(log)
|
|
||||||
|
|
||||||
assert log =~ "guild_id=#{message.guild_id}"
|
assert log =~ "guild_id=#{message.guild_id}"
|
||||||
assert log =~ "guild_name=#{@guild.get_guild_name!(message.guild_id)}"
|
assert log =~ "guild_name=#{@guild.get_guild_name!(message.guild_id)}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
defmodule PlantIdDiscordBotTest.FileServer.MimeTypes do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias PlantIdDiscordBot.FileServer.MimeTypes
|
||||||
|
doctest MimeTypes
|
||||||
|
|
||||||
|
describe "detect_mime_type/1" do
|
||||||
|
test "detects webp" do
|
||||||
|
bin = <<0x52, 0x49, 0x46, 0x46, 0x00>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/webp"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects jpeg" do
|
||||||
|
bin = <<0xFF, 0xD8, 0xFF, 0x00>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/jpeg"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects png" do
|
||||||
|
bin = <<0x89, 0x50, 0x4E, 0x47, 0x00>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/png"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects gif" do
|
||||||
|
bin = <<0x47, 0x49, 0x46, 0x38, 0x00>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/gif"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects bmp" do
|
||||||
|
bin = <<0x42, 0x4D, 0x00>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/bmp"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects tiff" do
|
||||||
|
bin = <<0x49, 0x49, 0x2A, 0x00, 0x00>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/tiff"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error for unsupported type" do
|
||||||
|
bin = <<0x00, 0x11, 0x22, 0x33>>
|
||||||
|
assert MimeTypes.detect_mime_type(bin) == {:error, "Unsupported file type"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -63,4 +63,19 @@ defmodule PlantIdDiscordBotTest.PlantNet.Parser do
|
|||||||
"My best guess is **Prunus cerasifera** with a confidence of **88%**.\n\nSpecies info from plant databases:\n[GBIF](<https://www.gbif.org/species/3021730>) | [PFAF](<https://pfaf.org/user/Plant.aspx?LatinName=/Prunus+cerasifera>) | [POWO](<https://powo.science.kew.org/taxon/729568-1>)\n\nConservation status: Data Deficient\n\nAlternatives include **Prunus × cistena**."
|
"My best guess is **Prunus cerasifera** with a confidence of **88%**.\n\nSpecies info from plant databases:\n[GBIF](<https://www.gbif.org/species/3021730>) | [PFAF](<https://pfaf.org/user/Plant.aspx?LatinName=/Prunus+cerasifera>) | [POWO](<https://powo.science.kew.org/taxon/729568-1>)\n\nConservation status: Data Deficient\n\nAlternatives include **Prunus × cistena**."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "iucn_parser/1" do
|
||||||
|
test "return correct string for abbreviation" do
|
||||||
|
assert Parser.iucn_parser("DD") == "Data Deficient"
|
||||||
|
assert Parser.iucn_parser("LC") == "Least Concern"
|
||||||
|
assert Parser.iucn_parser("NT") == "Near Threatened"
|
||||||
|
assert Parser.iucn_parser("VU") == "Vulnerable"
|
||||||
|
assert Parser.iucn_parser("EN") == "Endangered"
|
||||||
|
assert Parser.iucn_parser("CR") == "Critically Endangered"
|
||||||
|
assert Parser.iucn_parser("EW") == "Extinct in the Wild"
|
||||||
|
assert Parser.iucn_parser("EX") == "Extinct"
|
||||||
|
assert Parser.iucn_parser("NE") == "Not Evaluated"
|
||||||
|
assert Parser.iucn_parser("ABC") == "Unknown"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ ExUnit.start()
|
|||||||
|
|
||||||
defmodule PlantNetFixtures.Message do
|
defmodule PlantNetFixtures.Message do
|
||||||
@message %{
|
@message %{
|
||||||
channel_id: 1178600825380155412,
|
channel_id: 1_178_600_825_380_155_412,
|
||||||
guild_id: 1_002_507_312_159_797_318,
|
guild_id: 1_002_507_312_159_797_318,
|
||||||
attachments: [%{url: "http://invalid-url.com/image.jpg"}],
|
attachments: [%{url: "http://invalid-url.com/image.jpg"}]
|
||||||
}
|
}
|
||||||
|
|
||||||
def message(), do: @message
|
def message(), do: @message
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
defmodule PlantIdDiscordBotTest.Utils.Duration do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias PlantIdDiscordBot.Utils.Duration
|
||||||
|
|
||||||
|
doctest Duration
|
||||||
|
|
||||||
|
describe "sec_to_str/1" do
|
||||||
|
test "seconds" do
|
||||||
|
assert Duration.sec_to_str(5) == "5s"
|
||||||
|
refute Duration.sec_to_str(60) == "60s"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "minutes" do
|
||||||
|
assert Duration.sec_to_str(60) == "1m"
|
||||||
|
assert Duration.sec_to_str(75) == "1m 15s"
|
||||||
|
refute Duration.sec_to_str(3600) == "60m"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "hours" do
|
||||||
|
assert Duration.sec_to_str(3600) == "1h"
|
||||||
|
assert Duration.sec_to_str(7205) == "2h 5s"
|
||||||
|
assert Duration.sec_to_str(36_300) == "10h 5m"
|
||||||
|
assert Duration.sec_to_str(36_315) == "10h 5m 15s"
|
||||||
|
refute Duration.sec_to_str(86_400) == "24h"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "days" do
|
||||||
|
assert Duration.sec_to_str(86_400) == "1d"
|
||||||
|
assert Duration.sec_to_str(86_715) == "1d 5m 15s"
|
||||||
|
refute Duration.sec_to_str(604_800) == "7d"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "weeks" do
|
||||||
|
assert Duration.sec_to_str(604_800) == "1w"
|
||||||
|
assert Duration.sec_to_str(6_048_000) == "10w"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user