mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-29 20:43:23 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cccab2a985 | ||
|
|
1abaa90a7d | ||
|
|
6e1993ca8a | ||
|
|
171c821ac4 | ||
|
|
7ebf9186bf | ||
|
|
57d2f2baef | ||
|
|
0aee13878a | ||
|
|
f93ef0ca76 | ||
|
|
4ec03d8338 | ||
|
|
733482cd5c | ||
|
|
3969d1287d | ||
|
|
1aa7854b0d | ||
|
|
7b27d4a1a7 | ||
|
|
24ddb8771f | ||
|
|
7134714245 | ||
|
|
96b320ac26 | ||
|
|
b88e121b30 | ||
|
|
4ba4119c2b | ||
|
|
91d1ca201c | ||
|
|
8bf063a228 | ||
|
|
4f53de39b1 | ||
|
|
8c3804f107 | ||
|
|
1be4ec2b90 | ||
|
|
8f0ed44b11 | ||
|
|
cbadfc4ac4 | ||
|
|
3d88ae4452 | ||
|
|
07e2196eb4 | ||
|
|
6d99c54af7 | ||
|
|
2b7901e9a8 | ||
|
|
fb06dd1dbc | ||
|
|
d3b825529e | ||
|
|
ccf9c0db22 | ||
|
|
f8ba36b8be | ||
|
|
5bf9d99b3d | ||
|
|
7cad05342a | ||
|
|
6378754c57 |
90
CHANGELOG.md
90
CHANGELOG.md
@@ -2,6 +2,96 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.64.3](https://github.com/wanderer-industries/wanderer/compare/v1.64.2...v1.64.3) (2025-05-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed character tracking initialization logic & removed search caching
|
||||
|
||||
## [v1.64.2](https://github.com/wanderer-industries/wanderer/compare/v1.64.1...v1.64.2) (2025-05-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed tracking of ship & location for offline characters
|
||||
|
||||
## [v1.64.1](https://github.com/wanderer-industries/wanderer/compare/v1.64.0...v1.64.1) (2025-05-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed tracking stopped due to server errors
|
||||
|
||||
## [v1.64.0](https://github.com/wanderer-industries/wanderer/compare/v1.63.0...v1.64.0) (2025-05-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* api: add additional structure/signature methods (#365)
|
||||
|
||||
* api: add additional system/connections methods (#351)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed EOL connections cleanup
|
||||
|
||||
* Core: Avoid Zarzakh system in routes widget
|
||||
|
||||
* remove repeat errors for token refresh (#375)
|
||||
|
||||
* updated openapi spec for character activity (#374)
|
||||
|
||||
* removed error from characters endpoint, and updated routes (#372)
|
||||
|
||||
* cleanup examples for system and connections (#370)
|
||||
|
||||
* remove error on websocket reconnect (#367)
|
||||
|
||||
## [v1.63.0](https://github.com/wanderer-industries/wanderer/compare/v1.62.4...v1.63.0) (2025-05-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Updated map active characters page
|
||||
|
||||
## [v1.62.4](https://github.com/wanderer-industries/wanderer/compare/v1.62.3...v1.62.4) (2025-05-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map characters got untracked
|
||||
|
||||
## [v1.62.3](https://github.com/wanderer-industries/wanderer/compare/v1.62.2...v1.62.3) (2025-05-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map characters got untracked
|
||||
|
||||
## [v1.62.2](https://github.com/wanderer-industries/wanderer/compare/v1.62.1...v1.62.2) (2025-05-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed audit export API
|
||||
|
||||
## [v1.62.1](https://github.com/wanderer-industries/wanderer/compare/v1.62.0...v1.62.1) (2025-05-05)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -5,6 +5,16 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
@derive {Jason.Encoder, only: [
|
||||
:id,
|
||||
:map_id,
|
||||
:character_id,
|
||||
:tracked,
|
||||
:followed,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]}
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_character_settings_v1")
|
||||
|
||||
@@ -164,4 +164,23 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
identities do
|
||||
identity :uniq_system_eve_id, [:system_id, :eve_id]
|
||||
end
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:system_id,
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:updated,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -4,6 +4,27 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
|
||||
"""
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:solar_system_name,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]
|
||||
}
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
@@ -13,35 +13,40 @@ defmodule WandererApp.Application do
|
||||
WandererAppWeb.Telemetry,
|
||||
WandererApp.Vault,
|
||||
WandererApp.Repo,
|
||||
|
||||
{Phoenix.PubSub, name: WandererApp.PubSub, adapter_name: Phoenix.PubSub.PG2},
|
||||
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch,
|
||||
pools: %{
|
||||
default: [
|
||||
size: 25, # number of connections per pool
|
||||
count: 2, # number of pools (so total 50 connections)
|
||||
# number of connections per pool
|
||||
size: 25,
|
||||
# number of pools (so total 50 connections)
|
||||
count: 2
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
WandererApp.Cache,
|
||||
|
||||
Supervisor.child_spec({Cachex, name: :system_static_info_cache}, id: :system_static_info_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache}, id: :character_state_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :api_cache}, id: :api_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :system_static_info_cache},
|
||||
id: :system_static_info_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache},
|
||||
id: :character_state_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :tracked_characters},
|
||||
id: :tracked_characters_cache_worker
|
||||
),
|
||||
WandererApp.Scheduler,
|
||||
{Registry, keys: :unique, name: WandererApp.MapRegistry},
|
||||
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererApp.Zkb.Supervisor,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
@@ -49,11 +54,10 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Character.TrackerManager,
|
||||
WandererApp.Map.Manager,
|
||||
WandererApp.Map.ZkbDataFetcher,
|
||||
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
] ++
|
||||
maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
|
||||
opts = [strategy: :one_for_one, name: WandererApp.Supervisor]
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
status: binary()
|
||||
}
|
||||
|
||||
@online_error_timeout :timer.minutes(2)
|
||||
@online_error_timeout :timer.minutes(3)
|
||||
@forbidden_ttl :timer.minutes(1)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
|
||||
@@ -49,7 +49,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> new()
|
||||
end
|
||||
|
||||
def update_track_settings(character_id, track_settings) do
|
||||
def update_settings(character_id, track_settings) do
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
{:ok,
|
||||
@@ -103,7 +103,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> update_ship()
|
||||
end
|
||||
|
||||
def update_ship(%{character_id: character_id, track_ship: true} = character_state) do
|
||||
def update_ship(
|
||||
%{character_id: character_id, track_ship: true, is_online: true} = character_state
|
||||
) do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character()
|
||||
|> case do
|
||||
@@ -154,7 +156,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> update_location()
|
||||
end
|
||||
|
||||
def update_location(%{track_location: true, character_id: character_id} = character_state) do
|
||||
def update_location(
|
||||
%{track_location: true, is_online: true, character_id: character_id} = character_state
|
||||
) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden")
|
||||
@@ -305,14 +309,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_started")
|
||||
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
character_state
|
||||
| is_online: false,
|
||||
track_ship: false,
|
||||
track_location: false
|
||||
is_online: false
|
||||
})
|
||||
|
||||
:ok
|
||||
@@ -494,7 +493,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
state,
|
||||
location
|
||||
) do
|
||||
location = get_location(location)
|
||||
location = get_location(location)
|
||||
|
||||
if not is_location_started?(character_id) do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:start_solar_system_id", nil)
|
||||
@@ -544,14 +543,18 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
|
||||
defp is_location_updated?(
|
||||
%{solar_system_id: new_solar_system_id, station_id: new_station_id, structure_id: new_structure_id} = _location,
|
||||
%{
|
||||
solar_system_id: new_solar_system_id,
|
||||
station_id: new_station_id,
|
||||
structure_id: new_structure_id
|
||||
} = _location,
|
||||
solar_system_id,
|
||||
structure_id,
|
||||
station_id
|
||||
),
|
||||
do:
|
||||
solar_system_id != new_solar_system_id ||
|
||||
solar_system_id != new_solar_system_id ||
|
||||
solar_system_id != new_solar_system_id ||
|
||||
structure_id != new_structure_id ||
|
||||
station_id != new_station_id
|
||||
|
||||
@@ -724,14 +727,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
%{
|
||||
state
|
||||
| track_ship: false,
|
||||
track_online: false,
|
||||
track_location: false
|
||||
}
|
||||
state
|
||||
end
|
||||
|
||||
defp maybe_stop_tracking(
|
||||
|
||||
@@ -31,9 +31,7 @@ defmodule WandererApp.Character.TrackerManager do
|
||||
def init(args) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
{:ok, Impl.init(args |> Keyword.merge(characters: tracked_characters)), {:continue, :start}}
|
||||
{:ok, Impl.init(args), {:continue, :start}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -32,71 +32,72 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|> new()
|
||||
end
|
||||
|
||||
def start(%{opts: opts} = state) do
|
||||
opts[:characters]
|
||||
|> Enum.reduce(state, fn character_id, acc ->
|
||||
start_tracking(acc, character_id, %{})
|
||||
def start(state) do
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
tracked_characters
|
||||
|> Enum.each(fn character_id ->
|
||||
start_tracking(state, character_id, %{})
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def start_tracking(%__MODULE__{characters: characters} = state, character_id, opts) do
|
||||
case Enum.member?(characters, character_id) do
|
||||
true ->
|
||||
state
|
||||
def start_tracking(state, character_id, opts) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
false <- Enum.member?(characters, character_id) do
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
false ->
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
tracked_characters = [character_id | characters] |> Enum.uniq()
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
tracked_characters = [character_id | state.characters] |> Enum.uniq()
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
|
||||
%{state | characters: tracked_characters}
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def stop_tracking(%__MODULE__{characters: characters} = state, character_id) do
|
||||
case Enum.member?(characters, character_id) do
|
||||
true ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id, false)
|
||||
def stop_tracking(state, character_id) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
{:ok, %{start_time: start_time}} <-
|
||||
WandererApp.Character.get_character_state(character_id, false) do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
case character_state do
|
||||
nil ->
|
||||
state
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_started")
|
||||
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
|
||||
%{start_time: start_time} ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_started")
|
||||
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
tracked_characters =
|
||||
state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
%{state | characters: tracked_characters}
|
||||
end
|
||||
|
||||
false ->
|
||||
state
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def update_track_settings(
|
||||
@@ -118,7 +119,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
)
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_track_settings(character_id, track_settings)
|
||||
WandererApp.Character.Tracker.update_settings(character_id, track_settings)
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
else
|
||||
@@ -135,12 +136,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def get_characters(
|
||||
%{
|
||||
characters: characters
|
||||
} = state,
|
||||
state,
|
||||
_opts \\ []
|
||||
),
|
||||
do: {characters, state}
|
||||
) do
|
||||
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
{characters, state}
|
||||
end
|
||||
|
||||
def handle_event({ref, result}, state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
@@ -163,13 +164,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
def handle_info(
|
||||
:garbage_collect,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
|
||||
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
@@ -213,15 +213,15 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
WandererApp.Cache.get_and_remove!("character_untrack_queue", [])
|
||||
|> Task.async_stream(
|
||||
fn {map_id, character_id} ->
|
||||
WandererApp.Cache.delete("map_#{map_id}:character_#{character_id}:tracked")
|
||||
if not character_is_present(map_id, character_id) do
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
end
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
@@ -233,21 +233,23 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def handle_info({:stop_track, character_id}, state) do
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking")
|
||||
|> case do
|
||||
false ->
|
||||
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
|
||||
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
|
||||
state = state |> stop_tracking(character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
|
||||
|
||||
state
|
||||
|
||||
_ ->
|
||||
state
|
||||
if not WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking") do
|
||||
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
|
||||
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
|
||||
stop_tracking(state, character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_info(_event, state),
|
||||
do: state
|
||||
|
||||
defp character_is_present(map_id, character_id) do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
Enum.member?(presence_character_ids, character_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,11 +17,11 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
@unique_registry :unique_tracker_pool_registry
|
||||
|
||||
@update_location_interval :timer.seconds(2)
|
||||
@update_online_interval :timer.seconds(10)
|
||||
@update_online_interval :timer.seconds(5)
|
||||
@check_online_errors_interval :timer.seconds(30)
|
||||
@update_ship_interval :timer.seconds(5)
|
||||
@update_ship_interval :timer.seconds(2)
|
||||
@update_info_interval :timer.minutes(1)
|
||||
@update_wallet_interval :timer.minutes(5)
|
||||
@update_wallet_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(5)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
@@ -167,10 +167,22 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
def handle_info(
|
||||
:update_online,
|
||||
state
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :update_online, @update_online_interval)
|
||||
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
{:ok, updated_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(existing_settings)
|
||||
|
||||
:ok = untrack_characters([character], map_id, caller_pid)
|
||||
:ok = untrack([character], map_id, caller_pid)
|
||||
:ok = remove_characters([character], map_id)
|
||||
{:ok, updated_settings}
|
||||
else
|
||||
@@ -131,7 +131,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
{:ok, %{tracked: false} = existing_settings} ->
|
||||
if track do
|
||||
{:ok, updated_settings} = WandererApp.MapCharacterSettingsRepo.track(existing_settings)
|
||||
:ok = track_characters([character], map_id, true, caller_pid)
|
||||
:ok = track([character], map_id, true, caller_pid)
|
||||
:ok = add_characters([character], map_id, true)
|
||||
{:ok, updated_settings}
|
||||
else
|
||||
@@ -148,7 +148,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
tracked: true
|
||||
})
|
||||
|
||||
:ok = track_characters([character], map_id, true, caller_pid)
|
||||
:ok = track([character], map_id, true, caller_pid)
|
||||
:ok = add_characters([character], map_id, true)
|
||||
{:ok, settings}
|
||||
else
|
||||
@@ -161,61 +161,86 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
# Helper functions for character tracking
|
||||
def track_characters(_, _, false, _), do: :ok
|
||||
def track_characters([], _map_id, _is_track_character?, _), do: :ok
|
||||
|
||||
def track_characters([character | characters], map_id, true, caller_pid) do
|
||||
with :ok <- track_character(character, map_id, caller_pid) do
|
||||
track_characters(characters, map_id, true, caller_pid)
|
||||
def track([], _map_id, _is_track_character?, _), do: :ok
|
||||
|
||||
def track([character | characters], map_id, is_track_allowed, caller_pid) do
|
||||
with :ok <- track_character(character, map_id, is_track_allowed, caller_pid) do
|
||||
track(characters, map_id, is_track_allowed, caller_pid)
|
||||
end
|
||||
end
|
||||
|
||||
def track_character(
|
||||
%{
|
||||
id: character_id,
|
||||
eve_id: eve_id,
|
||||
corporation_id: corporation_id,
|
||||
alliance_id: alliance_id
|
||||
},
|
||||
map_id,
|
||||
caller_pid
|
||||
) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{})
|
||||
defp track_character(
|
||||
%{
|
||||
id: character_id,
|
||||
eve_id: eve_id
|
||||
},
|
||||
map_id,
|
||||
is_track_allowed,
|
||||
caller_pid
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
WandererAppWeb.Presence.update(caller_pid, map_id, character_id, %{
|
||||
tracked: is_track_allowed,
|
||||
from: DateTime.utc_now()
|
||||
})
|
||||
|> case do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
|
||||
{:error, :nopresence} ->
|
||||
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{
|
||||
tracked: is_track_allowed,
|
||||
from: DateTime.utc_now()
|
||||
})
|
||||
|
||||
case WandererApp.Cache.lookup!(cache_key, false) do
|
||||
true ->
|
||||
:ok
|
||||
error ->
|
||||
Logger.error("Failed to update presence: #{inspect(error)}")
|
||||
{:error, "Failed to update presence"}
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
|
||||
:ok = WandererApp.Cache.put(cache_key, true)
|
||||
end
|
||||
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
|
||||
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
else
|
||||
case WandererApp.Cache.lookup!(cache_key, false) do
|
||||
true ->
|
||||
Logger.error("caller_pid is required for tracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
|
||||
:ok = WandererApp.Cache.put(cache_key, true)
|
||||
end
|
||||
|
||||
if is_track_allowed do
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def untrack_characters(characters, map_id, caller_pid) do
|
||||
defp track_character(
|
||||
_character,
|
||||
_map_id,
|
||||
_is_track_allowed,
|
||||
_caller_pid
|
||||
) do
|
||||
Logger.error("caller_pid is required for tracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
|
||||
def untrack(characters, map_id, caller_pid) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
character_ids = characters |> Enum.map(& &1.id)
|
||||
|
||||
characters
|
||||
|> Enum.each(fn character ->
|
||||
WandererAppWeb.Presence.untrack(caller_pid, map_id, character.id)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"#{inspect(caller_pid)}_map_#{map_id}:character_#{character.id}:tracked",
|
||||
false
|
||||
)
|
||||
|
||||
:ok = Phoenix.PubSub.unsubscribe(WandererApp.PubSub, "character:#{character.eve_id}")
|
||||
WandererAppWeb.Presence.update(caller_pid, map_id, character.id, %{
|
||||
tracked: false,
|
||||
from: DateTime.utc_now()
|
||||
})
|
||||
end)
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(map_id, character_ids)
|
||||
|
||||
:ok
|
||||
else
|
||||
true ->
|
||||
|
||||
@@ -31,8 +31,11 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
avoid: []
|
||||
}
|
||||
|
||||
@zarzakh_system 30_100_000
|
||||
@default_avoid_systems [@zarzakh_system]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [max_retries: 1, retry_log_level: :warning]
|
||||
@retry_opts [max_retries: 0, retry_log_level: :warning]
|
||||
@timeout_opts [pool_timeout: 15_000, receive_timeout: :timer.seconds(30)]
|
||||
@api_retry_count 1
|
||||
|
||||
@@ -99,7 +102,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
chains = _remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
|
||||
chains =
|
||||
case routes_settings.include_cruise do
|
||||
@@ -170,7 +173,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
avoidance_list
|
||||
end
|
||||
|
||||
avoidance_list = [routes_settings.avoid | avoidance_list] |> List.flatten() |> Enum.uniq()
|
||||
avoidance_list =
|
||||
(@default_avoid_systems ++ [routes_settings.avoid | avoidance_list])
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
params =
|
||||
%{
|
||||
@@ -296,7 +302,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts)
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
@@ -319,7 +325,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def get_character_info(eve_id, opts \\ []) do
|
||||
case get(
|
||||
"/characters/#{eve_id}/",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
@@ -333,25 +340,35 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def get_custom_route_base_url, do: WandererApp.Env.custom_route_base_url()
|
||||
|
||||
def get_character_wallet(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "wallet", opts)
|
||||
do: get_character_auth_data(character_eve_id, "wallet", opts ++ @cache_opts)
|
||||
|
||||
def get_corporation_wallets(corporation_id, opts \\ []),
|
||||
do: _get_corporation_auth_data(corporation_id, "wallets", opts)
|
||||
do: get_corporation_auth_data(corporation_id, "wallets", opts)
|
||||
|
||||
def get_corporation_wallet_journal(corporation_id, division, opts \\ []),
|
||||
do: _get_corporation_auth_data(corporation_id, "wallets/#{division}/journal", opts)
|
||||
do:
|
||||
get_corporation_auth_data(
|
||||
corporation_id,
|
||||
"wallets/#{division}/journal",
|
||||
opts
|
||||
)
|
||||
|
||||
def get_corporation_wallet_transactions(corporation_id, division, opts \\ []),
|
||||
do: _get_corporation_auth_data(corporation_id, "wallets/#{division}/transactions", opts)
|
||||
do:
|
||||
get_corporation_auth_data(
|
||||
corporation_id,
|
||||
"wallets/#{division}/transactions",
|
||||
opts
|
||||
)
|
||||
|
||||
def get_character_location(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "location", opts)
|
||||
do: get_character_auth_data(character_eve_id, "location", opts ++ @cache_opts)
|
||||
|
||||
def get_character_online(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "online", opts)
|
||||
do: get_character_auth_data(character_eve_id, "online", opts ++ @cache_opts)
|
||||
|
||||
def get_character_ship(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "ship", opts)
|
||||
do: get_character_auth_data(character_eve_id, "ship", opts ++ @cache_opts)
|
||||
|
||||
def search(character_eve_id, opts \\ []) do
|
||||
search_val = to_string(opts[:params][:search] || "")
|
||||
@@ -366,7 +383,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
]
|
||||
|
||||
merged_opts = Keyword.put(opts, :params, query_params)
|
||||
_search(character_eve_id, search_val, categories_val, merged_opts)
|
||||
get_search(character_eve_id, search_val, categories_val, merged_opts)
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
@@ -374,11 +391,11 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
_get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
defp _remove_intersection(pairs_arr) do
|
||||
defp remove_intersection(pairs_arr) do
|
||||
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
|
||||
|
||||
tuples
|
||||
@@ -399,9 +416,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
|
||||
defp _get_routes(origin, destination, params, opts),
|
||||
do: _get_routes_eve(origin, destination, params, opts)
|
||||
do: get_routes_eve(origin, destination, params, opts)
|
||||
|
||||
defp _get_routes_eve(origin, destination, params, opts) do
|
||||
defp get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
@@ -410,7 +427,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
end
|
||||
|
||||
@@ -420,17 +438,19 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
do:
|
||||
get(
|
||||
"/alliances/#{alliance_eve_id}/#{info_path}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
defp _get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
opts
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
defp _get_character_auth_data(character_eve_id, info_path, opts) do
|
||||
defp get_character_auth_data(character_eve_id, info_path, opts) do
|
||||
path = "/characters/#{character_eve_id}/#{info_path}"
|
||||
|
||||
auth_opts =
|
||||
@@ -439,7 +459,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
character_id = opts |> Keyword.get(:character_id, nil)
|
||||
|
||||
if not _is_access_token_expired?(character_id) do
|
||||
if not is_access_token_expired?(character_id) do
|
||||
get(
|
||||
path,
|
||||
auth_opts,
|
||||
@@ -450,7 +470,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp _is_access_token_expired?(character_id) do
|
||||
defp is_access_token_expired?(character_id) do
|
||||
{:ok, %{expires_at: expires_at} = _character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
@@ -459,13 +479,13 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at - now <= 0
|
||||
end
|
||||
|
||||
defp _get_corporation_auth_data(corporation_eve_id, info_path, opts),
|
||||
defp get_corporation_auth_data(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
[params: opts[:params] || []] ++
|
||||
(opts |> get_auth_opts()),
|
||||
opts
|
||||
opts ++ @cache_opts
|
||||
)
|
||||
|
||||
defp with_user_agent_opts(opts) do
|
||||
@@ -487,12 +507,28 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
)
|
||||
|
||||
defp get(path, api_opts \\ [], opts \\ []) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
|
||||
_ ->
|
||||
do_get_request(path, api_opts, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ []) do
|
||||
try do
|
||||
case Req.get(
|
||||
"#{@base_url}#{path}",
|
||||
api_opts |> with_user_agent_opts() |> with_cache_opts() |> Keyword.merge(@retry_opts) |> Keyword.merge(@timeout_opts)
|
||||
api_opts
|
||||
|> with_user_agent_opts()
|
||||
|> with_cache_opts()
|
||||
|> Keyword.merge(@retry_opts)
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
{:ok, %{status: 200, body: body, headers: headers}} ->
|
||||
maybe_cache_response(path, body, headers, opts)
|
||||
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %{status: 504}} ->
|
||||
@@ -521,6 +557,30 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(path, body, %{"expires" => [expires]}, opts)
|
||||
when is_binary(path) and not is_nil(expires) do
|
||||
try do
|
||||
if opts |> Keyword.get(:cache, false) do
|
||||
cached_ttl =
|
||||
DateTime.diff(Timex.parse!(expires, "{RFC1123}"), DateTime.utc_now(), :millisecond)
|
||||
|
||||
Cachex.put(
|
||||
:api_cache,
|
||||
path,
|
||||
body,
|
||||
ttl: cached_ttl
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@logger.error(Exception.message(e))
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(_path, _body, _headers), do: :ok
|
||||
|
||||
defp post(url, opts) do
|
||||
try do
|
||||
case Req.post("#{url}", opts |> with_user_agent_opts()) do
|
||||
@@ -578,63 +638,91 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{expires_at: expires_at, refresh_token: refresh_token, scopes: scopes} = character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
case WandererApp.Ueberauth.Strategy.Eve.OAuth.get_refresh_token([],
|
||||
with_wallet: WandererApp.Character.can_track_wallet?(character),
|
||||
is_admin?: WandererApp.Character.can_track_corp_wallet?(character),
|
||||
token: %OAuth2.AccessToken{refresh_token: refresh_token}
|
||||
) do
|
||||
{:ok, %OAuth2.AccessToken{} = token} ->
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
refresh_token_result =
|
||||
WandererApp.Ueberauth.Strategy.Eve.OAuth.get_refresh_token([],
|
||||
with_wallet: WandererApp.Character.can_track_wallet?(character),
|
||||
is_admin?: WandererApp.Character.can_track_corp_wallet?(character),
|
||||
token: %OAuth2.AccessToken{refresh_token: refresh_token}
|
||||
)
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at
|
||||
})
|
||||
handle_refresh_token_result(refresh_token_result, character, character_id, expires_at, scopes)
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:token_updated
|
||||
)
|
||||
defp handle_refresh_token_result(
|
||||
{:ok, %OAuth2.AccessToken{} = token},
|
||||
character,
|
||||
character_id,
|
||||
_expires_at,
|
||||
scopes
|
||||
) do
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
|
||||
{:ok, token}
|
||||
WandererApp.Character.update_character(character_id, %{
|
||||
access_token: token.access_token,
|
||||
expires_at: token.expires_at
|
||||
})
|
||||
|
||||
{:error, {"invalid_grant", error_message}} ->
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
access_token: nil,
|
||||
refresh_token: nil,
|
||||
expires_at: expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:token_updated
|
||||
)
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{
|
||||
access_token: nil,
|
||||
refresh_token: nil,
|
||||
expires_at: expires_at,
|
||||
scopes: scopes
|
||||
})
|
||||
{:ok, token}
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
defp handle_refresh_token_result(
|
||||
{:error, {"invalid_grant", error_message}},
|
||||
character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
) do
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{error_message}")
|
||||
{:error, :invalid_grant}
|
||||
end
|
||||
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{error_message}")
|
||||
{:error, :invalid_grant}
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{} = error},
|
||||
character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
) do
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
{:error, :invalid_grant}
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(error, character, character_id, expires_at, scopes) do
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :failed}
|
||||
end
|
||||
|
||||
defp invalidate_character_tokens(character, character_id, expires_at, scopes) do
|
||||
attrs = %{access_token: nil, refresh_token: nil, expires_at: expires_at, scopes: scopes}
|
||||
|
||||
with {:ok, _} <- WandererApp.Api.Character.update(character, attrs),
|
||||
{:ok, _} <- WandererApp.Character.update_character(character_id, attrs) do
|
||||
:ok
|
||||
else
|
||||
error ->
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
{:error, :failed}
|
||||
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
end
|
||||
|
||||
defp map_route_info(
|
||||
|
||||
128
lib/wanderer_app/map/map_operations.ex
Normal file
128
lib/wanderer_app/map/map_operations.ex
Normal file
@@ -0,0 +1,128 @@
|
||||
# File: lib/wanderer_app/map/operations.ex
|
||||
defmodule WandererApp.Map.Operations do
|
||||
@moduledoc """
|
||||
Central entrypoint for map operations. Delegates responsibilities to specialized submodules:
|
||||
- Owner: Fetching and caching owner character info
|
||||
- Systems: CRUD and batch upsert for systems
|
||||
- Connections: CRUD and batch upsert for connections
|
||||
- Structures: CRUD for structures
|
||||
- Signatures: CRUD for signatures
|
||||
"""
|
||||
|
||||
alias WandererApp.Map.Operations.{
|
||||
Owner,
|
||||
Systems,
|
||||
Connections,
|
||||
Structures,
|
||||
Signatures
|
||||
}
|
||||
|
||||
# -- Owner Info -------------------------------------------------------------
|
||||
|
||||
@doc "Fetch cached main character info for a map owner"
|
||||
@spec get_owner_character_id(String.t()) ::
|
||||
{:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
|
||||
defdelegate get_owner_character_id(map_id), to: Owner
|
||||
|
||||
# -- Systems ----------------------------------------------------------------
|
||||
|
||||
@doc "List visible systems"
|
||||
@spec list_systems(String.t()) :: [map()]
|
||||
defdelegate list_systems(map_id), to: Systems
|
||||
|
||||
@doc "Get a specific system"
|
||||
@spec get_system(String.t(), integer()) :: {:ok, map()} | {:error, :not_found}
|
||||
defdelegate get_system(map_id, system_id), to: Systems
|
||||
|
||||
@doc "Create a system"
|
||||
@spec create_system(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate create_system(map_id, params), to: Systems
|
||||
|
||||
@doc "Update a system"
|
||||
@spec update_system(String.t(), integer(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_system(map_id, system_id, attrs), to: Systems
|
||||
|
||||
@doc "Delete a system"
|
||||
@spec delete_system(String.t(), integer()) :: {:ok, integer()} | {:error, term()}
|
||||
defdelegate delete_system(map_id, system_id), to: Systems
|
||||
|
||||
@doc "Upsert systems and connections in batch"
|
||||
@spec upsert_systems_and_connections(String.t(), [map()], [map()]) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate upsert_systems_and_connections(map_id, systems, connections), to: Systems
|
||||
|
||||
# -- Connections -----------------------------------------------------------
|
||||
|
||||
@doc "List all connections"
|
||||
@spec list_connections(String.t()) :: [map()]
|
||||
defdelegate list_connections(map_id), to: Connections
|
||||
|
||||
@doc "List connections for a specific system"
|
||||
@spec list_connections(String.t(), integer()) :: [map()]
|
||||
defdelegate list_connections(map_id, system_id), to: Connections
|
||||
|
||||
@doc "Get a connection"
|
||||
@spec get_connection(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate get_connection(map_id, connection_id), to: Connections
|
||||
|
||||
@doc "Create a connection"
|
||||
@spec create_connection(String.t(), map()) ::
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs), to: Connections
|
||||
|
||||
@doc "Force-create a connection with explicit character ID"
|
||||
@spec create_connection(String.t(), map(), integer()) ::
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
||||
|
||||
@doc "Update a connection"
|
||||
@spec update_connection(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_connection(map_id, connection_id, attrs), to: Connections
|
||||
|
||||
@doc "Delete a connection"
|
||||
@spec delete_connection(String.t(), integer(), integer()) :: :ok | {:error, term()}
|
||||
defdelegate delete_connection(map_id, src_id, tgt_id), to: Connections
|
||||
|
||||
@doc "Get a connection by source and target system IDs"
|
||||
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate get_connection_by_systems(map_id, source, target), to: Connections
|
||||
|
||||
# -- Structures ------------------------------------------------------------
|
||||
|
||||
@doc "List all structures"
|
||||
@spec list_structures(String.t()) :: [map()]
|
||||
defdelegate list_structures(map_id), to: Structures
|
||||
|
||||
@doc "Create a structure"
|
||||
@spec create_structure(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate create_structure(map_id, params), to: Structures
|
||||
|
||||
@doc "Update a structure"
|
||||
@spec update_structure(String.t(), String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_structure(map_id, struct_id, params), to: Structures
|
||||
|
||||
@doc "Delete a structure"
|
||||
@spec delete_structure(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_structure(map_id, struct_id), to: Structures
|
||||
|
||||
# -- Signatures ------------------------------------------------------------
|
||||
|
||||
@doc "List all signatures"
|
||||
@spec list_signatures(String.t()) :: [map()]
|
||||
defdelegate list_signatures(map_id), to: Signatures
|
||||
|
||||
@doc "Create a signature"
|
||||
@spec create_signature(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
|
||||
defdelegate create_signature(map_id, params), to: Signatures
|
||||
|
||||
@doc "Update a signature"
|
||||
@spec update_signature(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
defdelegate update_signature(map_id, sig_id, params), to: Signatures
|
||||
|
||||
@doc "Delete a signature in a map"
|
||||
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
||||
end
|
||||
@@ -340,7 +340,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
end)
|
||||
|
||||
{:error, :no_active_subscription} ->
|
||||
Logger.warn(
|
||||
Logger.warning(
|
||||
"Cannot create license for map #{map.id}: No active subscription found"
|
||||
)
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
end
|
||||
end
|
||||
|
||||
defp with_started_map(map_id, label \\ "operation", fun) when is_function(fun, 0) do
|
||||
defp with_started_map(map_id, label, fun) when is_function(fun, 0) do
|
||||
if WandererApp.Cache.lookup!("map_#{map_id}:started", false) do
|
||||
fun.()
|
||||
else
|
||||
|
||||
259
lib/wanderer_app/map/operations/connections.ex
Normal file
259
lib/wanderer_app/map/operations/connections.ex
Normal file
@@ -0,0 +1,259 @@
|
||||
defmodule WandererApp.Map.Operations.Connections do
|
||||
@moduledoc """
|
||||
CRUD and batch upsert for map connections.
|
||||
"""
|
||||
|
||||
alias Ash.Error.Invalid
|
||||
alias WandererApp.MapConnectionRepo
|
||||
alias WandererApp.Map.Server
|
||||
require Logger
|
||||
|
||||
@spec list_connections(String.t()) :: [map()] | {:error, atom()}
|
||||
def list_connections(map_id) do
|
||||
with {:ok, conns} <- MapConnectionRepo.get_by_map(map_id) do
|
||||
conns
|
||||
else
|
||||
{:error, err} ->
|
||||
Logger.warning("[list_connections] Repo error: #{inspect(err)}")
|
||||
{:error, :repo_error}
|
||||
other ->
|
||||
Logger.error("[list_connections] Unexpected repo result: #{inspect(other)}")
|
||||
{:error, :unexpected_repo_result}
|
||||
end
|
||||
end
|
||||
|
||||
@spec list_connections(String.t(), integer()) :: [map()]
|
||||
def list_connections(map_id, system_id) do
|
||||
list_connections(map_id)
|
||||
|> Enum.filter(fn c ->
|
||||
c.solar_system_source == system_id or c.solar_system_target == system_id
|
||||
end)
|
||||
end
|
||||
|
||||
@spec get_connection(String.t(), String.t()) :: {:ok, map()} | {:error, String.t()}
|
||||
def get_connection(map_id, conn_id) do
|
||||
case MapConnectionRepo.get_by_id(map_id, conn_id) do
|
||||
{:ok, conn} -> {:ok, conn}
|
||||
_ -> {:error, "Connection not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, map()} | {:skip, :exists} | {:error, atom()}
|
||||
def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do
|
||||
do_create(attrs, map_id, char_id)
|
||||
end
|
||||
|
||||
def create_connection(map_id, attrs, char_id) do
|
||||
do_create(attrs, map_id, char_id)
|
||||
end
|
||||
|
||||
defp do_create(attrs, map_id, char_id) do
|
||||
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
|
||||
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target") do
|
||||
info = %{
|
||||
solar_system_source_id: source,
|
||||
solar_system_target_id: target,
|
||||
character_id: char_id,
|
||||
type: parse_type(attrs["type"])
|
||||
}
|
||||
add_result = Server.add_connection(map_id, info)
|
||||
case add_result do
|
||||
:ok -> {:ok, :created}
|
||||
{:ok, []} ->
|
||||
Logger.warning("[do_create] Server.add_connection returned :ok, [] for map_id=#{inspect(map_id)}, source=#{inspect(source)}, target=#{inspect(target)}")
|
||||
{:error, :inconsistent_state}
|
||||
{:error, %Invalid{errors: errors}} = err ->
|
||||
if Enum.any?(errors, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
|
||||
{:error, _} = err ->
|
||||
Logger.error("[do_create] Server.add_connection error: #{inspect(err)}")
|
||||
{:error, :server_error}
|
||||
_ ->
|
||||
Logger.error("[do_create] Unexpected add_result: #{inspect(add_result)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
{:ok, []} ->
|
||||
Logger.warning("[do_create] Source or target system not found: attrs=#{inspect(attrs)}")
|
||||
{:error, :inconsistent_state}
|
||||
{:error, _} = err ->
|
||||
Logger.error("[do_create] parse_int error: #{inspect(err)}, attrs=#{inspect(attrs)}")
|
||||
{:error, :parse_error}
|
||||
_ ->
|
||||
Logger.error("[do_create] Unexpected error in preconditions: attrs=#{inspect(attrs)}")
|
||||
{:error, :unexpected_precondition_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_connection(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, conn_id, attrs) do
|
||||
with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id),
|
||||
result <- (
|
||||
try do
|
||||
_allowed_keys = [
|
||||
:mass_status,
|
||||
:ship_size_type,
|
||||
:type
|
||||
]
|
||||
_update_map =
|
||||
attrs
|
||||
|> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end)
|
||||
|> Enum.map(fn {k, v} -> {String.to_atom(k), v} end)
|
||||
|> Enum.into(%{})
|
||||
res = apply_connection_updates(map_id, conn_struct, attrs, char_id)
|
||||
res
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("[update_connection] Exception: #{inspect(error)}")
|
||||
{:error, :exception}
|
||||
end
|
||||
),
|
||||
:ok <- result,
|
||||
{:ok, updated_conn} <- MapConnectionRepo.get_by_id(map_id, conn_id) do
|
||||
{:ok, updated_conn}
|
||||
else
|
||||
{:error, err} -> {:error, err}
|
||||
_ -> {:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def update_connection(_conn, _conn_id, _attrs), do: {:error, :missing_params}
|
||||
|
||||
@spec delete_connection(Plug.Conn.t(), integer(), integer()) :: :ok | {:error, atom()}
|
||||
def delete_connection(%{assigns: %{map_id: map_id}} = _conn, src, tgt) do
|
||||
case Server.delete_connection(map_id, %{solar_system_source_id: src, solar_system_target_id: tgt}) do
|
||||
:ok -> :ok
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}")
|
||||
{:error, :not_found}
|
||||
{:error, _} = err ->
|
||||
Logger.error("[delete_connection] Server error: #{inspect(err)}")
|
||||
{:error, :server_error}
|
||||
_ ->
|
||||
Logger.error("[delete_connection] Unknown error")
|
||||
{:error, :unknown}
|
||||
end
|
||||
end
|
||||
def delete_connection(_conn, _src, _tgt), do: {:error, :missing_params}
|
||||
|
||||
@doc "Batch upsert for connections"
|
||||
@spec upsert_batch(Plug.Conn.t(), [map()]) :: %{created: integer(), updated: integer(), skipped: integer()}
|
||||
def upsert_batch(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conns) do
|
||||
_assigns = %{map_id: map_id, char_id: char_id}
|
||||
Enum.reduce(conns, %{created: 0, updated: 0, skipped: 0}, fn conn_attrs, acc ->
|
||||
case upsert_single(conn, conn_attrs) do
|
||||
{:ok, :created} -> %{acc | created: acc.created + 1}
|
||||
{:ok, :updated} -> %{acc | updated: acc.updated + 1}
|
||||
_ -> %{acc | skipped: acc.skipped + 1}
|
||||
end
|
||||
end)
|
||||
end
|
||||
def upsert_batch(_conn, _conns), do: %{created: 0, updated: 0, skipped: 0}
|
||||
|
||||
@doc "Upsert a single connection"
|
||||
@spec upsert_single(Plug.Conn.t(), map()) :: {:ok, :created | :updated} | {:error, atom()}
|
||||
def upsert_single(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conn_data) do
|
||||
source = conn_data["solar_system_source"] || conn_data[:solar_system_source]
|
||||
target = conn_data["solar_system_target"] || conn_data[:solar_system_target]
|
||||
with {:ok, %{} = existing_conn} <- get_connection_by_systems(map_id, source, target),
|
||||
{:ok, _} <- update_connection(conn, existing_conn.id, conn_data) do
|
||||
{:ok, :updated}
|
||||
else
|
||||
{:ok, nil} ->
|
||||
case create_connection(map_id, conn_data, char_id) do
|
||||
{:ok, _} -> {:ok, :created}
|
||||
{:skip, :exists} -> {:ok, :updated}
|
||||
err -> {:error, err}
|
||||
end
|
||||
{:error, _} = err ->
|
||||
Logger.warning("[upsert_single] Connection lookup error: #{inspect(err)}")
|
||||
{:error, :lookup_error}
|
||||
err ->
|
||||
Logger.error("[upsert_single] Update failed: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def upsert_single(_conn, _conn_data), do: {:error, :missing_params}
|
||||
|
||||
@doc "Get a connection by source and target system IDs"
|
||||
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
|
||||
def get_connection_by_systems(map_id, source, target) do
|
||||
with {:ok, conn} <- WandererApp.Map.find_connection(map_id, source, target) do
|
||||
if conn, do: {:ok, conn}, else: WandererApp.Map.find_connection(map_id, target, source)
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# -- Helpers ---------------------------------------------------------------
|
||||
|
||||
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||
defp parse_int(val, field) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> {:ok, i}
|
||||
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||
end
|
||||
end
|
||||
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
|
||||
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||
|
||||
defp parse_type(val) when is_integer(val), do: val
|
||||
defp parse_type(val) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> i
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
defp parse_type(_), do: 0
|
||||
|
||||
defp is_unique_constraint_error?(%{constraint: :unique}), do: true
|
||||
defp is_unique_constraint_error?(%{constraint: :unique_constraint}), do: true
|
||||
defp is_unique_constraint_error?(_), do: false
|
||||
|
||||
defp apply_connection_updates(map_id, conn, attrs, _char_id) do
|
||||
Enum.reduce_while(attrs, :ok, fn {key, val}, _acc ->
|
||||
result =
|
||||
case key do
|
||||
"mass_status" -> maybe_update_mass_status(map_id, conn, val)
|
||||
"ship_size_type" -> maybe_update_ship_size_type(map_id, conn, val)
|
||||
"type" -> maybe_update_type(map_id, conn, val)
|
||||
_ -> :ok
|
||||
end
|
||||
if result == :ok do
|
||||
{:cont, :ok}
|
||||
else
|
||||
{:halt, result}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
:ok -> :ok
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_mass_status(_map_id, _conn, nil), do: :ok
|
||||
defp maybe_update_mass_status(map_id, conn, value) do
|
||||
Server.update_connection_mass_status(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
mass_status: value
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_update_ship_size_type(_map_id, _conn, nil), do: :ok
|
||||
defp maybe_update_ship_size_type(map_id, conn, value) do
|
||||
Server.update_connection_ship_size_type(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
ship_size_type: value
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_update_type(_map_id, _conn, nil), do: :ok
|
||||
defp maybe_update_type(map_id, conn, value) do
|
||||
Server.update_connection_type(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
type: value
|
||||
})
|
||||
end
|
||||
|
||||
end
|
||||
75
lib/wanderer_app/map/operations/owner.ex
Normal file
75
lib/wanderer_app/map/operations/owner.ex
Normal file
@@ -0,0 +1,75 @@
|
||||
defmodule WandererApp.Map.Operations.Owner do
|
||||
@moduledoc """
|
||||
Handles fetching and caching of the main character info for a map owner.
|
||||
"""
|
||||
|
||||
# Cache TTL in milliseconds (24 hours)
|
||||
@owner_info_cache_ttl 86_400_000
|
||||
|
||||
alias WandererApp.{
|
||||
MapRepo,
|
||||
MapCharacterSettingsRepo,
|
||||
MapUserSettingsRepo,
|
||||
Cache
|
||||
}
|
||||
alias WandererApp.Character
|
||||
alias WandererApp.Character.TrackingUtils
|
||||
|
||||
@spec get_owner_character_id(String.t()) :: {:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
|
||||
def get_owner_character_id(map_id) do
|
||||
cache_key = "map_#{map_id}:owner_info"
|
||||
|
||||
case Cache.lookup!(cache_key) do
|
||||
nil ->
|
||||
with {:ok, owner} <- fetch_map_owner(map_id),
|
||||
{:ok, char_ids} <- fetch_character_ids(map_id),
|
||||
{:ok, characters} <- load_characters(char_ids),
|
||||
{:ok, user_settings} <- MapUserSettingsRepo.get(map_id, owner.id),
|
||||
{:ok, main} <- TrackingUtils.get_main_character(user_settings, characters, characters) do
|
||||
result = %{id: main.id, user_id: main.user_id}
|
||||
Cache.insert(cache_key, result, ttl: @owner_info_cache_ttl)
|
||||
{:ok, result}
|
||||
else
|
||||
{:error, msg} -> {:error, msg}
|
||||
_ -> {:error, "Failed to resolve main character"}
|
||||
end
|
||||
|
||||
cached ->
|
||||
{:ok, cached}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_owner(map_id) do
|
||||
case MapRepo.get(map_id, [:owner]) do
|
||||
{:ok, %{owner: %_{} = owner}} -> {:ok, owner}
|
||||
{:ok, %{owner: nil}} -> {:error, "Map has no owner"}
|
||||
{:error, _} -> {:error, "Map not found"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_character_ids(map_id) do
|
||||
case MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} when is_list(settings) and settings != [] ->
|
||||
{:ok, Enum.map(settings, & &1.character_id)}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, "No character settings found"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Failed to fetch character settings"}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_characters(ids) when is_list(ids) do
|
||||
ids
|
||||
|> Enum.map(&Character.get_character/1)
|
||||
|> Enum.flat_map(fn
|
||||
{:ok, ch} -> [ch]
|
||||
_ -> []
|
||||
end)
|
||||
|> case do
|
||||
[] -> {:error, "No valid characters found"}
|
||||
chars -> {:ok, chars}
|
||||
end
|
||||
end
|
||||
end
|
||||
114
lib/wanderer_app/map/operations/signatures.ex
Normal file
114
lib/wanderer_app/map/operations/signatures.ex
Normal file
@@ -0,0 +1,114 @@
|
||||
defmodule WandererApp.Map.Operations.Signatures do
|
||||
@moduledoc """
|
||||
CRUD for map signatures.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server
|
||||
|
||||
@spec list_signatures(String.t()) :: [map()]
|
||||
def list_signatures(map_id) do
|
||||
systems = Operations.list_systems(map_id)
|
||||
if systems != [] do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
|
||||
sigs
|
||||
else
|
||||
err ->
|
||||
Logger.error("[list_signatures] error: #{inspect(err)}")
|
||||
[]
|
||||
end
|
||||
end)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_signature(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def create_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do
|
||||
attrs = Map.put(params, "character_eve_id", char_id)
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: params["solar_system_id"],
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok -> {:ok, attrs}
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id, params) do
|
||||
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, system} <- MapSystem.by_id(sig.system_id) do
|
||||
base = %{
|
||||
"eve_id" => sig.eve_id,
|
||||
"name" => sig.name,
|
||||
"kind" => sig.kind,
|
||||
"group" => sig.group,
|
||||
"type" => sig.type,
|
||||
"custom_info" => sig.custom_info,
|
||||
"character_eve_id" => char_id,
|
||||
"description" => sig.description,
|
||||
"linked_system_id" => sig.linked_system_id
|
||||
}
|
||||
attrs = Map.merge(base, params)
|
||||
:ok = Server.update_signatures(map_id, %{
|
||||
added_signatures: [],
|
||||
updated_signatures: [attrs],
|
||||
removed_signatures: [],
|
||||
solar_system_id: system.solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
{:ok, attrs}
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def update_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec delete_signature(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
|
||||
def delete_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id) do
|
||||
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, system} <- MapSystem.by_id(sig.system_id) do
|
||||
removed = [%{
|
||||
"eve_id" => sig.eve_id,
|
||||
"name" => sig.name,
|
||||
"kind" => sig.kind,
|
||||
"group" => sig.group
|
||||
}]
|
||||
:ok = Server.update_signatures(map_id, %{
|
||||
added_signatures: [],
|
||||
updated_signatures: [],
|
||||
removed_signatures: removed,
|
||||
solar_system_id: system.solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("[delete_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_signature(_conn, _sig_id), do: {:error, :missing_params}
|
||||
end
|
||||
104
lib/wanderer_app/map/operations/structures.ex
Normal file
104
lib/wanderer_app/map/operations/structures.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule WandererApp.Map.Operations.Structures do
|
||||
@moduledoc """
|
||||
CRUD for map structures.
|
||||
"""
|
||||
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Api.MapSystem
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias WandererApp.Structure
|
||||
require Logger
|
||||
|
||||
@spec list_structures(String.t()) :: [map()]
|
||||
def list_structures(map_id) do
|
||||
with systems when is_list(systems) and systems != [] <- (
|
||||
case Operations.list_systems(map_id) do
|
||||
{:ok, systems} -> systems
|
||||
systems when is_list(systems) -> systems
|
||||
_ -> []
|
||||
end
|
||||
) do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, structs} <- MapSystemStructure.by_system_id(sys.id) do
|
||||
structs
|
||||
else
|
||||
_other -> []
|
||||
end
|
||||
end)
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_structure(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def create_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do
|
||||
with {:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: params["solar_system_id"]}),
|
||||
attrs <- Map.put(prepare_attrs(params), "system_id", system.id),
|
||||
:ok <- Structure.update_structures(system, [attrs], [], [], char_id, user_id),
|
||||
name = Map.get(attrs, "name"),
|
||||
structure_type_id = Map.get(attrs, "structureTypeId"),
|
||||
struct when not is_nil(struct) <-
|
||||
MapSystemStructure.by_system_id!(system.id)
|
||||
|> Enum.find(fn s -> s.name == name and s.structure_type_id == structure_type_id end) do
|
||||
{:ok, struct}
|
||||
else
|
||||
nil ->
|
||||
Logger.warning("[create_structure] Structure not found after creation")
|
||||
{:error, :structure_not_found}
|
||||
err ->
|
||||
Logger.error("[create_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_structure(_conn, _params), do: {:error, "missing params"}
|
||||
|
||||
@spec update_structure(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id, params) do
|
||||
with {:ok, struct} <- MapSystemStructure.by_id(struct_id),
|
||||
{:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: struct.solar_system_id}) do
|
||||
attrs = Map.merge(prepare_attrs(params), %{"id" => struct_id})
|
||||
:ok = Structure.update_structures(system, [], [attrs], [], char_id, user_id)
|
||||
case MapSystemStructure.by_id(struct_id) do
|
||||
{:ok, updated} -> {:ok, updated}
|
||||
err ->
|
||||
Logger.error("[update_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def update_structure(_conn, _struct_id, _params), do: {:error, "missing params"}
|
||||
|
||||
@spec delete_structure(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
|
||||
def delete_structure(%{assigns: %{map_id: _map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id) do
|
||||
with {:ok, struct} <- MapSystemStructure.by_id(struct_id),
|
||||
{:ok, system} <- MapSystem.by_id(struct.system_id) do
|
||||
:ok = Structure.update_structures(system, [], [], [%{"id" => struct_id}], char_id, user_id)
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("[delete_structure] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_structure(_conn, _struct_id), do: {:error, "missing params"}
|
||||
|
||||
defp prepare_attrs(params) do
|
||||
params
|
||||
|> Enum.map(fn
|
||||
{"structure_type", v} -> {"structureType", v}
|
||||
{"structure_type_id", v} -> {"structureTypeId", v}
|
||||
{"end_time", v} -> {"endTime", v}
|
||||
{k, v} -> {k, v}
|
||||
end)
|
||||
|> Map.new()
|
||||
|> Map.take(["name", "structureType", "structureTypeId", "status", "notes", "endTime"])
|
||||
end
|
||||
end
|
||||
195
lib/wanderer_app/map/operations/systems.ex
Normal file
195
lib/wanderer_app/map/operations/systems.ex
Normal file
@@ -0,0 +1,195 @@
|
||||
defmodule WandererApp.Map.Operations.Systems do
|
||||
@moduledoc """
|
||||
CRUD and batch upsert for map systems.
|
||||
"""
|
||||
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Map.Operations.Connections
|
||||
require Logger
|
||||
|
||||
@spec list_systems(String.t()) :: [map()]
|
||||
def list_systems(map_id) do
|
||||
with {:ok, systems} <- MapSystemRepo.get_visible_by_map(map_id) do
|
||||
systems
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_system(String.t(), integer()) :: {:ok, map()} | {:error, :not_found}
|
||||
def get_system(map_id, system_id) do
|
||||
MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id)
|
||||
end
|
||||
|
||||
@spec create_system(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def create_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, params) do
|
||||
do_create_system(map_id, user_id, char_id, params)
|
||||
end
|
||||
def create_system(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
# Private helper for batch upsert
|
||||
defp create_system_batch(%{map_id: map_id, user_id: user_id, char_id: char_id}, params) do
|
||||
do_create_system(map_id, user_id, char_id, params)
|
||||
end
|
||||
|
||||
defp do_create_system(map_id, user_id, char_id, params) do
|
||||
with {:ok, system_id} <- fetch_system_id(params),
|
||||
coords <- normalize_coordinates(params),
|
||||
:ok <- Server.add_system(map_id, %{solar_system_id: system_id, coordinates: coords}, user_id, char_id),
|
||||
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||
{:ok, system}
|
||||
else
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
Logger.warning("[do_create_system] Expected error: #{inspect(reason)}")
|
||||
{:error, :expected_error}
|
||||
_ ->
|
||||
Logger.error("[do_create_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_system(Plug.Conn.t(), integer(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_system(%{assigns: %{map_id: map_id}} = _conn, system_id, attrs) do
|
||||
with {:ok, current} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
||||
x_raw <- Map.get(attrs, "position_x", Map.get(attrs, :position_x, current.position_x)),
|
||||
y_raw <- Map.get(attrs, "position_y", Map.get(attrs, :position_y, current.position_y)),
|
||||
{:ok, x} <- parse_int(x_raw, "position_x"),
|
||||
{:ok, y} <- parse_int(y_raw, "position_y"),
|
||||
coords = %{x: x, y: y},
|
||||
:ok <- apply_system_updates(map_id, system_id, attrs, coords),
|
||||
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||
{:ok, system}
|
||||
else
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
Logger.warning("[update_system] Expected error: #{inspect(reason)}")
|
||||
{:error, :expected_error}
|
||||
_ ->
|
||||
Logger.error("[update_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def update_system(_conn, _system_id, _attrs), do: {:error, :missing_params}
|
||||
|
||||
@spec delete_system(Plug.Conn.t(), integer()) :: {:ok, integer()} | {:error, atom()}
|
||||
def delete_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, system_id) do
|
||||
with {:ok, _} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
|
||||
:ok <- Server.delete_systems(map_id, [system_id], user_id, char_id) do
|
||||
{:ok, 1}
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[delete_system] System not found: #{inspect(system_id)}")
|
||||
{:error, :not_found}
|
||||
_ ->
|
||||
Logger.error("[delete_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
def delete_system(_conn, _system_id), do: {:error, :missing_params}
|
||||
|
||||
@spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) :: {:ok, map()} | {:error, atom()}
|
||||
def upsert_systems_and_connections(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn, systems, connections) do
|
||||
assigns = %{map_id: map_id, user_id: user_id, char_id: char_id}
|
||||
{created_s, updated_s, _skipped_s} = upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0)
|
||||
conn_results =
|
||||
connections
|
||||
|> Enum.reduce(%{created: 0, updated: 0, skipped: 0}, fn conn_data, acc ->
|
||||
case Connections.upsert_single(conn, conn_data) do
|
||||
{:ok, :created} -> %{acc | created: acc.created + 1}
|
||||
{:ok, :updated} -> %{acc | updated: acc.updated + 1}
|
||||
_ -> %{acc | skipped: acc.skipped + 1}
|
||||
end
|
||||
end)
|
||||
{:ok, %{
|
||||
systems: %{created: created_s, updated: updated_s},
|
||||
connections: %{created: conn_results.created, updated: conn_results.updated}
|
||||
}}
|
||||
end
|
||||
def upsert_systems_and_connections(_conn, _systems, _connections), do: {:error, :missing_params}
|
||||
|
||||
# -- Internal Helpers -------------------------------------------------------
|
||||
|
||||
defp fetch_system_id(%{"solar_system_id" => id}), do: parse_int(id, "solar_system_id")
|
||||
defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id), do: parse_int(id, "solar_system_id")
|
||||
defp fetch_system_id(_), do: {:error, "Missing system identifier (id)"}
|
||||
|
||||
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||
defp parse_int(val, field) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> {:ok, i}
|
||||
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||
end
|
||||
end
|
||||
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
|
||||
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||
|
||||
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}}) when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
defp normalize_coordinates(params) do
|
||||
%{
|
||||
x: params |> Map.get("position_x", Map.get(params, :position_x, 0)),
|
||||
y: params |> Map.get("position_y", Map.get(params, :position_y, 0))
|
||||
}
|
||||
end
|
||||
|
||||
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
|
||||
with :ok <- Server.update_system_position(map_id, %{solar_system_id: system_id, position_x: round(x), position_y: round(y)}) do
|
||||
attrs
|
||||
|> Map.drop([:coordinates, :position_x, :position_y, :solar_system_id,
|
||||
"coordinates", "position_x", "position_y", "solar_system_id"])
|
||||
|> Enum.reduce_while(:ok, fn {key, val}, _ ->
|
||||
case update_system_field(map_id, system_id, to_string(key), val) do
|
||||
:ok -> {:cont, :ok}
|
||||
err -> {:halt, err}
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp update_system_field(map_id, system_id, field, val) do
|
||||
case field do
|
||||
"status" -> Server.update_system_status(map_id, %{solar_system_id: system_id, status: convert_status(val)})
|
||||
"description" -> Server.update_system_description(map_id, %{solar_system_id: system_id, description: val})
|
||||
"tag" -> Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val})
|
||||
"locked" ->
|
||||
bool = val in [true, "true", 1, "1"]
|
||||
Server.update_system_locked(map_id, %{solar_system_id: system_id, locked: bool})
|
||||
f when f in ["label", "labels"] ->
|
||||
labels = cond do
|
||||
is_list(val) -> val
|
||||
is_binary(val) -> String.split(val, ",", trim: true)
|
||||
true -> []
|
||||
end
|
||||
Server.update_system_labels(map_id, %{solar_system_id: system_id, labels: Enum.join(labels, ",")})
|
||||
"temporary_name" -> Server.update_system_temporary_name(map_id, %{solar_system_id: system_id, temporary_name: val})
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_status("CLEAR"), do: 0
|
||||
defp convert_status("DANGEROUS"), do: 1
|
||||
defp convert_status("OCCUPIED"), do: 2
|
||||
defp convert_status("MASS_CRITICAL"), do: 3
|
||||
defp convert_status("TIME_CRITICAL"), do: 4
|
||||
defp convert_status("REINFORCED"), do: 5
|
||||
defp convert_status(i) when is_integer(i), do: i
|
||||
defp convert_status(s) when is_binary(s) do
|
||||
case Integer.parse(s) do
|
||||
{i, _} -> i
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
defp convert_status(_), do: 0
|
||||
|
||||
defp upsert_each([], _fun, c, u, d), do: {c, u, d}
|
||||
defp upsert_each([item | rest], fun, c, u, d) do
|
||||
case fun.(item) do
|
||||
{:ok, _} -> upsert_each(rest, fun, c + 1, u, d)
|
||||
:ok -> upsert_each(rest, fun, c + 1, u, d)
|
||||
{:skip, _} -> upsert_each(rest, fun, c, u + 1, d)
|
||||
_ -> upsert_each(rest, fun, c, u, d + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -87,14 +87,11 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
:timer.minutes(get_eol_expire_timeout_mins())
|
||||
|
||||
def init_eol_cache(map_id, connections_eol_time) do
|
||||
eol_expire_timeout = get_eol_expire_timeout()
|
||||
|
||||
connections_eol_time
|
||||
|> Enum.each(fn {connection_id, connection_eol_time} ->
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
|
||||
connection_eol_time,
|
||||
ttl: eol_expire_timeout
|
||||
connection_eol_time
|
||||
)
|
||||
end)
|
||||
end
|
||||
@@ -178,8 +175,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
true ->
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
|
||||
DateTime.utc_now(),
|
||||
ttl: get_eol_expire_timeout()
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
_ ->
|
||||
|
||||
@@ -95,4 +95,12 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
do:
|
||||
connection
|
||||
|> WandererApp.Api.MapConnection.update_custom_info(update)
|
||||
|
||||
def get_by_id(map_id, id) do
|
||||
case WandererApp.Api.MapConnection.by_id(id) do
|
||||
{:ok, conn} when conn.map_id == map_id -> {:ok, conn}
|
||||
{:ok, _} -> {:error, :not_found}
|
||||
{:error, _} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ defmodule WandererApp.Structure do
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias WandererApp.Character
|
||||
|
||||
def update_structures(system, added, updated, removed, main_character_eve_id) do
|
||||
def update_structures(system, added, updated, removed, main_character_eve_id, user_id \\ nil) do
|
||||
Logger.info("[Structure] update_structures called by user_id=#{inspect(user_id)}")
|
||||
added_structs =
|
||||
parse_structures(added, main_character_eve_id, system)
|
||||
|> Enum.map(&Map.delete(&1, :id))
|
||||
@@ -105,7 +106,28 @@ defmodule WandererApp.Structure do
|
||||
# remove PK so Ash doesn't treat it as a new record
|
||||
updated_data = Map.delete(updated_data, :id)
|
||||
|
||||
new_record = MapSystemStructure.update(existing, updated_data)
|
||||
# Merge update data with existing record to avoid nil required fields
|
||||
merged_data = Map.merge(Map.from_struct(existing), updated_data, fn _k, v1, v2 -> if is_nil(v2), do: v1, else: v2 end)
|
||||
# Only keep fields accepted by Ash update action
|
||||
allowed_keys = [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
filtered_data = Map.take(merged_data, allowed_keys)
|
||||
Logger.info("[Structure] update_structures_in_db: calling update for id=#{existing.id} with: #{inspect(filtered_data)}")
|
||||
new_record = MapSystemStructure.update(existing, filtered_data)
|
||||
Logger.info("[Structure] update_structures_in_db: update result for id=#{existing.id}: #{inspect(new_record)}")
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[Structure] updated record =>\n" <> inspect(new_record, pretty: true)
|
||||
|
||||
@@ -162,7 +162,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
||||
"[KillsPreloader] Starting #{pass_type} pass => #{length(unique_systems)} systems"
|
||||
)
|
||||
|
||||
{final_state, kills_map} =
|
||||
{final_state, _kills_map} =
|
||||
unique_systems
|
||||
|> Task.async_stream(
|
||||
fn {_map_id, system_id} ->
|
||||
|
||||
@@ -23,7 +23,9 @@ defmodule WandererApp.Zkb.Supervisor do
|
||||
},
|
||||
opts: [
|
||||
name: {:local, :zkb_kills_provider},
|
||||
mint_upgrade_opts: [Mint.WebSocket.PerMessageDeflate]
|
||||
reconnect: true,
|
||||
reconnect_after: 5_000,
|
||||
max_reconnects: :infinity
|
||||
]
|
||||
},
|
||||
preloader_child
|
||||
|
||||
@@ -11,7 +11,6 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
use Retry
|
||||
|
||||
@heartbeat_interval 1_000
|
||||
@max_esi_retries 3
|
||||
|
||||
# Called by `KillsProvider.handle_connect`
|
||||
def handle_connect(_status, _headers, %{connected: _} = state) do
|
||||
|
||||
@@ -27,6 +27,5 @@ defmodule WandererAppWeb.ApiSpec do
|
||||
},
|
||||
security: [%{"bearerAuth" => []}]
|
||||
}
|
||||
|> OpenApiSpex.resolve_schema_modules()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.Api.{AccessList, Character}
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
import Ash.Query
|
||||
require Logger
|
||||
|
||||
@@ -245,7 +245,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
}}
|
||||
]
|
||||
def index(conn, params) do
|
||||
case Util.fetch_map_id(params) do
|
||||
case APIUtils.fetch_map_id(params) do
|
||||
{:ok, map_identifier} ->
|
||||
with {:ok, map} <- get_map(map_identifier),
|
||||
{:ok, loaded_map} <- Ash.load(map, acls: [:owner]) do
|
||||
@@ -320,7 +320,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
}}
|
||||
]
|
||||
def create(conn, params) do
|
||||
with {:ok, map_identifier} <- Util.fetch_map_id(params),
|
||||
with {:ok, map_identifier} <- APIUtils.fetch_map_id(params),
|
||||
{:ok, map} <- get_map(map_identifier),
|
||||
%{"acl" => acl_params} <- params,
|
||||
owner_eve_id when not is_nil(owner_eve_id) <- Map.get(acl_params, "owner_eve_id"),
|
||||
|
||||
@@ -3,8 +3,7 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.CachedInfo
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererApp.EveDataService
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
@system_static_response_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
@@ -87,8 +86,8 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
}
|
||||
]
|
||||
def show_system_static(conn, params) do
|
||||
with {:ok, solar_system_str} <- Util.require_param(params, "id"),
|
||||
{:ok, solar_system_id} <- Util.parse_int(solar_system_str) do
|
||||
with {:ok, solar_system_str} <- APIUtils.require_param(params, "id"),
|
||||
{:ok, solar_system_id} <- APIUtils.parse_int(solar_system_str) do
|
||||
case CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, system} ->
|
||||
# Get basic system data
|
||||
@@ -113,11 +112,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a system map to a JSON-friendly format.
|
||||
|
||||
Takes only the fields that are needed for the API response.
|
||||
"""
|
||||
defp static_system_to_json(system) do
|
||||
system
|
||||
|> Map.take([
|
||||
@@ -142,12 +136,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enhances system data with wormhole type information.
|
||||
|
||||
If the system has static wormholes, adds detailed information about each static.
|
||||
Otherwise, returns the original data unchanged.
|
||||
"""
|
||||
defp enhance_with_static_details(data) do
|
||||
if data[:statics] && length(data[:statics]) > 0 do
|
||||
# Add the enhanced static details to the response
|
||||
@@ -158,11 +146,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets detailed information for each static wormhole.
|
||||
|
||||
Uses the CachedInfo to get both wormhole type data and wormhole class data.
|
||||
"""
|
||||
defp get_static_details(statics) do
|
||||
# Get wormhole data from CachedInfo
|
||||
{:ok, wormhole_types} = CachedInfo.get_wormhole_types()
|
||||
@@ -186,12 +169,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates detailed wormhole information when the wormhole type is found.
|
||||
|
||||
Includes information about the destination and properties of the wormhole.
|
||||
Ensures that destination.id is always a string to match the OpenAPI schema.
|
||||
"""
|
||||
defp create_wormhole_details(wh_type, classes_by_id) do
|
||||
# Get destination class info
|
||||
dest_class = Map.get(classes_by_id, wh_type.dest)
|
||||
@@ -213,11 +190,6 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates fallback information when a wormhole type is not found.
|
||||
|
||||
Provides a placeholder structure with nil values for unknown wormhole types.
|
||||
"""
|
||||
defp create_fallback_wormhole_details(static_name) do
|
||||
%{
|
||||
name: static_name,
|
||||
|
||||
50
lib/wanderer_app_web/controllers/fallback_controller.ex
Normal file
50
lib/wanderer_app_web/controllers/fallback_controller.ex
Normal file
@@ -0,0 +1,50 @@
|
||||
defmodule WandererAppWeb.FallbackController do
|
||||
use WandererAppWeb, :controller
|
||||
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# Handles not_found errors from with/else
|
||||
def call(conn, {:error, :not_found}) do
|
||||
APIUtils.error_response(conn, :not_found, "Not found", "The requested resource could not be found")
|
||||
end
|
||||
|
||||
# Handles invalid_id errors
|
||||
def call(conn, {:error, :invalid_id}) do
|
||||
APIUtils.error_response(conn, :bad_request, "Invalid system ID")
|
||||
end
|
||||
|
||||
# Handles invalid_coordinates_format errors
|
||||
def call(conn, {:error, :invalid_coordinates_format}) do
|
||||
APIUtils.error_response(conn, :bad_request, "Invalid coordinates format. Use %{\"coordinates\" => %{\"x\" => number, \"y\" => number}}")
|
||||
end
|
||||
|
||||
# Handles not_associated errors
|
||||
def call(conn, {:error, :not_associated}) do
|
||||
APIUtils.error_response(conn, :not_found, "Connection not associated with specified system")
|
||||
end
|
||||
|
||||
# Handles not_involved errors
|
||||
def call(conn, {:error, :not_involved}) do
|
||||
APIUtils.error_response(conn, :bad_request, "Connection must involve specified system")
|
||||
end
|
||||
|
||||
# Handles creation_failed errors
|
||||
def call(conn, {:error, :creation_failed}) do
|
||||
APIUtils.error_response(conn, :internal_server_error, "Failed to create resource")
|
||||
end
|
||||
|
||||
# Handles deletion_failed errors
|
||||
def call(conn, {:error, :deletion_failed}) do
|
||||
APIUtils.error_response(conn, :internal_server_error, "Failed to delete resource")
|
||||
end
|
||||
|
||||
# Handles any other {:error, message} returns
|
||||
def call(conn, {:error, msg}) when is_binary(msg) do
|
||||
APIUtils.error_response(conn, :bad_request, msg)
|
||||
end
|
||||
|
||||
# Handles any other unmatched errors
|
||||
def call(conn, _error) do
|
||||
APIUtils.error_response(conn, :internal_server_error, "An unexpected error occurred")
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Inline Schemas
|
||||
@@ -117,8 +117,8 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
)
|
||||
|
||||
def index(conn, params) do
|
||||
with {:ok, map_id} <- Util.fetch_map_id(params),
|
||||
{:ok, period} <- Util.require_param(params, "period"),
|
||||
with {:ok, map_id} <- APIUtils.fetch_map_id(params),
|
||||
{:ok, period} <- APIUtils.require_param(params, "period"),
|
||||
query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
|
||||
{:ok, data} <-
|
||||
Api.read(query) do
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
# lib/wanderer_app_web/controllers/map_connection_api_controller.ex
|
||||
defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
@moduledoc """
|
||||
API controller for managing map connections.
|
||||
Provides operations to list, show, create, delete, and batch-delete connections, with legacy routing support.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
require Logger
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.Map, as: MapData
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.ResponseSchemas
|
||||
|
||||
action_fallback WandererAppWeb.FallbackController
|
||||
|
||||
# -- JSON Schemas --
|
||||
@connection_request_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||
type: %Schema{type: :integer, description: "Connection type (default 0)"},
|
||||
mass_status: %Schema{type: :integer, description: "Mass status (0-3)", nullable: true},
|
||||
time_status: %Schema{type: :integer, description: "Time status (0-3)", nullable: true},
|
||||
ship_size_type: %Schema{type: :integer, description: "Ship size limit (0-3)", nullable: true},
|
||||
locked: %Schema{type: :boolean, description: "Locked flag", nullable: true},
|
||||
custom_info: %Schema{type: :string, nullable: true, description: "Optional metadata"},
|
||||
wormhole_type: %Schema{type: :string, nullable: true, description: "Wormhole code"}
|
||||
},
|
||||
required: ~w(solar_system_source solar_system_target)a,
|
||||
example: %{
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
}
|
||||
|
||||
@list_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
map_id: %Schema{type: :string},
|
||||
solar_system_source: %Schema{type: :integer},
|
||||
solar_system_target: %Schema{type: :integer},
|
||||
type: %Schema{type: :integer},
|
||||
mass_status: %Schema{type: :integer},
|
||||
time_status: %Schema{type: :integer},
|
||||
ship_size_type: %Schema{type: :integer},
|
||||
locked: %Schema{type: :boolean},
|
||||
custom_info: %Schema{type: :string, nullable: true},
|
||||
wormhole_type: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [
|
||||
%{
|
||||
id: "conn-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@detail_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
map_id: %Schema{type: :string},
|
||||
solar_system_source: %Schema{type: :integer},
|
||||
solar_system_target: %Schema{type: :integer},
|
||||
type: %Schema{type: :integer},
|
||||
mass_status: %Schema{type: :integer},
|
||||
time_status: %Schema{type: :integer},
|
||||
ship_size_type: %Schema{type: :integer},
|
||||
locked: %Schema{type: :boolean},
|
||||
custom_info: %Schema{type: :string, nullable: true},
|
||||
wormhole_type: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
id: "conn-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -- Actions --
|
||||
|
||||
operation :index,
|
||||
summary: "List Map Connections",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List Map Connections",
|
||||
"application/json",
|
||||
@list_response_schema
|
||||
}
|
||||
]
|
||||
def index(%{assigns: %{map_id: map_id}} = conn, params) do
|
||||
with {:ok, src_filter} <- parse_optional(params, "solar_system_source"),
|
||||
{:ok, tgt_filter} <- parse_optional(params, "solar_system_target") do
|
||||
conns = MapData.list_connections!(map_id)
|
||||
conns =
|
||||
conns
|
||||
|> filter_by_source(src_filter)
|
||||
|> filter_by_target(tgt_filter)
|
||||
data = Enum.map(conns, &APIUtils.connection_to_json/1)
|
||||
APIUtils.respond_data(conn, data)
|
||||
else
|
||||
{:error, msg} when is_binary(msg) ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(:bad_request)
|
||||
|> APIUtils.error_response(:bad_request, msg)
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> Plug.Conn.put_status(:bad_request)
|
||||
|> APIUtils.error_response(:bad_request, "Invalid filter parameter")
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_optional(params, key) do
|
||||
case Map.get(params, key) do
|
||||
nil -> {:ok, nil}
|
||||
val -> APIUtils.parse_int(val)
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_by_source(conns, nil), do: conns
|
||||
defp filter_by_source(conns, s), do: Enum.filter(conns, &(&1.solar_system_source == s))
|
||||
|
||||
defp filter_by_target(conns, nil), do: conns
|
||||
defp filter_by_target(conns, t), do: Enum.filter(conns, &(&1.solar_system_target == t))
|
||||
|
||||
operation :show,
|
||||
summary: "Show Connection (by id or by source/target)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: false],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
case Operations.get_connection(map_id, id) do
|
||||
{:ok, conn_struct} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(conn_struct))
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||
with {:ok, source} <- APIUtils.parse_int(src),
|
||||
{:ok, target} <- APIUtils.parse_int(tgt),
|
||||
{:ok, conn_struct} <- Operations.get_connection_by_systems(map_id, source, target) do
|
||||
APIUtils.respond_data(conn, APIUtils.connection_to_json(conn_struct))
|
||||
else
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
operation :create,
|
||||
summary: "Create Connection",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
system_id: [in: :path, type: :string, required: false]
|
||||
],
|
||||
request_body: {"Connection create", "application/json", @connection_request_schema},
|
||||
responses: ResponseSchemas.create_responses(@detail_response_schema)
|
||||
def create(conn, params) do
|
||||
case Operations.create_connection(conn, params) do
|
||||
{:ok, conn_struct} when is_map(conn_struct) ->
|
||||
conn
|
||||
|> APIUtils.respond_data(APIUtils.connection_to_json(conn_struct), :created)
|
||||
{:ok, :created} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(%{data: %{result: "created"}})
|
||||
{:skip, :exists} ->
|
||||
conn
|
||||
|> put_status(:ok)
|
||||
|> json(%{data: %{result: "exists"}})
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: reason})
|
||||
_other ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Unexpected error"})
|
||||
end
|
||||
end
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete Connection (by id or by source/target)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: false],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
responses: ResponseSchemas.delete_responses(nil)
|
||||
def delete(%{assigns: %{map_id: _map_id}} = conn, %{"id" => id}) do
|
||||
delete_connection_id(conn, id)
|
||||
end
|
||||
|
||||
def delete(%{assigns: %{map_id: _map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||
delete_by_systems(conn, src, tgt)
|
||||
end
|
||||
|
||||
# Private helpers for delete/2
|
||||
|
||||
defp delete_connection_id(conn, id) do
|
||||
case Operations.get_connection(conn, id) do
|
||||
{:ok, conn_struct} ->
|
||||
source_id = conn_struct.solar_system_source
|
||||
target_id = conn_struct.solar_system_target
|
||||
case Operations.delete_connection(conn, source_id, target_id) do
|
||||
:ok -> {:ok, conn_struct}
|
||||
error -> error
|
||||
end
|
||||
_ -> {:error, :invalid_id}
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_by_systems(conn, src, tgt) do
|
||||
with {:ok, source} <- APIUtils.parse_int(src),
|
||||
{:ok, target} <- APIUtils.parse_int(tgt) do
|
||||
do_delete_by_systems(conn, source, target, src, tgt)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.error("[delete_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)}")
|
||||
{:error, :not_found}
|
||||
{:error, reason} ->
|
||||
Logger.error("[delete_connection] Error: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
error ->
|
||||
Logger.error("[delete_connection] Unexpected error: #{inspect(error)}")
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete_by_systems(conn, source, target, src, tgt) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, source, target) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[delete_connection] No connection found for source=#{inspect(source)}, target=#{inspect(target)}")
|
||||
try_reverse_delete(conn, source, target, src, tgt)
|
||||
{:ok, conn_struct} ->
|
||||
case Operations.delete_connection(conn, conn_struct.solar_system_source, conn_struct.solar_system_target) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
error -> {:error, error}
|
||||
end
|
||||
{:error, _} ->
|
||||
try_reverse_delete(conn, source, target, src, tgt)
|
||||
end
|
||||
end
|
||||
|
||||
defp try_reverse_delete(conn, source, target, src, tgt) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, target, source) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[delete_connection] No connection found for source=#{inspect(target)}, target=#{inspect(source)}")
|
||||
{:error, :not_found}
|
||||
{:ok, conn_struct} ->
|
||||
case Operations.delete_connection(conn, conn_struct.solar_system_source, conn_struct.solar_system_target) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
error -> {:error, error}
|
||||
end
|
||||
{:error, reason} ->
|
||||
Logger.error("[delete_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)} (both orders)")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
operation :update,
|
||||
summary: "Update Connection (by id or by source/target)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: false],
|
||||
solar_system_source: [in: :query, type: :integer, required: false],
|
||||
solar_system_target: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
request_body: {"Connection update", "application/json", @connection_request_schema},
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
def update(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
allowed_fields = ["mass_status", "ship_size_type", "locked", "custom_info", "type"]
|
||||
attrs =
|
||||
conn.body_params
|
||||
|> Map.take(allowed_fields)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
update_by_id(conn, map_id, id, attrs)
|
||||
end
|
||||
|
||||
def update(%{assigns: %{map_id: map_id}} = conn, %{"solar_system_source" => src, "solar_system_target" => tgt}) do
|
||||
allowed_fields = ["mass_status", "ship_size_type", "locked", "custom_info", "type"]
|
||||
attrs =
|
||||
conn.body_params
|
||||
|> Map.take(allowed_fields)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
update_by_systems(conn, map_id, src, tgt, attrs)
|
||||
end
|
||||
|
||||
# Private helpers for update/2
|
||||
|
||||
defp update_by_id(conn, _map_id, id, attrs) do
|
||||
case Operations.update_connection(conn, id, attrs) do
|
||||
{:ok, updated_conn} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(updated_conn))
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp update_by_systems(conn, _map_id, src, tgt, attrs) do
|
||||
require Logger
|
||||
with {:ok, source} <- APIUtils.parse_int(src),
|
||||
{:ok, target} <- APIUtils.parse_int(tgt) do
|
||||
do_update_by_systems(conn, source, target, src, tgt, attrs)
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.error("[update_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)}")
|
||||
{:error, :not_found}
|
||||
{:error, reason} ->
|
||||
Logger.error("[update_connection] Error: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
error ->
|
||||
Logger.error("[update_connection] Unexpected error: #{inspect(error)}")
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update_by_systems(conn, source, target, src, tgt, attrs) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, source, target) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[update_connection] No connection found for source=#{inspect(source)}, target=#{inspect(target)}")
|
||||
try_reverse_update(conn, source, target, src, tgt, attrs)
|
||||
{:ok, conn_struct} ->
|
||||
do_update_connection(conn, conn_struct.id, attrs)
|
||||
{:error, _} ->
|
||||
try_reverse_update(conn, source, target, src, tgt, attrs)
|
||||
end
|
||||
end
|
||||
|
||||
defp try_reverse_update(conn, source, target, src, tgt, attrs) do
|
||||
map_id = conn.assigns.map_id
|
||||
case Operations.get_connection_by_systems(map_id, target, source) do
|
||||
{:ok, nil} ->
|
||||
Logger.error("[update_connection] No connection found for source=#{inspect(target)}, target=#{inspect(source)}")
|
||||
{:error, :not_found}
|
||||
{:ok, conn_struct} ->
|
||||
do_update_connection(conn, conn_struct.id, attrs)
|
||||
{:error, reason} ->
|
||||
Logger.error("[update_connection] Connection not found for source=#{inspect(src)}, target=#{inspect(tgt)} (both orders)")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update_connection(conn, id, attrs) do
|
||||
case Operations.update_connection(conn, id, attrs) do
|
||||
{:ok, updated_conn} -> APIUtils.respond_data(conn, APIUtils.connection_to_json(updated_conn))
|
||||
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} ->
|
||||
Logger.error("[update_connection] Ash update NotFound for id=#{id}")
|
||||
{:error, :not_found}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
@deprecated "Use GET /api/maps/:map_identifier/systems instead"
|
||||
operation :list_all_connections,
|
||||
summary: "List All Connections (Legacy)",
|
||||
deprecated: true,
|
||||
parameters: [map_id: [in: :query]],
|
||||
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||
def list_all_connections(%{assigns: %{map_id: map_id}} = conn, _params) do
|
||||
connections = Operations.list_connections(map_id)
|
||||
data = Enum.map(connections, &APIUtils.connection_to_json/1)
|
||||
APIUtils.respond_data(conn, data)
|
||||
end
|
||||
end
|
||||
477
lib/wanderer_app_web/controllers/map_system_api_controller.ex
Normal file
477
lib/wanderer_app_web/controllers/map_system_api_controller.ex
Normal file
@@ -0,0 +1,477 @@
|
||||
# lib/wanderer_app_web/controllers/map_system_api_controller.ex
|
||||
defmodule WandererAppWeb.MapSystemAPIController do
|
||||
@moduledoc """
|
||||
API controller for managing map systems and their associated connections.
|
||||
Provides CRUD operations and batch upsert for systems and connections.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||
|
||||
action_fallback WandererAppWeb.FallbackController
|
||||
|
||||
# -- JSON Schemas --
|
||||
@map_system_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Map system UUID"},
|
||||
map_id: %Schema{type: :string, description: "Map UUID"},
|
||||
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
|
||||
region_name: %Schema{type: :string, description: "EVE region name"},
|
||||
position_x: %Schema{type: :number, format: :float, description: "X coordinate"},
|
||||
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
|
||||
status: %Schema{type: :string, description: "System status"},
|
||||
visible: %Schema{type: :boolean, description: "Visibility flag"},
|
||||
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag"},
|
||||
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||
},
|
||||
required: ~w(id map_id solar_system_id)a
|
||||
}
|
||||
|
||||
@system_request_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
|
||||
position_x: %Schema{type: :number, format: :float, description: "X coordinate"},
|
||||
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
|
||||
status: %Schema{type: :string, description: "System status"},
|
||||
visible: %Schema{type: :boolean, description: "Visibility flag"},
|
||||
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag"},
|
||||
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||
},
|
||||
required: ~w(solar_system_id)a,
|
||||
example: %{
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
||||
@system_update_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_name: %Schema{type: :string, description: "EVE solar system name", nullable: true},
|
||||
position_x: %Schema{type: :number, format: :float, description: "X coordinate", nullable: true},
|
||||
position_y: %Schema{type: :number, format: :float, description: "Y coordinate", nullable: true},
|
||||
status: %Schema{type: :string, description: "System status", nullable: true},
|
||||
visible: %Schema{type: :boolean, description: "Visibility flag", nullable: true},
|
||||
description: %Schema{type: :string, nullable: true, description: "Custom description"},
|
||||
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
|
||||
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
|
||||
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
|
||||
},
|
||||
example: %{
|
||||
solar_system_name: "Jita",
|
||||
position_x: 101.0,
|
||||
position_y: 202.0,
|
||||
visible: false,
|
||||
status: "active",
|
||||
tag: "HQ",
|
||||
locked: true
|
||||
}
|
||||
}
|
||||
|
||||
@map_connection_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Connection UUID"},
|
||||
map_id: %Schema{type: :string, description: "Map UUID"},
|
||||
solar_system_source: %Schema{type: :integer},
|
||||
solar_system_target: %Schema{type: :integer},
|
||||
type: %Schema{type: :integer},
|
||||
mass_status: %Schema{type: :integer, nullable: true},
|
||||
time_status: %Schema{type: :integer, nullable: true},
|
||||
ship_size_type: %Schema{type: :integer, nullable: true},
|
||||
locked: %Schema{type: :boolean},
|
||||
custom_info: %Schema{type: :string, nullable: true},
|
||||
wormhole_type: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
required: ~w(id map_id solar_system_source solar_system_target)a
|
||||
}
|
||||
|
||||
@list_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %Schema{type: :array, items: @map_system_schema},
|
||||
connections: %Schema{type: :array, items: @map_connection_schema}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
systems: [
|
||||
%{
|
||||
id: "sys-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
region_name: "The Forge",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
status: "active",
|
||||
visible: true,
|
||||
description: "Trade hub",
|
||||
tag: "HQ",
|
||||
locked: false,
|
||||
temporary_name: nil,
|
||||
labels: ["market", "hub"]
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
%{
|
||||
id: "conn-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0,
|
||||
mass_status: 1,
|
||||
time_status: 2,
|
||||
ship_size_type: 1,
|
||||
locked: false,
|
||||
custom_info: "Frigate only",
|
||||
wormhole_type: "C2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@detail_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: @map_system_schema
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
id: "sys-uuid-1",
|
||||
map_id: "map-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
region_name: "The Forge",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
status: "active",
|
||||
visible: true,
|
||||
description: "Trade hub",
|
||||
tag: "HQ",
|
||||
locked: false,
|
||||
temporary_name: nil,
|
||||
labels: ["market", "hub"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@delete_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{deleted: %Schema{type: :boolean, description: "Deleted flag"}},
|
||||
required: ["deleted"],
|
||||
example: %{deleted: true}
|
||||
}
|
||||
|
||||
@batch_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %Schema{
|
||||
type: :object,
|
||||
properties: %{created: %Schema{type: :integer}, updated: %Schema{type: :integer}},
|
||||
required: ~w(created updated)a
|
||||
},
|
||||
connections: %Schema{
|
||||
type: :object,
|
||||
properties: %{created: %Schema{type: :integer}, updated: %Schema{type: :integer}, deleted: %Schema{type: :integer}},
|
||||
required: ~w(created updated deleted)a
|
||||
}
|
||||
},
|
||||
required: ~w(systems connections)a
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: %{
|
||||
systems: %{created: 2, updated: 1},
|
||||
connections: %{created: 1, updated: 0, deleted: 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@batch_delete_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
system_ids: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :integer},
|
||||
description: "IDs to delete"
|
||||
},
|
||||
connection_ids: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Connection UUIDs to delete",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
required: ["system_ids"],
|
||||
example: %{
|
||||
system_ids: [30_000_142, 30_000_143],
|
||||
connection_ids: ["conn-uuid-1", "conn-uuid-2"]
|
||||
}
|
||||
}
|
||||
|
||||
@batch_delete_response_schema %Schema{
|
||||
type: :object,
|
||||
properties: %{deleted_count: %Schema{type: :integer, description: "Deleted count"}},
|
||||
required: ["deleted_count"],
|
||||
example: %{deleted_count: 2}
|
||||
}
|
||||
|
||||
@batch_request_schema ApiSchemas.data_wrapper(%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %Schema{type: :array, items: @system_request_schema},
|
||||
connections: %Schema{type: :array, items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_source: %Schema{type: :integer, description: "Source system ID"},
|
||||
solar_system_target: %Schema{type: :integer, description: "Target system ID"},
|
||||
type: %Schema{type: :integer, description: "Connection type (default 0)"},
|
||||
mass_status: %Schema{type: :integer, description: "Mass status (0-3)", nullable: true},
|
||||
time_status: %Schema{type: :integer, description: "Time decay status (0-3)", nullable: true},
|
||||
ship_size_type: %Schema{type: :integer, description: "Ship size limit (0-3)", nullable: true},
|
||||
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
|
||||
custom_info: %Schema{type: :string, description: "Optional metadata", nullable: true}
|
||||
},
|
||||
required: ~w(solar_system_source solar_system_target)a
|
||||
}}
|
||||
},
|
||||
example: %{
|
||||
systems: [
|
||||
%{
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
position_x: 100.5,
|
||||
position_y: 200.3,
|
||||
visible: true
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
%{
|
||||
solar_system_source: 30_000_142,
|
||||
solar_system_target: 30_000_144,
|
||||
type: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
# -- Actions --
|
||||
|
||||
operation :index,
|
||||
summary: "List Map Systems and Connections",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List Map Systems and Connections",
|
||||
"application/json",
|
||||
@list_response_schema
|
||||
}
|
||||
]
|
||||
def index(%{assigns: %{map_id: map_id}} = conn, _params) do
|
||||
systems = Operations.list_systems(map_id) |> Enum.map(&APIUtils.map_system_to_json/1)
|
||||
connections = Operations.list_connections(map_id) |> Enum.map(&APIUtils.connection_to_json/1)
|
||||
APIUtils.respond_data(conn, %{systems: systems, connections: connections})
|
||||
end
|
||||
|
||||
operation :show,
|
||||
summary: "Show Map System",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: true]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
with {:ok, system_id} <- APIUtils.parse_int(id),
|
||||
{:ok, system} <- Operations.get_system(map_id, system_id) do
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||
end
|
||||
end
|
||||
|
||||
operation :create,
|
||||
summary: "Upsert Systems and Connections (batch or single)",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Systems+Connections upsert", "application/json", @batch_request_schema},
|
||||
responses: ResponseSchemas.standard_responses(@batch_response_schema)
|
||||
def create(conn, params) do
|
||||
systems = Map.get(params, "systems", [])
|
||||
connections = Map.get(params, "connections", [])
|
||||
case Operations.upsert_systems_and_connections(conn, systems, connections) do
|
||||
{:ok, result} ->
|
||||
APIUtils.respond_data(conn, result)
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
operation :update,
|
||||
summary: "Update System",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: true]
|
||||
],
|
||||
request_body: {"System update request", "application/json", @system_update_schema},
|
||||
responses: ResponseSchemas.update_responses(@detail_response_schema)
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||
{:ok, attrs} <- APIUtils.extract_update_params(params),
|
||||
update_attrs = Map.put(attrs, "solar_system_id", sid),
|
||||
{:ok, system} <- Operations.update_system(conn, sid, update_attrs) do
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||
end
|
||||
end
|
||||
|
||||
operation :delete,
|
||||
summary: "Batch Delete Systems and Connections",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
]
|
||||
],
|
||||
request_body: {"Batch delete", "application/json", @batch_delete_schema},
|
||||
responses: ResponseSchemas.standard_responses(@batch_delete_response_schema)
|
||||
def delete(conn, params) do
|
||||
system_ids = Map.get(params, "system_ids", [])
|
||||
connection_ids = Map.get(params, "connection_ids", [])
|
||||
|
||||
deleted_systems = Enum.map(system_ids, &delete_system_id(conn, &1))
|
||||
deleted_connections = Enum.map(connection_ids, &delete_connection_id(conn, &1))
|
||||
|
||||
systems_deleted = Enum.count(deleted_systems, &match?({:ok, _}, &1))
|
||||
connections_deleted = Enum.count(deleted_connections, &match?({:ok, _}, &1))
|
||||
deleted_count = systems_deleted + connections_deleted
|
||||
|
||||
APIUtils.respond_data(conn, %{deleted_count: deleted_count})
|
||||
end
|
||||
|
||||
defp delete_system_id(conn, id) do
|
||||
case APIUtils.parse_int(id) do
|
||||
{:ok, sid} -> Operations.delete_system(conn, sid)
|
||||
_ -> {:error, :invalid_id}
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_connection_id(conn, id) do
|
||||
case Operations.get_connection(conn, id) do
|
||||
{:ok, conn_struct} ->
|
||||
source_id = conn_struct.solar_system_source
|
||||
target_id = conn_struct.solar_system_target
|
||||
case Operations.delete_connection(conn, source_id, target_id) do
|
||||
:ok -> {:ok, conn_struct}
|
||||
error -> error
|
||||
end
|
||||
_ -> {:error, :invalid_id}
|
||||
end
|
||||
end
|
||||
|
||||
operation :delete_single,
|
||||
summary: "Delete a single Map System",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "my-map-slug or map UUID"
|
||||
],
|
||||
id: [in: :path, type: :string, required: true]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@delete_response_schema)
|
||||
def delete_single(conn, %{"id" => id}) do
|
||||
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||
{:ok, _} <- Operations.delete_system(conn, sid) do
|
||||
APIUtils.respond_data(conn, %{deleted: true})
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> APIUtils.respond_data(%{deleted: false, error: "System not found"})
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> APIUtils.respond_data(%{deleted: false, error: "Failed to delete system", reason: reason})
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> APIUtils.respond_data(%{deleted: false, error: "Invalid system ID format"})
|
||||
end
|
||||
end
|
||||
|
||||
# -- Legacy endpoints --
|
||||
|
||||
operation :list_systems,
|
||||
summary: "List Map Systems (Legacy)",
|
||||
deprecated: true,
|
||||
description: "Deprecated, use GET /api/maps/:map_identifier/systems instead",
|
||||
parameters: [map_id: [in: :query]],
|
||||
responses: ResponseSchemas.standard_responses(@list_response_schema)
|
||||
defdelegate list_systems(conn, params), to: __MODULE__, as: :index
|
||||
|
||||
operation :show_system,
|
||||
summary: "Show Map System (Legacy)",
|
||||
deprecated: true,
|
||||
description: "Deprecated, use GET /api/maps/:map_identifier/systems/:id instead",
|
||||
parameters: [map_id: [in: :query], id: [in: :query]],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
defdelegate show_system(conn, params), to: __MODULE__, as: :show
|
||||
|
||||
end
|
||||
@@ -0,0 +1,169 @@
|
||||
defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
alias WandererApp.Map.Operations, as: MapOperations
|
||||
|
||||
@moduledoc """
|
||||
API controller for managing map system signatures.
|
||||
"""
|
||||
|
||||
# Inlined OpenAPI schema for a map system signature
|
||||
@signature_schema %OpenApiSpex.Schema{
|
||||
title: "MapSystemSignature",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
character_eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
description: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
type: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
linked_system_id: %OpenApiSpex.Schema{type: :integer, nullable: true},
|
||||
kind: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
group: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
custom_info: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
updated: %OpenApiSpex.Schema{type: :integer, nullable: true},
|
||||
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
|
||||
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: [
|
||||
:id, :system_id, :eve_id, :character_eve_id
|
||||
],
|
||||
example: %{
|
||||
id: "sig-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
eve_id: "ABC-123",
|
||||
character_eve_id: "123456789",
|
||||
name: "Wormhole K162",
|
||||
description: "Leads to unknown space",
|
||||
type: "Wormhole",
|
||||
linked_system_id: 30000144,
|
||||
kind: "cosmic_signature",
|
||||
group: "wormhole",
|
||||
custom_info: "Fresh",
|
||||
updated: 1,
|
||||
inserted_at: "2025-04-30T10:00:00Z",
|
||||
updated_at: "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
@doc """
|
||||
List all signatures for a map.
|
||||
"""
|
||||
operation :index,
|
||||
summary: "List all signatures for a map",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"List of signatures", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @signature_schema
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [@signature_schema.example]
|
||||
}
|
||||
}}]
|
||||
def index(conn, _params) do
|
||||
map_id = conn.assigns.map_id
|
||||
signatures = MapOperations.list_signatures(map_id)
|
||||
json(conn, %{data: signatures})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Show a single signature by ID.
|
||||
"""
|
||||
operation :show,
|
||||
summary: "Show a single signature by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"Signature", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}}]
|
||||
def show(conn, %{"id" => id}) do
|
||||
map_id = conn.assigns.map_id
|
||||
case MapSystemSignature.by_id(id) do
|
||||
{:ok, signature} ->
|
||||
case WandererApp.Api.MapSystem.by_id(signature.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: signature})
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
end
|
||||
_ -> conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new signature.
|
||||
"""
|
||||
operation :create,
|
||||
summary: "Create a new signature",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Signature", "application/json", @signature_schema},
|
||||
responses: [created: {"Created signature", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}}]
|
||||
def create(conn, params) do
|
||||
case MapOperations.create_signature(conn, params) do
|
||||
{:ok, sig} -> conn |> put_status(:created) |> json(%{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a signature by ID.
|
||||
"""
|
||||
operation :update,
|
||||
summary: "Update a signature by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Signature update", "application/json", @signature_schema},
|
||||
responses: [ok: {"Updated signature", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}}]
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
case MapOperations.update_signature(conn, id, params) do
|
||||
{:ok, sig} -> json(conn, %{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a signature by ID.
|
||||
"""
|
||||
operation :delete,
|
||||
summary: "Delete a signature by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [no_content: {"Deleted", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
example: %{}
|
||||
}}]
|
||||
def delete(conn, %{"id" => id}) do
|
||||
case MapOperations.delete_signature(conn, id) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,192 @@
|
||||
defmodule WandererAppWeb.MapSystemStructureAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.Map.Operations, as: MapOperations
|
||||
|
||||
@moduledoc """
|
||||
API controller for managing map system structures.
|
||||
Includes legacy structure-timers endpoint (deprecated).
|
||||
"""
|
||||
|
||||
# Inlined OpenAPI schema for a map system structure
|
||||
@structure_schema %Schema{
|
||||
title: "MapSystemStructure",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, format: :uuid},
|
||||
system_id: %Schema{type: :string, format: :uuid},
|
||||
solar_system_name: %Schema{type: :string},
|
||||
solar_system_id: %Schema{type: :integer},
|
||||
structure_type_id: %Schema{type: :string},
|
||||
structure_type: %Schema{type: :string},
|
||||
character_eve_id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
notes: %Schema{type: :string, nullable: true},
|
||||
owner_name: %Schema{type: :string, nullable: true},
|
||||
owner_ticker: %Schema{type: :string, nullable: true},
|
||||
owner_id: %Schema{type: :string, nullable: true},
|
||||
status: %Schema{type: :string, nullable: true},
|
||||
end_time: %Schema{type: :string, format: :date_time, nullable: true},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: [
|
||||
:id, :system_id, :solar_system_name, :solar_system_id, :structure_type_id, :structure_type, :character_eve_id, :name
|
||||
],
|
||||
example: %{
|
||||
id: "struct-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_name: "Jita",
|
||||
solar_system_id: 30000142,
|
||||
structure_type_id: "35832",
|
||||
structure_type: "Astrahus",
|
||||
character_eve_id: "123456789",
|
||||
name: "Jita Trade Hub",
|
||||
notes: "Main market structure",
|
||||
owner_name: "Wanderer Corp",
|
||||
owner_ticker: "WANDR",
|
||||
owner_id: "corp-uuid-1",
|
||||
status: "anchoring",
|
||||
end_time: "2025-05-01T12:00:00Z",
|
||||
inserted_at: "2025-04-30T10:00:00Z",
|
||||
updated_at: "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
@doc """
|
||||
List all structures for a map.
|
||||
"""
|
||||
operation :index,
|
||||
summary: "List all structures for a map",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"List of structures", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @structure_schema
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
data: [@structure_schema.example]
|
||||
}
|
||||
}}]
|
||||
def index(conn, _params) do
|
||||
map_id = conn.assigns.map_id
|
||||
structures = MapOperations.list_structures(map_id)
|
||||
json(conn, %{data: structures})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Show a single structure by ID.
|
||||
"""
|
||||
operation :show,
|
||||
summary: "Show a single structure by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Structure UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"Structure", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @structure_schema},
|
||||
example: %{data: @structure_schema.example}
|
||||
}}]
|
||||
def show(conn, %{"id" => id}) do
|
||||
map_id = conn.assigns.map_id
|
||||
case MapSystemStructure.by_id(id) do
|
||||
{:ok, structure} ->
|
||||
case WandererApp.Api.MapSystem.by_id(structure.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: structure})
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Structure not found"})
|
||||
end
|
||||
_ -> conn |> put_status(:not_found) |> json(%{error: "Structure not found"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new structure.
|
||||
"""
|
||||
operation :create,
|
||||
summary: "Create a new structure",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Structure", "application/json", @structure_schema},
|
||||
responses: [created: {"Created structure", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @structure_schema},
|
||||
example: %{data: @structure_schema.example}
|
||||
}}]
|
||||
def create(conn, params) do
|
||||
case MapOperations.create_structure(conn, params) do
|
||||
{:ok, struct} -> conn |> put_status(:created) |> json(%{data: struct})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a structure by ID.
|
||||
"""
|
||||
operation :update,
|
||||
summary: "Update a structure by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Structure UUID", type: :string, required: true]
|
||||
],
|
||||
request_body: {"Structure update", "application/json", @structure_schema},
|
||||
responses: [ok: {"Updated structure", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @structure_schema},
|
||||
example: %{data: @structure_schema.example}
|
||||
}}]
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
case MapOperations.update_structure(conn, id, params) do
|
||||
{:ok, struct} -> json(conn, %{data: struct})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a structure by ID.
|
||||
"""
|
||||
operation :delete,
|
||||
summary: "Delete a structure by ID",
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true],
|
||||
id: [in: :path, description: "Structure UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [no_content: {"Deleted", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
example: %{}
|
||||
}}]
|
||||
def delete(conn, %{"id" => id}) do
|
||||
case MapOperations.delete_structure(conn, id) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@deprecated "Use /structures instead. This endpoint will be removed in a future release."
|
||||
Legacy: Get structure timers for a map.
|
||||
"""
|
||||
operation :structure_timers,
|
||||
summary: "Get structure timers for a map (Legacy)",
|
||||
deprecated: true,
|
||||
parameters: [
|
||||
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
|
||||
],
|
||||
responses: [ok: {"Structure timers", "application/json", %Schema{type: :array, items: %Schema{type: :object}}}]
|
||||
def structure_timers(conn, _params) do
|
||||
map_id = conn.assigns.map_id
|
||||
structures = MapOperations.list_structures(map_id)
|
||||
json(conn, %{data: structures})
|
||||
end
|
||||
end
|
||||
21
lib/wanderer_app_web/controllers/plugs/assign_map_owner.ex
Normal file
21
lib/wanderer_app_web/controllers/plugs/assign_map_owner.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule WandererAppWeb.Plugs.AssignMapOwner do
|
||||
import Plug.Conn
|
||||
|
||||
alias WandererApp.Map.Operations
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
map_id = conn.assigns[:map_id]
|
||||
case Operations.get_owner_character_id(map_id) do
|
||||
{:ok, %{id: char_id, user_id: user_id}} ->
|
||||
conn
|
||||
|> assign(:owner_character_id, char_id)
|
||||
|> assign(:owner_user_id, user_id)
|
||||
_ ->
|
||||
conn
|
||||
|> assign(:owner_character_id, nil)
|
||||
|> assign(:owner_user_id, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,52 +1,116 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
@moduledoc """
|
||||
A plug that checks the "Authorization: Bearer <token>" header
|
||||
against the map's stored public_api_key. Halts with 401 if invalid.
|
||||
"""
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.Map, as: ApiMap
|
||||
alias WandererAppWeb.Schemas.ResponseSchemas, as: R
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def call(conn, _opts) do
|
||||
header = get_req_header(conn, "authorization") |> List.first()
|
||||
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
|
||||
{:ok, map_id} <- fetch_map_id(conn),
|
||||
{:ok, map} <- ApiMap.by_id(map_id),
|
||||
true <- is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token)
|
||||
do
|
||||
conn
|
||||
|> assign(:map, map)
|
||||
|> assign(:map_id, map.id)
|
||||
else
|
||||
[] ->
|
||||
Logger.warning("Missing or invalid 'Bearer' token")
|
||||
conn |> respond(401, "Missing or invalid 'Bearer' token") |> halt()
|
||||
|
||||
case header do
|
||||
"Bearer " <> incoming_token ->
|
||||
case fetch_map(conn.query_params) do
|
||||
{:ok, map} ->
|
||||
if map.public_api_key == incoming_token do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, Jason.encode!(%{error: "Unauthorized (invalid token for map)"}))
|
||||
|> halt()
|
||||
end
|
||||
{:error, :bad_request, msg} ->
|
||||
Logger.warning("Bad request: #{msg}")
|
||||
conn |> respond(400, msg) |> halt()
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(404, Jason.encode!(%{error: "Map not found"}))
|
||||
|> halt()
|
||||
end
|
||||
{:error, :not_found, msg} ->
|
||||
Logger.warning("Not found: #{msg}")
|
||||
conn |> respond(404, msg) |> halt()
|
||||
|
||||
_ ->
|
||||
{:error, _} ->
|
||||
Logger.warning("Map identifier required")
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, Jason.encode!(%{error: "Missing or invalid 'Bearer' token"}))
|
||||
|> respond(400, "Map identifier required. Provide `map_identifier` in the path or `map_id`/`slug` in query.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map(query_params) do
|
||||
case Util.fetch_map_id(query_params) do
|
||||
{:ok, map_id} ->
|
||||
WandererApp.Api.Map.by_id(map_id)
|
||||
false ->
|
||||
Logger.warning("Unauthorized: invalid token for map #{inspect(conn.params["map_identifier"])}")
|
||||
conn |> respond(401, "Unauthorized (invalid token for map)") |> halt()
|
||||
|
||||
error ->
|
||||
error
|
||||
Logger.error("Unexpected error: #{inspect(error)}")
|
||||
conn |> respond(500, "Unexpected error") |> halt()
|
||||
end
|
||||
end
|
||||
|
||||
# Try unified path param first, then fall back to legacy query params
|
||||
defp fetch_map_id(%Plug.Conn{params: %{"map_identifier" => id}}) when is_binary(id) and id != "" do
|
||||
resolve_identifier(id)
|
||||
end
|
||||
defp fetch_map_id(conn), do: legacy_fetch(conn)
|
||||
|
||||
# Try ID lookup first, then slug lookup
|
||||
defp resolve_identifier(id) do
|
||||
case ApiMap.by_id(id) do
|
||||
{:ok, %{id: map_id}} ->
|
||||
{:ok, map_id}
|
||||
|
||||
_ ->
|
||||
case ApiMap.get_map_by_slug(id) do
|
||||
{:ok, %{id: map_id}} ->
|
||||
{:ok, map_id}
|
||||
|
||||
_ ->
|
||||
{:error, :not_found, "Map not found for identifier: #{id}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy: check assigns, then params["map_id"], then params["slug"]
|
||||
defp legacy_fetch(conn) do
|
||||
map_id_from_assign = conn.assigns[:map_id]
|
||||
map_id_param = conn.params["map_id"]
|
||||
slug_param = conn.params["slug"]
|
||||
|
||||
cond do
|
||||
is_binary(map_id_from_assign) and map_id_from_assign != "" ->
|
||||
{:ok, map_id_from_assign}
|
||||
|
||||
is_binary(map_id_param) and map_id_param != "" ->
|
||||
{:ok, map_id_param}
|
||||
|
||||
is_binary(slug_param) and slug_param != "" ->
|
||||
case ApiMap.get_map_by_slug(slug_param) do
|
||||
{:ok, %{id: map_id}} -> {:ok, map_id}
|
||||
_ -> {:error, :not_found, "Map not found for slug: #{slug_param}"}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :bad_request,
|
||||
"Map identifier required. Provide `map_identifier` in the path or `map_id`/`slug` in query."}
|
||||
end
|
||||
end
|
||||
|
||||
# Pick the right shared schema and send JSON
|
||||
defp respond(conn, status, msg) do
|
||||
{_desc, content_type, _schema} =
|
||||
case status do
|
||||
400 -> R.bad_request(msg)
|
||||
401 -> R.unauthorized(msg)
|
||||
404 -> R.not_found(msg)
|
||||
500 -> R.internal_server_error(msg)
|
||||
_ -> R.internal_server_error("Unexpected error")
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_resp_content_type(content_type)
|
||||
|> send_resp(status, Jason.encode!(%{error: msg}))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,13 @@ defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
require Logger
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
case fetch_map_id(conn.query_params) do
|
||||
# First check if map_id is already in conn.assigns (from CheckMapApiKey)
|
||||
case get_map_id_from_assigns_or_params(conn) do
|
||||
{:ok, map_id} ->
|
||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||
|
||||
@@ -28,6 +30,17 @@ defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
||||
end
|
||||
end
|
||||
|
||||
# First try to get map_id from conn.assigns
|
||||
defp get_map_id_from_assigns_or_params(conn) do
|
||||
if Map.has_key?(conn.assigns, :map_id) do
|
||||
Logger.debug("Found map_id in conn.assigns: #{conn.assigns.map_id}")
|
||||
{:ok, conn.assigns.map_id}
|
||||
else
|
||||
# Fall back to query params if not in assigns
|
||||
fetch_map_id(conn.query_params)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
defmodule WandererAppWeb.UtilAPIController do
|
||||
@moduledoc """
|
||||
Utility functions for parameter handling, fetch helpers, etc.
|
||||
"""
|
||||
|
||||
alias WandererApp.Api
|
||||
|
||||
def fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
|
||||
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
||||
case Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} ->
|
||||
{:ok, map.id}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:error, "No map found for slug=#{slug}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(_),
|
||||
do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
|
||||
# Require a given param to be present and non-empty
|
||||
def require_param(params, key) do
|
||||
case params[key] do
|
||||
nil -> {:error, "Missing required param: #{key}"}
|
||||
"" -> {:error, "Param #{key} cannot be empty"}
|
||||
val -> {:ok, val}
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a string into an integer
|
||||
def parse_int(str) do
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
301
lib/wanderer_app_web/helpers/api_utils.ex
Normal file
301
lib/wanderer_app_web/helpers/api_utils.ex
Normal file
@@ -0,0 +1,301 @@
|
||||
defmodule WandererAppWeb.Helpers.APIUtils do
|
||||
@moduledoc """
|
||||
Unified helper module for API operations:
|
||||
- Parameter parsing and validation
|
||||
- Map ID resolution
|
||||
- Standardized responses
|
||||
- JSON serialization
|
||||
"""
|
||||
|
||||
# Explicit imports to avoid unnecessary dependencies
|
||||
import Plug.Conn, only: [put_status: 2]
|
||||
import Phoenix.Controller, only: [json: 2]
|
||||
|
||||
alias WandererApp.Api.Map, as: MapApi
|
||||
alias WandererApp.Api.MapSolarSystem
|
||||
require Logger
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Map ID Resolution
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec fetch_map_id(map()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
def fetch_map_id(%{"map_id" => id}) when is_binary(id) do
|
||||
case Ecto.UUID.cast(id) do
|
||||
{:ok, _} -> {:ok, id}
|
||||
:error -> {:error, "Invalid UUID format for map_id: #{id}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) do
|
||||
case MapApi.get_map_by_slug(slug) do
|
||||
{:ok, %{id: id}} -> {:ok, id}
|
||||
_ -> {:error, "No map found for slug=#{slug}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Parameter Validators and Parsers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec require_param(map(), String.t()) :: {:ok, any()} | {:error, String.t()}
|
||||
def require_param(params, key) do
|
||||
case Map.fetch(params, key) do
|
||||
{:ok, val} when is_binary(val) ->
|
||||
trimmed = String.trim(val)
|
||||
if trimmed == "" do
|
||||
{:error, "Param #{key} cannot be empty"}
|
||||
else
|
||||
{:ok, trimmed}
|
||||
end
|
||||
|
||||
{:ok, val} ->
|
||||
{:ok, val}
|
||||
|
||||
:error ->
|
||||
{:error, "Missing required param: #{key}"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_int(binary() | integer()) :: {:ok, integer()} | {:error, String.t()}
|
||||
def parse_int(str) when is_binary(str) do
|
||||
Logger.debug("Parsing integer from: #{inspect(str)}")
|
||||
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer format: #{str}"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_int(num) when is_integer(num), do: {:ok, num}
|
||||
def parse_int(other), do: {:error, "Expected integer or string, got: #{inspect(other)}"}
|
||||
|
||||
@spec parse_int!(binary() | integer()) :: integer()
|
||||
def parse_int!(str) do
|
||||
case parse_int(str) do
|
||||
{:ok, num} -> num
|
||||
{:error, msg} -> raise ArgumentError, msg
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_uuid(any()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
def validate_uuid(id) when is_binary(id) do
|
||||
case Ecto.UUID.cast(id) do
|
||||
{:ok, uuid} -> {:ok, uuid}
|
||||
:error -> {:error, "Invalid UUID format: #{id}"}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_uuid(_), do: {:error, "ID must be a UUID string"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Parameter Extraction
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
Extract and validate parameters for upserting a system.
|
||||
Returns {:ok, attrs} or {:error, error_message}.
|
||||
"""
|
||||
@spec extract_upsert_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def extract_upsert_params(params) when is_map(params) do
|
||||
required = ["solar_system_id"]
|
||||
optional = [
|
||||
"solar_system_name", "position_x", "position_y", "coordinates",
|
||||
"status", "visible", "description", "tag",
|
||||
"locked", "temporary_name", "labels"
|
||||
]
|
||||
|
||||
case Map.fetch(params, "solar_system_id") do
|
||||
:error -> {:error, "Missing solar_system_id in request body"}
|
||||
{:ok, _} ->
|
||||
params
|
||||
|> Map.take(required ++ optional)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extract and validate parameters for updating a system.
|
||||
Returns {:ok, attrs} or {:error, error_message}.
|
||||
"""
|
||||
@spec extract_update_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def extract_update_params(params) when is_map(params) do
|
||||
allowed = [
|
||||
"solar_system_name", "position_x", "position_y", "coordinates",
|
||||
"status", "visible", "description", "tag",
|
||||
"locked", "temporary_name", "labels"
|
||||
]
|
||||
|
||||
attrs =
|
||||
params
|
||||
|> Map.take(allowed)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, attrs}
|
||||
end
|
||||
|
||||
@spec normalize_connection_params(map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def normalize_connection_params(params) do
|
||||
# Convert all keys to strings for consistent access
|
||||
string_params = for {k, v} <- params, into: %{} do
|
||||
{to_string(k), v}
|
||||
end
|
||||
|
||||
# Define parameter mappings for normalization
|
||||
aliases = %{
|
||||
"source" => "solar_system_source",
|
||||
"source_id" => "solar_system_source",
|
||||
"target" => "solar_system_target",
|
||||
"target_id" => "solar_system_target"
|
||||
}
|
||||
|
||||
# Normalize parameters using aliases
|
||||
normalized_params = Enum.reduce(aliases, string_params, fn {alias_key, std_key}, acc ->
|
||||
if Map.has_key?(acc, alias_key) && !Map.has_key?(acc, std_key) do
|
||||
Map.put(acc, std_key, acc[alias_key])
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
# Handle required parameters
|
||||
with {:ok, src} <- parse_to_int(normalized_params["solar_system_source"], "solar_system_source"),
|
||||
{:ok, tgt} <- parse_to_int(normalized_params["solar_system_target"], "solar_system_target") do
|
||||
|
||||
# Handle optional parameters with sane defaults
|
||||
type = normalized_params["type"] || 0
|
||||
mass_status = normalized_params["mass_status"] || 0
|
||||
time_status = normalized_params["time_status"] || 0
|
||||
ship_size_type = normalized_params["ship_size_type"] || 0
|
||||
# Coerce to boolean; accept "true"/"false", 1/0, etc.
|
||||
locked =
|
||||
case normalized_params["locked"] do
|
||||
val when val in [true, "true", 1, "1"] -> true
|
||||
val when val in [false, "false", 0, "0"] -> false
|
||||
nil -> false
|
||||
other -> other # keep unknowns for caller-side validation
|
||||
end
|
||||
custom_info = normalized_params["custom_info"]
|
||||
wormhole_type = normalized_params["wormhole_type"]
|
||||
|
||||
# Build standardized attrs map
|
||||
attrs = %{
|
||||
"solar_system_source" => src,
|
||||
"solar_system_target" => tgt,
|
||||
"type" => parse_optional_int(type, 0),
|
||||
"mass_status" => parse_optional_int(mass_status, 0),
|
||||
"time_status" => parse_optional_int(time_status, 0),
|
||||
"ship_size_type" => parse_optional_int(ship_size_type, 0)
|
||||
}
|
||||
|
||||
# Add non-nil optional attributes
|
||||
attrs = if is_nil(locked), do: attrs, else: Map.put(attrs, "locked", locked)
|
||||
attrs = if is_nil(custom_info), do: attrs, else: Map.put(attrs, "custom_info", custom_info)
|
||||
attrs = if is_nil(wormhole_type), do: attrs, else: Map.put(attrs, "wormhole_type", wormhole_type)
|
||||
|
||||
{:ok, attrs}
|
||||
else
|
||||
{:error, msg} -> {:error, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to handle various input formats
|
||||
defp parse_to_int(nil, field), do: {:error, "Missing #{field}"}
|
||||
defp parse_to_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||
defp parse_to_int(val, field) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, ""} -> {:ok, i}
|
||||
:error -> {:error, "Invalid #{field}: #{val}"}
|
||||
_ -> {:error, "Invalid #{field}: #{val}"}
|
||||
end
|
||||
end
|
||||
defp parse_to_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
||||
|
||||
defp parse_optional_int(nil, default), do: default
|
||||
defp parse_optional_int(i, _default) when is_integer(i), do: i
|
||||
defp parse_optional_int(s, default) when is_binary(s) do
|
||||
case Integer.parse(s) do
|
||||
{i, _} -> i
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Standardized JSON Responses
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec respond_data(Plug.Conn.t(), any(), atom() | integer()) :: Plug.Conn.t()
|
||||
def respond_data(conn, data, status \\ :ok) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(%{data: data})
|
||||
end
|
||||
|
||||
@spec error_response(Plug.Conn.t(), atom() | integer(), String.t(), map() | nil) :: Plug.Conn.t()
|
||||
def error_response(conn, status, message, details \\ nil) do
|
||||
body = if details, do: %{error: message, details: details}, else: %{error: message}
|
||||
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(body)
|
||||
end
|
||||
|
||||
@spec error_not_found(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
|
||||
def error_not_found(conn, message), do: error_response(conn, :not_found, message)
|
||||
|
||||
@doc """
|
||||
Formats error messages for consistent display.
|
||||
"""
|
||||
@spec format_error(any()) :: String.t()
|
||||
def format_error(error) when is_binary(error), do: error
|
||||
def format_error(error) when is_atom(error), do: Atom.to_string(error)
|
||||
def format_error(error), do: inspect(error)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# JSON Serialization
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@spec map_system_to_json(struct()) :: map()
|
||||
def map_system_to_json(system) do
|
||||
base =
|
||||
Map.take(system, ~w(
|
||||
id map_id solar_system_id custom_name temporary_name description tag labels
|
||||
locked visible status position_x position_y inserted_at updated_at
|
||||
)a)
|
||||
|
||||
original = get_original_name(system.solar_system_id)
|
||||
name = pick_name(system)
|
||||
|
||||
base
|
||||
|> Map.put(:original_name, original)
|
||||
|> Map.put(:name, name)
|
||||
end
|
||||
|
||||
defp get_original_name(id) do
|
||||
case MapSolarSystem.by_solar_system_id(id) do
|
||||
{:ok, sys} -> sys.solar_system_name
|
||||
_ -> "System #{id}"
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_name(%{temporary_name: t, custom_name: c, solar_system_id: id}) do
|
||||
cond do
|
||||
t not in [nil, ""] -> t
|
||||
c not in [nil, ""] -> c
|
||||
true -> get_original_name(id)
|
||||
end
|
||||
end
|
||||
|
||||
@spec connection_to_json(struct()) :: map()
|
||||
def connection_to_json(conn) do
|
||||
Map.take(conn, ~w(
|
||||
id map_id solar_system_source solar_system_target mass_status
|
||||
time_status ship_size_type type wormhole_type inserted_at updated_at
|
||||
)a)
|
||||
end
|
||||
end
|
||||
@@ -17,24 +17,26 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
|> handle_info_or_assign(assigns)}
|
||||
end
|
||||
|
||||
# attr(:groups, :any, required: true)
|
||||
# attr(:character_settings, :any, required: true)
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
<ul :for={group <- @groups} class="space-y-4 border-t border-b border-gray-200 py-4">
|
||||
<ul :for={group <- @groups} class="border-t border-b border-gray-200 py-0">
|
||||
<li :for={character <- group.characters}>
|
||||
<div class="flex items-center justify-between w-full space-x-2 p-1 hover:bg-gray-900">
|
||||
<.character_entry character={character} character_settings={@character_settings} />
|
||||
<.character_entry character={character} />
|
||||
<button
|
||||
:if={character.tracked}
|
||||
phx-click="untrack"
|
||||
phx-value-event-data={character.id}
|
||||
class="btn btn-sm btn-icon"
|
||||
class="btn btn-sm btn-icon py-1"
|
||||
>
|
||||
<.icon name="hero-eye-slash" class="h-5 w-5" /> Untrack
|
||||
</button>
|
||||
|
||||
<span :if={not character.tracked} class="text-white rounded-full px-2">
|
||||
Viewer
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -43,31 +45,43 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
end
|
||||
|
||||
attr(:character, :any, required: true)
|
||||
attr(:character_settings, :any, required: true)
|
||||
|
||||
defp character_entry(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center gap-3 text-sm w-[450px]">
|
||||
<span
|
||||
:if={is_tracked?(@character.id, @character_settings)}
|
||||
class="text-green-500 rounded-full px-2 py-1"
|
||||
>
|
||||
Tracked
|
||||
<div class="flex flex-col p-4 items-center gap-2 tooltip tooltip-top" data-tip="Active from">
|
||||
<span class="text-green-500 rounded-full px-2 py-1 whitespace-nowrap">
|
||||
<.local_time id={@character.id} at={@character.from} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="avatar">
|
||||
<div class="rounded-md w-8 h-8">
|
||||
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<span class="whitespace-nowrap">{@character.name}</span>
|
||||
<span :if={@character.alliance_ticker} class="whitespace-nowrap">
|
||||
[{@character.alliance_ticker}]
|
||||
</span>
|
||||
<span :if={@character.corporation_ticker} class="whitespace-nowrap">
|
||||
[{@character.corporation_ticker}]
|
||||
</span>
|
||||
|
||||
<span :if={is_online?(@character.id)} class="text-green-500 rounded-full px-2 py-1">
|
||||
Online
|
||||
</span>
|
||||
<span :if={not is_online?(@character.id)} class="text-red-500 rounded-full px-2 py-1">
|
||||
Offline
|
||||
</span>
|
||||
<div class="avatar">
|
||||
<div class="rounded-md w-8 h-8">
|
||||
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<span>{@character.name}</span>
|
||||
<span :if={@character.alliance_ticker}>[{@character.alliance_ticker}]</span>
|
||||
<span :if={@character.corporation_ticker}>[{@character.corporation_ticker}]</span>
|
||||
|
||||
<span :if={@character.tracked} class="text-green-500 rounded-full px-2 py-1">
|
||||
Tracked
|
||||
</span>
|
||||
|
||||
<span :if={not @character.tracked} class="text-red-500 rounded-full px-2 py-1 whitespace-nowrap">
|
||||
Not Tracked
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@@ -79,12 +93,6 @@ defmodule WandererAppWeb.MapCharacters do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp is_tracked?(character_id, character_settings) do
|
||||
Enum.any?(character_settings, fn setting ->
|
||||
setting.character_id == character_id && setting.tracked
|
||||
end)
|
||||
end
|
||||
|
||||
defp is_online?(character_id) do
|
||||
{:ok, state} = WandererApp.Character.get_character_state(character_id)
|
||||
state.is_online
|
||||
|
||||
@@ -335,7 +335,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|
||||
defp handle_tracking_event({:track_characters, map_characters, track_character}, socket, map_id) do
|
||||
:ok =
|
||||
WandererApp.Character.TrackingUtils.track_characters(
|
||||
WandererApp.Character.TrackingUtils.track(
|
||||
map_characters,
|
||||
map_id,
|
||||
track_character,
|
||||
|
||||
@@ -56,18 +56,12 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
case track_character do
|
||||
false ->
|
||||
:ok =
|
||||
WandererApp.Character.TrackingUtils.untrack_characters(
|
||||
map_characters,
|
||||
map_id,
|
||||
self()
|
||||
)
|
||||
|
||||
:ok = WandererApp.Character.TrackingUtils.untrack(map_characters, map_id, self())
|
||||
:ok = WandererApp.Character.TrackingUtils.remove_characters(map_characters, map_id)
|
||||
|
||||
_ ->
|
||||
:ok =
|
||||
WandererApp.Character.TrackingUtils.track_characters(
|
||||
WandererApp.Character.TrackingUtils.track(
|
||||
map_characters,
|
||||
map_id,
|
||||
true,
|
||||
@@ -232,7 +226,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
def handle_ui_event(
|
||||
_event,
|
||||
_body,
|
||||
%{assigns: %{has_tracked_characters?: false}} =
|
||||
%{assigns: %{has_tracked_characters?: false, can_track?: true}} =
|
||||
socket
|
||||
) do
|
||||
Process.send_after(self(), %{event: :show_tracking}, 10)
|
||||
@@ -248,7 +242,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
def handle_ui_event(
|
||||
event,
|
||||
body,
|
||||
%{assigns: %{main_character_id: main_character_id}} =
|
||||
%{assigns: %{main_character_id: main_character_id, can_track?: true}} =
|
||||
socket
|
||||
)
|
||||
when is_nil(main_character_id) do
|
||||
@@ -266,7 +260,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
{:ok, map_server_started} = WandererApp.Cache.lookup("map_#{map_id}:started", false)
|
||||
|
||||
if map_server_started do
|
||||
Process.send_after(self(), %{event: :map_server_started}, 10)
|
||||
Process.send_after(self(), %{event: :map_server_started}, 50)
|
||||
else
|
||||
WandererApp.Map.Manager.start_map(map_id)
|
||||
end
|
||||
@@ -439,6 +433,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
tracked_characters: tracked_characters,
|
||||
has_tracked_characters?: has_tracked_characters?,
|
||||
user_permissions:
|
||||
@@ -460,7 +455,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
end
|
||||
|
||||
events =
|
||||
case not has_tracked_characters? do
|
||||
case track_character && not has_tracked_characters? do
|
||||
true ->
|
||||
events ++ [:empty_tracked_characters]
|
||||
|
||||
@@ -468,13 +463,28 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
events
|
||||
end
|
||||
|
||||
character_limit_reached? = present_character_ids |> Enum.count() >= characters_limit
|
||||
|
||||
events =
|
||||
case present_character_ids |> Enum.count() < characters_limit do
|
||||
true ->
|
||||
cond do
|
||||
# in case user has not tracked any character track his main character as viewer
|
||||
track_character && not has_tracked_characters? ->
|
||||
main_character = Enum.find(current_user.characters, &(&1.id == main_character_id))
|
||||
events ++ [{:track_characters, [main_character], false}]
|
||||
|
||||
track_character && not character_limit_reached? ->
|
||||
events ++ [{:track_characters, tracked_characters, track_character}]
|
||||
|
||||
_ ->
|
||||
track_character && character_limit_reached? ->
|
||||
events ++ [:map_character_limit]
|
||||
|
||||
# in case user has view only permissions track his main character as viewer
|
||||
not track_character ->
|
||||
main_character = Enum.find(current_user.characters, &(&1.id == main_character_id))
|
||||
events ++ [{:track_characters, [main_character], track_character}]
|
||||
|
||||
true ->
|
||||
events
|
||||
end
|
||||
|
||||
initial_data =
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule WandererAppWeb.MapStructuresEventHandler do
|
||||
alias WandererApp.Api.MapSystem
|
||||
alias WandererApp.Structure
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
alias WandererAppWeb. MapCoreEventHandler
|
||||
|
||||
def handle_server_event(%{event: :structures_updated, payload: _solar_system_id}, socket) do
|
||||
socket
|
||||
|
||||
@@ -5,6 +5,8 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|
||||
alias WandererAppWeb.MapCharacters
|
||||
|
||||
@refresh_interval :timer.seconds(30)
|
||||
|
||||
def mount(
|
||||
%{"slug" => map_slug} = _params,
|
||||
_session,
|
||||
@@ -44,6 +46,15 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
:refresh_tracking_data,
|
||||
socket
|
||||
) do
|
||||
Process.send_after(self(), :refresh_tracking_data, @refresh_interval)
|
||||
{:noreply, socket |> load_characters()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
_event,
|
||||
@@ -101,17 +112,35 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
Process.send_after(self(), :refresh_tracking_data, @refresh_interval)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :map_characters)
|
||||
|> assign(:page_title, "Map - Characters")
|
||||
|> load_characters()
|
||||
end
|
||||
|
||||
defp get_all_characters(map_id) do
|
||||
{:ok, present_characters} =
|
||||
WandererApp.Cache.lookup(
|
||||
"map_#{map_id}:presence_data",
|
||||
[]
|
||||
)
|
||||
|
||||
present_characters =
|
||||
present_characters
|
||||
|> Enum.map(fn character ->
|
||||
character |> Map.merge(WandererApp.Character.get_character!(character.character_id))
|
||||
end)
|
||||
|
||||
present_characters
|
||||
end
|
||||
|
||||
defp load_characters(%{assigns: %{map_id: map_id}} = socket) do
|
||||
map_characters =
|
||||
map_id
|
||||
|> WandererApp.Map.list_characters()
|
||||
|> Enum.map(&map_ui_character/1)
|
||||
|> get_all_characters()
|
||||
|> Enum.map(fn character -> map_ui_character(map_id, character) end)
|
||||
|
||||
groups =
|
||||
map_characters
|
||||
@@ -132,20 +161,22 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|> assign(:groups, groups)
|
||||
end
|
||||
|
||||
defp map_ui_character(character),
|
||||
do:
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
:user_id,
|
||||
:eve_id,
|
||||
:name,
|
||||
:online,
|
||||
:corporation_id,
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker
|
||||
])
|
||||
defp map_ui_character(map_id, character) do
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
:user_id,
|
||||
:eve_id,
|
||||
:name,
|
||||
:online,
|
||||
:corporation_id,
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker,
|
||||
:from,
|
||||
:tracked
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<.link navigate={~p"/#{@map_slug}"} class="text-neutral-100">
|
||||
<%= @map_name %>
|
||||
</.link>
|
||||
- Characters [<%= @characters_count %>]
|
||||
- Active Characters [<%= @characters_count %>]
|
||||
</span>
|
||||
</nav>
|
||||
<main
|
||||
@@ -24,7 +24,6 @@
|
||||
id="map-characters"
|
||||
notify_to={self()}
|
||||
groups={@groups}
|
||||
character_settings={@character_settings}
|
||||
event_name="character_event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -291,6 +291,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
|
||||
def push_map_event(socket, _type, _body), do: socket
|
||||
|
||||
def map_ui_character_stat(nil), do: nil
|
||||
|
||||
def map_ui_character_stat(character),
|
||||
do:
|
||||
character
|
||||
|
||||
@@ -10,19 +10,39 @@ defmodule WandererAppWeb.Presence do
|
||||
end
|
||||
|
||||
def handle_metas(map_id, %{joins: _joins, leaves: _leaves}, presences, state) do
|
||||
presence_character_ids =
|
||||
presence_data =
|
||||
presences
|
||||
|> Enum.map(fn {character_id, _} -> character_id end)
|
||||
|> Enum.map(fn {character_id, meta} ->
|
||||
from =
|
||||
meta
|
||||
|> Enum.map(& &1.from)
|
||||
|> Enum.sort(&(DateTime.compare(&1, &2) != :gt))
|
||||
|> List.first()
|
||||
|
||||
any_tracked = Enum.any?(meta, fn %{tracked: tracked} -> tracked end)
|
||||
|
||||
%{character_id: character_id, tracked: any_tracked, from: from}
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} ->
|
||||
character_id
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", presence_character_ids)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_data",
|
||||
presence_data
|
||||
)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def presence_character_ids(map_id) do
|
||||
map_id
|
||||
|> list()
|
||||
|> Enum.map(fn {character_id, _} -> character_id end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -169,6 +169,7 @@ defmodule WandererAppWeb.Router do
|
||||
pipeline :api_map do
|
||||
plug WandererAppWeb.Plugs.CheckMapApiKey
|
||||
plug WandererAppWeb.Plugs.CheckMapSubscription
|
||||
plug WandererAppWeb.Plugs.AssignMapOwner
|
||||
end
|
||||
|
||||
pipeline :api_kills do
|
||||
@@ -206,17 +207,42 @@ defmodule WandererAppWeb.Router do
|
||||
scope "/api/map", WandererAppWeb do
|
||||
pipe_through [:api, :api_map]
|
||||
get "/audit", MapAuditAPIController, :index
|
||||
get "/systems", MapAPIController, :list_systems
|
||||
get "/system", MapAPIController, :show_system
|
||||
get "/connections", MapAPIController, :list_connections
|
||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||
# Deprecated routes - use /api/maps/:map_identifier/systems instead
|
||||
get "/systems", MapSystemAPIController, :list_systems
|
||||
get "/system", MapSystemAPIController, :show_system
|
||||
get "/connections", MapConnectionAPIController, :list_all_connections
|
||||
get "/characters", MapAPIController, :list_tracked_characters
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
get "/character-activity", MapAPIController, :character_activity
|
||||
get "/user_characters", MapAPIController, :user_characters
|
||||
|
||||
get "/acls", MapAccessListAPIController, :index
|
||||
post "/acls", MapAccessListAPIController, :create
|
||||
end
|
||||
|
||||
#
|
||||
# Unified RESTful routes for systems & connections by slug or ID
|
||||
#
|
||||
scope "/api/maps/:map_identifier", WandererAppWeb do
|
||||
pipe_through [:api, :api_map]
|
||||
|
||||
patch "/connections", MapConnectionAPIController, :update
|
||||
delete "/connections", MapConnectionAPIController, :delete
|
||||
delete "/systems", MapSystemAPIController, :delete
|
||||
resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
|
||||
resources "/connections", MapConnectionAPIController, only: [:index, :show, :create, :update, :delete], param: "id"
|
||||
resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
|
||||
get "/user-characters", MapAPIController, :show_user_characters
|
||||
get "/tracked-characters", MapAPIController, :show_tracked_characters
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Other API routes
|
||||
#
|
||||
scope "/api/characters", WandererAppWeb do
|
||||
pipe_through [:api, :api_character]
|
||||
get "/", CharactersAPIController, :index
|
||||
|
||||
156
lib/wanderer_app_web/schemas/api_schemas.ex
Normal file
156
lib/wanderer_app_web/schemas/api_schemas.ex
Normal file
@@ -0,0 +1,156 @@
|
||||
defmodule WandererAppWeb.Schemas.ApiSchemas do
|
||||
@moduledoc """
|
||||
Shared OpenAPI schema definitions for the Wanderer API.
|
||||
|
||||
This module defines common schema components that can be reused
|
||||
across different controller specifications.
|
||||
"""
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
# Standard response wrappers
|
||||
def data_wrapper(schema) do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: schema
|
||||
},
|
||||
required: ["data"]
|
||||
}
|
||||
end
|
||||
|
||||
# Standard error responses
|
||||
def error_response(description \\ "Error") do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %Schema{type: :string, description: "Brief error message"},
|
||||
details: %Schema{type: :string, description: "Detailed explanation", nullable: true},
|
||||
code: %Schema{type: :string, description: "Optional error code", nullable: true}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{"error" => description, "details" => "Additional information about the error"}
|
||||
}
|
||||
end
|
||||
|
||||
# Common entity schemas
|
||||
def character_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
eve_id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
corporation_id: %Schema{type: :string},
|
||||
corporation_ticker: %Schema{type: :string},
|
||||
alliance_id: %Schema{type: :string},
|
||||
alliance_ticker: %Schema{type: :string}
|
||||
},
|
||||
required: ["eve_id", "name"]
|
||||
}
|
||||
end
|
||||
|
||||
# Common system schema based on what we've seen in controllers
|
||||
def solar_system_basic_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_id: %Schema{type: :integer},
|
||||
solar_system_name: %Schema{type: :string},
|
||||
region_id: %Schema{type: :integer},
|
||||
region_name: %Schema{type: :string},
|
||||
constellation_id: %Schema{type: :integer},
|
||||
constellation_name: %Schema{type: :string},
|
||||
security: %Schema{type: :string}
|
||||
},
|
||||
required: ["solar_system_id", "solar_system_name"]
|
||||
}
|
||||
end
|
||||
|
||||
# Map schema with common fields
|
||||
def map_basic_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
slug: %Schema{type: :string},
|
||||
description: %Schema{type: :string},
|
||||
owner_id: %Schema{type: :string},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["id", "name", "slug"]
|
||||
}
|
||||
end
|
||||
|
||||
# License schema
|
||||
def license_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
license_key: %Schema{type: :string},
|
||||
is_valid: %Schema{type: :boolean},
|
||||
expire_at: %Schema{type: :string, format: :date_time},
|
||||
map_id: %Schema{type: :string}
|
||||
},
|
||||
required: ["id", "license_key", "is_valid", "map_id"]
|
||||
}
|
||||
end
|
||||
|
||||
# Access list schema
|
||||
def access_list_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
description: %Schema{type: :string},
|
||||
owner_id: %Schema{type: :string},
|
||||
api_key: %Schema{type: :string},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["id", "name"]
|
||||
}
|
||||
end
|
||||
|
||||
# Access list member schema
|
||||
def access_list_member_schema do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
role: %Schema{type: :string},
|
||||
eve_character_id: %Schema{type: :string},
|
||||
eve_corporation_id: %Schema{type: :string},
|
||||
eve_alliance_id: %Schema{type: :string},
|
||||
inserted_at: %Schema{type: :string, format: :date_time},
|
||||
updated_at: %Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["id", "name", "role"]
|
||||
}
|
||||
end
|
||||
|
||||
# Common paginated response wrapper
|
||||
def paginated_response(items_schema) do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: items_schema,
|
||||
pagination: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
page: %Schema{type: :integer},
|
||||
page_size: %Schema{type: :integer},
|
||||
total_pages: %Schema{type: :integer},
|
||||
total_count: %Schema{type: :integer}
|
||||
},
|
||||
required: ["page", "page_size", "total_count"]
|
||||
}
|
||||
},
|
||||
required: ["data", "pagination"]
|
||||
}
|
||||
end
|
||||
end
|
||||
114
lib/wanderer_app_web/schemas/response_schemas.ex
Normal file
114
lib/wanderer_app_web/schemas/response_schemas.ex
Normal file
@@ -0,0 +1,114 @@
|
||||
defmodule WandererAppWeb.Schemas.ResponseSchemas do
|
||||
@moduledoc """
|
||||
Standard response schema definitions for API responses.
|
||||
|
||||
This module provides helper functions to create standardized
|
||||
HTTP response schemas for OpenAPI documentation.
|
||||
"""
|
||||
|
||||
alias WandererAppWeb.Schemas.ApiSchemas
|
||||
|
||||
# Standard response status codes
|
||||
def ok(schema, description \\ "Successful operation") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
schema
|
||||
}
|
||||
end
|
||||
|
||||
def created(schema, description \\ "Resource created") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
schema
|
||||
}
|
||||
end
|
||||
|
||||
def bad_request(description \\ "Bad request") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def not_found(description \\ "Resource not found") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def internal_server_error(description \\ "Internal server error") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def unauthorized(description \\ "Unauthorized") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
def forbidden(description \\ "Forbidden") do
|
||||
{
|
||||
description,
|
||||
"application/json",
|
||||
ApiSchemas.error_response(description)
|
||||
}
|
||||
end
|
||||
|
||||
# Helper for common response patterns
|
||||
def standard_responses(success_schema, success_description \\ "Successful operation") do
|
||||
[
|
||||
ok: ok(success_schema, success_description),
|
||||
bad_request: bad_request(),
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
|
||||
# Helper for create operation responses
|
||||
def create_responses(created_schema, created_description \\ "Resource created") do
|
||||
[
|
||||
created: created(created_schema, created_description),
|
||||
bad_request: bad_request(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
|
||||
# Helper for update operation responses
|
||||
def update_responses(updated_schema, updated_description \\ "Resource updated") do
|
||||
[
|
||||
ok: ok(updated_schema, updated_description),
|
||||
bad_request: bad_request(),
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
|
||||
# Helper for delete operation responses
|
||||
def delete_responses(deleted_schema \\ nil, deleted_description \\ "Resource deleted") do
|
||||
if deleted_schema do
|
||||
[
|
||||
ok: ok(deleted_schema, deleted_description),
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
else
|
||||
[
|
||||
no_content:
|
||||
{deleted_description <> " (no content)", nil, nil},
|
||||
not_found: not_found(),
|
||||
internal_server_error: internal_server_error()
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.62.1"
|
||||
@version "1.64.3"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
915
priv/posts/2025/05-07-systems-connections-api.md
Normal file
915
priv/posts/2025/05-07-systems-connections-api.md
Normal file
@@ -0,0 +1,915 @@
|
||||
%{
|
||||
title: "Guide: Systems and Connections API",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/03-06-systems/api-endpoints.png",
|
||||
tags: ~w(api map systems connections documentation),
|
||||
description: "Detailed guide for Wanderer's systems and connections API endpoints, including batch operations, updates, and deletions."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# Guide to Wanderer's Systems and Connections API
|
||||
|
||||
## Introduction
|
||||
|
||||
This guide covers Wanderer's dedicated API endpoints for managing systems and connections on your maps. These endpoints provide fine-grained control over individual systems and connections, as well as batch operations for efficient updates.
|
||||
|
||||
With these APIs, you can:
|
||||
|
||||
- Create, update, and delete individual systems
|
||||
- Create, update, and delete individual connections
|
||||
- Perform batch operations on systems and connections
|
||||
- Query system and connection details
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require a Map API Token, which you can generate in your map settings. Pass the token in the Authorization header:
|
||||
|
||||
```bash
|
||||
Authorization: Bearer <YOUR_MAP_TOKEN>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Systems Endpoints
|
||||
|
||||
### 1. List Systems
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/systems
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all systems and their connections for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"systems": [
|
||||
{
|
||||
"id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear",
|
||||
"visible": true,
|
||||
"description": "Trade hub",
|
||||
"tag": "TRADE",
|
||||
"locked": false,
|
||||
"labels": ["market", "highsec"],
|
||||
"map_id": "<MAP_UUID>"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "<CONNECTION_UUID>",
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"time_status": 0,
|
||||
"ship_size_type": 1,
|
||||
"wormhole_type": "K162",
|
||||
"count_of_passage": 0,
|
||||
"locked": false,
|
||||
"custom_info": "Fresh hole"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Single System
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/systems/:id
|
||||
```
|
||||
|
||||
- **Description:** Retrieves details for a specific system.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the system's solar_system_id.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear",
|
||||
"visible": true,
|
||||
"description": "Trade hub",
|
||||
"tag": "TRADE",
|
||||
"locked": false,
|
||||
"labels": ["market", "highsec"],
|
||||
"map_id": "<MAP_UUID>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create/Update System
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/systems
|
||||
PUT /api/maps/:map_identifier/systems/:id
|
||||
```
|
||||
|
||||
- **Description:** Creates a new system or updates an existing one.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required for PUT) — the system's solar_system_id.
|
||||
|
||||
#### Example Create Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear",
|
||||
"visible": true,
|
||||
"description": "Trade hub",
|
||||
"tag": "TRADE",
|
||||
"locked": false,
|
||||
"labels": ["market", "highsec"]
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
#### Example Update Request
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"status": "hostile",
|
||||
"description": "Hostiles reported",
|
||||
"tag": "DANGER"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||
```
|
||||
|
||||
### 4. Delete System
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/systems/:id
|
||||
```
|
||||
|
||||
- **Description:** Deletes a specific system and its associated connections.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the system's solar_system_id.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems/30000142"
|
||||
```
|
||||
|
||||
### 5. Batch Delete Systems
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/systems
|
||||
```
|
||||
|
||||
- **Description:** Deletes multiple systems and their connections in a single operation.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"system_ids": [30000142, 30000144, 30000145]
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connections Endpoints
|
||||
|
||||
### 1. List Connections
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all connections for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<CONNECTION_UUID>",
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"time_status": 0,
|
||||
"ship_size_type": 1,
|
||||
"wormhole_type": "K162",
|
||||
"count_of_passage": 0,
|
||||
"locked": false,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Connection
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Creates a new connection between two systems.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"time_status": 0,
|
||||
"ship_size_type": 1,
|
||||
"locked": false,
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections"
|
||||
```
|
||||
|
||||
### 3. Update Connection
|
||||
|
||||
```bash
|
||||
PATCH /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing connection's properties.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- Query parameters:
|
||||
- `solar_system_source` (required) — source system ID
|
||||
- `solar_system_target` (required) — target system ID
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PATCH \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"mass_status": 1,
|
||||
"time_status": 1,
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections?solar_system_source=30000142&solar_system_target=30000144"
|
||||
```
|
||||
|
||||
### 4. Delete Connection
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/connections
|
||||
```
|
||||
|
||||
- **Description:** Deletes a connection between two systems.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- Query parameters:
|
||||
- `solar_system_source` (required) — source system ID
|
||||
- `solar_system_target` (required) — target system ID
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/connections?solar_system_source=30000142&solar_system_target=30000144"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch Operations
|
||||
|
||||
### 1. Batch Upsert Systems and Connections
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/systems
|
||||
```
|
||||
|
||||
- **Description:** Creates or updates multiple systems and connections in a single operation.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"systems": [
|
||||
{
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"position_x": 100.5,
|
||||
"position_y": 200.3,
|
||||
"status": "clear"
|
||||
},
|
||||
{
|
||||
"solar_system_id": 30000144,
|
||||
"solar_system_name": "Perimeter",
|
||||
"position_x": 150.5,
|
||||
"position_y": 250.3,
|
||||
"status": "clear"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"solar_system_source": 30000142,
|
||||
"solar_system_target": 30000144,
|
||||
"type": 0,
|
||||
"mass_status": 0,
|
||||
"ship_size_type": 1
|
||||
}
|
||||
]
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"systems": {
|
||||
"created": 2,
|
||||
"updated": 0
|
||||
},
|
||||
"connections": {
|
||||
"created": 1,
|
||||
"updated": 0,
|
||||
"deleted": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The response includes counts for:
|
||||
- Systems created and updated
|
||||
- Connections created, updated, and deleted (if any)
|
||||
|
||||
Note: The `deleted` count in connections will be 0 for batch operations as deletion is handled through separate endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Backup and Restore Map State
|
||||
|
||||
We provide a utility script that demonstrates how to use these endpoints to backup and restore your map state:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup_restore_test.sh
|
||||
|
||||
# 1. Backup current state
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems" \
|
||||
> map_backup.json
|
||||
|
||||
# 2. Delete everything (after confirmation)
|
||||
read -p "Delete all systems? (y/N) " confirm
|
||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||
# Get system IDs
|
||||
systems=$(cat map_backup.json | jq -r '.data.systems[].solar_system_id')
|
||||
|
||||
# Create deletion payload
|
||||
payload=$(jq -n --argjson ids "$(echo "$systems" | jq -R . | jq -s .)" \
|
||||
'{system_ids: $ids}')
|
||||
|
||||
# Delete all systems
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
fi
|
||||
|
||||
# 3. Restore from backup (after confirmation)
|
||||
read -p "Restore from backup? (y/N) " confirm
|
||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||
# Extract systems and connections
|
||||
backup_data=$(cat map_backup.json)
|
||||
systems=$(echo "$backup_data" | jq '.data.systems')
|
||||
connections=$(echo "$backup_data" | jq '.data.connections')
|
||||
|
||||
# Create restore payload
|
||||
payload="{\"systems\": $systems, \"connections\": $connections}"
|
||||
|
||||
# Restore everything
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/systems"
|
||||
fi
|
||||
```
|
||||
|
||||
This script demonstrates a practical application of the batch operations endpoints for backing up and restoring map data.
|
||||
|
||||
---
|
||||
|
||||
## Structures Endpoints
|
||||
|
||||
### 1. List Structures
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/structures
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all structures for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Structure
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/structures/:id
|
||||
```
|
||||
|
||||
- **Description:** Retrieves details for a specific structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the structure's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures/<STRUCTURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Structure
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/structures
|
||||
```
|
||||
|
||||
- **Description:** Creates a new structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"solar_system_id": 30000142,
|
||||
"solar_system_name": "Jita",
|
||||
"structure_type_id": "35832",
|
||||
"structure_type": "Astrahus",
|
||||
"character_eve_id": "123456789",
|
||||
"name": "Jita Trade Hub",
|
||||
"notes": "Main market structure",
|
||||
"owner_name": "Wanderer Corp",
|
||||
"owner_ticker": "WANDR",
|
||||
"owner_id": "corp-uuid-1",
|
||||
"status": "anchoring",
|
||||
"end_time": "2025-05-01T12:00:00Z",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Structure
|
||||
|
||||
```bash
|
||||
PUT /api/maps/:map_identifier/structures/:id
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the structure's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"status": "anchored",
|
||||
"notes": "Updated via API"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures/<STRUCTURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<STRUCTURE_UUID>",
|
||||
"status": "anchored",
|
||||
"notes": "Updated via API"
|
||||
// ... other fields ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Delete Structure
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/structures/:id
|
||||
```
|
||||
|
||||
- **Description:** Deletes a specific structure.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the structure's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/structures/<STRUCTURE_UUID>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signatures Endpoints
|
||||
|
||||
### 1. List Signatures
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/signatures
|
||||
```
|
||||
|
||||
- **Description:** Retrieves all signatures for the specified map.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811",
|
||||
"character_eve_id": "123456789",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Signature
|
||||
|
||||
```bash
|
||||
GET /api/maps/:map_identifier/signatures/:id
|
||||
```
|
||||
|
||||
- **Description:** Retrieves details for a specific signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the signature's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures/<SIGNATURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"system_id": "<SYSTEM_UUID>",
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811",
|
||||
"character_eve_id": "123456789",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Signature
|
||||
|
||||
```bash
|
||||
POST /api/maps/:map_identifier/signatures
|
||||
```
|
||||
|
||||
- **Description:** Creates a new signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"eve_id": "ABC-123",
|
||||
"name": "Wormhole K162",
|
||||
"description": "Leads to unknown space",
|
||||
"type": "Wormhole",
|
||||
"linked_system_id": 30000144,
|
||||
"kind": "cosmic_signature",
|
||||
"group": "wormhole",
|
||||
"custom_info": "Fresh",
|
||||
"solar_system_id": 31001394,
|
||||
"solar_system_name": "J214811",
|
||||
"character_eve_id": "123456789",
|
||||
"inserted_at": "2025-04-30T10:00:00Z",
|
||||
"updated_at": "2025-04-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Signature
|
||||
|
||||
```bash
|
||||
PUT /api/maps/:map_identifier/signatures/:id
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the signature's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"description": "Updated via API",
|
||||
"custom_info": "Updated info"
|
||||
}' \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures/<SIGNATURE_UUID>"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "<SIGNATURE_UUID>",
|
||||
"description": "Updated via API",
|
||||
"custom_info": "Updated info"
|
||||
// ... other fields ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Delete Signature
|
||||
|
||||
```bash
|
||||
DELETE /api/maps/:map_identifier/signatures/:id
|
||||
```
|
||||
|
||||
- **Description:** Deletes a specific signature.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_identifier` (required) — the map's slug or UUID.
|
||||
- `id` (required) — the signature's UUID.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
||||
"https://wanderer.example.com/api/maps/your-map-slug/signatures/<SIGNATURE_UUID>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
These endpoints provide powerful tools for managing your map's systems and connections programmatically. Key features include:
|
||||
|
||||
1. Individual system and connection management
|
||||
2. Efficient batch operations
|
||||
3. Flexible update options
|
||||
4. Robust error handling
|
||||
5. Consistent response formats
|
||||
|
||||
For the most up-to-date and interactive documentation, remember to check the Swagger UI at `/swaggerui`.
|
||||
|
||||
If you have questions about these endpoints or need assistance, please reach out to the Wanderer Team.
|
||||
|
||||
----
|
||||
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
|
||||
----
|
||||
77
priv/posts/2025/05-11-map-active-characters.md
Normal file
77
priv/posts/2025/05-11-map-active-characters.md
Normal file
@@ -0,0 +1,77 @@
|
||||
%{
|
||||
title: "Map Active Characters Page — Interface Guide",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/2025/05-11-map-active-characters/cover.png",
|
||||
tags: ~w(characters interface guide map security),
|
||||
description: "This interface is essential for managing access and tracking behavior on shared maps, especially in large organizations."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### Introduction
|
||||
|
||||

|
||||

|
||||
|
||||
This page displays **only currently active characters** — those who have the map page open in an active browser tab or window.
|
||||
|
||||
### Key Use Cases:
|
||||
- Identify active pilots on your map
|
||||
- Monitor user activity and access level
|
||||
- Manage tracked status to stay within subscription limits
|
||||
|
||||
---
|
||||
|
||||
## 👤 Character Grouping by User
|
||||
|
||||
Each user may have multiple EVE Online characters authorized. On this page:
|
||||
- Characters are **grouped under their owning user**
|
||||
- Admins can easily see which characters belong to the same person
|
||||
- Useful for distinguishing between multiboxers or corp mates sharing access
|
||||
|
||||
---
|
||||
|
||||
## 📋 Character Info Displayed
|
||||
|
||||

|
||||
|
||||
Each tracked character on this page includes:
|
||||
|
||||
| Field | Description |
|
||||
|--------------------|-----------------------------------------------------------------------------|
|
||||
| **Active From** | Timestamp indicating when the character opened the map (based on real-time browser tab activity) |
|
||||
| **Character Info** | Character Name, Corporation, and Alliance |
|
||||
| **Tracked Status** | Whether the character is being actively tracked on the map |
|
||||
| **Online Status** | Online/offline status (in-game) |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Admin Actions
|
||||
|
||||
Map **owners and administrators** can:
|
||||
|
||||
- ✅ **See viewer-only access**: Identify characters who can view but are not being tracked.
|
||||
- 🚫 **Force Untrack** characters:
|
||||
- Stop tracking & remove characters from map.
|
||||
- Useful to stay within your character limit or reset tracking manually.
|
||||
- Note: The user should re-enable tracking later if needed manually in map tracking settings.
|
||||
|
||||
---
|
||||
|
||||
## 🛑 Notes & Recommendations
|
||||
|
||||
- A character is **counted toward the Characters Limit** of the map's subscription **only when tracked**.
|
||||
- Tracking begins **as soon as the character opens the map page** in any tab/browser.
|
||||
- Closing all tabs with the map **automatically stops tracking** for that character (after a small period about 15 minutes).
|
||||
- This system ensures you always have a live (tracking data automatically updated every 30 seconds), accurate picture of map usage across your team.
|
||||
|
||||
---
|
||||
|
||||
By using the **Map Active Characters** page, admins can efficiently manage map activity, maintain security, and optimize performance across their team or alliance.
|
||||
|
||||
---
|
||||
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
|
||||
---
|
||||
@@ -340,7 +340,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
471,23,Corporate Hangar Array,0,1,0,1,0,1
|
||||
472,7,System Scanner,0,0,0,0,0,1
|
||||
473,23,Tracking Array,0,1,0,1,0,1
|
||||
474,17,Acceleration Gate Keys,0,0,0,0,0,1
|
||||
474,17,Acceleration Gate Keys,0,1,0,0,0,1
|
||||
475,7,Microwarpdrive,96,0,0,0,0,0
|
||||
476,8,XL Torpedo,1349,0,0,0,1,1
|
||||
477,9,Mining Barge Blueprint,0,1,0,0,0,1
|
||||
@@ -1010,7 +1010,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
1405,65,Laboratory,None,0,0,0,0,0
|
||||
1406,65,Refinery,None,0,1,0,0,1
|
||||
1407,65,Observatory Array,None,0,0,0,0,0
|
||||
1408,65,Upwell Jump Gate,None,0,1,0,0,1
|
||||
1408,65,Upwell Jump Bridge,None,0,1,0,0,1
|
||||
1409,65,Administration Hub,None,0,0,0,0,0
|
||||
1410,65,Advertisement Center,None,0,0,0,0,0
|
||||
1411,11,Amarr Navy Roaming Cruiser,None,0,0,0,0,0
|
||||
@@ -1295,7 +1295,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
1924,65,♦ Stronghold,None,0,0,0,0,0
|
||||
1925,11,Irregular Industrial Command Ship,None,0,0,0,0,0
|
||||
1926,11,Irregular Freighter,None,0,0,0,0,0
|
||||
1927,11,Irregular Structure,None,0,0,0,0,0
|
||||
1927,11,Irregular Structure,None,0,1,0,0,0
|
||||
1928,11,Irregular Container,None,0,0,0,0,0
|
||||
1929,11,Irregular - Unidentified,None,0,0,0,0,0
|
||||
1933,66,Structure Composite Reactor Rig M - TE,None,0,0,0,0,1
|
||||
@@ -1531,6 +1531,10 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
|
||||
4821,17,Atavum,None,1,0,0,0,1
|
||||
4824,17,Infomorph Systems,None,1,0,0,0,1
|
||||
4825,2,Local Beacon,None,0,1,0,0,0
|
||||
4827,17,EDENCOM Data,None,1,0,0,0,1
|
||||
4828,2,Pirate Spawners,None,0,0,0,0,0
|
||||
4843,17,Limited Rarities,None,1,0,0,0,1
|
||||
4857,25,Tyranite,15,0,1,0,0,1
|
||||
350858,350001,Infantry Weapons,None,1,0,0,0,0
|
||||
351064,350001,Infantry Dropsuits,None,1,0,0,0,0
|
||||
351121,350001,Infantry Modules,None,1,0,0,0,0
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -5431,12 +5431,12 @@ regionID,constellationID,solarSystemID,solarSystemName,x,y,z,xMin,xMax,yMin,yMax
|
||||
10000069,20000782,30045353,Pynekastoh,-218294709389294016.0000000000,57890482798287904.0000000000,108746979052662000.0000000000,-215460829363407008.0000000000,-215443912092723008.0000000000,58478339404338496.0000000000,58495256675023104.0000000000,-107169521884028000.0000000000,-107152604613344000.0000000000,1.1560000000,0,0,0,0,0,0,None,0.2396328126,500001,710889342216.0000000000,45047,None
|
||||
10000069,20000782,30045354,Reitsato,-197894305085120992.0000000000,65621164096811400.0000000000,125071865404206000.0000000000,-192367367542551008.0000000000,-192346438189638016.0000000000,62054618448878800.0000000000,62075547801792096.0000000000,-129051005274330000.0000000000,-129030075921416992.0000000000,0.0106400000,0,0,0,0,0,0,None,0.1895104797,500001,1757422438016.0000000000,8,None
|
||||
10001000,20010000,30100000,Zarzakh,4732782451200000.0000000000,2722598544370000.0000000000,-1508346782640000.0000000000,4732781451210000.0000000000,4732783451210000.0000000000,2722597544370000.0000000000,2722599544370000.0000000000,-1508347782640000.0000000000,-1508345782640000.0000000000,0E-10,0,0,0,0,0,0,None,-1.0000000000,500029,3112788232450.0000000000,3796,None
|
||||
11000033,21000334,31000001,J055520,6824391110928870400.0000000000,1700713810475010048.0000000000,-9808807909109499904.0000000000,6824376151141799936.0000000000,6824406070715939840.0000000000,1700698850687940096.0000000000,1700728770262080000.0000000000,-9808822868896569344.0000000000,-9808792949322430464.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000002,J110145,6862328572493299712.0000000000,1716315417584930048.0000000000,-9835131116484100096.0000000000,6862313612706230272.0000000000,6862343532280380416.0000000000,1716300457797860096.0000000000,1716330377372000000.0000000000,-9835146076271169536.0000000000,-9835116156697030656.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000003,J164710,6800690715289709568.0000000000,1667864493217910016.0000000000,-9815994309006979072.0000000000,6800675755502640128.0000000000,6800705675076780032.0000000000,1667849533430840064.0000000000,1667879453004979968.0000000000,-9816009268794050560.0000000000,-9815979349219909632.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000004,J200727,6827372579272930304.0000000000,1725234432784110080.0000000000,-9821296571091089408.0000000000,6827357619485859840.0000000000,6827387539059999744.0000000000,1725219472997040128.0000000000,1725249392571180032.0000000000,-9821311530878160896.0000000000,-9821281611304019968.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000001,J055520,6824391110928870400.0000000000,1700713810475010048.0000000000,-9808807909109499904.0000000000,6824376151141799936.0000000000,6824406070715939840.0000000000,1700698850687940096.0000000000,1700728770262080000.0000000000,-9808822868896569344.0000000000,-9808792949322430464.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500027,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000002,J110145,6862328572493299712.0000000000,1716315417584930048.0000000000,-9835131116484100096.0000000000,6862313612706230272.0000000000,6862343532280380416.0000000000,1716300457797860096.0000000000,1716330377372000000.0000000000,-9835146076271169536.0000000000,-9835116156697030656.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500002,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000003,J164710,6800690715289709568.0000000000,1667864493217910016.0000000000,-9815994309006979072.0000000000,6800675755502640128.0000000000,6800705675076780032.0000000000,1667849533430840064.0000000000,1667879453004979968.0000000000,-9816009268794050560.0000000000,-9815979349219909632.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500003,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000004,J200727,6827372579272930304.0000000000,1725234432784110080.0000000000,-9821296571091089408.0000000000,6827357619485859840.0000000000,6827387539059999744.0000000000,1725219472997040128.0000000000,1725249392571180032.0000000000,-9821311530878160896.0000000000,-9821281611304019968.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500001,14959787070000.0000000000,34331,None
|
||||
11000031,21000324,31000005,Thera,7201177000000000000.0000000000,1534300000000000000.0000000000,-9501332482538399744.0000000000,7321174000000000000.0000000000,7321180000000000000.0000000000,1533300000000000000.0000000000,1535300000000000000.0000000000,-9394335000000000000.0000000000,-9394329000000000000.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,3112788232446.8598632812,34331,None
|
||||
11000033,21000334,31000006,J174618,6851740425709679616.0000000000,1679294718115650048.0000000000,-9788179393983350784.0000000000,6851725465922610176.0000000000,6851755385496760320.0000000000,1679279758328580096.0000000000,1679309677902720000.0000000000,-9788194353770420224.0000000000,-9788164434196279296.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,14959787070000.0000000000,34331,None
|
||||
11000033,21000334,31000006,J174618,6851740425709679616.0000000000,1679294718115650048.0000000000,-9788179393983350784.0000000000,6851725465922610176.0000000000,6851755385496760320.0000000000,1679279758328580096.0000000000,1679309677902720000.0000000000,-9788194353770420224.0000000000,-9788164434196279296.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,500026,14959787070000.0000000000,34331,None
|
||||
11000001,21000311,31000007,J105443,7644843937233080320.0000000000,-5747794381266180.0000000000,-9482937421590790144.0000000000,7644840278395700224.0000000000,7644847596070469632.0000000000,-5751453218652410.0000000000,-5744135543879960.0000000000,9482933762753400832.0000000000,9482941080428179456.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,3658837386225.7099609375,45038,None
|
||||
11000001,21000311,31000008,J100744,7646115934669320192.0000000000,45486224544419904.0000000000,-9479841645153390592.0000000000,7646114732911429632.0000000000,7646117136427200512.0000000000,45485022786539904.0000000000,45487426302299800.0000000000,9479840443395510272.0000000000,9479842846911270912.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,1201757879938.6899414062,45033,None
|
||||
11000001,21000311,31000009,J225046,7639560557040199680.0000000000,-15009347794601900.0000000000,-9477365158238810112.0000000000,7639551529002940416.0000000000,7639569585077449728.0000000000,-15018375831855400.0000000000,-15000319757348400.0000000000,9477356130201550848.0000000000,9477374186276059136.0000000000,0E-10,0,0,0,0,0,0,None,-0.9900000000,None,9028037253510.2304687500,45038,None
|
||||
|
||||
|
Can't render this file because it is too large.
|
163
test/manual/api/map_api_backup_restore_test.sh
Executable file
163
test/manual/api/map_api_backup_restore_test.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/bin/bash
|
||||
# test/manual/api/backup_restore_test.sh
|
||||
# ─── Backup and Restore Test for Map Systems and Connections ────────────────────────
|
||||
#
|
||||
# Usage:
|
||||
# ./backup_restore_test.sh # Run with default settings
|
||||
# ./backup_restore_test.sh -v # Run in verbose mode
|
||||
# ./backup_restore_test.sh -h # Show help
|
||||
#
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
# Set to "true" to see detailed output, "false" for minimal output
|
||||
VERBOSE=${VERBOSE:-false}
|
||||
|
||||
# Parse command line options
|
||||
while getopts "vh" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
VERBOSE=true
|
||||
;;
|
||||
h)
|
||||
echo "Usage: $0 [-v] [-h]"
|
||||
echo " -v Verbose mode (show detailed output)"
|
||||
echo " -h Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
|
||||
# File to store backup data
|
||||
BACKUP_FILE="/tmp/wanderer_map_backup.json"
|
||||
|
||||
# ─── UTILITY FUNCTIONS ─────────────────────────────────────────────────────
|
||||
|
||||
# Function to backup current map state
|
||||
backup_map_state() {
|
||||
echo "==== Backing Up Map State ===="
|
||||
|
||||
echo "Fetching current map state..."
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
echo "$response" > "$BACKUP_FILE"
|
||||
|
||||
local system_count=$(echo "$response" | jq '.data.systems | length')
|
||||
local conn_count=$(echo "$response" | jq '.data.connections | length')
|
||||
|
||||
echo "✅ Backed up $system_count systems and $conn_count connections to $BACKUP_FILE"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Backup data:" && cat "$BACKUP_FILE" | jq '.'
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to backup map state. Status: $status"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to delete all systems (which will cascade to connections)
|
||||
delete_all() {
|
||||
echo "==== Deleting All Systems ===="
|
||||
|
||||
# Get current systems
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local system_ids=$(echo "$response" | jq -r '.data.systems[].solar_system_id')
|
||||
|
||||
if [ -z "$system_ids" ]; then
|
||||
echo "No systems to delete."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Convert system IDs to JSON array and create payload
|
||||
local system_ids_json=$(echo "$system_ids" | jq -R . | jq -s .)
|
||||
local payload=$(jq -n --argjson system_ids "$system_ids_json" '{system_ids: $system_ids}')
|
||||
|
||||
# Send batch delete request
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Successfully deleted all systems and their connections"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to delete systems. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to fetch systems for deletion. Status: $status"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to restore map state from backup
|
||||
restore_map_state() {
|
||||
echo "==== Restoring Map State ===="
|
||||
|
||||
if [ ! -f "$BACKUP_FILE" ]; then
|
||||
echo "❌ No backup file found at $BACKUP_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local backup_data=$(cat "$BACKUP_FILE")
|
||||
local systems=$(echo "$backup_data" | jq '.data.systems')
|
||||
local connections=$(echo "$backup_data" | jq '.data.connections')
|
||||
|
||||
# Create payload for batch upsert
|
||||
local payload="{\"systems\": $systems, \"connections\": $connections}"
|
||||
|
||||
# Send batch upsert request
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local systems_created=$(echo "$response" | jq '.data.systems.created')
|
||||
local systems_updated=$(echo "$response" | jq '.data.systems.updated')
|
||||
local conns_created=$(echo "$response" | jq '.data.connections.created')
|
||||
local conns_updated=$(echo "$response" | jq '.data.connections.updated')
|
||||
|
||||
echo "✅ Restore successful:"
|
||||
echo " Systems: $systems_created created, $systems_updated updated"
|
||||
echo " Connections: $conns_created created, $conns_updated updated"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to restore map state. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── MAIN EXECUTION FLOW ─────────────────────────────────────────────────
|
||||
|
||||
echo "Starting backup/restore test sequence..."
|
||||
|
||||
# Step 1: Backup current state
|
||||
backup_map_state || { echo "Backup failed, aborting."; exit 1; }
|
||||
|
||||
echo -e "\nBackup complete. Press Enter to proceed with deletion..."
|
||||
read -r
|
||||
|
||||
# Step 2: Delete everything
|
||||
delete_all || { echo "Deletion failed, aborting."; exit 1; }
|
||||
|
||||
echo -e "\nDeletion complete. Press Enter to proceed with restore..."
|
||||
read -r
|
||||
|
||||
# Step 3: Restore from backup
|
||||
restore_map_state || { echo "Restore failed."; exit 1; }
|
||||
|
||||
echo -e "\nTest sequence completed."
|
||||
exit 0
|
||||
462
test/manual/api/structure_signature_api_tests.sh
Executable file
462
test/manual/api/structure_signature_api_tests.sh
Executable file
@@ -0,0 +1,462 @@
|
||||
#!/bin/bash
|
||||
# test/manual/api/structure_signature_api_tests.sh
|
||||
# ─── Manual API Tests for Map Structure and Signature APIs ────────────────
|
||||
#
|
||||
# Usage:
|
||||
# ./structure_signature_api_tests.sh # Run all tests with menu selection
|
||||
# ./structure_signature_api_tests.sh create # Run only creation tests
|
||||
# ./structure_signature_api_tests.sh update # Run only update tests
|
||||
# ./structure_signature_api_tests.sh delete # Run only deletion tests
|
||||
# ./structure_signature_api_tests.sh -v # Run in verbose mode
|
||||
#
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
echo "DEBUG: Script started"
|
||||
|
||||
#set -x # Enable shell debug output
|
||||
|
||||
VERBOSE=${VERBOSE:-false}
|
||||
|
||||
trap 'echo -e "\n❌ ERROR: Script failed at line $LINENO. Last command: $BASH_COMMAND" >&2' ERR
|
||||
|
||||
while getopts "vh" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
VERBOSE=true
|
||||
;;
|
||||
h)
|
||||
echo "Usage: $0 [-v] [-h] [all|create|update|delete]"
|
||||
echo " -v Verbose mode (show detailed test output)"
|
||||
echo " -h Show this help message"
|
||||
echo " all Run all tests (default with menu)"
|
||||
echo " create Run only creation tests"
|
||||
echo " update Run only update tests"
|
||||
echo " delete Run only deletion tests"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
COMMAND=${1:-"all"}
|
||||
|
||||
STRUCTURES_FILE="/tmp/wanderer_test_structures.txt"
|
||||
SIGNATURES_FILE="/tmp/wanderer_test_signatures.txt"
|
||||
CREATED_STRUCTURE_IDS=""
|
||||
CREATED_SIGNATURE_IDS=""
|
||||
|
||||
save_structures() {
|
||||
echo "DEBUG: Entering save_structures"
|
||||
if ! echo "$CREATED_STRUCTURE_IDS" > "$STRUCTURES_FILE"; then
|
||||
echo "ERROR: Failed to write to $STRUCTURES_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "DEBUG: Successfully wrote to $STRUCTURES_FILE"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Saved $(wc -w < "$STRUCTURES_FILE") structures to $STRUCTURES_FILE"; fi
|
||||
}
|
||||
load_structures() {
|
||||
if [ -f "$STRUCTURES_FILE" ]; then
|
||||
CREATED_STRUCTURE_IDS=$(cat "$STRUCTURES_FILE")
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Loaded $(wc -w < "$STRUCTURES_FILE") structures from $STRUCTURES_FILE"; fi
|
||||
else
|
||||
CREATED_STRUCTURE_IDS=""
|
||||
fi
|
||||
}
|
||||
save_signatures() {
|
||||
echo "$CREATED_SIGNATURE_IDS" > "$SIGNATURES_FILE"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Saved $(wc -w < "$SIGNATURES_FILE") signatures to $SIGNATURES_FILE"; fi
|
||||
}
|
||||
load_signatures() {
|
||||
if [ -f "$SIGNATURES_FILE" ]; then
|
||||
CREATED_SIGNATURE_IDS=$(cat "$SIGNATURES_FILE")
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Loaded $(wc -w < "$SIGNATURES_FILE") signatures from $SIGNATURES_FILE"; fi
|
||||
else
|
||||
CREATED_SIGNATURE_IDS=""
|
||||
fi
|
||||
}
|
||||
add_to_list() {
|
||||
local list="$1"
|
||||
local item="$2"
|
||||
if [ -z "$list" ]; then
|
||||
echo "$item"
|
||||
else
|
||||
echo "$list $item"
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch the first available system (ID and name) from the API
|
||||
get_first_system() {
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
# Try .data as array
|
||||
local count=$(echo "$response" | jq -er 'if (.data | type == "array") then (.data | length) else 0 end' 2>/dev/null)
|
||||
for i in $(seq 0 $((count-1))); do
|
||||
local uuid=$(echo "$response" | jq -er ".data[$i].id // empty" 2>/dev/null)
|
||||
local eve_id=$(echo "$response" | jq -er ".data[$i].solar_system_id // empty" 2>/dev/null)
|
||||
local name=$(echo "$response" | jq -er ".data[$i].name // .data[$i].solar_system_name // empty" 2>/dev/null)
|
||||
if [[ -n "$uuid" && -n "$eve_id" && -n "$name" ]]; then
|
||||
echo "$uuid:$eve_id:$name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Try .data.systems as array
|
||||
local count2=$(echo "$response" | jq -er 'if (.data.systems | type == "array") then (.data.systems | length) else 0 end' 2>/dev/null)
|
||||
for i in $(seq 0 $((count2-1))); do
|
||||
local uuid=$(echo "$response" | jq -er ".data.systems[$i].id // empty" 2>/dev/null)
|
||||
local eve_id=$(echo "$response" | jq -er ".data.systems[$i].solar_system_id // empty" 2>/dev/null)
|
||||
local name=$(echo "$response" | jq -er ".data.systems[$i].name // .data.systems[$i].solar_system_name // empty" 2>/dev/null)
|
||||
if [[ -n "$uuid" && -n "$eve_id" && -n "$name" ]]; then
|
||||
echo "$uuid:$eve_id:$name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "ERROR: No valid system found in API response. Available systems:" >&2
|
||||
echo "$response" | jq '.' >&2
|
||||
exit 1
|
||||
else
|
||||
echo "ERROR: Failed to fetch systems (status $status)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── STRUCTURE TESTS ─────────────────────────────────────────────
|
||||
create_structure() {
|
||||
local sys_info=$(get_first_system)
|
||||
local system_uuid=$(echo "$sys_info" | cut -d: -f1)
|
||||
local eve_system_id=$(echo "$sys_info" | cut -d: -f2)
|
||||
local system_name=$(echo "$sys_info" | cut -d: -f3-)
|
||||
echo "==== Creating Structure in system $system_name ($eve_system_id, $system_uuid) ===="
|
||||
local payload=$(jq -n --arg sid "$eve_system_id" --arg name "$system_name" '{
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_name: $name,
|
||||
solar_system_id: ($sid|tonumber),
|
||||
structure_type_id: "35832",
|
||||
structure_type: "Astrahus",
|
||||
character_eve_id: "123456789",
|
||||
name: "Jita Trade Hub",
|
||||
notes: "Main market structure",
|
||||
owner_name: "Wanderer Corp",
|
||||
owner_ticker: "WANDR",
|
||||
owner_id: "corp-uuid-1",
|
||||
status: "anchoring",
|
||||
end_time: "2025-05-05T12:00:00Z"
|
||||
}')
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/structures" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local id=$(parse_response "$raw" | jq -r '.data.id')
|
||||
CREATED_STRUCTURE_IDS=$(add_to_list "$CREATED_STRUCTURE_IDS" "$id")
|
||||
echo "✅ Created structure with ID: $id"
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to create structure. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
save_structures
|
||||
echo "DEBUG: End of create_structure, about to return"
|
||||
}
|
||||
|
||||
list_structures() {
|
||||
echo "==== Listing Structures ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/structures")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local count=$(parse_response "$raw" | jq '.data | length')
|
||||
echo "✅ Listed $count structures"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$(parse_response "$raw")" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to list structures. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_structure() {
|
||||
load_structures
|
||||
local id=$(echo "$CREATED_STRUCTURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No structure ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Show Structure $id ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/structures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local data=$(parse_response "$raw")
|
||||
local name=$(echo "$data" | jq -r '.data.name')
|
||||
local status_val=$(echo "$data" | jq -r '.data.status')
|
||||
local notes=$(echo "$data" | jq -r '.data.notes')
|
||||
echo "✅ Showed structure $id: name='$name', status='$status_val', notes='$notes'"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$data" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to show structure $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_structure() {
|
||||
load_structures
|
||||
local id=$(echo "$CREATED_STRUCTURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No structure ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Updating Structure $id ===="
|
||||
local payload=$(jq -n '{status: "anchored", notes: "Updated via test"}')
|
||||
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/structures/$id" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated structure $id"
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to update structure $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
delete_structure() {
|
||||
load_structures
|
||||
local id=$(echo "$CREATED_STRUCTURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No structure ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Deleting Structure $id ===="
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/structures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Deleted structure $id"
|
||||
CREATED_STRUCTURE_IDS=""
|
||||
save_structures
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to delete structure $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── SIGNATURE TESTS ─────────────────────────────────────────────
|
||||
create_signature() {
|
||||
local sys_info=$(get_first_system)
|
||||
echo "DEBUG: sys_info='$sys_info'"
|
||||
local system_uuid=$(echo "$sys_info" | cut -d: -f1)
|
||||
local system_id=$(echo "$sys_info" | cut -d: -f2)
|
||||
local system_name=$(echo "$sys_info" | cut -d: -f3-)
|
||||
echo "DEBUG: system_id='$system_id' (should be a number like 31001394)"
|
||||
if [[ -z "$system_id" ]]; then
|
||||
echo "ERROR: system_id is empty. sys_info='$sys_info'" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Generate a unique, valid-looking eve_id (e.g., ABC-123)
|
||||
local eve_id=$(cat /dev/urandom | tr -dc 'A-Z' | fold -w 3 | head -n 1)-$(shuf -i 100-999 -n 1)
|
||||
echo "==== Creating Signature in system $system_name ($system_id, $system_uuid) with eve_id $eve_id ===="
|
||||
local payload=$(jq -n --arg sid "$system_id" --arg name "$system_name" --arg eve_id "$eve_id" '{
|
||||
eve_id: $eve_id,
|
||||
name: "Wormhole K162",
|
||||
description: "Leads to unknown space",
|
||||
type: "Wormhole",
|
||||
linked_system_id: 30000144,
|
||||
kind: "cosmic_signature",
|
||||
group: "wormhole",
|
||||
custom_info: "Fresh",
|
||||
solar_system_id: ($sid|tonumber),
|
||||
solar_system_name: $name
|
||||
}')
|
||||
echo "DEBUG: payload=$payload"
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/signatures" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
# Now list signatures and find the one with this eve_id
|
||||
local list_raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/signatures")
|
||||
local id=$(parse_response "$list_raw" | jq -r --arg eve_id "$eve_id" '.data[] | select(.eve_id == $eve_id) | .id' | head -n 1)
|
||||
if [[ -z "$id" ]]; then
|
||||
echo "❌ ERROR: Created signature not found in list (eve_id: $eve_id)" >&2
|
||||
exit 1
|
||||
fi
|
||||
CREATED_SIGNATURE_IDS=$(add_to_list "$CREATED_SIGNATURE_IDS" "$id")
|
||||
save_signatures
|
||||
echo "✅ Created signature with eve_id: $eve_id and ID: $id"
|
||||
else
|
||||
echo "❌ ERROR: Failed to create signature (status $status)" >&2
|
||||
echo "$raw" | parse_response | jq . >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
list_signatures() {
|
||||
echo "==== Listing Signatures ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/signatures")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local count=$(parse_response "$raw" | jq '.data | length')
|
||||
echo "✅ Listed $count signatures"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$(parse_response "$raw")" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to list signatures. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_signature() {
|
||||
load_signatures
|
||||
local id=$(echo "$CREATED_SIGNATURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No signature ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Show Signature $id ===="
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local data=$(parse_response "$raw")
|
||||
local eve_id=$(echo "$data" | jq -r '.data.eve_id')
|
||||
local name=$(echo "$data" | jq -r '.data.name')
|
||||
local description=$(echo "$data" | jq -r '.data.description')
|
||||
local custom_info=$(echo "$data" | jq -r '.data.custom_info')
|
||||
echo "✅ Showed signature $id: eve_id='$eve_id', name='$name', description='$description', custom_info='$custom_info'"
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "$data" | jq '.'; fi
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to show signature $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_signature() {
|
||||
load_signatures
|
||||
local id=$(echo "$CREATED_SIGNATURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No signature ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Get the EVE system ID for the update payload
|
||||
local sys_info=$(get_first_system)
|
||||
local system_id=$(echo "$sys_info" | cut -d: -f2)
|
||||
echo "==== Updating Signature $id ===="
|
||||
local payload=$(jq -n --arg sid "$system_id" '{description: "Updated via test", custom_info: "Updated info", solar_system_id: ($sid|tonumber) }')
|
||||
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$id" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated signature $id"
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to update signature $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
delete_signature() {
|
||||
load_signatures
|
||||
local id=$(echo "$CREATED_SIGNATURE_IDS" | awk '{print $1}')
|
||||
if [ -z "$id" ]; then
|
||||
echo -e "\n❌ ERROR: No signature ID found. Run creation first." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==== Deleting Signature $id ===="
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$id")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Deleted signature $id"
|
||||
CREATED_SIGNATURE_IDS=""
|
||||
save_signatures
|
||||
else
|
||||
echo -e "\n❌ ERROR: Failed to delete signature $id. Status: $status" >&2
|
||||
if [[ "$VERBOSE" == "true" ]]; then echo "Response: $(parse_response "$raw")" >&2; fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
echo "===== Map Structure & Signature API Tests ====="
|
||||
echo "1. Run all tests in sequence (with pauses)"
|
||||
echo "2. Create structure"
|
||||
echo "3. List structures"
|
||||
echo "4. Show structure"
|
||||
echo "5. Update structure"
|
||||
echo "6. Delete structure"
|
||||
echo "7. Create signature"
|
||||
echo "8. List signatures"
|
||||
echo "9. Show signature"
|
||||
echo "10. Update signature"
|
||||
echo "11. Delete signature"
|
||||
echo "12. Exit"
|
||||
echo "==============================================="
|
||||
echo "Enter your choice [1-12]: "
|
||||
}
|
||||
|
||||
case "$COMMAND" in
|
||||
"all")
|
||||
if [ -t 0 ]; then
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
case $choice in
|
||||
1)
|
||||
create_structure
|
||||
echo "DEBUG: After calling create_structure in menu, exit code $?"
|
||||
echo "DEBUG: After create_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
list_structures; echo "DEBUG: After list_structures, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_structure; echo "DEBUG: After show_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
update_structure; echo "DEBUG: After update_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_structure; echo "DEBUG: After show_structure (post-update), exit code $?"; read -p "Press Enter to continue..."
|
||||
delete_structure; echo "DEBUG: After delete_structure, exit code $?"; read -p "Press Enter to continue..."
|
||||
create_signature; echo "DEBUG: After create_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
list_signatures; echo "DEBUG: After list_signatures, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_signature; echo "DEBUG: After show_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
update_signature; echo "DEBUG: After update_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
show_signature; echo "DEBUG: After show_signature (post-update), exit code $?"; read -p "Press Enter to continue..."
|
||||
delete_signature; echo "DEBUG: After delete_signature, exit code $?"; read -p "Press Enter to continue..."
|
||||
echo "All tests completed."
|
||||
show_menu
|
||||
read -r choice
|
||||
continue
|
||||
;;
|
||||
2) create_structure ;;
|
||||
3) list_structures ;;
|
||||
4) show_structure ;;
|
||||
5) update_structure ;;
|
||||
6) delete_structure ;;
|
||||
7) create_signature ;;
|
||||
8) list_signatures ;;
|
||||
9) show_signature ;;
|
||||
10) update_signature ;;
|
||||
11) delete_signature ;;
|
||||
12)
|
||||
read -p "Clean up any remaining test data before exiting? (y/n): " confirm
|
||||
if [[ "$confirm" =~ ^[Yy] ]]; then
|
||||
delete_structure
|
||||
delete_signature
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Invalid option. Please try again." ;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
create_structure; list_structures; show_structure; update_structure; show_structure; delete_structure
|
||||
create_signature; list_signatures; show_signature; update_signature; show_signature; delete_signature
|
||||
fi
|
||||
;;
|
||||
"create")
|
||||
create_structure; create_signature ;;
|
||||
"update")
|
||||
update_structure; update_signature ;;
|
||||
"delete")
|
||||
delete_structure; delete_signature ;;
|
||||
*)
|
||||
echo "Invalid command: $COMMAND"
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
echo "DEBUG: End of script reached"
|
||||
531
test/manual/api/system_api_legacy_tests.sh
Executable file
531
test/manual/api/system_api_legacy_tests.sh
Executable file
@@ -0,0 +1,531 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─── Legacy Map endpoint tests ───────────────────────────────────────────────────────
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/utils.sh"
|
||||
|
||||
# Track created IDs for cleanup - use space-delimited strings to match utils.sh
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
|
||||
# Optional environment variables to control verbosity:
|
||||
# VERBOSE_LOGGING=1 - Show full API responses
|
||||
QUIET_MODE=1 # Show minimal output (just test names and results)
|
||||
|
||||
# DUMP RESPONSE - Call this to see the complete raw API response
|
||||
dump_complete_response() {
|
||||
local url="$1"
|
||||
|
||||
# Only show full response dumps if VERBOSE_LOGGING is set
|
||||
if [ "${VERBOSE_LOGGING:-0}" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "🔍 DUMPING COMPLETE RESPONSE FOR: $url"
|
||||
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||
curl -s -H "Authorization: Bearer $API_TOKEN" "$url"
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
else
|
||||
# In non-verbose mode, just do the curl but don't show output
|
||||
curl -s -H "Authorization: Bearer $API_TOKEN" "$url" > /dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Initial test to show raw API response structure for system endpoint
|
||||
test_dump_system_response() {
|
||||
# If verbose logging is not enabled, skip this test
|
||||
if [ "${VERBOSE_LOGGING:-0}" -ne 1 ]; then
|
||||
#echo "Skipping raw response dump (enable with VERBOSE_LOGGING=1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local id="30000142" # Jita
|
||||
echo "Getting complete raw API response for system ID $id..."
|
||||
dump_complete_response "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$id"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Helper function to add element to space-delimited string list
|
||||
add_to_list() {
|
||||
local list="$1"
|
||||
local item="$2"
|
||||
if [ -z "$list" ]; then
|
||||
echo "$item"
|
||||
else
|
||||
echo "$list $item"
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to count items in a space-delimited list
|
||||
count_items() {
|
||||
local list="$1"
|
||||
if [ -z "$list" ]; then
|
||||
echo "0"
|
||||
else
|
||||
echo "$list" | wc -w
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse JSON response with error handling
|
||||
parse_response() {
|
||||
local raw="$1"
|
||||
|
||||
# Skip HTTP headers and get the JSON body
|
||||
local json_body=$(echo "$raw" | sed '1,/^\s*$/d')
|
||||
|
||||
# If JSON is valid, return it. Otherwise, return empty object
|
||||
if echo "$json_body" | jq . >/dev/null 2>&1; then
|
||||
echo "$json_body"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get and display detailed system information including visibility
|
||||
fetch_system_details() {
|
||||
local system_id=$1
|
||||
local verbose=${2:-0} # Default to non-verbose mode
|
||||
|
||||
# Skip detailed output in quiet mode
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Fetching system details for ID $system_id..."
|
||||
fi
|
||||
|
||||
# Get the complete raw response
|
||||
local raw
|
||||
raw=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id")
|
||||
|
||||
# Only show raw response in verbose mode
|
||||
if [ "$verbose" -eq 1 ] && [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Raw response from curl:"
|
||||
echo "$raw" | jq '.' 2>/dev/null || echo "$raw"
|
||||
fi
|
||||
|
||||
# Extract key information
|
||||
local name=""
|
||||
local visible=""
|
||||
|
||||
# First attempt to extract from data wrapper
|
||||
if echo "$raw" | jq -e '.data' >/dev/null 2>&1; then
|
||||
name=$(echo "$raw" | jq -r '.data.name // .data.solar_system_name // ""')
|
||||
visible=$(echo "$raw" | jq -r '.data.visible // ""')
|
||||
else
|
||||
# Use grep as a last resort
|
||||
if echo "$raw" | grep -q '"visible":true'; then
|
||||
visible="true"
|
||||
elif echo "$raw" | grep -q '"visible":false'; then
|
||||
visible="false"
|
||||
fi
|
||||
|
||||
if echo "$raw" | grep -q '"name":"[^"]*"'; then
|
||||
name=$(echo "$raw" | grep -o '"name":"[^"]*"' | head -1 | cut -d':' -f2 | tr -d '"')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show results only if not in quiet mode
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "SYSTEM NAME: $name"
|
||||
echo "VISIBILITY: $visible"
|
||||
fi
|
||||
|
||||
# Return success if we found both name and visibility
|
||||
if [ ! -z "$name" ] && [ ! -z "$visible" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_direct_api_access() {
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
[[ "$status" =~ ^2[0-9]{2}$ ]]
|
||||
}
|
||||
|
||||
test_missing_params() {
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems")
|
||||
status=$(parse_status "$raw")
|
||||
[[ "$status" =~ ^4[0-9]{2}$ ]]
|
||||
}
|
||||
|
||||
test_invalid_auth() {
|
||||
local old="$API_TOKEN" raw status
|
||||
API_TOKEN="invalid-token"
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
API_TOKEN="$old"
|
||||
[[ "$status" == "401" || "$status" == "403" ]]
|
||||
}
|
||||
|
||||
test_invalid_slug() {
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=nonexistent")
|
||||
status=$(parse_status "$raw")
|
||||
[[ "$status" =~ ^4[0-9]{2}$ ]]
|
||||
}
|
||||
|
||||
# Create and then show systems for legacy API
|
||||
test_show_systems() {
|
||||
# Use two well-known systems (use actual EVE IDs for clarity)
|
||||
local jita_id=30000142 # Jita
|
||||
local amarr_id=30002187 # Amarr
|
||||
local success_count=0
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Creating and verifying systems: Jita and Amarr"
|
||||
fi
|
||||
|
||||
# Create first system - Jita with coordinates
|
||||
local payload raw status response
|
||||
payload=$(jq -n \
|
||||
--argjson sid "$jita_id" \
|
||||
--argjson visible true \
|
||||
'{solar_system_id:$sid,solar_system_name:"Jita",coordinates:{"x":100,"y":200},visible:$visible}')
|
||||
|
||||
# Create the system using the RESTful API
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" == "201" || "$status" == "200" ]]; then
|
||||
success_count=$((success_count + 1))
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$jita_id")
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Created Jita system (ID: $jita_id)"
|
||||
echo "Verifying system $jita_id is visible after creation..."
|
||||
fi
|
||||
|
||||
# Allow a moment for system to be registered
|
||||
sleep 1
|
||||
|
||||
# Verify the system is visible
|
||||
fetch_system_details "$jita_id"
|
||||
else
|
||||
echo "Warning: Couldn't create Jita system, status: $status"
|
||||
fi
|
||||
|
||||
# Create second system - Amarr with coordinates
|
||||
payload=$(jq -n \
|
||||
--argjson sid "$amarr_id" \
|
||||
--argjson visible true \
|
||||
'{solar_system_id:$sid,solar_system_name:"Amarr",coordinates:{"x":300,"y":400},visible:$visible}')
|
||||
|
||||
# Create the system using the RESTful API
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" == "201" || "$status" == "200" ]]; then
|
||||
success_count=$((success_count + 1))
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$amarr_id")
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Created Amarr system (ID: $amarr_id)"
|
||||
echo "Verifying system $amarr_id is visible after creation..."
|
||||
fi
|
||||
|
||||
# Allow a moment for system to be registered
|
||||
sleep 1
|
||||
|
||||
# Verify the system is visible
|
||||
fetch_system_details "$amarr_id"
|
||||
else
|
||||
echo "Warning: Couldn't create Amarr system, status: $status"
|
||||
fi
|
||||
|
||||
# If we couldn't create any systems, test fails
|
||||
if [ $success_count -eq 0 ]; then
|
||||
echo "Couldn't create any test systems for legacy API"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify systems are in the list API
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Checking if systems appear in the list API after creation..."
|
||||
fi
|
||||
|
||||
raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
status=$(parse_status "$raw")
|
||||
response_body=$(echo "$raw" | sed '1,/^\s*$/d')
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
# Parse the response appropriately depending on structure
|
||||
local data_array=""
|
||||
|
||||
# Check if the response has data array structure
|
||||
if echo "$response_body" | jq -e '.data' >/dev/null 2>&1; then
|
||||
data_array=$(echo "$response_body" | jq '.data')
|
||||
else
|
||||
data_array="$response_body"
|
||||
fi
|
||||
|
||||
# Check each created system
|
||||
local all_systems_in_list=true
|
||||
for sid in $CREATED_SYSTEM_IDS; do
|
||||
if echo "$data_array" | jq -e ".[] | select(.solar_system_id == $sid)" >/dev/null 2>&1; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ System $sid appears in list API after creation"
|
||||
fi
|
||||
else
|
||||
all_systems_in_list=false
|
||||
echo "⚠ WARNING: System $sid does not appear in list API after creation"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "ERROR: Failed to get systems list: status $status"
|
||||
fi
|
||||
|
||||
# Now test the legacy API endpoint for each created system
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Verifying systems are accessible via legacy API..."
|
||||
fi
|
||||
|
||||
local legacy_success=true
|
||||
|
||||
for sid in $CREATED_SYSTEM_IDS; do
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/system?id=$sid&slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ ! "$status" =~ ^2[0-9]{2}$ ]]; then
|
||||
echo "Failed to retrieve system $sid via legacy API: status $status"
|
||||
legacy_success=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$legacy_success" = "true" ] && [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ All systems accessible via legacy API"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
test_verify_connections() {
|
||||
# Even if we don't have systems, we can still test the legacy connections API endpoint
|
||||
# by checking that it returns a valid response
|
||||
local raw status response
|
||||
|
||||
# Try to check all connections via legacy API
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/connections?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
# If the endpoint exists and returns a success status, the test passes
|
||||
if [[ "$status" =~ ^2[0-9]{2}$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
test_delete_systems() {
|
||||
# If we don't have system IDs, skip the test
|
||||
if [ $(count_items "$CREATED_SYSTEM_IDS") -eq 0 ]; then
|
||||
echo "No systems to delete, skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local success_count=0
|
||||
local total_systems=$(count_items "$CREATED_SYSTEM_IDS")
|
||||
local deleted_ids=""
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "TEST: Delete Systems API"
|
||||
echo "------------------------"
|
||||
echo "Testing system deletion for existing systems in map $MAP_SLUG"
|
||||
echo "Systems to delete: $CREATED_SYSTEM_IDS"
|
||||
fi
|
||||
|
||||
# Try batch delete first
|
||||
if [ $(count_items "$CREATED_SYSTEM_IDS") -gt 1 ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Attempting batch delete of systems: $CREATED_SYSTEM_IDS"
|
||||
fi
|
||||
|
||||
local payload=$(echo "$CREATED_SYSTEM_IDS" | tr ' ' '\n' | jq -R . | jq -s '{system_ids: .}')
|
||||
local raw status
|
||||
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems/batch_delete" "$payload")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Batch delete successful"
|
||||
fi
|
||||
|
||||
# Verify systems are gone from the list
|
||||
sleep 1
|
||||
local list_response
|
||||
list_response=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
|
||||
# Check if all systems are gone
|
||||
local all_deleted=1
|
||||
for system_id in $CREATED_SYSTEM_IDS; do
|
||||
if echo "$list_response" | jq -e --arg id "$system_id" '.data[] | select(.solar_system_id == ($id|tonumber) and .visible == true)' >/dev/null 2>&1; then
|
||||
all_deleted=0
|
||||
else
|
||||
success_count=$((success_count + 1))
|
||||
deleted_ids=$(add_to_list "$deleted_ids" "$system_id")
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ System $system_id no longer visible in list API after batch deletion"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $all_deleted -eq 1 ]; then
|
||||
# Update the list of created systems to remove successfully deleted ones
|
||||
for id in $deleted_ids; do
|
||||
CREATED_SYSTEM_IDS=$(echo "$CREATED_SYSTEM_IDS" | sed "s/\b$id\b//g" | tr -s ' ' | sed 's/^ //g' | sed 's/ $//g')
|
||||
done
|
||||
|
||||
# If batch delete worked for all systems, we're done
|
||||
if [ $success_count -eq $total_systems ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✅ All systems successfully deleted via batch delete"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Batch delete failed with status $status, trying individual deletes"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If batch delete didn't work, try individual deletes
|
||||
for system_id in $CREATED_SYSTEM_IDS; do
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Attempting to delete system with ID: $system_id"
|
||||
fi
|
||||
|
||||
local raw status
|
||||
|
||||
# Use the RESTful DELETE endpoint
|
||||
raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Delete API call successful for system $system_id"
|
||||
fi
|
||||
|
||||
# Allow time for change to propagate
|
||||
sleep 1
|
||||
|
||||
# Get the complete system list after deletion
|
||||
local list_response
|
||||
list_response=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
|
||||
# Check if the system appears in the list (deleted systems shouldn't appear or should be invisible)
|
||||
local system_still_visible=0
|
||||
|
||||
if echo "$list_response" | jq -e --arg id "$system_id" '.data[] | select(.solar_system_id == ($id|tonumber) and .visible == true)' >/dev/null 2>&1; then
|
||||
system_still_visible=1
|
||||
fi
|
||||
|
||||
if [ $system_still_visible -eq 0 ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ System $system_id no longer visible in list API after deletion"
|
||||
fi
|
||||
success_count=$((success_count + 1))
|
||||
deleted_ids=$(add_to_list "$deleted_ids" "$system_id")
|
||||
fi
|
||||
else
|
||||
echo "❌ Failed to delete system $system_id: status $status"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update the list of created systems to remove successfully deleted ones
|
||||
for id in $deleted_ids; do
|
||||
CREATED_SYSTEM_IDS=$(echo "$CREATED_SYSTEM_IDS" | sed "s/\b$id\b//g" | tr -s ' ' | sed 's/^ //g' | sed 's/ $//g')
|
||||
done
|
||||
|
||||
# Report results
|
||||
if [ $success_count -eq $total_systems ]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✅ All systems successfully deleted (no longer visible in list API): $success_count / $total_systems"
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
echo "⚠ Some systems still appear visible in list API after deletion: $success_count / $total_systems deleted"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test the system list API endpoint
|
||||
test_system_list() {
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Testing system list API endpoint..."
|
||||
fi
|
||||
|
||||
local raw status
|
||||
raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "ERROR: Failed to get system list: status $status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test legacy system list endpoint too
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "Testing legacy system list API endpoint..."
|
||||
fi
|
||||
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "ERROR: Failed to get legacy system list: status $status"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check that both APIs return the same number of systems
|
||||
local restful_count=$(echo "$raw" | sed '1,/^\s*$/d' | jq '.data | length // length')
|
||||
raw=$(make_request GET "$API_BASE_URL/api/map/systems?slug=$MAP_SLUG")
|
||||
local legacy_count=$(echo "$raw" | sed '1,/^\s*$/d' | jq '.data | length // length')
|
||||
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "RESTful API returned $restful_count systems, Legacy API returned $legacy_count systems"
|
||||
fi
|
||||
|
||||
if [[ "$restful_count" == "$legacy_count" ]]; then
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo "✓ Both APIs return the same number of systems"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: APIs return different numbers of systems"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Execute Tests ────────────────────────────────────────────────────────────
|
||||
# Function to run a test and report success/failure
|
||||
run_test() {
|
||||
local name="$1"
|
||||
local func="$2"
|
||||
|
||||
# Only print test name if not in quiet mode
|
||||
if [ "${QUIET_MODE:-0}" -ne 1 ]; then
|
||||
echo -n "Testing: $name... "
|
||||
fi
|
||||
|
||||
# Run the test function
|
||||
if $func; then
|
||||
echo "✅ $name"
|
||||
return 0
|
||||
else
|
||||
echo "❌ $name"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_test "Dump Raw API Response" test_dump_system_response
|
||||
run_test "Direct API access" test_direct_api_access
|
||||
run_test "Missing params (4xx)" test_missing_params
|
||||
run_test "Invalid auth (401/403)" test_invalid_auth
|
||||
run_test "Invalid slug on GET" test_invalid_slug
|
||||
run_test "Show systems" test_show_systems
|
||||
run_test "System list" test_system_list
|
||||
run_test "Verify connections" test_verify_connections
|
||||
run_test "Delete systems" test_delete_systems
|
||||
682
test/manual/api/system_api_tests.sh
Executable file
682
test/manual/api/system_api_tests.sh
Executable file
@@ -0,0 +1,682 @@
|
||||
#!/bin/bash
|
||||
# test/manual/api/improved_api_tests.sh
|
||||
# ─── Improved API Tests for Map System and Connection APIs ────────────────────────
|
||||
#
|
||||
# Usage:
|
||||
# ./improved_api_tests.sh # Run all tests with menu selection
|
||||
# ./improved_api_tests.sh create # Run only creation tests
|
||||
# ./improved_api_tests.sh update # Run only update tests
|
||||
# ./improved_api_tests.sh delete # Run only deletion tests
|
||||
# ./improved_api_tests.sh -v # Run in verbose mode
|
||||
#
|
||||
source "$(dirname "$0")/utils.sh"
|
||||
|
||||
# Set to "true" to see detailed output, "false" for minimal output
|
||||
VERBOSE=${VERBOSE:-false}
|
||||
|
||||
# Parse command line options
|
||||
while getopts "vh" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
VERBOSE=true
|
||||
;;
|
||||
h)
|
||||
echo "Usage: $0 [-v] [-h] [all|create|update|delete]"
|
||||
echo " -v Verbose mode (show detailed test output)"
|
||||
echo " -h Show this help message"
|
||||
echo " all Run all tests (default with menu)"
|
||||
echo " create Run only creation tests"
|
||||
echo " update Run only update tests"
|
||||
echo " delete Run only deletion tests"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
COMMAND=${1:-"all"}
|
||||
|
||||
# File to store system and connection IDs for persistence between command runs
|
||||
SYSTEMS_FILE="/tmp/wanderer_test_systems.txt"
|
||||
CONNECTIONS_FILE="/tmp/wanderer_test_connections.txt"
|
||||
|
||||
# Track created IDs for cleanup
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
|
||||
# Array of valid EVE system IDs and names (first 5 for individual creation)
|
||||
declare -a EVE_SYSTEMS=(
|
||||
"30005304:Alentene"
|
||||
"30003380:Alf"
|
||||
"30003811:Algasienan"
|
||||
"30004972:Algogille"
|
||||
"30002698:Aliette"
|
||||
)
|
||||
|
||||
# Next 5 for batch upsert
|
||||
declare -a BATCH_EVE_SYSTEMS=(
|
||||
"30002754:Alikara"
|
||||
"30002712:Alillere"
|
||||
"30003521:Alkabsi"
|
||||
"30000034:Alkez"
|
||||
"30004995:Allamotte"
|
||||
)
|
||||
|
||||
# ─── UTILITY FUNCTIONS ─────────────────────────────────────────────────────
|
||||
|
||||
# Function to save created system IDs to file
|
||||
save_systems() {
|
||||
echo "$CREATED_SYSTEM_IDS" > "$SYSTEMS_FILE"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Saved $(wc -w < "$SYSTEMS_FILE") systems to $SYSTEMS_FILE"
|
||||
}
|
||||
|
||||
# Function to load system IDs from file
|
||||
load_systems() {
|
||||
if [ -f "$SYSTEMS_FILE" ]; then
|
||||
CREATED_SYSTEM_IDS=$(cat "$SYSTEMS_FILE")
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Loaded $(wc -w < "$SYSTEMS_FILE") systems from $SYSTEMS_FILE"
|
||||
else
|
||||
echo "No systems file found at $SYSTEMS_FILE. Run creation tests first."
|
||||
CREATED_SYSTEM_IDS=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to save created connection IDs to file
|
||||
save_connections() {
|
||||
echo "$CREATED_CONNECTION_IDS" > "$CONNECTIONS_FILE"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Saved $(wc -w < "$CONNECTIONS_FILE") connections to $CONNECTIONS_FILE"
|
||||
}
|
||||
|
||||
# Function to load connection IDs from file
|
||||
load_connections() {
|
||||
if [ -f "$CONNECTIONS_FILE" ]; then
|
||||
CREATED_CONNECTION_IDS=$(cat "$CONNECTIONS_FILE")
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Loaded $(wc -w < "$CONNECTIONS_FILE") connections from $CONNECTIONS_FILE"
|
||||
else
|
||||
echo "No connections file found at $CONNECTIONS_FILE. Run creation tests first."
|
||||
CREATED_CONNECTION_IDS=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to add item to space-delimited list
|
||||
add_to_list() {
|
||||
local list="$1"
|
||||
local item="$2"
|
||||
if [ -z "$list" ]; then
|
||||
echo "$item"
|
||||
else
|
||||
echo "$list $item"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── TEST FUNCTIONS ─────────────────────────────────────────────────────
|
||||
|
||||
# FUNCTION: Create systems
|
||||
create_systems() {
|
||||
echo "==== Creating Systems ===="
|
||||
local system_count=0
|
||||
local center_x=500
|
||||
local center_y=500
|
||||
local radius=250
|
||||
|
||||
# Only clear the systems file if we're starting fresh
|
||||
> "$SYSTEMS_FILE"
|
||||
CREATED_SYSTEM_IDS=""
|
||||
|
||||
# Build all system payloads as a JSON array
|
||||
local systems_payload="["
|
||||
local num_systems=${#EVE_SYSTEMS[@]}
|
||||
for i in $(seq 0 $((num_systems-1))); do
|
||||
IFS=':' read -r system_id system_name <<< "${EVE_SYSTEMS[$i]}"
|
||||
local angle=$(echo "scale=6; $i * 6.28318 / $num_systems" | bc -l)
|
||||
local x=$(echo "scale=2; $center_x + $radius * c($angle)" | bc -l)
|
||||
local y=$(echo "scale=2; $center_y + $radius * s($angle)" | bc -l)
|
||||
local system_json=$(jq -n \
|
||||
--argjson sid "$system_id" \
|
||||
--arg name "$system_name" \
|
||||
--argjson x "$x" \
|
||||
--argjson y "$y" \
|
||||
'{
|
||||
solar_system_id: $sid,
|
||||
solar_system_name: $name,
|
||||
position_x: $x,
|
||||
position_y: $y,
|
||||
status: "clear",
|
||||
visible: true,
|
||||
description: "Test system",
|
||||
tag: "TEST",
|
||||
locked: false
|
||||
}')
|
||||
systems_payload+="$system_json"
|
||||
if [ $i -lt $((num_systems-1)) ]; then
|
||||
systems_payload+=","
|
||||
fi
|
||||
done
|
||||
systems_payload+="]"
|
||||
|
||||
# Wrap in the 'systems' key
|
||||
local payload="{\"systems\": $systems_payload}"
|
||||
|
||||
# Send the batch create request
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Created all systems in batch"
|
||||
# Track the system IDs for later cleanup
|
||||
for i in $(seq 0 $((num_systems-1))); do
|
||||
IFS=':' read -r system_id _ <<< "${EVE_SYSTEMS[$i]}"
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$system_id")
|
||||
system_count=$((system_count+1))
|
||||
done
|
||||
else
|
||||
echo "❌ Failed to create systems in batch. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
|
||||
echo "Total systems created: $system_count/$num_systems"
|
||||
save_systems
|
||||
|
||||
# Validate actual state after creation
|
||||
echo "Validating systems after dedicated creation:"
|
||||
list_systems_and_connections
|
||||
}
|
||||
|
||||
# FUNCTION: Create connections
|
||||
create_connections() {
|
||||
echo "==== Creating Connections ===="
|
||||
load_systems
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "No systems available. Run system creation first."
|
||||
return 1
|
||||
fi
|
||||
> "$CONNECTIONS_FILE"
|
||||
CREATED_CONNECTION_IDS=""
|
||||
local connection_count=0
|
||||
local total_connections=0
|
||||
local system_array=($CREATED_SYSTEM_IDS)
|
||||
|
||||
echo "Testing dedicated connection endpoints..."
|
||||
# Create connections one by one using the dedicated endpoint
|
||||
for i in $(seq 0 $((${#system_array[@]}-1))); do
|
||||
local source=${system_array[$i]}
|
||||
local target=${system_array[$(( (i+1) % ${#system_array[@]} ))]}
|
||||
total_connections=$((total_connections+1))
|
||||
|
||||
# Create single connection payload
|
||||
local payload=$(jq -n \
|
||||
--argjson source "$source" \
|
||||
--argjson target "$target" \
|
||||
'{
|
||||
solar_system_source: $source,
|
||||
solar_system_target: $target,
|
||||
type: 0,
|
||||
mass_status: 0,
|
||||
time_status: 0,
|
||||
ship_size_type: 1,
|
||||
wormhole_type: "K162",
|
||||
count_of_passage: 0
|
||||
}')
|
||||
|
||||
# Send create request to dedicated endpoint
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/connections" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Created connection from $source to $target"
|
||||
local response=$(parse_response "$raw")
|
||||
# Store source and target for later use
|
||||
CREATED_CONNECTION_IDS=$(add_to_list "$CREATED_CONNECTION_IDS" "${source}:${target}")
|
||||
connection_count=$((connection_count+1))
|
||||
else
|
||||
echo "❌ Failed to create connection from $source to $target. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total connections created via dedicated endpoint: $connection_count/$total_connections"
|
||||
save_connections
|
||||
|
||||
# Always validate actual state after connection creation
|
||||
echo "Validating connections after dedicated creation:"
|
||||
list_systems_and_connections
|
||||
|
||||
echo -e "\nTesting batch upsert functionality..."
|
||||
# Build batch upsert payload using BATCH_EVE_SYSTEMS
|
||||
local batch_systems_json="["
|
||||
local batch_connections_json="["
|
||||
local num_batch_systems=${#BATCH_EVE_SYSTEMS[@]}
|
||||
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||
IFS=':' read -r system_id system_name <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||
local angle=$(echo "scale=6; $i * 6.28318 / $num_batch_systems" | bc -l)
|
||||
local x=$(echo "scale=2; 500 + 250 * c($angle)" | bc -l)
|
||||
local y=$(echo "scale=2; 500 + 250 * s($angle)" | bc -l)
|
||||
local system_json=$(jq -n \
|
||||
--argjson sid "$system_id" \
|
||||
--arg name "$system_name" \
|
||||
--argjson x "$x" \
|
||||
--argjson y "$y" \
|
||||
'{
|
||||
solar_system_id: $sid,
|
||||
solar_system_name: $name,
|
||||
position_x: $x,
|
||||
position_y: $y,
|
||||
status: "clear",
|
||||
visible: true,
|
||||
description: "Test system (batch)",
|
||||
tag: "BATCH",
|
||||
locked: false
|
||||
}')
|
||||
batch_systems_json+="$system_json"
|
||||
if [ $i -lt $((num_batch_systems-1)) ]; then
|
||||
batch_systems_json+=","
|
||||
fi
|
||||
# Build connections in a ring
|
||||
local source=$system_id
|
||||
local next_index=$(( (i+1) % num_batch_systems ))
|
||||
IFS=':' read -r target_id _ <<< "${BATCH_EVE_SYSTEMS[$next_index]}"
|
||||
batch_connections_json+="{\"solar_system_source\":$source,\"solar_system_target\":$target_id,\"mass_status\":0,\"ship_size_type\":1,\"type\":0}"
|
||||
if [ $i -lt $((num_batch_systems-1)) ]; then
|
||||
batch_connections_json+=","
|
||||
fi
|
||||
done
|
||||
batch_systems_json+="]"
|
||||
batch_connections_json+="]"
|
||||
|
||||
echo "[SCRIPT] Batch upsert systems: $batch_systems_json"
|
||||
echo "[SCRIPT] Batch upsert connections: $batch_connections_json"
|
||||
|
||||
# Check for API_TOKEN
|
||||
if [ -z "$API_TOKEN" ]; then
|
||||
echo "❌ API_TOKEN is not set. Please export API_TOKEN before running the script."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Send batch upsert request
|
||||
local response=$(curl -s -X POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-d "{\"systems\":$batch_systems_json,\"connections\":$batch_connections_json}")
|
||||
|
||||
echo "[SCRIPT] Batch upsert response: $response"
|
||||
|
||||
# Debug: List all connections after batch upsert
|
||||
echo "[SCRIPT] Listing all connections after batch upsert:"
|
||||
local list_raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local list_status=$(parse_status "$list_raw")
|
||||
if [[ "$list_status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local list_response=$(parse_response "$list_raw")
|
||||
echo "$list_response" | jq -c '.data.connections[] | {id: .id, source: .solar_system_source, target: .solar_system_target, mass_status: .mass_status, ship_size_type: .ship_size_type, type: .type}'
|
||||
else
|
||||
echo "[SCRIPT] Failed to list connections after batch upsert. Status: $list_status"
|
||||
fi
|
||||
|
||||
# Add batch system IDs to CREATED_SYSTEM_IDS
|
||||
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||
IFS=':' read -r system_id _ <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||
CREATED_SYSTEM_IDS=$(add_to_list "$CREATED_SYSTEM_IDS" "$system_id")
|
||||
done
|
||||
|
||||
# Add batch connection pairs to CREATED_CONNECTION_IDS
|
||||
for i in $(seq 0 $((num_batch_systems-1))); do
|
||||
IFS=':' read -r source _ <<< "${BATCH_EVE_SYSTEMS[$i]}"
|
||||
next_index=$(( (i+1) % num_batch_systems ))
|
||||
IFS=':' read -r target _ <<< "${BATCH_EVE_SYSTEMS[$next_index]}"
|
||||
CREATED_CONNECTION_IDS=$(add_to_list "$CREATED_CONNECTION_IDS" "${source}:${target}")
|
||||
done
|
||||
save_systems
|
||||
save_connections
|
||||
|
||||
list_systems_and_connections
|
||||
|
||||
echo "Total connections updated: $connection_count/${#system_array[@]}"
|
||||
}
|
||||
|
||||
# FUNCTION: Update systems
|
||||
update_systems() {
|
||||
echo "==== Updating Systems ===="
|
||||
load_systems
|
||||
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "No systems available. Run system creation first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local update_count=0
|
||||
local system_array=($CREATED_SYSTEM_IDS)
|
||||
local num_systems=${#system_array[@]}
|
||||
|
||||
for i in $(seq 0 $((num_systems-1))); do
|
||||
local system_id=${system_array[$i]}
|
||||
|
||||
# Get system name from EVE_SYSTEMS array if available
|
||||
local system_name="System $system_id"
|
||||
for j in $(seq 0 $((${#EVE_SYSTEMS[@]}-1))); do
|
||||
IFS=':' read -r curr_id curr_name <<< "${EVE_SYSTEMS[$j]}"
|
||||
if [ "$curr_id" = "$system_id" ]; then
|
||||
system_name=$curr_name
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Updating system $((i+1))/$num_systems: $system_name (ID: $system_id)"
|
||||
|
||||
# Create update payload with new values
|
||||
local status_values=("clear" "friendly" "hostile" "occupied")
|
||||
local status=${status_values[$((RANDOM % 4))]}
|
||||
local desc="Updated description for $system_name"
|
||||
local tag="UPDATED"
|
||||
|
||||
local payload=$(jq -n \
|
||||
--arg status "$status" \
|
||||
--arg desc "$desc" \
|
||||
--arg tag "$tag" \
|
||||
'{
|
||||
status: $status,
|
||||
description: $desc,
|
||||
tag: $tag,
|
||||
locked: false
|
||||
}')
|
||||
|
||||
# Send the update request
|
||||
local raw=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$system_id" "$payload")
|
||||
local status_code=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated system $system_name with status: $status"
|
||||
update_count=$((update_count+1))
|
||||
else
|
||||
echo "❌ Failed to update system $system_name. Status: $status_code"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total systems updated: $update_count/$num_systems"
|
||||
}
|
||||
|
||||
# FUNCTION: Update connections
|
||||
update_connections() {
|
||||
echo "==== Updating Connections ===="
|
||||
load_systems
|
||||
load_connections
|
||||
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ] || [ -z "$CREATED_CONNECTION_IDS" ]; then
|
||||
echo "No systems or connections available. Run creation tests first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Testing connection updates..."
|
||||
local update_count=0
|
||||
local conn_array=($CREATED_CONNECTION_IDS)
|
||||
|
||||
for triple in "${conn_array[@]}"; do
|
||||
local source=$(echo $triple | cut -d: -f1)
|
||||
local target=$(echo $triple | cut -d: -f2)
|
||||
|
||||
# Create update payload
|
||||
local mass_values=(0 1 2)
|
||||
local ship_values=(0 1 2 3)
|
||||
local mass=${mass_values[$((RANDOM % 3))]}
|
||||
local ship=${ship_values[$((RANDOM % 4))]}
|
||||
local payload=$(jq -n \
|
||||
--argjson mass "$mass" \
|
||||
--argjson ship "$ship" \
|
||||
'{
|
||||
mass_status: $mass,
|
||||
ship_size_type: $ship
|
||||
}')
|
||||
|
||||
# Try source/target update
|
||||
local raw=$(make_request PATCH "$API_BASE_URL/api/maps/$MAP_SLUG/connections?solar_system_source=$source&solar_system_target=$target" "$payload")
|
||||
local status_code=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Updated connection $source->$target"
|
||||
update_count=$((update_count+1))
|
||||
else
|
||||
echo "❌ Failed to update connection $source->$target. Status: $status_code"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total connections updated: $update_count/${#conn_array[@]}"
|
||||
|
||||
echo -e "\nTesting batch connection updates..."
|
||||
# Create batch update payload for all connections
|
||||
local batch_connections="["
|
||||
local first=true
|
||||
for triple in "${conn_array[@]}"; do
|
||||
local source=$(echo $triple | cut -d: -f1)
|
||||
local target=$(echo $triple | cut -d: -f2)
|
||||
|
||||
local mass=${mass_values[$((RANDOM % 3))]}
|
||||
local ship=${ship_values[$((RANDOM % 4))]}
|
||||
|
||||
if [ "$first" = true ]; then
|
||||
first=false
|
||||
else
|
||||
batch_connections+=","
|
||||
fi
|
||||
|
||||
batch_connections+=$(jq -n \
|
||||
--argjson source "$source" \
|
||||
--argjson target "$target" \
|
||||
--argjson mass "$mass" \
|
||||
--argjson ship "$ship" \
|
||||
'{
|
||||
solar_system_source: $source,
|
||||
solar_system_target: $target,
|
||||
mass_status: $mass,
|
||||
ship_size_type: $ship
|
||||
}')
|
||||
done
|
||||
batch_connections+="]"
|
||||
|
||||
local batch_payload="{\"connections\": $batch_connections}"
|
||||
local raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$batch_payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local updated_count=$(echo "$response" | jq '.data.connections.updated')
|
||||
if [ "$updated_count" != "null" ]; then
|
||||
echo "✅ Batch update successful - Updated connections: $updated_count"
|
||||
else
|
||||
echo "❌ Batch update returned null for updated count"
|
||||
fi
|
||||
else
|
||||
echo "❌ Batch update failed. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
}
|
||||
|
||||
# FUNCTION: List systems and connections
|
||||
list_systems_and_connections() {
|
||||
echo "==== Listing Systems and Connections ===="
|
||||
load_systems
|
||||
if [ -z "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "No systems available. Run system creation first."
|
||||
return 1
|
||||
fi
|
||||
echo "Testing list all systems and connections endpoint"
|
||||
local raw=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
local response=$(parse_response "$raw")
|
||||
local system_count=$(echo "$response" | jq '.data.systems | length')
|
||||
local conn_count=$(echo "$response" | jq '.data.connections | length')
|
||||
echo "✅ Listed $system_count systems and $conn_count connections"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "$response" | jq '.'
|
||||
return 0
|
||||
else
|
||||
echo "❌ Failed to list systems and connections. Status: $status"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# FUNCTION: Delete connections and systems
|
||||
delete_everything() {
|
||||
echo "==== Deleting Connections and Systems ===="
|
||||
load_connections
|
||||
load_systems
|
||||
|
||||
echo "Cleaning up connections..."
|
||||
# Delete connections using source/target pairs
|
||||
local conn_array=($CREATED_CONNECTION_IDS)
|
||||
for triple in "${conn_array[@]}"; do
|
||||
local source=$(echo $triple | cut -d: -f1)
|
||||
local target=$(echo $triple | cut -d: -f2)
|
||||
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/connections?solar_system_source=$source&solar_system_target=$target")
|
||||
local status=$(parse_status "$raw")
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Deleted connection $source->$target"
|
||||
else
|
||||
echo "❌ Failed to delete connection $source->$target. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Cleaning up systems..."
|
||||
# Use batch delete for systems
|
||||
local system_array=($CREATED_SYSTEM_IDS)
|
||||
echo "Attempting batch delete of systems..."
|
||||
echo "System ${system_array[@]}"
|
||||
|
||||
local system_ids_json=$(printf '%s\n' "${system_array[@]}" | jq -R . | jq -s .)
|
||||
local payload=$(jq -n --argjson system_ids "$system_ids_json" '{system_ids: $system_ids}')
|
||||
local raw=$(make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems" "$payload")
|
||||
local status=$(parse_status "$raw")
|
||||
|
||||
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "✅ Batch delete successful for all systems"
|
||||
> "$SYSTEMS_FILE"
|
||||
> "$CONNECTIONS_FILE"
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
else
|
||||
echo "❌ Batch delete failed. Status: $status"
|
||||
[[ "$VERBOSE" == "true" ]] && echo "Response: $(parse_response "$raw")"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── MENU AND INTERACTION LOGIC ─────────────────────────────────────────
|
||||
|
||||
show_menu() {
|
||||
echo "===== Map System and Connection API Tests ====="
|
||||
echo "1. Run all tests in sequence (with pauses)"
|
||||
echo "2. Create systems"
|
||||
echo "3. Create connections"
|
||||
echo "4. Update systems"
|
||||
echo "5. Update connections"
|
||||
echo "6. List systems and connections"
|
||||
echo "7. Delete everything"
|
||||
echo "8. Exit"
|
||||
echo "================================================"
|
||||
echo "Enter your choice [1-8]: "
|
||||
}
|
||||
|
||||
# ─── MAIN EXECUTION FLOW ─────────────────────────────────────────────────
|
||||
|
||||
# Main execution based on command
|
||||
case "$COMMAND" in
|
||||
"all")
|
||||
# If no specific command was provided, show the menu
|
||||
if [ -t 0 ]; then # Only show menu if running interactively
|
||||
# Interactive mode with menu
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
# Run all tests in sequence with pauses
|
||||
create_systems || echo "System creation failed/skipped"
|
||||
echo "Press Enter to continue with connection creation..."
|
||||
read -r
|
||||
|
||||
create_connections || echo "Connection creation failed/skipped"
|
||||
echo "Press Enter to continue with system updates..."
|
||||
read -r
|
||||
|
||||
update_systems || echo "System update failed/skipped"
|
||||
echo "Press Enter to continue with connection updates..."
|
||||
read -r
|
||||
|
||||
update_connections || echo "Connection update failed/skipped"
|
||||
echo "Press Enter to continue with listing tests..."
|
||||
read -r
|
||||
|
||||
list_systems_and_connections || echo "Listing failed/skipped"
|
||||
echo "Press Enter to continue with deletion..."
|
||||
read -r
|
||||
|
||||
delete_everything || echo "Cleanup failed/skipped"
|
||||
echo "All tests completed."
|
||||
;;
|
||||
2)
|
||||
create_systems
|
||||
;;
|
||||
3)
|
||||
create_connections
|
||||
;;
|
||||
4)
|
||||
update_systems
|
||||
;;
|
||||
5)
|
||||
update_connections
|
||||
;;
|
||||
6)
|
||||
list_systems_and_connections
|
||||
;;
|
||||
7)
|
||||
delete_everything
|
||||
;;
|
||||
8)
|
||||
# Offer to clean up before exiting
|
||||
read -p "Clean up any remaining test data before exiting? (y/n): " confirm
|
||||
if [[ "$confirm" =~ ^[Yy] ]]; then
|
||||
delete_everything
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option. Please try again."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
# Non-interactive mode, run all tests in sequence
|
||||
create_systems || echo "System creation failed/skipped"
|
||||
create_connections || echo "Connection creation failed/skipped"
|
||||
update_systems || echo "System update failed/skipped"
|
||||
update_connections || echo "Connection update failed/skipped"
|
||||
list_systems_and_connections || echo "Listing failed/skipped"
|
||||
delete_everything || echo "Cleanup failed/skipped"
|
||||
fi
|
||||
;;
|
||||
"create")
|
||||
create_systems
|
||||
create_connections
|
||||
;;
|
||||
"update")
|
||||
update_systems
|
||||
update_connections
|
||||
list_systems_and_connections
|
||||
;;
|
||||
"delete")
|
||||
delete_everything
|
||||
;;
|
||||
*)
|
||||
echo "Invalid command: $COMMAND"
|
||||
echo "Use -h for help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
167
test/manual/api/utils.sh
Executable file
167
test/manual/api/utils.sh
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
# ─── Dependencies ─────────────────────────────────────────────────────────────
|
||||
for cmd in curl jq; do
|
||||
if ! command -v "$cmd" > /dev/null 2>&1; then
|
||||
echo "Error: '$cmd' is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# ─── Load .env if present ─────────────────────────────────────────────────────
|
||||
load_env_file() {
|
||||
echo "📄 Loading env file: $1"
|
||||
set -o allexport
|
||||
source "$1"
|
||||
set +o allexport
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||
load_env_file "$SCRIPT_DIR/.env"
|
||||
fi
|
||||
|
||||
# Check if API_TOKEN is set
|
||||
: "${API_TOKEN:?Error: API_TOKEN environment variable not set}"
|
||||
|
||||
# ─── HTTP Request Helper ──────────────────────────────────────────────────────
|
||||
make_request() {
|
||||
local method=$1 url=$2 data=${3:-}
|
||||
local curl_cmd=(curl -s -w $'\n%{http_code}' -H "Authorization: Bearer $API_TOKEN")
|
||||
|
||||
if [ "$method" != "GET" ]; then
|
||||
curl_cmd+=(-X "$method" -H "Content-Type: application/json")
|
||||
fi
|
||||
|
||||
if [ -n "$data" ]; then
|
||||
curl_cmd+=(-d "$data")
|
||||
fi
|
||||
|
||||
"${curl_cmd[@]}" "$url"
|
||||
}
|
||||
|
||||
# ─── Response Parsers ─────────────────────────────────────────────────────────
|
||||
parse_response() { # strips the final newline+status line
|
||||
local raw="$1"
|
||||
echo "${raw%$'\n'*}"
|
||||
}
|
||||
|
||||
parse_status() { # returns only the status code (last line)
|
||||
local raw="$1"
|
||||
echo "${raw##*$'\n'}"
|
||||
}
|
||||
|
||||
# ─── Assertion Helper ─────────────────────────────────────────────────────────
|
||||
verify_http_code() {
|
||||
local got=$1 want=$2 label=$3
|
||||
if [ "$got" -eq "$want" ]; then
|
||||
return 0
|
||||
else
|
||||
echo "🚫 $label: expected HTTP $want, got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Test Runner & Summary ────────────────────────────────────────────────────
|
||||
# Only initialize counters once to accumulate across multiple suite sources
|
||||
if [ -z "${TOTAL_TESTS+x}" ]; then
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
FAILED_LIST=""
|
||||
fi
|
||||
|
||||
run_test() {
|
||||
local label=$1 fn=$2
|
||||
TOTAL_TESTS=$((TOTAL_TESTS+1))
|
||||
if "$fn"; then
|
||||
echo "✅ $label"
|
||||
PASSED_TESTS=$((PASSED_TESTS+1))
|
||||
else
|
||||
echo "❌ $label"
|
||||
FAILED_TESTS=$((FAILED_TESTS+1))
|
||||
FAILED_LIST="$FAILED_LIST $label"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Cleanup on Exit ──────────────────────────────────────────────────────────
|
||||
CREATED_SYSTEM_IDS=""
|
||||
CREATED_CONNECTION_IDS=""
|
||||
|
||||
cleanup_map_systems() {
|
||||
# First delete connections
|
||||
if [ -n "$CREATED_CONNECTION_IDS" ]; then
|
||||
echo "Cleaning up connections..."
|
||||
for conn_id in $CREATED_CONNECTION_IDS; do
|
||||
# Try with a direct DELETE request to the connection endpoint
|
||||
make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/connections/$conn_id" > /dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Then delete systems
|
||||
if [ -n "$CREATED_SYSTEM_IDS" ]; then
|
||||
echo "Cleaning up systems..."
|
||||
|
||||
# First try batch delete if we have multiple systems
|
||||
if [ $(echo "$CREATED_SYSTEM_IDS" | wc -w) -gt 1 ]; then
|
||||
echo "Attempting batch delete of systems..."
|
||||
|
||||
# Use the official batch_delete endpoint
|
||||
local payload=$(echo "$CREATED_SYSTEM_IDS" | tr ' ' '\n' | jq -R . | jq -s '{system_ids: .}')
|
||||
local raw
|
||||
raw=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/systems/batch_delete" "$payload" 2>/dev/null) || true
|
||||
|
||||
# Check if batch delete was successful by looking for systems
|
||||
sleep 1
|
||||
local success=1
|
||||
|
||||
for sys_id in $CREATED_SYSTEM_IDS; do
|
||||
# Check if system still exists and is visible
|
||||
local check=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id"; then
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id.*\"visible\":true"; then
|
||||
success=0
|
||||
else
|
||||
echo "System $sys_id exists but is not visible (batch delete worked)"
|
||||
fi
|
||||
else
|
||||
echo "System $sys_id no longer found (batch delete worked)"
|
||||
fi
|
||||
done
|
||||
|
||||
# If batch delete was successful for all systems, we're done
|
||||
if [ $success -eq 1 ]; then
|
||||
echo "✅ Batch delete successful for all systems"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# If batch delete failed or we have only one system, try individual deletes
|
||||
echo "Performing individual system deletions..."
|
||||
|
||||
for sys_id in $CREATED_SYSTEM_IDS; do
|
||||
echo "Deleting system $sys_id..."
|
||||
|
||||
# Try standard DELETE request
|
||||
make_request DELETE "$API_BASE_URL/api/maps/$MAP_SLUG/systems/$sys_id" > /dev/null 2>&1 || true
|
||||
|
||||
# Verify the system was deleted or at least made invisible
|
||||
sleep 1
|
||||
local check=$(curl -s -H "Authorization: Bearer $API_TOKEN" "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
|
||||
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id"; then
|
||||
if echo "$check" | grep -q "\"solar_system_id\":$sys_id.*\"visible\":true"; then
|
||||
echo "⚠️ System $sys_id is still visible after all deletion attempts"
|
||||
else
|
||||
echo "System $sys_id exists but is not visible (deletion worked)"
|
||||
fi
|
||||
else
|
||||
echo "System $sys_id no longer found (deletion worked)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
#trap cleanup_map_systems EXIT
|
||||
Reference in New Issue
Block a user