Projects and Diseases placeholder application commands (#17)

* fix module name in backup

* save projects to persistent_term

* retrieve projects on bot startup

* remove commented out code

* register project autocomplete

* placeholder text for project

* placeholder disease command
This commit is contained in:
2025-12-08 08:53:48 +01:00
committed by GitHub
parent 02372f2076
commit 0eddf04678
11 changed files with 182 additions and 27 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ jobs:
tar -xzf release.tar.gz tar -xzf release.tar.gz
# save metrics and stop the bot - bot runs as a service # save metrics and stop the bot - bot runs as a service
current_release/bin/plantid_discord_bot rpc "PlantidDiscordBot.Metrics.backup()" current_release/bin/plantid_discord_bot rpc "PlantIdDiscordBot.Metrics.backup()"
echo "${{ secrets.SUDO_PASSWORD }}" | sudo -S sudo service plantid_discord_bot stop echo "${{ secrets.SUDO_PASSWORD }}" | sudo -S sudo service plantid_discord_bot stop
# switch releases # switch releases
+3
View File
@@ -46,6 +46,9 @@ 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 development
DISCORD_DEV_GUILD_ID= Your guild ID to populate application commands immediately
# needed in production # 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
``` ```
+3 -1
View File
@@ -3,10 +3,12 @@
## 🚀 High Priority ## 🚀 High Priority
- [x] Update deps and run on Elixir 1.18 / otp 28 - [x] Update deps and run on Elixir 1.18 / otp 28
- [ ] Pull projects on a regular basis
- [ ] identify by project
- [ ] identify by country
- [ ] Run static analysis tools and fix code - [ ] Run static analysis tools and fix code
- [ ] Increase test coverage - [ ] Increase test coverage
- [ ] Fix spec errors - [ ] Fix spec errors
- [ ] Improve error return message
## 📦 Medium Priority ## 📦 Medium Priority
+2 -1
View File
@@ -3,7 +3,8 @@ import Config
config :plantid_discord_bot, config :plantid_discord_bot,
api: Nostrum.Api, api: Nostrum.Api,
guild: PlantIdDiscordBot.Guild, guild: PlantIdDiscordBot.Guild,
port: 4321 port: 4321,
dev_guild_id: System.get_env("DISCORD_DEV_GUILD_ID")
config :plantid_discord_bot, :environment, :dev config :plantid_discord_bot, :environment, :dev
+2 -1
View File
@@ -6,7 +6,8 @@ config :plantid_discord_bot,
api: Nostrum.Api, api: Nostrum.Api,
plantnet_api_key: System.get_env("PLANTNET_API_KEY"), plantnet_api_key: System.get_env("PLANTNET_API_KEY"),
fileserver_url: System.get_env("PLANTID_FILESERVER_URL", "http://localhost:4321"), fileserver_url: System.get_env("PLANTID_FILESERVER_URL", "http://localhost:4321"),
metrics_webhook_url: System.get_env("PLANTID_LOGS_DISCORD_WEBHOOK_URL") metrics_webhook_url: System.get_env("PLANTID_LOGS_DISCORD_WEBHOOK_URL"),
dev_guild_id: System.get_env("DISCORD_DEV_GUILD_ID")
config :nostrum, config :nostrum,
token: System.get_env("PLANTID_DISCORD_BOT_TOKEN") token: System.get_env("PLANTID_DISCORD_BOT_TOKEN")
+19
View File
@@ -38,6 +38,25 @@ defmodule PlantIdDiscordBot.Consumer.Commands do
name: "servers", name: "servers",
description: "All servers that this bot belongs to", description: "All servers that this bot belongs to",
options: [] options: []
},
%{
name: "diseases",
description: "Identify diseases from photos",
options: []
},
%{
name: "projects",
description: "Plants grouped by region or type, for identification",
options: [
%{
# STRING
type: 3,
name: "project",
description: "Search for a project",
required: true,
autocomplete: true
}
]
} }
] ]
end end
+19 -7
View File
@@ -9,10 +9,26 @@ defmodule PlantIdDiscordBot.Consumer do
@global_application_commands Consumer.Commands.global_application_commands() @global_application_commands Consumer.Commands.global_application_commands()
def handle_event({:READY, _data, _ws_state}) do def handle_event({:READY, _data, _ws_state}) do
Api.create_global_application_command(@global_application_commands) Projects.fetch_projects()
guild_id = Application.get_env(:plantid_discord_bot, :dev_guild_id)
# Register commands instantly in your test server
Enum.each(@global_application_commands, fn cmd ->
Api.create_guild_application_command(guild_id, cmd)
end)
# Only register global commands in production
if Mix.env() == :prod do
Api.create_global_application_command(@global_application_commands)
end
Api.update_status(:online, "Guess the Plant | /help") Api.update_status(:online, "Guess the Plant | /help")
end end
def handle_event({:INTERACTION_CREATE, %{type: 4} = interaction, _ws_state}) do
PlantIdDiscordBot.Cog.Projects.autocomplete(interaction)
end
def handle_event({:INTERACTION_CREATE, %{data: %{name: command}} = interaction, _ws_state}) do def handle_event({:INTERACTION_CREATE, %{data: %{name: command}} = interaction, _ws_state}) do
case command do case command do
"source" -> Cog.Info.source(interaction) "source" -> Cog.Info.source(interaction)
@@ -22,6 +38,8 @@ defmodule PlantIdDiscordBot.Consumer do
"stats" -> Cog.Info.stats(interaction) "stats" -> Cog.Info.stats(interaction)
"status" -> Cog.Info.status(interaction) "status" -> Cog.Info.status(interaction)
"servers" -> Cog.Info.servers(interaction) "servers" -> Cog.Info.servers(interaction)
"diseases" -> Cog.Diseases.diseases(interaction)
"projects" -> Cog.Projects.projects(interaction)
end end
end end
@@ -30,11 +48,5 @@ defmodule PlantIdDiscordBot.Consumer do
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
+16
View File
@@ -0,0 +1,16 @@
defmodule PlantIdDiscordBot.Cog.Diseases do
@moduledoc """
Functions for using the /diseases application command.
"""
alias PlantIdDiscordBot.PlantNet.Projects
@api Application.compile_env(:plantid_discord_bot, :api)
@doc """
Sends an invite link for this bot.
"""
def diseases(interaction) do
message = "Diseases coming soon."
@api.create_interaction_response(interaction, %{type: 4, data: %{content: message}})
end
end
+71
View File
@@ -0,0 +1,71 @@
defmodule PlantIdDiscordBot.Cog.Projects do
@moduledoc """
Functions for using the /projects application command.
"""
alias PlantIdDiscordBot.PlantNet.Projects
@api Application.compile_env(:plantid_discord_bot, :api)
# ---------------------------------------------------------
# 1. AUTOCOMPLETE HANDLER
# ---------------------------------------------------------
def autocomplete(interaction) do
# Extract what the user is typing (may be nil)
[%{value: user_input}] = interaction.data.options
user_input = user_input || ""
# Fetch your project list
projects = Projects.retrieve_projects()
# Filter based on description
suggestions =
projects
|> Enum.filter(fn p ->
String.contains?(
String.downcase(p["description"]),
String.downcase(user_input)
)
end)
|> Enum.take(25)
|> Enum.map(fn p ->
%{
# what the user sees
name: p["description"],
# what your command receives
value: p["id"]
}
end)
# Respond with autocomplete choices
@api.create_interaction_response(interaction, %{
type: 8,
data: %{choices: suggestions}
})
end
# ---------------------------------------------------------
# 2. FINAL COMMAND EXECUTION
# ---------------------------------------------------------
def projects(interaction) do
# Extract selected project ID
project_id =
interaction.data.options
|> Enum.find(&(&1.name == "project"))
|> then(&(&1 && &1.value))
@api.create_interaction_response(interaction, %{
type: 4,
data: %{
content:
case project_id do
nil ->
"Please choose a project using autocomplete."
id ->
# "You selected project: **#{id}**"
"Identification by project coming soon."
end
}
})
end
end
-16
View File
@@ -142,21 +142,5 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
"\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
+46
View File
@@ -0,0 +1,46 @@
defmodule PlantIdDiscordBot.PlantNet.Projects do
@moduledoc """
Functions related to PlantNet's projecs
"""
require Logger
alias HTTPoison.Response
@plantnet_api_base_url Application.compile_env(:plantid_discord_bot, :plantnet_api_base_url)
def fetch_projects do
try do
build_query_uri()
|> get_response()
|> parse_response()
|> save_projects()
rescue
e ->
Logger.error(Exception.format(:error, e, __STACKTRACE__))
end
end
defp build_query_uri do
URI.parse("#{@plantnet_api_base_url}/projects")
|> URI.append_query("lang=en")
|> URI.append_query("type=kt")
|> URI.append_query("api-key=#{Application.get_env(:plantid_discord_bot, :plantnet_api_key)}")
|> URI.to_string()
end
defp get_response(query_uri), do: HTTPoison.get!(query_uri)
defp parse_response(%Response{status_code: 200, body: body}), do: Jason.decode!(body)
defp parse_response(%Response{status_code: 401, body: body}) do
Logger.critical("Unauthorized request to PlantNet API: #{body}")
end
defp save_projects(projects), do: :persistent_term.put(:projects, projects)
def retrieve_projects, do: :persistent_term.get(:projects)
def find_project_by_description(description) do
retrieve_projects()
|> Enum.find(&(&1["description"] == description))
end
end