defmodule AprsmeWeb.Api.V1.CallsignController do @moduledoc """ API v1 controller for callsign-related endpoints. """ use AprsmeWeb, :controller alias Aprsme.ApiMetrics alias Aprsme.ErrorHandler alias Aprsme.Packets alias Aprsme.PlausibleAnalytics alias AprsmeWeb.Api.V1.CallsignJSON action_fallback AprsmeWeb.Api.V1.FallbackController @doc """ Get the most recent packet for a given callsign. ## Parameters * `callsign` - The callsign to search for, with optional SSID (e.g., "N0CALL" or "N0CALL-9") ## Returns * 200 - Success with packet data * 404 - No packets found for the callsign * 400 - Invalid callsign format """ def show(conn, %{"callsign" => callsign}) do PlausibleAnalytics.track(conn, "api_callsign_lookup", %{callsign: String.upcase(String.trim(callsign))}) ApiMetrics.track(%{}, "api_callsign_lookup") with {:ok, normalized_callsign} <- validate_callsign(callsign), {:ok, packet} <- get_latest_packet(normalized_callsign) do conn |> put_status(:ok) |> put_view(json: CallsignJSON) |> render(:show, packet: packet) else {:error, :invalid_callsign} -> {:error, "Invalid callsign format"} {:error, :not_found} -> conn |> put_status(:ok) |> put_view(json: CallsignJSON) |> render(:not_found, callsign: callsign) {:error, reason} -> {:error, reason} end end defp validate_callsign(callsign) when is_binary(callsign) do normalized = callsign |> String.trim() |> String.upcase() check_callsign_format(validate_callsign_format(normalized), normalized) end defp check_callsign_format(true, normalized), do: {:ok, normalized} defp check_callsign_format(false, _normalized), do: {:error, :invalid_callsign} defp validate_callsign_format(callsign) do # Basic callsign validation # Matches patterns like: N0CALL, N0CALL-9, W1ABC-15, etc. # This is a simplified regex - a full implementation might be more comprehensive callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/ String.match?(callsign, callsign_regex) and String.length(callsign) <= 12 end defp get_latest_packet(callsign) do # Get the most recent packet for this callsign regardless of age or type # Use cached version for better performance with error handling fn -> case Packets.get_latest_packet_for_callsign(callsign) do nil -> {:error, :not_found} packet -> {:ok, packet} end end |> ErrorHandler.with_error_handling( context: %{callsign: callsign, operation: :get_latest_packet}, max_retries: 1, retry_delay: 500 ) |> case do {:ok, result} -> result {:error, error} -> {:error, error} end end end