mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-10 01:35:33 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280a286266 | ||
|
|
d5c18b5de3 | ||
|
|
7452e5d011 | ||
|
|
71674b0d52 | ||
|
|
5b4824bd5d | ||
|
|
deda16a7da | ||
|
|
0b7c067de7 | ||
|
|
0d0db8c129 | ||
|
|
9f1b7994a3 | ||
|
|
378df0ac70 | ||
|
|
0e4a132f69 | ||
|
|
631746375d | ||
|
|
7dc01dad54 | ||
|
|
8a9807d3e5 | ||
|
|
39df3c97ce | ||
|
|
46c1ccdfcc | ||
|
|
8817536038 | ||
|
|
c3bb23a6ee | ||
|
|
7e9c4c575e | ||
|
|
5a70eee91e | ||
|
|
228f6990a1 | ||
|
|
d80ed0e70e | ||
|
|
4576c75737 | ||
|
|
67764faaa7 | ||
|
|
91dd0b27ae | ||
|
|
99dcf49fbc | ||
|
|
6fb3edbfd6 | ||
|
|
26f13ce857 | ||
|
|
e9b475c0a8 | ||
|
|
7752010092 | ||
|
|
d3705b3ed7 | ||
|
|
1394e2897e | ||
|
|
5117a1c5af | ||
|
|
3c62403f33 | ||
|
|
a4760f5162 | ||
|
|
b071070431 | ||
|
|
3bcb9628e7 | ||
|
|
e62c4cf5bf | ||
|
|
af46962ce4 | ||
|
|
0b0967830b | ||
|
|
172251a208 |
90
CHANGELOG.md
90
CHANGELOG.md
@@ -2,6 +2,96 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.77.6](https://github.com/wanderer-industries/wanderer/compare/v1.77.5...v1.77.6) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added grace period to reduce false-positive cases
|
||||
|
||||
## [v1.77.5](https://github.com/wanderer-industries/wanderer/compare/v1.77.4...v1.77.5) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* resolve tracking issues
|
||||
|
||||
## [v1.77.4](https://github.com/wanderer-industries/wanderer/compare/v1.77.3...v1.77.4) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* pr feedback
|
||||
|
||||
* ensure pub/sub occurs after acl api change
|
||||
|
||||
## [v1.77.3](https://github.com/wanderer-industries/wanderer/compare/v1.77.2...v1.77.3) (2025-08-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
## [v1.77.2](https://github.com/wanderer-industries/wanderer/compare/v1.77.1...v1.77.2) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* update system signature api to return correct system id
|
||||
|
||||
## [v1.77.1](https://github.com/wanderer-industries/wanderer/compare/v1.77.0...v1.77.1) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.77.0](https://github.com/wanderer-industries/wanderer/compare/v1.76.13...v1.77.0) (2025-08-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Reduced DB calls to check existing system jumps
|
||||
|
||||
## [v1.76.13](https://github.com/wanderer-industries/wanderer/compare/v1.76.12...v1.76.13) (2025-08-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed maps start timeout
|
||||
|
||||
## [v1.76.12](https://github.com/wanderer-industries/wanderer/compare/v1.76.11...v1.76.12) (2025-08-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Reduced ESI api calls to update character corp/ally info
|
||||
|
||||
## [v1.76.11](https://github.com/wanderer-industries/wanderer/compare/v1.76.10...v1.76.11) (2025-08-20)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.76.10](https://github.com/wanderer-industries/wanderer/compare/v1.76.9...v1.76.10) (2025-08-18)
|
||||
|
||||
|
||||
|
||||
@@ -10,13 +10,6 @@ import { renderName } from './renderName.tsx';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
|
||||
const renderSignatureNameOrGroup = (row: SystemSignature) => {
|
||||
if (row.name && row.name !== 'Unknown') {
|
||||
return renderName(row);
|
||||
}
|
||||
return row.group ? <span title={row.group}>{row.group}</span> : null;
|
||||
};
|
||||
|
||||
export const renderInfoColumn = (row: SystemSignature) => {
|
||||
if (!row.group || row.group === SignatureGroup.Wormhole) {
|
||||
const customInfo = parseSignatureCustomInfo(row.custom_info);
|
||||
@@ -25,8 +18,6 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-center gap-[4px]">
|
||||
{renderSignatureNameOrGroup(row)}
|
||||
|
||||
{customInfo.isEOL && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
@@ -74,7 +65,7 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
{renderSignatureNameOrGroup(row)}{' '}
|
||||
{renderName(row)}{' '}
|
||||
{row.description && (
|
||||
<WdTooltipWrapper content={row.description}>
|
||||
<span className={clsx(PrimeIcons.EXCLAMATION_CIRCLE, 'text-[12px]')}></span>
|
||||
|
||||
@@ -129,6 +129,8 @@ config :wanderer_app,
|
||||
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
|
||||
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
|
||||
admins: admins,
|
||||
base_metrics_only:
|
||||
System.get_env("WANDERER_BASE_METRICS_ONLY", "false") |> String.to_existing_atom(),
|
||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Api.Character do
|
||||
update :update_corporation do
|
||||
require_atomic? false
|
||||
|
||||
accept([:corporation_id, :corporation_name, :corporation_ticker, :alliance_id])
|
||||
accept([:corporation_id, :corporation_name, :corporation_ticker])
|
||||
end
|
||||
|
||||
update :update_alliance do
|
||||
|
||||
@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
accept [
|
||||
:map_id,
|
||||
:character_id,
|
||||
:tracked,
|
||||
:followed
|
||||
:tracked
|
||||
]
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
|
||||
@@ -54,6 +54,7 @@ defmodule WandererApp.Application do
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererAppWeb.PresenceGracePeriodManager,
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
|
||||
@@ -113,6 +113,63 @@ defmodule WandererApp.CachedInfo do
|
||||
end
|
||||
end
|
||||
|
||||
def get_solar_system_jumps() do
|
||||
case WandererApp.Cache.lookup(:solar_system_jumps) do
|
||||
{:ok, nil} ->
|
||||
data = WandererApp.EveDataService.get_solar_system_jumps_data()
|
||||
|
||||
cache_items(data, :solar_system_jumps)
|
||||
|
||||
{:ok, data}
|
||||
|
||||
{:ok, data} ->
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
||||
|
||||
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
# Create normalized cache key (smaller ID first for bidirectional lookup)
|
||||
{id1, id2} =
|
||||
if from_solar_system_id < to_solar_system_id do
|
||||
{from_solar_system_id, to_solar_system_id}
|
||||
else
|
||||
{to_solar_system_id, from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
|
||||
case WandererApp.Cache.lookup(cache_key) do
|
||||
{:ok, nil} ->
|
||||
# Build jump index if not exists
|
||||
build_jump_index()
|
||||
WandererApp.Cache.lookup(cache_key)
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp build_jump_index() do
|
||||
case get_solar_system_jumps() do
|
||||
{:ok, jumps} ->
|
||||
jumps
|
||||
|> Enum.each(fn jump ->
|
||||
{id1, id2} =
|
||||
if jump.from_solar_system_id < jump.to_solar_system_id do
|
||||
{jump.from_solar_system_id, jump.to_solar_system_id}
|
||||
else
|
||||
{jump.to_solar_system_id, jump.from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
WandererApp.Cache.put(cache_key, jump)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def get_wormhole_types!() do
|
||||
case get_wormhole_types() do
|
||||
{:ok, wormhole_types} ->
|
||||
|
||||
@@ -263,7 +263,7 @@ defmodule WandererApp.Character do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
|
||||
defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
|
||||
{:ok, tracking_paused} =
|
||||
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defstruct [
|
||||
:character_id,
|
||||
:alliance_id,
|
||||
:corporation_id,
|
||||
:opts,
|
||||
server_online: true,
|
||||
start_time: nil,
|
||||
@@ -21,6 +22,8 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
character_id: integer,
|
||||
alliance_id: integer,
|
||||
corporation_id: integer,
|
||||
opts: map,
|
||||
server_online: boolean,
|
||||
start_time: DateTime.t(),
|
||||
@@ -49,8 +52,15 @@ defmodule WandererApp.Character.Tracker do
|
||||
def new(args), do: __struct__(args)
|
||||
|
||||
def init(args) do
|
||||
character_id = args[:character_id]
|
||||
|
||||
{:ok, %{corporation_id: corporation_id, alliance_id: alliance_id}} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
%{
|
||||
character_id: args[:character_id],
|
||||
character_id: character_id,
|
||||
corporation_id: corporation_id,
|
||||
alliance_id: alliance_id,
|
||||
start_time: DateTime.utc_now(),
|
||||
opts: args
|
||||
}
|
||||
@@ -193,7 +203,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
access_token: access_token,
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, online} ->
|
||||
{:ok, online} when is_map(online) ->
|
||||
online = get_online(online)
|
||||
|
||||
if online.online == true do
|
||||
@@ -258,7 +268,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character online tracking failed",
|
||||
Logger.warning("ESI_ERROR: Character online tracking failed #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -388,12 +398,21 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:ok, %{eve_id: eve_id, tracking_pool: tracking_pool}} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, _info} ->
|
||||
character_eve_id = eve_id |> String.to_integer()
|
||||
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
|
||||
WandererApp.Character.update_character_state(character_id, update)
|
||||
alliance_id = character_aff_info |> Map.get("alliance_id")
|
||||
corporation_id = character_aff_info |> Map.get("corporation_id")
|
||||
|
||||
updated_state =
|
||||
character_state
|
||||
|> maybe_update_corporation(corporation_id)
|
||||
|> maybe_update_alliance(alliance_id)
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, updated_state)
|
||||
|
||||
:ok
|
||||
|
||||
@@ -975,7 +994,38 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: old_alliance_id} = state,
|
||||
alliance_id
|
||||
)
|
||||
when old_alliance_id != alliance_id and is_nil(alliance_id) do
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
Character.update_alliance(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:alliance",
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: old_alliance_id} = state,
|
||||
alliance_id
|
||||
)
|
||||
when old_alliance_id != alliance_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
@@ -1015,7 +1065,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
|
||||
defp maybe_update_alliance(state, _alliance_id), do: state
|
||||
|
||||
defp maybe_update_corporation(
|
||||
%{character_id: character_id, corporation_id: old_corporation_id} = state,
|
||||
corporation_id
|
||||
)
|
||||
when old_corporation_id != corporation_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
@@ -1027,16 +1083,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
alliance_id = Map.get(corporation_info, "alliance_id")
|
||||
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corporation_name,
|
||||
corporation_ticker: corporation_ticker,
|
||||
alliance_id: alliance_id
|
||||
corporation_ticker: corporation_ticker
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
@@ -1057,8 +1110,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|
||||
|> maybe_update_alliance()
|
||||
|> Map.merge(%{corporation_id: corporation_id})
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
@@ -1072,6 +1124,8 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_corporation(state, _corporation_id), do: state
|
||||
|
||||
defp maybe_update_ship(
|
||||
%{
|
||||
character_id: character_id
|
||||
@@ -1153,58 +1207,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
structure_id != new_structure_id ||
|
||||
station_id != new_station_id
|
||||
|
||||
defp maybe_update_corporation(
|
||||
state,
|
||||
character_eve_id
|
||||
)
|
||||
when not is_nil(character_eve_id) and is_integer(character_eve_id) do
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
|
||||
update_corporation(state, character_aff_info |> Map.get("corporation_id"))
|
||||
|
||||
_error ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_corporation(
|
||||
state,
|
||||
_info
|
||||
),
|
||||
do: state
|
||||
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: alliance_id} =
|
||||
state
|
||||
) do
|
||||
case alliance_id do
|
||||
nil ->
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
Character.update_alliance(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:alliance",
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
state
|
||||
|
||||
_ ->
|
||||
update_alliance(state, alliance_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_wallet(
|
||||
%{character_id: character_id} =
|
||||
state,
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
@check_start_queue_interval :timer.seconds(1)
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@untrack_characters_interval :timer.minutes(5)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
|
||||
@@ -54,6 +54,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
true
|
||||
)
|
||||
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[character_id],
|
||||
@@ -69,29 +71,25 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
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
|
||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
[],
|
||||
fn tracked_characters ->
|
||||
tracked_characters
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
@@ -129,7 +127,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
[{map_id, character_id} | untrack_queue]
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
@check_ship_errors_interval :timer.minutes(1)
|
||||
@check_location_errors_interval :timer.minutes(1)
|
||||
@update_ship_interval :timer.seconds(2)
|
||||
@update_info_interval :timer.minutes(1)
|
||||
@update_info_interval :timer.minutes(2)
|
||||
@update_wallet_interval :timer.minutes(1)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
with {:ok, character} <-
|
||||
WandererApp.Character.get_by_eve_id(character_eve_id),
|
||||
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
|
||||
{:ok, %{tracked: is_tracked}} <-
|
||||
do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
# Determine which event to send based on tracking mode and previous state
|
||||
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
Builds tracking data for all characters with access to a map.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
|
||||
{:ok, character_settings} <-
|
||||
WandererApp.Character.Activity.get_map_character_settings(map_id),
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
# Map characters to tracking data
|
||||
{:ok, characters_data} =
|
||||
build_character_tracking_data(characters_with_access, character_settings)
|
||||
build_character_tracking_data(characters_with_access)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_access, characters_with_access)
|
||||
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
# Helper to build tracking data for each character
|
||||
defp build_character_tracking_data(characters, character_settings) do
|
||||
defp build_character_tracking_data(characters) do
|
||||
{:ok,
|
||||
Enum.map(characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
|
||||
%{
|
||||
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
|
||||
tracked: (setting && setting.tracked) || false
|
||||
tracked: char.tracked
|
||||
}
|
||||
end)}
|
||||
end
|
||||
|
||||
# Private implementation of update character tracking
|
||||
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||
|> case do
|
||||
# Untracking flow
|
||||
{:ok, %{tracked: true} = existing_settings} ->
|
||||
|
||||
@@ -11,49 +11,50 @@ defmodule WandererApp.Env do
|
||||
def vsn(), do: Application.spec(@app)[:vsn]
|
||||
|
||||
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
|
||||
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
|
||||
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
|
||||
def invites, do: get_key(:invites, false)
|
||||
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
|
||||
def base_metrics_only(), do: get_key(:base_metrics_only, false)
|
||||
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
|
||||
def invites(), do: get_key(:invites, false)
|
||||
|
||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||
def websocket_events_enabled?, do: get_key(:websocket_events_enabled, false)
|
||||
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
||||
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
||||
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
|
||||
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "active_tracking_pool"
|
||||
)
|
||||
def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
|
||||
def active_tracking_pool(), do: get_key(:active_tracking_pool, "default")
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "tracking_pool_max_size"
|
||||
)
|
||||
def tracking_pool_max_size, do: get_key(:tracking_pool_max_size, 300)
|
||||
def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
|
||||
def character_api_disabled?, do: get_key(:character_api_disabled, false)
|
||||
def wanderer_kills_service_enabled?, do: get_key(:wanderer_kills_service_enabled, false)
|
||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins, do: get_key(:admins, [])
|
||||
def admin_username, do: get_key(:admin_username)
|
||||
def admin_password, do: get_key(:admin_password)
|
||||
def corp_wallet, do: get_key(:corp_wallet, "")
|
||||
def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
|
||||
def corp_eve_id, do: get_key(:corp_id, -1)
|
||||
def subscription_settings, do: get_key(:subscription_settings)
|
||||
def tracking_pool_max_size(), do: get_key(:tracking_pool_max_size, 300)
|
||||
def character_tracking_pause_disabled?(), do: get_key(:character_tracking_pause_disabled, true)
|
||||
def character_api_disabled?(), do: get_key(:character_api_disabled, false)
|
||||
def wanderer_kills_service_enabled?(), do: get_key(:wanderer_kills_service_enabled, false)
|
||||
def wallet_tracking_enabled?(), do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins(), do: get_key(:admins, [])
|
||||
def admin_username(), do: get_key(:admin_username)
|
||||
def admin_password(), do: get_key(:admin_password)
|
||||
def corp_wallet(), do: get_key(:corp_wallet, "")
|
||||
def corp_wallet_eve_id(), do: get_key(:corp_wallet_eve_id, "-1")
|
||||
def corp_eve_id(), do: get_key(:corp_id, -1)
|
||||
def subscription_settings(), do: get_key(:subscription_settings)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
)
|
||||
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
|
||||
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
|
||||
|
||||
def sse_enabled? do
|
||||
def sse_enabled?() do
|
||||
Application.get_env(@app, :sse, [])
|
||||
|> Keyword.get(:enabled, false)
|
||||
end
|
||||
|
||||
def webhooks_enabled? do
|
||||
def webhooks_enabled?() do
|
||||
Application.get_env(@app, :external_events, [])
|
||||
|> Keyword.get(:webhooks_enabled, false)
|
||||
end
|
||||
@@ -62,19 +63,19 @@ defmodule WandererApp.Env do
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-auto-expire-hours"
|
||||
)
|
||||
def map_connection_auto_expire_hours, do: get_key(:map_connection_auto_expire_hours)
|
||||
def map_connection_auto_expire_hours(), do: get_key(:map_connection_auto_expire_hours)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-auto-eol-hours"
|
||||
)
|
||||
def map_connection_auto_eol_hours, do: get_key(:map_connection_auto_eol_hours)
|
||||
def map_connection_auto_eol_hours(), do: get_key(:map_connection_auto_eol_hours)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-eol-expire-timeout-mins"
|
||||
)
|
||||
def map_connection_eol_expire_timeout_mins,
|
||||
def map_connection_eol_expire_timeout_mins(),
|
||||
do: get_key(:map_connection_eol_expire_timeout_mins)
|
||||
|
||||
def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
|
||||
@@ -83,7 +84,7 @@ defmodule WandererApp.Env do
|
||||
A single map containing environment variables
|
||||
made available to react
|
||||
"""
|
||||
def to_client_env do
|
||||
def to_client_env() do
|
||||
%{detailedKillsDisabled: not wanderer_kills_service_enabled?()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -287,8 +287,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_alliance_info(eve_id, opts \\ []) do
|
||||
case _get_alliance_info(eve_id, "", opts) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
case get_alliance_info(eve_id, "", opts) do
|
||||
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
@@ -309,8 +309,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_corporation_info(eve_id, opts \\ []) do
|
||||
case _get_corporation_info(eve_id, "", opts) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
case get_corporation_info(eve_id, "", opts) do
|
||||
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
@@ -327,7 +327,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts,
|
||||
@cache_opts
|
||||
) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
@@ -434,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
|
||||
|
||||
defp _get_alliance_info(alliance_eve_id, info_path, opts),
|
||||
defp get_alliance_info(alliance_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/alliances/#{alliance_eve_id}/#{info_path}",
|
||||
@@ -442,7 +442,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
defp _get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
defp get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
|
||||
@@ -8,6 +8,19 @@ defmodule WandererApp.Map.Manager do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Map.ServerSupervisor
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
|
||||
@maps_start_per_second 10
|
||||
@maps_start_interval 1000
|
||||
@maps_queue :maps_queue
|
||||
@garbage_collection_interval :timer.hours(1)
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
@signatures_cleanup_interval :timer.minutes(30)
|
||||
@delete_after_minutes 30
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
# Test-aware async task runner
|
||||
defp safe_async_task(fun) do
|
||||
@@ -25,20 +38,6 @@ defmodule WandererApp.Map.Manager do
|
||||
end
|
||||
end
|
||||
|
||||
alias WandererApp.Map.ServerSupervisor
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
|
||||
@maps_start_per_second 5
|
||||
@maps_start_interval 1000
|
||||
@maps_queue :maps_queue
|
||||
@garbage_collection_interval :timer.hours(1)
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
@signatures_cleanup_interval :timer.minutes(30)
|
||||
@delete_after_minutes 30
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
def start_map(map_id) when is_binary(map_id),
|
||||
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
|
||||
|
||||
@@ -247,22 +246,29 @@ defmodule WandererApp.Map.Manager do
|
||||
Logger.debug(fn -> "All maps started" end)
|
||||
else
|
||||
# In production, run async as normal
|
||||
tasks =
|
||||
for chunk <- chunks do
|
||||
task =
|
||||
Task.async(fn ->
|
||||
chunk
|
||||
|> Enum.map(&start_map_server/1)
|
||||
end)
|
||||
chunks
|
||||
|> Task.async_stream(
|
||||
fn chunk ->
|
||||
chunk
|
||||
|> Enum.map(&start_map_server/1)
|
||||
|
||||
:timer.sleep(@maps_start_interval)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(60)
|
||||
)
|
||||
|> Enum.each(fn result ->
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
task
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
Logger.debug(fn -> "Waiting for maps to start" end)
|
||||
Task.await_many(tasks)
|
||||
Logger.debug(fn -> "All maps started" end)
|
||||
Logger.info(fn -> "All maps started" end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
|
||||
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id),
|
||||
do:
|
||||
map_id
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
|
||||
map_id
|
||||
|> map_pid()
|
||||
|> case do
|
||||
pid when is_pid(pid) ->
|
||||
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
|
||||
|
||||
_ ->
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", false)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
|
||||
do:
|
||||
|
||||
@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
|
||||
sigs
|
||||
# Add solar_system_id to each signature and remove system_id
|
||||
Enum.map(sigs, fn sig ->
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, sys.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
end)
|
||||
else
|
||||
err ->
|
||||
Logger.error("[list_signatures] error: #{inspect(err)}")
|
||||
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
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)
|
||||
%{"solar_system_id" => solar_system_id} = params
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
# Convert solar_system_id to system_id for internal use
|
||||
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("character_eve_id", char_id)
|
||||
|> Map.put("system_id", system.id)
|
||||
|> Map.delete("solar_system_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}
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
# Try to fetch the created signature to return with proper fields
|
||||
with {:ok, sigs} <-
|
||||
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
|
||||
sig when not is_nil(sig) <- List.first(sigs) do
|
||||
result =
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
{:ok, result}
|
||||
else
|
||||
_ ->
|
||||
# Fallback: return attrs with solar_system_id added
|
||||
attrs_result =
|
||||
attrs
|
||||
|> Map.put(:solar_system_id, solar_system_id)
|
||||
|> Map.drop(["system_id"])
|
||||
|
||||
{:ok, attrs_result}
|
||||
end
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
)
|
||||
|
||||
{:error, :system_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _invalid} = _params
|
||||
),
|
||||
do: {:error, :missing_params}
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
|
||||
{:ok, attrs}
|
||||
# Fetch the updated signature to return with proper fields
|
||||
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
|
||||
result =
|
||||
updated_sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
_ -> {:ok, attrs}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
|
||||
@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
map_update = %{acls: map.acls, scope: map.scope}
|
||||
|
||||
WandererApp.Map.update_map(map_id, map_update)
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
broadcast_acl_updates({:ok, result}, map_id)
|
||||
|
||||
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_updated(map_id, acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:ok =
|
||||
acl_id
|
||||
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_deleted(map_id, _acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
character_ids =
|
||||
map_id
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def update_tracked_characters(map_id) do
|
||||
Task.start_link(fn ->
|
||||
{:ok, map_tracked_character_ids} =
|
||||
{:ok, all_map_tracked_character_ids} =
|
||||
map_id
|
||||
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|
||||
|> case do
|
||||
@@ -67,30 +67,19 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
map_active_tracked_characters =
|
||||
map_tracked_character_ids
|
||||
|> Enum.filter(fn character -> character in tracked_characters end)
|
||||
|
||||
{:ok, old_map_tracked_characters} =
|
||||
{:ok, actual_map_tracked_characters} =
|
||||
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
|
||||
|
||||
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
|
||||
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
|
||||
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
|
||||
characters_to_remove,
|
||||
fn ids ->
|
||||
(ids ++ characters_to_remove) |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
|
||||
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
@@ -98,7 +87,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
is_character_map_active?(map_id, character_id)
|
||||
character_map_active = is_character_map_active?(map_id, character_id)
|
||||
|
||||
character_map_active
|
||||
|> untrack_character(map_id, character_id)
|
||||
end)
|
||||
end
|
||||
@@ -126,15 +117,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def cleanup_characters(map_id, owner_id) do
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
WandererApp.Cache.get_and_remove(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
acls =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:acls, [])
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
invalidate_character_ids
|
||||
|> Task.async_stream(
|
||||
@@ -186,11 +180,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:error, reason} ->
|
||||
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
end
|
||||
|
||||
defp remove_and_untrack_characters(map_id, character_ids) do
|
||||
|
||||
@@ -527,33 +527,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
|
||||
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
|
||||
{:ok, known_jumps} =
|
||||
WandererApp.Api.MapSolarSystemJumps.find(%{
|
||||
before_system_id: from_solar_system_id,
|
||||
current_system_id: to_solar_system_id
|
||||
})
|
||||
|
||||
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
|
||||
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
|
||||
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
|
||||
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
|
||||
known_jumps |> Enum.empty?()
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
to_system_static_info.system_class in @known_space and
|
||||
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
|
||||
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
|
||||
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
|
||||
known_jumps |> Enum.empty?()
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
to_system_static_info.system_class in @known_space and
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -570,6 +567,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
case WandererApp.CachedInfo.get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
{:ok, jump} when not is_nil(jump) -> {:ok, [jump]}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_system_static_info(solar_system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, system_static_info} when not is_nil(system_static_info) ->
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
]
|
||||
|
||||
@systems_cleanup_timeout :timer.minutes(30)
|
||||
@characters_cleanup_timeout :timer.minutes(1)
|
||||
@characters_cleanup_timeout :timer.minutes(5)
|
||||
@connections_cleanup_timeout :timer.minutes(2)
|
||||
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@@ -100,7 +100,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Process.send_after(self(), :update_presence, @update_presence_timeout)
|
||||
Process.send_after(self(), :cleanup_connections, 5_000)
|
||||
Process.send_after(self(), :cleanup_systems, 10_000)
|
||||
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
|
||||
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", true)
|
||||
@@ -127,6 +127,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
|
||||
|
||||
WandererApp.Cache.delete("map_#{map_id}:started")
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
|
||||
|
||||
@@ -278,7 +279,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
end
|
||||
|
||||
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
|
||||
AclsImpl.handle_acl_updated(map_id, acl_id)
|
||||
AclsImpl.handle_acl_deleted(map_id, acl_id)
|
||||
|
||||
state
|
||||
end
|
||||
@@ -580,18 +581,27 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
characters_ids =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
{:ok, old_presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:old_presence_character_ids", [])
|
||||
|
||||
new_present_character_ids =
|
||||
presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(old_presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
not_present_character_ids =
|
||||
characters_ids
|
||||
old_presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
CharactersImpl.track_characters(map_id, presence_character_ids)
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:old_presence_character_ids",
|
||||
presence_character_ids
|
||||
)
|
||||
|
||||
CharactersImpl.track_characters(map_id, new_present_character_ids)
|
||||
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
|
||||
|
||||
broadcast!(
|
||||
|
||||
@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
|
||||
end
|
||||
end
|
||||
|
||||
def load_characters(map, character_settings, user_id) do
|
||||
def load_characters(map, user_id) do
|
||||
{:ok, user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
characters =
|
||||
map_available_characters =
|
||||
map
|
||||
|> get_map_available_characters(user_characters)
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
|
||||
map.id,
|
||||
map_available_characters |> Enum.map(& &1.id)
|
||||
)
|
||||
|
||||
characters =
|
||||
map_available_characters
|
||||
|> Enum.map(fn c ->
|
||||
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
|
||||
end)
|
||||
@@ -176,48 +185,57 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map_characters-#{map_id}",
|
||||
opts: [ttl: :timer.seconds(2)]
|
||||
)
|
||||
defp _get_map_characters(%{id: map_id} = map) do
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}}
|
||||
map_characters =
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
map_characters ->
|
||||
{:ok, map_characters}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_available_characters(map, user_characters) do
|
||||
@@ -227,7 +245,7 @@ defmodule WandererApp.Maps do
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}} = _get_map_characters(map)
|
||||
}} = get_map_characters(map)
|
||||
|
||||
user_characters
|
||||
|> Enum.filter(fn c ->
|
||||
|
||||
@@ -29,15 +29,24 @@ defmodule WandererApp.Metrics.PromExPlugin do
|
||||
|
||||
@impl true
|
||||
def event_metrics(_opts) do
|
||||
[
|
||||
base_metrics = [
|
||||
user_event_metrics(),
|
||||
character_event_metrics(),
|
||||
map_event_metrics(),
|
||||
map_subscription_metrics(),
|
||||
map_subscription_metrics()
|
||||
]
|
||||
|
||||
advanced_metrics = [
|
||||
character_event_metrics(),
|
||||
characters_distribution_event_metrics(),
|
||||
esi_event_metrics(),
|
||||
json_api_metrics()
|
||||
]
|
||||
|
||||
if WandererApp.Env.base_metrics_only() do
|
||||
base_metrics
|
||||
else
|
||||
base_metrics ++ advanced_metrics
|
||||
end
|
||||
end
|
||||
|
||||
defp user_event_metrics do
|
||||
@@ -227,8 +236,8 @@ defmodule WandererApp.Metrics.PromExPlugin do
|
||||
defp get_esi_error_tag_values(metadata) do
|
||||
%{
|
||||
endpoint: Map.get(metadata, :endpoint, "unknown"),
|
||||
error_type: to_string(Map.get(metadata, :error_type, "unknown")),
|
||||
tracking_pool: Map.get(metadata, :tracking_pool, "unknown")
|
||||
error_type: inspect(Map.get(metadata, :error_type, "unknown")),
|
||||
tracking_pool: Map.get(metadata, :tracking_pool, "default")
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -53,20 +53,8 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
def get_tracked_by_map_all(map_id),
|
||||
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
|
||||
|
||||
def get_by_map(map_id, character_id) do
|
||||
case get_by_map_filtered(map_id, [character_id]) do
|
||||
{:ok, [setting | _]} ->
|
||||
{:ok, setting}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def track(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -75,6 +63,7 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def untrack(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -83,22 +72,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def track!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case track(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to track: #{inspect(error)}"
|
||||
error -> raise "Failed to track: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def untrack!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case untrack(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to untrack: #{inspect(error)}"
|
||||
error -> raise "Failed to untrack: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,22 +100,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def follow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.follow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case follow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to follow: #{inspect(error)}"
|
||||
error -> raise "Failed to follow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.unfollow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case unfollow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
error -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -596,10 +596,24 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
acl -> acl.id
|
||||
end)
|
||||
|
||||
updated_acls = current_acl_ids ++ [new_acl_id]
|
||||
updated_acls =
|
||||
if new_acl_id in current_acl_ids do
|
||||
current_acl_ids
|
||||
else
|
||||
current_acl_ids ++ [new_acl_id]
|
||||
end
|
||||
|
||||
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
|
||||
{:ok, updated_map} ->
|
||||
# Only broadcast if we actually added a new ACL
|
||||
unless new_acl_id in current_acl_ids do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"maps:#{loaded_map.id}",
|
||||
{:map_acl_updated, [new_acl_id], []}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, updated_map}
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -192,6 +192,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_added
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -199,6 +201,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member added event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
end
|
||||
|
||||
@@ -300,6 +305,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_updated
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -307,6 +314,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member updated event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
end
|
||||
|
||||
@@ -385,6 +395,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_removed
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -392,6 +404,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member removed event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
end
|
||||
|
||||
@@ -417,6 +432,14 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# Private Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
solar_system_id: %OpenApiSpex.Schema{type: :integer},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
character_eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
@@ -31,13 +31,13 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
},
|
||||
required: [
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:eve_id,
|
||||
:character_eve_id
|
||||
],
|
||||
example: %{
|
||||
id: "sig-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
eve_id: "ABC-123",
|
||||
character_eve_id: "123456789",
|
||||
name: "Wormhole K162",
|
||||
@@ -122,7 +122,15 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController 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})
|
||||
# Add solar_system_id and remove system_id
|
||||
# Convert to a plain map to avoid encoder issues
|
||||
signature_data =
|
||||
signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
json(conn, %{data: signature_data})
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
|
||||
@@ -13,6 +13,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.User
|
||||
alias WandererApp.SecurityAudit
|
||||
alias WandererApp.Audit.RequestContext
|
||||
@@ -140,43 +141,60 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
defp authenticate_bearer_token(conn) do
|
||||
case get_req_header(conn, "authorization") do
|
||||
["Bearer " <> token] ->
|
||||
validate_api_token(token)
|
||||
validate_api_token(conn, token)
|
||||
|
||||
_ ->
|
||||
{:error, "Missing or invalid authorization header"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_api_token(token) do
|
||||
# Look up the map by its public API key
|
||||
case find_map_by_api_key(token) do
|
||||
{:ok, map} when not is_nil(map) ->
|
||||
# Get the actual owner of the map
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
# Return the map owner as the authenticated user
|
||||
{:ok, user, map}
|
||||
defp validate_api_token(conn, token) do
|
||||
# Check for map identifier in path params
|
||||
# According to PR feedback, routes supply params["map_identifier"]
|
||||
case conn.params["map_identifier"] do
|
||||
nil ->
|
||||
# No map identifier in path - this might be a general API endpoint
|
||||
# For now, we'll return an error since we need to validate against a specific map
|
||||
{:error, "Authentication failed", :no_map_context}
|
||||
|
||||
identifier ->
|
||||
# Resolve the identifier (could be UUID or slug)
|
||||
case resolve_map_identifier(identifier) do
|
||||
{:ok, map} ->
|
||||
# Validate the token matches this specific map's API key
|
||||
if is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token) do
|
||||
# Get the map owner
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
{:ok, user, map}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
end
|
||||
else
|
||||
{:error, "Authentication failed", :invalid_token_for_map}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
{:error, "Authentication failed", :map_not_found}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_map_by_api_key(api_key) do
|
||||
# Import necessary modules
|
||||
import Ash.Query
|
||||
# Helper to resolve map by ID or slug
|
||||
defp resolve_map_identifier(identifier) do
|
||||
alias WandererApp.Api.Map
|
||||
|
||||
# Query for map with matching public API key
|
||||
Map
|
||||
|> filter(public_api_key == ^api_key)
|
||||
|> Ash.read_one()
|
||||
# Try as UUID first
|
||||
case Map.by_id(identifier) do
|
||||
{:ok, map} ->
|
||||
{:ok, map}
|
||||
|
||||
_ ->
|
||||
# Try as slug
|
||||
Map.get_map_by_slug(identifier)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_user_role(user) do
|
||||
|
||||
@@ -38,8 +38,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
:ok = track_characters(characters)
|
||||
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
|
||||
@@ -15,30 +15,6 @@ defmodule WandererAppWeb.Endpoint do
|
||||
max_age: 24 * 60 * 60 * 180
|
||||
]
|
||||
|
||||
# @impl SiteEncrypt
|
||||
# def certification do
|
||||
# SiteEncrypt.configure(
|
||||
# client: :native,
|
||||
# mode: :auto,
|
||||
# days_to_renew: 30,
|
||||
# domains: ["dev.wanderer.deadly-w.space"],
|
||||
# emails: ["dmitriypopovsamara@gmail.com"],
|
||||
# db_folder: System.get_env("SITE_ENCRYPT_DB", Path.join("tmp", "site_encrypt_db")),
|
||||
# backup: Path.join(Path.join("tmp", "site_encrypt_db"), "site_encrypt_backup.tgz"),
|
||||
# directory_url:
|
||||
# case System.get_env("CERT_MODE", "local") do
|
||||
# "local" ->
|
||||
# {:internal, port: 4001}
|
||||
|
||||
# "staging" ->
|
||||
# "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
|
||||
# "production" ->
|
||||
# "https://acme-v02.api.letsencrypt.org/directory"
|
||||
# end
|
||||
# )
|
||||
# end
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [compress: true, connect_info: [session: @session_options]]
|
||||
|
||||
|
||||
@@ -75,13 +75,12 @@ defmodule WandererAppWeb.CharactersLive do
|
||||
def handle_event("delete", %{"character_id" => character_id}, socket) do
|
||||
WandererApp.Character.TrackerManager.stop_tracking(character_id)
|
||||
|
||||
{:ok, map_user_settings} =
|
||||
{:ok, map_character_settings} =
|
||||
WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
|
||||
|
||||
map_user_settings
|
||||
map_character_settings
|
||||
|> Enum.each(fn settings ->
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.untrack()
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
|
||||
end)
|
||||
|
||||
{:ok, updated_character} =
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
|
||||
|
||||
{:ok,
|
||||
@@ -14,7 +14,6 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
characters: [],
|
||||
selected_map: nil,
|
||||
selected_map_slug: nil,
|
||||
user_id: user_id,
|
||||
maps: maps |> Enum.sort_by(& &1.name, :asc)
|
||||
)}
|
||||
end
|
||||
@@ -37,24 +36,22 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
end
|
||||
|
||||
defp apply_action(socket, :characters, %{"slug" => map_slug} = _params) do
|
||||
selected_map = socket.assigns.maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
|
||||
|
||||
user_id = socket.assigns.user_id
|
||||
defp apply_action(
|
||||
%{assigns: %{current_user: current_user, maps: maps}} = socket,
|
||||
:characters,
|
||||
%{"slug" => map_slug} = _params
|
||||
) do
|
||||
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
|> assign(
|
||||
selected_map: selected_map,
|
||||
selected_map_slug: map_slug,
|
||||
character_settings: character_settings
|
||||
selected_map_slug: map_slug
|
||||
)
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, character_settings, user_id)
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -71,55 +68,36 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
|
||||
def handle_event(
|
||||
"toggle_track",
|
||||
%{"character_id" => character_id},
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
) do
|
||||
selected_map = socket.assigns.selected_map
|
||||
character_settings = socket.assigns.character_settings
|
||||
|
||||
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
|
||||
nil ->
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id,
|
||||
tracked: true
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
character_setting ->
|
||||
case character_setting.tracked do
|
||||
true ->
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.untrack!()
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_setting.character_id
|
||||
])
|
||||
|
||||
_ ->
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.track!()
|
||||
end
|
||||
end
|
||||
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: false} ->
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
|
||||
characters =
|
||||
characters
|
||||
|> Enum.map(fn c ->
|
||||
WandererApp.Maps.map_character(
|
||||
c,
|
||||
character_settings |> Enum.find(&(&1.character_id == c.id))
|
||||
)
|
||||
end)
|
||||
%{tracked: true} ->
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_id
|
||||
])
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(character_settings: character_settings)
|
||||
|> assign_async(:characters, fn ->
|
||||
{:ok, %{characters: characters}}
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
|
||||
@@ -244,38 +244,41 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
{:ok, user_settings} =
|
||||
WandererApp.MapUserSettingsRepo.create_or_update(map_id, current_user_id, settings)
|
||||
|
||||
{:ok, map_user_settings} =
|
||||
user_settings
|
||||
|> WandererApp.Api.MapUserSettings.update_main_character(%{
|
||||
main_character_eve_id: "#{character_eve_id}"
|
||||
})
|
||||
case Ash.update(user_settings, %{main_character_eve_id: "#{character_eve_id}"},
|
||||
action: :update_main_character
|
||||
) do
|
||||
{:ok, map_user_settings} ->
|
||||
{:ok, tracking_data} =
|
||||
WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user_id)
|
||||
|
||||
{:ok, tracking_data} =
|
||||
WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user_id)
|
||||
{main_character_id, main_character_eve_id} =
|
||||
WandererApp.Character.TrackingUtils.get_main_character(
|
||||
map_user_settings,
|
||||
current_user_characters,
|
||||
current_user_characters
|
||||
)
|
||||
|> case do
|
||||
{:ok, main_character} when not is_nil(main_character) ->
|
||||
{main_character.id, main_character.eve_id}
|
||||
|
||||
{main_character_id, main_character_eve_id} =
|
||||
WandererApp.Character.TrackingUtils.get_main_character(
|
||||
map_user_settings,
|
||||
current_user_characters,
|
||||
current_user_characters
|
||||
)
|
||||
|> case do
|
||||
{:ok, main_character} when not is_nil(main_character) ->
|
||||
{main_character.id, main_character.eve_id}
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 50)
|
||||
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 50)
|
||||
{:reply, %{data: tracking_data},
|
||||
socket
|
||||
|> assign(
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_id: main_character_id,
|
||||
main_character_eve_id: main_character_eve_id
|
||||
)}
|
||||
|
||||
{:reply, %{data: tracking_data},
|
||||
socket
|
||||
|> assign(
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_id: main_character_id,
|
||||
main_character_eve_id: main_character_eve_id
|
||||
)}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to update main character: #{inspect(reason)}")
|
||||
{:reply, %{error: "Failed to update main character"}, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
@@ -333,21 +336,18 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
def needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
characters,
|
||||
character_settings,
|
||||
user_permissions
|
||||
) do
|
||||
tracked_count =
|
||||
characters
|
||||
|> Enum.count(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting && setting.tracked
|
||||
char.tracked
|
||||
end)
|
||||
|
||||
untracked_count =
|
||||
characters
|
||||
|> Enum.count(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting == nil || !setting.tracked
|
||||
!char.tracked
|
||||
end)
|
||||
|
||||
user_permissions.track_character &&
|
||||
|
||||
@@ -422,14 +422,11 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
current_user_characters |> Enum.map(& &1.id)
|
||||
),
|
||||
{:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, character_settings} <-
|
||||
WandererApp.Character.Activity.get_map_character_settings(map_id),
|
||||
{:ok, %{characters: available_map_characters}} =
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
tracked_data =
|
||||
get_tracked_data(
|
||||
available_map_characters,
|
||||
character_settings,
|
||||
user_permissions,
|
||||
only_tracked_characters
|
||||
)
|
||||
@@ -473,15 +470,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
defp get_tracked_data(
|
||||
available_map_characters,
|
||||
character_settings,
|
||||
user_permissions,
|
||||
only_tracked_characters
|
||||
) do
|
||||
tracked_characters =
|
||||
available_map_characters
|
||||
|> Enum.filter(fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
setting != nil && setting.tracked == true
|
||||
char.tracked
|
||||
end)
|
||||
|
||||
all_tracked? =
|
||||
@@ -492,7 +487,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
MapCharactersEventHandler.needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
available_map_characters,
|
||||
character_settings,
|
||||
user_permissions
|
||||
)
|
||||
|
||||
|
||||
@@ -75,26 +75,10 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
if not must_select? do
|
||||
socket
|
||||
else
|
||||
# Check if we already selected this exact system for this char:
|
||||
last_selected =
|
||||
WandererApp.Cache.lookup!(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
nil
|
||||
)
|
||||
|
||||
if last_selected == solar_system_id do
|
||||
# same system => skip
|
||||
socket
|
||||
else
|
||||
# new system => update cache + push event
|
||||
WandererApp.Cache.put(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
end
|
||||
# Always select the system when auto-select is enabled (following or select_on_spash).
|
||||
# The frontend will handle deselecting other systems
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,24 +24,8 @@ defmodule WandererAppWeb.Presence do
|
||||
%{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_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_data",
|
||||
presence_data
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
# Delegate all cache operations to the PresenceGracePeriodManager
|
||||
WandererAppWeb.PresenceGracePeriodManager.process_presence_change(map_id, presence_data)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
@@ -0,0 +1,233 @@
|
||||
defmodule WandererAppWeb.PresenceGracePeriodManager do
|
||||
@moduledoc """
|
||||
Manages grace period for character presence tracking.
|
||||
|
||||
This module prevents rapid start/stop cycles of character tracking
|
||||
by introducing a 5-minute grace period before stopping tracking
|
||||
for characters that leave presence.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
# 30 minutes
|
||||
@grace_period_ms :timer.minutes(30)
|
||||
@check_remove_queue_interval :timer.seconds(30)
|
||||
|
||||
defstruct pending_removals: %{}, timers: %{}, to_remove: []
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Process presence changes with grace period logic.
|
||||
|
||||
Updates the cache with the final list of character IDs that should be tracked,
|
||||
accounting for the grace period.
|
||||
"""
|
||||
def process_presence_change(map_id, presence_data) do
|
||||
GenServer.cast(__MODULE__, {:process_presence_change, map_id, presence_data})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
{:ok, %__MODULE__{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:process_presence_change, map_id, presence_data}, state) do
|
||||
# Extract currently tracked character IDs from presence data
|
||||
current_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
# Get previous tracked character IDs from cache
|
||||
previous_tracked_character_ids = get_previous_character_ids(map_id)
|
||||
|
||||
current_set = MapSet.new(current_tracked_character_ids)
|
||||
previous_set = MapSet.new(previous_tracked_character_ids)
|
||||
|
||||
# Characters that just joined (not in previous, but in current)
|
||||
newly_joined = MapSet.difference(current_set, previous_set)
|
||||
|
||||
# Characters that just left (in previous, but not in current)
|
||||
newly_left = MapSet.difference(previous_set, current_set)
|
||||
|
||||
# Process newly joined characters - cancel any pending removals
|
||||
state =
|
||||
state
|
||||
|> cancel_pending_removals(map_id, current_set)
|
||||
|> schedule_removals(map_id, newly_left)
|
||||
|
||||
# Process newly left characters - schedule them for removal after grace period
|
||||
# Calculate the final character IDs (current + still pending removal)
|
||||
pending_for_map = get_pending_removals_for_map(state, map_id)
|
||||
|
||||
final_character_ids = MapSet.union(current_set, pending_for_map) |> MapSet.to_list()
|
||||
|
||||
# Update cache with final character IDs (includes grace period logic)
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", final_character_ids)
|
||||
|
||||
# Only update presence_data if the character IDs actually changed
|
||||
if final_character_ids != previous_tracked_character_ids do
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", presence_data)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:grace_period_expired, map_id, character_id}, state) do
|
||||
Logger.debug(fn -> "Grace period expired for character #{character_id} on map #{map_id}" end)
|
||||
|
||||
# Remove from pending removals and timers
|
||||
state =
|
||||
state
|
||||
|> remove_pending_removal(map_id, character_id)
|
||||
|> remove_after_grace_period(map_id, character_id)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_remove_queue, state) do
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
remove_from_cache_after_grace_period(state)
|
||||
{:noreply, %{state | to_remove: []}}
|
||||
end
|
||||
|
||||
defp cancel_pending_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
acc_state
|
||||
|
||||
timer_ref ->
|
||||
Logger.debug(fn ->
|
||||
"Cancelling grace period for character #{character_id} on map #{map_id} (rejoined)"
|
||||
end)
|
||||
|
||||
Process.cancel_timer(timer_ref)
|
||||
remove_pending_removal(acc_state, map_id, character_id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp schedule_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
# Only schedule if not already pending
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"Scheduling grace period for character #{character_id} on map #{map_id}"
|
||||
end)
|
||||
|
||||
timer_ref =
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:grace_period_expired, map_id, character_id},
|
||||
@grace_period_ms
|
||||
)
|
||||
|
||||
add_pending_removal(acc_state, map_id, character_id, timer_ref)
|
||||
|
||||
_ ->
|
||||
acc_state
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_pending_removal(state, map_id, character_id, timer_ref) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.put(state.pending_removals, pending_key, true),
|
||||
timers: Map.put(state.timers, pending_key, timer_ref)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_pending_removal(state, map_id, character_id) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.delete(state.pending_removals, pending_key),
|
||||
timers: Map.delete(state.timers, pending_key)
|
||||
}
|
||||
end
|
||||
|
||||
defp get_timer_ref(state, map_id, character_id) do
|
||||
Map.get(state.timers, {map_id, character_id})
|
||||
end
|
||||
|
||||
defp get_previous_character_ids(map_id) do
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_character_ids") do
|
||||
nil -> []
|
||||
character_ids -> character_ids
|
||||
end
|
||||
end
|
||||
|
||||
defp get_pending_removals_for_map(state, map_id) do
|
||||
state.pending_removals
|
||||
|> Enum.filter(fn {{pending_map_id, _character_id}, _} -> pending_map_id == map_id end)
|
||||
|> Enum.map(fn {{_map_id, character_id}, _} -> character_id end)
|
||||
|> MapSet.new()
|
||||
end
|
||||
|
||||
defp remove_after_grace_period(%{to_remove: to_remove} = state, map_id, character_id_to_remove) do
|
||||
%{
|
||||
state
|
||||
| to_remove:
|
||||
(to_remove ++ [{map_id, character_id_to_remove}])
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_from_cache_after_grace_period(%{to_remove: to_remove} = state) do
|
||||
# Get current presence data to recalculate without the expired character
|
||||
to_remove
|
||||
|> Enum.each(fn {map_id, character_id_to_remove} ->
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_data") do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
presence_data ->
|
||||
# Recalculate tracked character IDs from current presence data
|
||||
updated_presence_data =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{character_id: character_id} ->
|
||||
character_id != character_id_to_remove
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
updated_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_data", updated_presence_data)
|
||||
# Update both caches
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"Updated cache after grace period for map #{map_id}, tracked characters: #{inspect(presence_tracked_character_ids)}"
|
||||
end)
|
||||
end
|
||||
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.76.10"
|
||||
@version "1.77.6"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -100,7 +100,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "creates a new signature with valid parameters", %{conn: conn, map: map} do
|
||||
signature_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => "Test Signature",
|
||||
@@ -132,7 +132,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles signature creation with minimal required fields", %{conn: conn, map: map} do
|
||||
minimal_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_143,
|
||||
"eve_id" => "XYZ-456",
|
||||
"character_eve_id" => "987654321"
|
||||
}
|
||||
@@ -152,7 +152,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles signature creation with all optional fields", %{conn: conn, map: map} do
|
||||
complete_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_144,
|
||||
"eve_id" => "DEF-789",
|
||||
"character_eve_id" => "456789123",
|
||||
"name" => "Complete Signature",
|
||||
@@ -181,7 +181,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
map = Factory.insert(:map)
|
||||
|
||||
signature_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_145,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "123456789"
|
||||
}
|
||||
@@ -392,11 +392,11 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "validates signature creation with invalid data types", %{conn: conn, map: map} do
|
||||
invalid_params = [
|
||||
%{"system_id" => "not-a-uuid", "eve_id" => "ABC", "character_eve_id" => "123"},
|
||||
%{"system_id" => Ecto.UUID.generate(), "eve_id" => 123, "character_eve_id" => "123"},
|
||||
%{"system_id" => Ecto.UUID.generate(), "eve_id" => "ABC", "character_eve_id" => 123},
|
||||
%{"solar_system_id" => "not-an-integer", "eve_id" => "ABC", "character_eve_id" => "123"},
|
||||
%{"solar_system_id" => 30_000_142, "eve_id" => 123, "character_eve_id" => "123"},
|
||||
%{"solar_system_id" => 30_000_142, "eve_id" => "ABC", "character_eve_id" => 123},
|
||||
%{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC",
|
||||
"character_eve_id" => "123",
|
||||
"linked_system_id" => "not-an-integer"
|
||||
@@ -426,7 +426,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
long_string = String.duplicate("a", 1000)
|
||||
|
||||
long_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_146,
|
||||
"eve_id" => "LONG-123",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => long_string,
|
||||
@@ -448,7 +448,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles special characters in signature data", %{conn: conn, map: map} do
|
||||
special_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_147,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => "Special chars: àáâãäåæçèéêë",
|
||||
@@ -470,7 +470,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "handles empty string values", %{conn: conn, map: map} do
|
||||
empty_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_148,
|
||||
"eve_id" => "",
|
||||
"character_eve_id" => "",
|
||||
"name" => "",
|
||||
@@ -537,7 +537,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
if length(data) > 0 do
|
||||
signature = List.first(data)
|
||||
assert Map.has_key?(signature, "id")
|
||||
assert Map.has_key?(signature, "system_id")
|
||||
assert Map.has_key?(signature, "solar_system_id")
|
||||
assert Map.has_key?(signature, "eve_id")
|
||||
assert Map.has_key?(signature, "character_eve_id")
|
||||
end
|
||||
@@ -564,7 +564,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
|
||||
test "created signature response structure", %{conn: conn, map: map} do
|
||||
signature_params = %{
|
||||
"system_id" => Ecto.UUID.generate(),
|
||||
"solar_system_id" => 30_000_149,
|
||||
"eve_id" => "TEST-001",
|
||||
"character_eve_id" => "123456789",
|
||||
"name" => "Test Signature"
|
||||
@@ -582,7 +582,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
|
||||
case response do
|
||||
%{"data" => data} ->
|
||||
# Should have signature structure
|
||||
assert Map.has_key?(data, "id") or Map.has_key?(data, "system_id")
|
||||
assert Map.has_key?(data, "id") or Map.has_key?(data, "solar_system_id")
|
||||
|
||||
%{"error" => _error} ->
|
||||
# Error response is also valid
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
describe "parameter validation" do
|
||||
test "validates missing connection assigns for create_signature" do
|
||||
conn = %{assigns: %{}}
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
result = Signatures.create_signature(conn, params)
|
||||
assert {:error, :missing_params} = result
|
||||
@@ -209,7 +209,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
}
|
||||
|
||||
# Test with minimal required parameters
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
result = Signatures.create_signature(conn, params)
|
||||
@@ -416,7 +416,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
nil
|
||||
]
|
||||
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
Enum.each(malformed_conns, fn conn ->
|
||||
# This should either crash (expected) or return error
|
||||
@@ -462,17 +462,16 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
tasks =
|
||||
Enum.map(1..3, fn i ->
|
||||
Task.async(fn ->
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
params = %{"solar_system_id" => "3000014#{i}"}
|
||||
Signatures.create_signature(conn, params)
|
||||
end)
|
||||
params = %{"solar_system_id" => 30_000_140 + i}
|
||||
result = Signatures.create_signature(conn, params)
|
||||
# We expect either system_not_found (system doesn't exist in test)
|
||||
# or the MapTestHelpers would have caught the map server error
|
||||
assert {:error, :system_not_found} = result
|
||||
end)
|
||||
end)
|
||||
|
||||
# All tasks should complete without crashing
|
||||
Enum.each(tasks, fn task ->
|
||||
assert Task.await(task) == :ok
|
||||
end)
|
||||
Enum.each(tasks, &Task.await/1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -669,7 +668,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
}
|
||||
]
|
||||
|
||||
params = %{"solar_system_id" => "30000142"}
|
||||
params = %{"solar_system_id" => 30_000_142}
|
||||
|
||||
Enum.each(assign_variations, fn assigns ->
|
||||
conn = %{assigns: assigns}
|
||||
|
||||
Reference in New Issue
Block a user