mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
feat: add websocket and webhooks for events
This commit is contained in:
218
lib/wanderer_app_web/controllers/map_events_api_controller.ex
Normal file
218
lib/wanderer_app_web/controllers/map_events_api_controller.ex
Normal file
@@ -0,0 +1,218 @@
|
||||
defmodule WandererAppWeb.MapEventsAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.ExternalEvents.MapEventRelay
|
||||
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Schema Definitions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@event_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, description: "ULID event identifier"},
|
||||
map_id: %OpenApiSpex.Schema{type: :string, description: "Map UUID"},
|
||||
type: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
enum: [
|
||||
"add_system",
|
||||
"deleted_system",
|
||||
"system_metadata_changed",
|
||||
"system_renamed",
|
||||
"signature_added",
|
||||
"signature_removed",
|
||||
"signatures_updated",
|
||||
"connection_added",
|
||||
"connection_removed",
|
||||
"connection_updated",
|
||||
"character_added",
|
||||
"character_removed",
|
||||
"character_updated",
|
||||
"map_kill"
|
||||
],
|
||||
description: "Event type"
|
||||
},
|
||||
payload: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
description: "Event-specific payload data",
|
||||
additionalProperties: true
|
||||
},
|
||||
ts: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
format: :date_time,
|
||||
description: "Event timestamp (ISO8601)"
|
||||
}
|
||||
},
|
||||
required: [:id, :map_id, :type, :payload, :ts],
|
||||
example: %{
|
||||
id: "01J7KZXYZ123456789ABCDEF",
|
||||
map_id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
type: "add_system",
|
||||
payload: %{
|
||||
solar_system_id: 30000142,
|
||||
solar_system_name: "Jita"
|
||||
},
|
||||
ts: "2025-01-20T12:34:56Z"
|
||||
}
|
||||
}
|
||||
|
||||
@events_response_schema ApiSchemas.data_wrapper(
|
||||
%OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @event_schema
|
||||
}
|
||||
)
|
||||
|
||||
@events_list_params %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
since: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
format: :date_time,
|
||||
description: "Return events after this timestamp (ISO8601)"
|
||||
},
|
||||
limit: %OpenApiSpex.Schema{
|
||||
type: :integer,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 100,
|
||||
description: "Maximum number of events to return"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# OpenApiSpex Operations
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
operation :list_events,
|
||||
summary: "List recent events for a map",
|
||||
description: """
|
||||
Retrieves recent events for the specified map. This endpoint provides a way to catch up on missed events
|
||||
after a WebSocket disconnection. Events are retained for approximately 10 minutes.
|
||||
""",
|
||||
tags: ["Map Events"],
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map UUID or slug",
|
||||
type: :string,
|
||||
required: true
|
||||
],
|
||||
since: [
|
||||
in: :query,
|
||||
description: "Return events after this timestamp (ISO8601)",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "2025-01-20T12:00:00Z"
|
||||
],
|
||||
limit: [
|
||||
in: :query,
|
||||
description: "Maximum number of events to return (1-100)",
|
||||
type: :integer,
|
||||
required: false
|
||||
]
|
||||
],
|
||||
responses: %{
|
||||
200 => {"Success", "application/json", @events_response_schema},
|
||||
400 => ResponseSchemas.bad_request("Invalid parameters"),
|
||||
401 => ResponseSchemas.bad_request("Unauthorized"),
|
||||
404 => ResponseSchemas.not_found("Map not found"),
|
||||
500 => ResponseSchemas.internal_server_error("Internal server error")
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Controller Actions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def list_events(conn, %{"map_identifier" => map_identifier} = params) do
|
||||
with {:ok, map} <- get_map(conn, map_identifier),
|
||||
{:ok, since} <- parse_since_param(params),
|
||||
{:ok, limit} <- parse_limit_param(params) do
|
||||
|
||||
# If no 'since' parameter provided, default to 10 minutes ago
|
||||
since_datetime = since || DateTime.add(DateTime.utc_now(), -10, :minute)
|
||||
|
||||
# Check if MapEventRelay is running before calling
|
||||
events = if Process.whereis(MapEventRelay) do
|
||||
try do
|
||||
MapEventRelay.get_events_since(map.id, since_datetime, limit)
|
||||
catch
|
||||
:exit, {:noproc, _} ->
|
||||
Logger.error("MapEventRelay process not available")
|
||||
[]
|
||||
|
||||
:exit, reason ->
|
||||
Logger.error("Failed to get events from MapEventRelay: #{inspect(reason)}")
|
||||
[]
|
||||
end
|
||||
else
|
||||
Logger.error("MapEventRelay is not running")
|
||||
[]
|
||||
end
|
||||
|
||||
# Events are already in JSON format from ETS
|
||||
|
||||
json(conn, %{data: events})
|
||||
else
|
||||
{:error, :map_not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Map not found"})
|
||||
|
||||
{:error, :invalid_since} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Invalid 'since' parameter. Must be ISO8601 datetime."})
|
||||
|
||||
{:error, :invalid_limit} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Invalid 'limit' parameter. Must be between 1 and 100."})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Internal server error"})
|
||||
end
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Private Functions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map(conn, map_identifier) do
|
||||
# The map should already be loaded by the CheckMapApiKey plug
|
||||
case conn.assigns[:map] do
|
||||
nil -> {:error, :map_not_found}
|
||||
map -> {:ok, map}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_since_param(%{"since" => since_str}) when is_binary(since_str) do
|
||||
case DateTime.from_iso8601(since_str) do
|
||||
{:ok, datetime, _offset} -> {:ok, datetime}
|
||||
{:error, _} -> {:error, :invalid_since}
|
||||
end
|
||||
end
|
||||
defp parse_since_param(_), do: {:ok, nil}
|
||||
|
||||
defp parse_limit_param(%{"limit" => limit_str}) when is_binary(limit_str) do
|
||||
case Integer.parse(limit_str) do
|
||||
{limit, ""} when limit >= 1 and limit <= 100 -> {:ok, limit}
|
||||
_ -> {:error, :invalid_limit}
|
||||
end
|
||||
end
|
||||
defp parse_limit_param(%{"limit" => limit}) when is_integer(limit) do
|
||||
if limit >= 1 and limit <= 100 do
|
||||
{:ok, limit}
|
||||
else
|
||||
{:error, :invalid_limit}
|
||||
end
|
||||
end
|
||||
defp parse_limit_param(_), do: {:ok, 100}
|
||||
end
|
||||
Reference in New Issue
Block a user