From a298152bc8d54ddff52f80326eaee49f61aa783a Mon Sep 17 00:00:00 2001 From: Dmitry Popov Date: Mon, 26 May 2025 12:09:09 +0200 Subject: [PATCH] fix(Core): Fixed map character tracking issues --- .../api/map_character_settings.ex | 59 ++++- lib/wanderer_app/character.ex | 60 ++++- .../character/tracker_manager_impl.ex | 14 +- lib/wanderer_app/map.ex | 2 +- .../map/server/map_server_characters_impl.ex | 33 ++- .../map/server/map_server_impl.ex | 8 + .../map_character_settings_repo.ex | 24 ++ ...074425_add_map_character_tracking_info.exs | 31 +++ .../20250526074425.json | 206 ++++++++++++++++++ 9 files changed, 419 insertions(+), 18 deletions(-) create mode 100644 priv/repo/migrations/20250526074425_add_map_character_tracking_info.exs create mode 100644 priv/resource_snapshots/repo/map_character_settings_v1/20250526074425.json diff --git a/lib/wanderer_app/api/map_character_settings.ex b/lib/wanderer_app/api/map_character_settings.ex index 099e9e2a..2bd0a82d 100644 --- a/lib/wanderer_app/api/map_character_settings.ex +++ b/lib/wanderer_app/api/map_character_settings.ex @@ -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 diff --git a/lib/wanderer_app/character.ex b/lib/wanderer_app/character.ex index c36ada5b..8cb1868f 100644 --- a/lib/wanderer_app/character.ex +++ b/lib/wanderer_app/character.ex @@ -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) diff --git a/lib/wanderer_app/character/tracker_manager_impl.ex b/lib/wanderer_app/character/tracker_manager_impl.ex index 36195e12..2bdb8165 100644 --- a/lib/wanderer_app/character/tracker_manager_impl.ex +++ b/lib/wanderer_app/character/tracker_manager_impl.ex @@ -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", []) diff --git a/lib/wanderer_app/map.ex b/lib/wanderer_app/map.ex index aca75a48..d4c0fb73 100644 --- a/lib/wanderer_app/map.ex +++ b/lib/wanderer_app/map.ex @@ -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()} diff --git a/lib/wanderer_app/map/server/map_server_characters_impl.ex b/lib/wanderer_app/map/server/map_server_characters_impl.ex index 07891cca..f5fee5c1 100644 --- a/lib/wanderer_app/map/server/map_server_characters_impl.ex +++ b/lib/wanderer_app/map/server/map_server_characters_impl.ex @@ -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 diff --git a/lib/wanderer_app/map/server/map_server_impl.ex b/lib/wanderer_app/map/server/map_server_impl.ex index 094b9109..2a8ca6e2 100644 --- a/lib/wanderer_app/map/server/map_server_impl.ex +++ b/lib/wanderer_app/map/server/map_server_impl.ex @@ -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 diff --git a/lib/wanderer_app/repositories/map_character_settings_repo.ex b/lib/wanderer_app/repositories/map_character_settings_repo.ex index a37c0414..d26688ce 100644 --- a/lib/wanderer_app/repositories/map_character_settings_repo.ex +++ b/lib/wanderer_app/repositories/map_character_settings_repo.ex @@ -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(%{ diff --git a/priv/repo/migrations/20250526074425_add_map_character_tracking_info.exs b/priv/repo/migrations/20250526074425_add_map_character_tracking_info.exs new file mode 100644 index 00000000..f7922990 --- /dev/null +++ b/priv/repo/migrations/20250526074425_add_map_character_tracking_info.exs @@ -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 diff --git a/priv/resource_snapshots/repo/map_character_settings_v1/20250526074425.json b/priv/resource_snapshots/repo/map_character_settings_v1/20250526074425.json new file mode 100644 index 00000000..b49345c1 --- /dev/null +++ b/priv/resource_snapshots/repo/map_character_settings_v1/20250526074425.json @@ -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" +} \ No newline at end of file