fix(Core): Fixed map character tracking issues

This commit is contained in:
Dmitry Popov
2025-05-26 12:09:09 +02:00
parent 3e9241892e
commit a298152bc8
9 changed files with 419 additions and 18 deletions
+57 -2
View File
@@ -3,7 +3,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
data_layer: AshPostgres.DataLayer,
extensions: [AshCloak]
@derive {Jason.Encoder,
only: [
@@ -24,8 +25,10 @@ defmodule WandererApp.Api.MapCharacterSettings do
code_interface do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:update, action: :update)
define(:read_by_map, action: :read_by_map)
define(:read_by_map_and_character, action: :read_by_map_and_character)
define(:by_map_filtered, action: :by_map_filtered)
define(:tracked_by_map_filtered, action: :tracked_by_map_filtered)
define(:tracked_by_character, action: :tracked_by_character)
@@ -45,7 +48,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
:tracked
]
defaults [:read, :update, :destroy]
defaults [:read, :destroy]
create :create do
primary? true
@@ -92,6 +95,15 @@ defmodule WandererApp.Api.MapCharacterSettings do
filter(expr(map_id == ^arg(:map_id)))
end
read :read_by_map_and_character do
get? true
argument(:map_id, :string, allow_nil?: false)
argument(:character_id, :uuid, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and character_id == ^arg(:character_id)))
end
read :tracked_by_map_all do
argument(:map_id, :string, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and tracked == true))
@@ -102,6 +114,20 @@ defmodule WandererApp.Api.MapCharacterSettings do
filter(expr(character_id == ^arg(:character_id) and tracked == true))
end
update :update do
primary? true
require_atomic? false
accept([
:ship,
:ship_name,
:ship_item_id,
:solar_system_id,
:structure_id,
:station_id
])
end
update :track do
accept [:map_id, :character_id]
argument :map_id, :string, allow_nil?: false
@@ -159,6 +185,28 @@ defmodule WandererApp.Api.MapCharacterSettings do
end
end
cloak do
vault(WandererApp.Vault)
attributes([
:ship,
:ship_name,
:ship_item_id,
:solar_system_id,
:structure_id,
:station_id
])
decrypt_by_default([
:ship,
:ship_name,
:ship_item_id,
:solar_system_id,
:structure_id,
:station_id
])
end
attributes do
uuid_primary_key :id
@@ -172,6 +220,13 @@ defmodule WandererApp.Api.MapCharacterSettings do
allow_nil? true
end
attribute :solar_system_id, :integer
attribute :structure_id, :integer
attribute :station_id, :integer
attribute :ship, :integer
attribute :ship_name, :string
attribute :ship_item_id, :integer
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end
+58 -2
View File
@@ -7,6 +7,15 @@ defmodule WandererApp.Character do
@read_character_wallet_scope "esi-wallet.read_character_wallet.v1"
@read_corp_wallet_scope "esi-wallet.read_corporation_wallets.v1"
@default_character_tracking_data %{
solar_system_id: nil,
structure_id: nil,
station_id: nil,
ship: nil,
ship_name: nil,
ship_item_id: nil
}
@decorate cacheable(
cache: WandererApp.Cache,
key: "characters-#{character_eve_id}"
@@ -45,6 +54,32 @@ defmodule WandererApp.Character do
end
end
def get_map_character(map_id, character_id) do
case get_character(character_id) do
{:ok, character} ->
{:ok,
character
|> maybe_merge_map_character_settings(
map_id,
WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
)}
_ ->
{:ok, nil}
end
end
def get_map_character!(map_id, character_id) do
case get_map_character(map_id, character_id) do
{:ok, character} ->
character
_ ->
Logger.error("Failed to get map character #{map_id} #{character_id}")
nil
end
end
def get_character_eve_ids!(character_ids),
do:
character_ids
@@ -146,7 +181,7 @@ defmodule WandererApp.Character do
params: opts[:params]
) do
{:ok, result} ->
{:ok, result |> _prepare_search_results()}
{:ok, result |> prepare_search_results()}
{:error, error} ->
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
@@ -208,7 +243,28 @@ defmodule WandererApp.Character do
end
end
defp _prepare_search_results(result) do
defp maybe_merge_map_character_settings(character, map_id, true), do: character
defp maybe_merge_map_character_settings(
%{id: character_id} = character,
map_id,
_character_is_present
) do
WandererApp.MapCharacterSettingsRepo.get(map_id, character_id)
|> case do
{:ok, settings} when not is_nil(settings) ->
character
|> Map.put(:online, false)
|> Map.merge(settings)
_ ->
character
|> Map.put(:online, false)
|> Map.merge(@default_character_tracking_data)
end
end
defp prepare_search_results(result) do
{:ok, characters} =
_load_eve_info(Map.get(result, "character"), :get_character_info, &_map_character_info/1)
@@ -220,6 +220,18 @@ defmodule WandererApp.Character.TrackerManager.Impl do
track: false
})
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, _updated} =
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
ship: character.ship,
ship_name: character.ship_name,
ship_item_id: character.ship_item_id,
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
})
WandererApp.Character.update_character_state(character_id, character_state)
end
end,
@@ -246,7 +258,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def handle_info(_event, state),
do: state
defp character_is_present(map_id, character_id) do
def character_is_present(map_id, character_id) do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
+1 -1
View File
@@ -96,7 +96,7 @@ defmodule WandererApp.Map do
map_id
|> get_map!()
|> Map.get(:characters, [])
|> Enum.map(&WandererApp.Character.get_character!(&1))
|> Enum.map(fn character_id -> WandererApp.Character.get_map_character!(map_id, character_id) end)
def list_systems(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:systems, Map.new()) |> Map.values()}
@@ -17,13 +17,13 @@ defmodule WandererApp.Map.Server.CharactersImpl do
map_id: map_id,
tracked: track_character
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
{:ok, character} <- WandererApp.Character.get_map_character(map_id, character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, character} = WandererApp.Character.get_map_character(map_id, character_id)
Impl.broadcast!(map_id, :character_added, character)
:ok
end
@@ -35,7 +35,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def remove_character(map_id, character_id) do
Task.start_link(fn ->
with :ok <- WandererApp.Map.remove_character(map_id, character_id),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
{:ok, character} <- WandererApp.Character.get_map_character(map_id, character_id) do
Impl.broadcast!(map_id, :character_removed, character)
:telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1})
@@ -90,17 +90,26 @@ defmodule WandererApp.Map.Server.CharactersImpl do
do:
character_ids
|> Enum.each(fn character_id ->
# TODO consider storing character data in MapCharacterSettings
# remove_character(map_id, character_id)
if is_character_map_active?(map_id, character_id) do
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: false
})
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: false
})
Impl.broadcast!(map_id, :untrack_character, character_id)
Impl.broadcast!(map_id, :untrack_character, character_id)
end
end)
def is_character_map_active?(map_id, character_id) do
case WandererApp.Character.get_character_state(character_id) do
{:ok, %{active_maps: active_maps}} ->
map_id in active_maps
_ ->
false
end
end
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
@@ -276,7 +285,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
defp update_character(map_id, character_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, character} = WandererApp.Character.get_map_character(map_id, character_id)
Impl.broadcast!(map_id, :character_updated, character)
end
@@ -451,6 +451,14 @@ defmodule WandererApp.Map.Server.Impl do
|> WandererApp.Map.update_subscription_settings!(subscription_settings)
|> WandererApp.Map.add_systems!(systems)
|> WandererApp.Map.add_connections!(connections)
|> WandererApp.Map.add_characters!(characters)
character_ids =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:characters, [])
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
%{state | map: map, map_opts: map_options(options)}
end
@@ -1,10 +1,34 @@
defmodule WandererApp.MapCharacterSettingsRepo do
use WandererApp, :repository
def get(map_id, character_id) do
case WandererApp.Api.MapCharacterSettings.read_by_map_and_character(%{
map_id: map_id,
character_id: character_id
}) do
{:ok, settings} ->
{:ok, settings}
{:error, reason} ->
{:ok, nil}
end
end
def create(settings) do
WandererApp.Api.MapCharacterSettings.create(settings)
end
def update(map_id, character_id, updated_settings) do
case get(map_id, character_id) do
{:ok, settings} when not is_nil(settings) ->
settings
|> WandererApp.Api.MapCharacterSettings.update(updated_settings)
{:error, reason} ->
{:ok, nil}
end
end
def get_tracked_by_map_filtered(map_id, character_ids),
do:
WandererApp.Api.MapCharacterSettings.tracked_by_map_filtered(%{
@@ -0,0 +1,31 @@
defmodule WandererApp.Repo.Migrations.AddMapCharacterTrackingInfo do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_character_settings_v1) do
add :encrypted_ship, :binary
add :encrypted_ship_name, :binary
add :encrypted_ship_item_id, :binary
add :encrypted_solar_system_id, :binary
add :encrypted_structure_id, :binary
add :encrypted_station_id, :binary
end
end
def down do
alter table(:map_character_settings_v1) do
remove :encrypted_station_id
remove :encrypted_structure_id
remove :encrypted_solar_system_id
remove :encrypted_ship_item_id
remove :encrypted_ship_name
remove :encrypted_ship
end
end
end
@@ -0,0 +1,206 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "tracked",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "followed",
"type": "boolean"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": true,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_character_settings_v1_map_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "maps_v1"
},
"size": null,
"source": "map_id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": true,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_character_settings_v1_character_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "character_v1"
},
"size": null,
"source": "character_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship_name",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship_item_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_solar_system_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_structure_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_station_id",
"type": "binary"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "3F585D1C545A5264AEFA05502C0F625F9B27B15CA36699DCF37E4F834E6339AE",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "map_character_settings_v1_uniq_map_character_index",
"keys": [
{
"type": "atom",
"value": "map_id"
},
{
"type": "atom",
"value": "character_id"
}
],
"name": "uniq_map_character",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "map_character_settings_v1"
}