diff --git a/assets/js/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx b/assets/js/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx index 474aff45..d2ccd35b 100644 --- a/assets/js/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx +++ b/assets/js/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx @@ -6,6 +6,8 @@ import { PrimeIcons } from 'primereact/api'; import { ContextMenuSystemProps } from '@/hooks/Mapper/components/contexts'; import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks'; import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components'; +import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api'; +import { UserPermission } from '@/hooks/Mapper/types/permissions.ts'; export const useContextMenuSystemItems = ({ onDeleteSystem, @@ -25,6 +27,7 @@ export const useContextMenuSystemItems = ({ const getStatus = useStatusMenu(systems, systemId, onSystemStatus); const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog); const getWaypointMenu = useWaypointMenu(onWaypointSet); + const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]); return useMemo(() => { const system = systemId ? getSystemById(systems, systemId) : undefined; @@ -58,19 +61,25 @@ export const useContextMenuSystemItems = ({ command: onHubToggle, }, ...(system.locked - ? [ - { - label: 'Unlock', - icon: PrimeIcons.LOCK_OPEN, - command: onLockToggle, - }, - ] + ? canLockSystem + ? [ + { + label: 'Unlock', + icon: PrimeIcons.LOCK_OPEN, + command: onLockToggle, + }, + ] + : [] : [ - { - label: 'Lock', - icon: PrimeIcons.LOCK, - command: onLockToggle, - }, + ...(canLockSystem + ? [ + { + label: 'Lock', + icon: PrimeIcons.LOCK, + command: onLockToggle, + }, + ] + : []), { separator: true }, { label: 'Delete', @@ -80,6 +89,7 @@ export const useContextMenuSystemItems = ({ ]), ]; }, [ + canLockSystem, systems, systemId, getTags, diff --git a/assets/js/hooks/Mapper/components/map/MapProvider.tsx b/assets/js/hooks/Mapper/components/map/MapProvider.tsx index a80dfa29..61e51157 100644 --- a/assets/js/hooks/Mapper/components/map/MapProvider.tsx +++ b/assets/js/hooks/Mapper/components/map/MapProvider.tsx @@ -32,6 +32,7 @@ const INITIAL_DATA: MapData = { visibleNodes: new Set(), showKSpaceBG: false, isThickConnections: false, + userPermissions: {}, }; export interface MapContextProps { diff --git a/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx b/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx index 1d6df42d..bd5e5947 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx +++ b/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx @@ -1,6 +1,6 @@ import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils'; -import { createContext, Dispatch, ForwardedRef, forwardRef, RefObject, SetStateAction, useContext } from 'react'; -import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types'; +import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext } from 'react'; +import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types'; import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks'; import { WithChildren } from '@/hooks/Mapper/types/common.ts'; import useLocalStorageState from 'use-local-storage-state'; @@ -25,6 +25,7 @@ const INITIAL_DATA: MapRootData = { selectedSystems: [], selectedConnections: [], + userPermissions: {}, }; export enum InterfaceStoredSettingsProps { diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/index.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/index.ts index 0272837d..62d0e70f 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/index.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/index.ts @@ -1,5 +1,6 @@ export * from './useMapInit'; export * from './useMapUpdated'; +export * from './useMapCheckPermissions'; export * from './useRoutes'; export * from './useCommandsConnections'; export * from './useCommandsSystems'; diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapCheckPermissions.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapCheckPermissions.ts new file mode 100644 index 00000000..d95d7aeb --- /dev/null +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapCheckPermissions.ts @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; +import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; +import { UserPermission } from '@/hooks/Mapper/types/permissions.ts'; + +export const useMapCheckPermissions = (permissions: UserPermission[]) => { + const { + data: { userPermissions }, + } = useMapRootState(); + + return useMemo(() => permissions.every(x => userPermissions[x]), [permissions, userPermissions]); +}; diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts index d2bfad9a..6a3cf9b3 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts @@ -19,6 +19,7 @@ export const useMapInit = () => { user_characters, present_characters, hubs, + user_permissions, }: CommandInit) => { const updateData: Partial = {}; @@ -51,6 +52,10 @@ export const useMapInit = () => { updateData.connections = connections; } + if (user_permissions) { + updateData.userPermissions = user_permissions; + } + if (hubs) { updateData.hubs = hubs; } diff --git a/assets/js/hooks/Mapper/types/index.ts b/assets/js/hooks/Mapper/types/index.ts index 02f0aa4a..f3b0fec1 100644 --- a/assets/js/hooks/Mapper/types/index.ts +++ b/assets/js/hooks/Mapper/types/index.ts @@ -6,3 +6,4 @@ export * from './system'; export * from './mapUnionTypes'; export * from './signatures'; export * from './connectionPassages'; +export * from './permissions'; diff --git a/assets/js/hooks/Mapper/types/mapHandlers.ts b/assets/js/hooks/Mapper/types/mapHandlers.ts index bd22067a..f9430742 100644 --- a/assets/js/hooks/Mapper/types/mapHandlers.ts +++ b/assets/js/hooks/Mapper/types/mapHandlers.ts @@ -4,6 +4,7 @@ import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts'; import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts'; import { RoutesList } from '@/hooks/Mapper/types/routes.ts'; import { Kill } from '@/hooks/Mapper/types/kills.ts'; +import { UserPermissions } from '@/hooks/Mapper/types'; export enum Commands { init = 'init', @@ -58,7 +59,7 @@ export type CommandInit = { characters: CharacterTypeRaw[]; present_characters: string[]; user_characters: string[]; - user_permissions: any; + user_permissions: UserPermissions; hubs: string[]; routes: RoutesList; reset?: boolean; diff --git a/assets/js/hooks/Mapper/types/mapUnionTypes.ts b/assets/js/hooks/Mapper/types/mapUnionTypes.ts index f05a20cd..ab0b51e3 100644 --- a/assets/js/hooks/Mapper/types/mapUnionTypes.ts +++ b/assets/js/hooks/Mapper/types/mapUnionTypes.ts @@ -4,6 +4,7 @@ import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts'; import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts'; import { RoutesList } from '@/hooks/Mapper/types/routes.ts'; import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts'; +import { UserPermissions } from '@/hooks/Mapper/types'; export type MapUnionTypes = { wormholesData: Record; @@ -17,4 +18,5 @@ export type MapUnionTypes = { routes?: RoutesList; kills: Record; connections: SolarSystemConnection[]; + userPermissions: Partial; }; diff --git a/assets/js/hooks/Mapper/types/permissions.ts b/assets/js/hooks/Mapper/types/permissions.ts new file mode 100644 index 00000000..322bc128 --- /dev/null +++ b/assets/js/hooks/Mapper/types/permissions.ts @@ -0,0 +1,19 @@ +export enum UserPermission { + ADMIN_MAP = 'admin_map', + MANAGE_MAP = 'manage_map', + VIEW_SYSTEM = 'view_system', + VIEW_CHARACTER = 'view_character', + VIEW_CONNECTION = 'view_connection', + ADD_SYSTEM = 'add_system', + ADD_CONNECTION = 'add_connection', + UPDATE_SYSTEM = 'update_system', + TRACK_CHARACTER = 'track_character', + DELETE_CONNECTION = 'delete_connection', + DELETE_SYSTEM = 'delete_system', + LOCK_SYSTEM = 'lock_system', + ADD_ACL = 'add_acl', + DELETE_ACL = 'delete_acl', + DELETE_MAP = 'delete_map', +} + +export type UserPermissions = Record; diff --git a/lib/wanderer_app/character.ex b/lib/wanderer_app/character.ex index 9705cb0e..59d73794 100644 --- a/lib/wanderer_app/character.ex +++ b/lib/wanderer_app/character.ex @@ -61,12 +61,18 @@ defmodule WandererApp.Character do end) end - def get_character_state(character_id) do + def get_character_state(character_id, init_if_empty? \\ true) do case Cachex.get(:character_state_cache, character_id) do {:ok, nil} -> - character_state = WandererApp.Character.Tracker.init(character_id: character_id) - Cachex.put(:character_state_cache, character_id, character_state) - {:ok, character_state} + case init_if_empty? do + true -> + character_state = WandererApp.Character.Tracker.init(character_id: character_id) + Cachex.put(:character_state_cache, character_id, character_state) + {:ok, character_state} + + _ -> + {:ok, nil} + end {:ok, character_state} -> {:ok, character_state} diff --git a/lib/wanderer_app/character/tracker_manager_impl.ex b/lib/wanderer_app/character/tracker_manager_impl.ex index 94de40f3..bef0f2b3 100644 --- a/lib/wanderer_app/character/tracker_manager_impl.ex +++ b/lib/wanderer_app/character/tracker_manager_impl.ex @@ -83,22 +83,28 @@ defmodule WandererApp.Character.TrackerManager.Impl do end def stop_tracking(%__MODULE__{} = state, character_id) do - {:ok, %{start_time: start_time}} = WandererApp.Character.get_character_state(character_id) + {:ok, character_state} = WandererApp.Character.get_character_state(character_id, false) - 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}) - Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end) + case character_state do + nil -> + state - WandererApp.Cache.delete("character:#{character_id}:location_started") - WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id") - WandererApp.Character.delete_character_state(character_id) + %{start_time: start_time} -> + duration = DateTime.diff(DateTime.utc_now(), start_time, :second) + :telemetry.execute([:wanderer_app, :character, :tracker, :running], %{duration: duration}) + :telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1}) + Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end) - tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end) + WandererApp.Cache.delete("character:#{character_id}:location_started") + WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id") + WandererApp.Character.delete_character_state(character_id) - WandererApp.Cache.insert("tracked_characters", tracked_characters) + tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end) - %{state | characters: tracked_characters} + WandererApp.Cache.insert("tracked_characters", tracked_characters) + + %{state | characters: tracked_characters} + end end def update_track_settings( @@ -429,8 +435,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do end def handle_info({:stop_track, character_id}, state) do - @logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end) - stop_tracking(state, character_id) + WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking") + |> case do + false -> + WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true) + Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end) + state = state |> stop_tracking(character_id) + WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking") + + state + + _ -> + state + end end def handle_info(_event, state), diff --git a/lib/wanderer_app/map/map_server_impl.ex b/lib/wanderer_app/map/map_server_impl.ex index 476073ca..070b7890 100644 --- a/lib/wanderer_app/map/map_server_impl.ex +++ b/lib/wanderer_app/map/map_server_impl.ex @@ -4,7 +4,7 @@ defmodule WandererApp.Map.Server.Impl do """ require Logger - alias WandererApp.Map.Server.ConnectionsImpl + alias WandererApp.Map.Server.{AclsImpl, CharactersImpl, ConnectionsImpl, SystemsImpl} @enforce_keys [ :map_id @@ -21,13 +21,8 @@ defmodule WandererApp.Map.Server.Impl do @characters_cleanup_timeout :timer.minutes(1) @connections_cleanup_timeout :timer.minutes(2) - @system_auto_expire_minutes 15 - - @ddrt Application.compile_env(:wanderer_app, :ddrt) - @logger Application.compile_env(:wanderer_app, :logger) @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client) @backup_state_timeout :timer.minutes(1) - @system_inactive_timeout :timer.minutes(15) @update_presence_timeout :timer.seconds(1) @update_characters_timeout :timer.seconds(1) @update_tracked_characters_timeout :timer.seconds(1) @@ -37,7 +32,7 @@ defmodule WandererApp.Map.Server.Impl do def init(args) do map_id = args[:map_id] - @logger.info("Starting map server for #{map_id}") + Logger.info("Starting map server for #{map_id}") ErrorTracker.set_context(%{map_id: map_id}) WandererApp.Cache.insert("map_#{map_id}:started", false) @@ -70,17 +65,17 @@ defmodule WandererApp.Map.Server.Impl do systems, connections ) - |> init_map_systems(systems) + |> SystemsImpl.init_map_systems(systems) |> init_map_cache() else error -> - @logger.error("Failed to load map state: #{inspect(error, pretty: true)}") + Logger.error("Failed to load map state: #{inspect(error, pretty: true)}") state end end def start_map(%__MODULE__{map: map, map_id: map_id} = state) do - with :ok <- track_acls(map.acls |> Enum.map(& &1.id)) do + with :ok <- AclsImpl.track_acls(map.acls |> Enum.map(& &1.id)) do @pubsub_client.subscribe( WandererApp.PubSub, "maps:#{map_id}" @@ -103,13 +98,13 @@ defmodule WandererApp.Map.Server.Impl do state else error -> - @logger.error("Failed to start map: #{inspect(error, pretty: true)}") + Logger.error("Failed to start map: #{inspect(error, pretty: true)}") state end end def stop_map(%{map_id: map_id} = state) do - @logger.debug(fn -> "Stopping map server for #{map_id}" end) + Logger.debug(fn -> "Stopping map server for #{map_id}" end) WandererApp.Cache.delete("map_#{map_id}:started") @@ -121,231 +116,49 @@ defmodule WandererApp.Map.Server.Impl do def get_map(%{map: map} = _state), do: {:ok, map} - def get_characters(%{map_id: map_id} = _state), - do: {:ok, map_id |> WandererApp.Map.list_characters()} + defdelegate get_characters(state), to: CharactersImpl - def add_character(%{map_id: map_id} = state, %{id: character_id} = character, track_character) do - Task.start_link(fn -> - with :ok <- map_id |> WandererApp.Map.add_character(character), - {:ok, _} <- - WandererApp.MapCharacterSettingsRepo.create(%{ - character_id: character_id, - map_id: map_id, - tracked: track_character - }), - {:ok, character} <- WandererApp.Character.get_character(character_id) do - 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) - broadcast!(map_id, :character_added, character) - :ok - end - end) - - state - end + defdelegate add_character(state, character, track_character), to: CharactersImpl def remove_character(%{map_id: map_id} = state, 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 - broadcast!(map_id, :character_removed, character) - - :telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1}) - - :ok - else - {:error, _error} -> - :ok - end - end) + CharactersImpl.remove_character(map_id, character_id) state end def untrack_characters(%{map_id: map_id} = state, characters_ids) do - map_id - |> _untrack_characters(characters_ids) + CharactersImpl.untrack_characters(map_id, characters_ids) state end - def add_system( - %{map_id: map_id} = state, - %{ - solar_system_id: solar_system_id - } = system_info, - user_id, - character_id - ) do - case map_id |> WandererApp.Map.check_location(%{solar_system_id: solar_system_id}) do - {:ok, _location} -> - state |> _add_system(system_info, user_id, character_id) + defdelegate add_system(state, system_info, user_id, character_id), to: SystemsImpl - {:error, :already_exists} -> - state - end - end + defdelegate delete_systems( + state, + removed_ids, + user_id, + character_id + ), + to: SystemsImpl - def update_system_name( - state, - update - ), - do: state |> update_system(:update_name, [:name], update) + defdelegate update_system_name(state, update), to: SystemsImpl - def update_system_description( - state, - update - ), - do: state |> update_system(:update_description, [:description], update) + defdelegate update_system_description(state, update), to: SystemsImpl - def update_system_status( - state, - update - ), - do: state |> update_system(:update_status, [:status], update) + defdelegate update_system_status(state, update), to: SystemsImpl - def update_system_tag( - state, - update - ), - do: state |> update_system(:update_tag, [:tag], update) + defdelegate update_system_tag(state, update), to: SystemsImpl - def update_system_locked( - state, - update - ), - do: state |> update_system(:update_locked, [:locked], update) + defdelegate update_system_locked(state, update), to: SystemsImpl - def update_system_labels( - state, - update - ), - do: state |> update_system(:update_labels, [:labels], update) + defdelegate update_system_labels(state, update), to: SystemsImpl - def update_system_position( - %{rtree_name: rtree_name} = state, - update - ), - do: - state - |> update_system( - :update_position, - [:position_x, :position_y], - update, - fn updated_system -> - @ddrt.update( - updated_system.solar_system_id, - WandererApp.Map.PositionCalculator.get_system_bounding_rect(updated_system), - rtree_name - ) - end - ) + defdelegate update_system_position(state, update), to: SystemsImpl - def add_hub( - %{map_id: map_id} = state, - hub_info - ) do - with :ok <- WandererApp.Map.add_hub(map_id, hub_info), - {:ok, hubs} = map_id |> WandererApp.Map.list_hubs(), - {:ok, _} <- - WandererApp.MapRepo.update_hubs(map_id, hubs) do - broadcast!(map_id, :update_map, %{hubs: hubs}) - state - else - error -> - @logger.error("Failed to add hub: #{inspect(error, pretty: true)}") - state - end - end + defdelegate add_hub(state, hub_info), to: SystemsImpl - def remove_hub( - %{map_id: map_id} = state, - hub_info - ) do - with :ok <- WandererApp.Map.remove_hub(map_id, hub_info), - {:ok, hubs} = map_id |> WandererApp.Map.list_hubs(), - {:ok, _} <- - WandererApp.MapRepo.update_hubs(map_id, hubs) do - broadcast!(map_id, :update_map, %{hubs: hubs}) - state - else - error -> - @logger.error("Failed to remove hub: #{inspect(error, pretty: true)}") - state - end - end - - def delete_systems( - %{map_id: map_id, rtree_name: rtree_name} = state, - removed_ids, - user_id, - character_id - ) do - connections_to_remove = - removed_ids - |> Enum.map(fn solar_system_id -> - WandererApp.Map.find_connections(map_id, solar_system_id) - end) - |> List.flatten() - |> Enum.uniq_by(& &1.id) - - :ok = WandererApp.Map.remove_connections(map_id, connections_to_remove) - :ok = WandererApp.Map.remove_systems(map_id, removed_ids) - - removed_ids - |> Enum.each(fn solar_system_id -> - map_id - |> WandererApp.MapSystemRepo.remove_from_map(solar_system_id) - |> case do - {:ok, _} -> - :ok - - {:error, error} -> - @logger.error("Failed to remove system from map: #{inspect(error, pretty: true)}") - :ok - end - end) - - connections_to_remove - |> Enum.each(fn connection -> - @logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end) - WandererApp.MapConnectionRepo.destroy(map_id, connection) - end) - - @ddrt.delete(removed_ids, rtree_name) - - broadcast!(map_id, :remove_connections, connections_to_remove) - broadcast!(map_id, :systems_removed, removed_ids) - - case not is_nil(user_id) do - true -> - {:ok, _} = - WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{ - character_id: character_id, - user_id: user_id, - map_id: map_id, - solar_system_ids: removed_ids - }) - - :telemetry.execute( - [:wanderer_app, :map, :systems, :remove], - %{count: removed_ids |> Enum.count()} - ) - - :ok - - _ -> - :ok - end - - state - end + defdelegate remove_hub(state, hub_info), to: SystemsImpl defdelegate add_connection(state, connection_info), to: ConnectionsImpl @@ -391,78 +204,7 @@ defmodule WandererApp.Map.Server.Impl do def handle_event(:update_characters, %{map_id: map_id} = state) do Process.send_after(self(), :update_characters, @update_characters_timeout) - WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", []) - |> Enum.map(fn character_id -> - Task.start_link(fn -> - character_updates = - maybe_update_online(map_id, character_id) ++ - maybe_update_location(map_id, character_id) ++ - maybe_update_ship(map_id, character_id) ++ - maybe_update_alliance(map_id, character_id) ++ - maybe_update_corporation(map_id, character_id) - - character_updates - |> Enum.filter(fn update -> update != :skip end) - |> Enum.map(fn update -> - update - |> case do - {:character_location, location_info, old_location_info} -> - update_location( - character_id, - location_info, - old_location_info, - state - ) - - :broadcast - - {:character_ship, _info} -> - :broadcast - - {:character_online, _info} -> - :broadcast - - {:character_alliance, _info} -> - WandererApp.Cache.insert_or_update( - "map_#{map_id}:invalidate_character_ids", - [character_id], - fn ids -> - [character_id | ids] - end - ) - - :broadcast - - {:character_corporation, _info} -> - WandererApp.Cache.insert_or_update( - "map_#{map_id}:invalidate_character_ids", - [character_id], - fn ids -> - [character_id | ids] - end - ) - - :broadcast - - _ -> - :skip - end - end) - |> Enum.filter(fn update -> update != :skip end) - |> Enum.uniq() - |> Enum.each(fn update -> - case update do - :broadcast -> - _update_character(map_id, character_id) - - _ -> - :ok - end - end) - - :ok - end) - end) + CharactersImpl.update_characters(state) state end @@ -470,25 +212,7 @@ defmodule WandererApp.Map.Server.Impl do def handle_event(:update_tracked_characters, %{map_id: map_id} = state) do Process.send_after(self(), :update_tracked_characters, @update_tracked_characters_timeout) - Task.start_link(fn -> - {:ok, map_tracked_character_ids} = - map_id - |> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all() - |> case do - {:ok, settings} -> {:ok, settings |> Enum.map(&Map.get(&1, :character_id))} - _ -> {: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) - - WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters) - - :ok - end) + CharactersImpl.update_tracked_characters(map_id) state end @@ -510,90 +234,15 @@ defmodule WandererApp.Map.Server.Impl do def handle_event( {:map_acl_updated, added_acls, removed_acls}, - %{map_id: map_id, map: old_map} = state + state ) do - {:ok, map} = - WandererApp.MapRepo.get(map_id, - acls: [ - :owner_id, - members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id] - ] - ) - - track_acls(added_acls) - - result = - (added_acls ++ removed_acls) - |> Task.async_stream( - fn acl_id -> - update_acl(acl_id) - end, - max_concurrency: 10, - timeout: :timer.seconds(15) - ) - |> Enum.reduce( - %{ - eve_alliance_ids: [], - eve_character_ids: [], - eve_corporation_ids: [] - }, - fn result, acc -> - case result do - {:ok, val} -> - {:ok, - %{ - eve_alliance_ids: eve_alliance_ids, - eve_character_ids: eve_character_ids, - eve_corporation_ids: eve_corporation_ids - }} = val - - %{ - acc - | eve_alliance_ids: eve_alliance_ids ++ acc.eve_alliance_ids, - eve_character_ids: eve_character_ids ++ acc.eve_character_ids, - eve_corporation_ids: eve_corporation_ids ++ acc.eve_corporation_ids - } - - error -> - @logger.error("Failed to update map #{map_id} acl: #{inspect(error, pretty: true)}") - - acc - end - end - ) - - map_update = %{acls: map.acls, scope: map.scope} - - WandererApp.Map.update_map(map_id, map_update) - - broadcast_acl_updates({:ok, result}, map_id) - - %{state | map: Map.merge(old_map, map_update)} + state |> AclsImpl.handle_map_acl_updated(added_acls, removed_acls) end - def handle_event({:acl_updated, %{acl_id: acl_id}}, %{map_id: map_id, map: old_map} = state) do - {:ok, map} = - WandererApp.MapRepo.get(map_id, - acls: [ - :owner_id, - members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id] - ] - ) + def handle_event({:acl_updated, %{acl_id: acl_id}}, %{map_id: map_id} = state) do + AclsImpl.handle_acl_updated(map_id, acl_id) - if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do - map_update = %{acls: map.acls} - - WandererApp.Map.update_map(map_id, map_update) - - :ok = - acl_id - |> update_acl() - |> broadcast_acl_updates(map_id) - - state - else - state - end + state end def handle_event(:cleanup_connections, state) do @@ -605,69 +254,7 @@ defmodule WandererApp.Map.Server.Impl do def handle_event(:cleanup_characters, %{map_id: map_id, map: %{owner_id: owner_id}} = state) do Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout) - {:ok, invalidate_character_ids} = - WandererApp.Cache.lookup( - "map_#{map_id}:invalidate_character_ids", - [] - ) - - invalidate_character_ids - |> Task.async_stream( - fn character_id -> - character_id - |> WandererApp.Character.get_character() - |> case do - {:ok, character} -> - acls = - map_id - |> WandererApp.Map.get_map!() - |> Map.get(:acls, []) - - [character_permissions] = - WandererApp.Permissions.check_characters_access([character], acls) - - map_permissions = - WandererApp.Permissions.get_map_permissions( - character_permissions, - owner_id, - [character_id] - ) - - case map_permissions do - %{view_system: false} -> - {:remove_character, character_id} - - %{track_character: false} -> - {:remove_character, character_id} - - _ -> - :ok - end - - _ -> - :ok - end - end, - timeout: :timer.seconds(60), - max_concurrency: System.schedulers_online(), - on_timeout: :kill_task - ) - |> Enum.each(fn - {:ok, {:remove_character, character_id}} -> - state |> remove_and_untrack_characters([character_id]) - :ok - - {:ok, _result} -> - :ok - - {:error, reason} -> - @logger.error("Error in cleanup_characters: #{inspect(reason)}") - end) - - WandererApp.Cache.insert( - "map_#{map_id}:invalidate_character_ids", - [] - ) + CharactersImpl.cleanup_characters(map_id, owner_id) state end @@ -675,44 +262,7 @@ defmodule WandererApp.Map.Server.Impl do def handle_event(:cleanup_systems, %{map_id: map_id} = state) do Process.send_after(self(), :cleanup_systems, @systems_cleanup_timeout) - expired_systems = - map_id - |> WandererApp.Map.list_systems!() - |> Enum.filter(fn %{ - id: system_id, - visible: system_visible, - locked: system_locked, - solar_system_id: solar_system_id - } = _system -> - last_updated_time = - WandererApp.Cache.get("map_#{map_id}:system_#{system_id}:last_activity") - - if system_visible and not system_locked and - (is_nil(last_updated_time) or - DateTime.diff(DateTime.utc_now(), last_updated_time, :minute) >= - @system_auto_expire_minutes) do - no_active_connections? = - map_id - |> WandererApp.Map.find_connections(solar_system_id) - |> Enum.empty?() - - no_active_characters? = - map_id |> WandererApp.Map.get_system_characters(solar_system_id) |> Enum.empty?() - - no_active_connections? and no_active_characters? - else - false - end - end) - |> Enum.map(& &1.solar_system_id) - - case expired_systems |> Enum.empty?() do - false -> - state |> delete_systems(expired_systems, nil, nil) - - _ -> - state - end + state |> SystemsImpl.cleanup_systems() end def handle_event(:subscription_settings_updated, %{map: map, map_id: map_id} = state) do @@ -757,254 +307,11 @@ defmodule WandererApp.Map.Server.Impl do :ok end - defp remove_and_untrack_characters(%{map_id: map_id} = state, character_ids) do - Logger.warning(fn -> - "Map #{map_id} - remove and untrack characters #{inspect(character_ids)}" - end) - - map_id - |> _untrack_characters(character_ids) - - map_id - |> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(character_ids) - |> case do - {:ok, settings} -> - settings - |> Enum.each(fn s -> - s |> WandererApp.MapCharacterSettingsRepo.untrack() - state |> remove_character(s.character_id) - end) - - _ -> - :ok - end - end - defp can_broadcast?(map_id), do: not WandererApp.Cache.lookup!("map_#{map_id}:importing", false) and WandererApp.Cache.lookup!("map_#{map_id}:started", false) - defp update_location( - character_id, - location, - old_location, - %{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state - ) do - case is_nil(old_location.solar_system_id) and - ConnectionsImpl.can_add_location(map.scope, location.solar_system_id) do - true -> - :ok = maybe_add_system(map_id, location, nil, rtree_name, map_opts) - - _ -> - ConnectionsImpl.is_connection_valid( - map.scope, - old_location.solar_system_id, - location.solar_system_id - ) - |> case do - true -> - :ok = maybe_add_system(map_id, location, old_location, rtree_name, map_opts) - :ok = maybe_add_system(map_id, old_location, location, rtree_name, map_opts) - - :ok = - ConnectionsImpl.maybe_add_connection(map_id, location, old_location, character_id) - - _ -> - :ok - end - end - end - - defp maybe_update_location(map_id, character_id) do - WandererApp.Cache.lookup!( - "character:#{character_id}:location_started", - false - ) - |> case do - true -> - {:ok, old_solar_system_id} = - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id") - - {:ok, %{solar_system_id: solar_system_id}} = - WandererApp.Character.get_character(character_id) - - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:solar_system_id", - solar_system_id - ) - - case solar_system_id != old_solar_system_id do - true -> - [ - {:character_location, %{solar_system_id: solar_system_id}, - %{solar_system_id: old_solar_system_id}} - ] - - _ -> - [:skip] - end - - false -> - {:ok, old_solar_system_id} = - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id") - - {:ok, %{solar_system_id: solar_system_id} = _character} = - WandererApp.Character.get_character(character_id) - - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:solar_system_id", - solar_system_id - ) - - if is_nil(old_solar_system_id) or solar_system_id != old_solar_system_id do - [ - {:character_location, %{solar_system_id: solar_system_id}, %{solar_system_id: nil}} - ] - else - [:skip] - end - end - end - - defp maybe_update_alliance(map_id, character_id) do - with {:ok, old_alliance_id} <- - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:alliance_id"), - {:ok, %{alliance_id: alliance_id}} <- - WandererApp.Character.get_character(character_id) do - case old_alliance_id != alliance_id do - true -> - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:alliance_id", - alliance_id - ) - - [{:character_alliance, %{alliance_id: alliance_id}}] - - _ -> - [:skip] - end - else - error -> - @logger.error("Failed to update alliance: #{inspect(error, pretty: true)}") - [:skip] - end - end - - defp maybe_update_corporation(map_id, character_id) do - with {:ok, old_corporation_id} <- - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:corporation_id"), - {:ok, %{corporation_id: corporation_id}} <- - WandererApp.Character.get_character(character_id) do - case old_corporation_id != corporation_id do - true -> - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:corporation_id", - corporation_id - ) - - [{:character_corporation, %{corporation_id: corporation_id}}] - - _ -> - [:skip] - end - else - error -> - @logger.error("Failed to update corporation: #{inspect(error, pretty: true)}") - [:skip] - end - end - - defp maybe_update_online(map_id, character_id) do - with {:ok, old_online} <- - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:online"), - {:ok, %{online: online}} <- - WandererApp.Character.get_character(character_id) do - case old_online != online do - true -> - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:online", - online - ) - - [{:character_online, %{online: online}}] - - _ -> - [:skip] - end - else - error -> - @logger.error("Failed to update online: #{inspect(error, pretty: true)}") - [:skip] - end - end - - defp maybe_update_ship(map_id, character_id) do - with {:ok, old_ship_type_id} <- - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_type_id"), - {:ok, old_ship_name} <- - WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_name"), - {:ok, %{ship: ship_type_id, ship_name: ship_name}} <- - WandererApp.Character.get_character(character_id) do - case old_ship_type_id != ship_type_id or - old_ship_name != ship_name do - true -> - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:ship_type_id", - ship_type_id - ) - - WandererApp.Cache.insert( - "map:#{map_id}:character:#{character_id}:ship_name", - ship_name - ) - - [{:character_ship, %{ship: ship_type_id, ship_name: ship_name}}] - - _ -> - [:skip] - end - else - error -> - @logger.error("Failed to update ship: #{inspect(error, pretty: true)}") - [:skip] - end - end - - defp update_system( - %{map_id: map_id} = state, - update_method, - attributes, - update, - callback_fn \\ nil - ) do - with :ok <- WandererApp.Map.update_system_by_solar_system_id(map_id, update), - {:ok, system} <- - WandererApp.MapSystemRepo.get_by_map_and_solar_system_id( - map_id, - update.solar_system_id - ), - {:ok, update_map} <- get_update_map(update, attributes) do - {:ok, updated_system} = - apply(WandererApp.MapSystemRepo, update_method, [ - system, - update_map - ]) - - if not is_nil(callback_fn) do - callback_fn.(updated_system) - end - - update_map_system_last_activity(map_id, updated_system) - - state - else - error -> - @logger.error("Fail ed to update system: #{inspect(error, pretty: true)}") - state - end - end - def get_update_map(update, attributes), do: {:ok, @@ -1012,105 +319,6 @@ defmodule WandererApp.Map.Server.Impl do map |> Map.put_new(attribute, get_in(update, [Access.key(attribute)])) end)} - defp _add_system( - %{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state, - %{ - solar_system_id: solar_system_id, - coordinates: coordinates - } = system_info, - user_id, - character_id - ) do - %{"x" => x, "y" => y} = - coordinates - |> case do - %{"x" => x, "y" => y} -> - %{"x" => x, "y" => y} - - _ -> - %{x: x, y: y} = - WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts) - - %{"x" => x, "y" => y} - end - - {:ok, system} = - case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do - {:ok, existing_system} when not is_nil(existing_system) -> - use_old_coordinates = Map.get(system_info, :use_old_coordinates, false) - - if use_old_coordinates do - @ddrt.insert( - {solar_system_id, - WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ - position_x: existing_system.position_x, - position_y: existing_system.position_y - })}, - rtree_name - ) - - existing_system - |> WandererApp.MapSystemRepo.update_visible(%{visible: true}) - else - @ddrt.insert( - {solar_system_id, - WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ - position_x: x, - position_y: y - })}, - rtree_name - ) - - existing_system - |> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y}) - |> WandererApp.MapSystemRepo.cleanup_labels!(map_opts) - |> WandererApp.MapSystemRepo.cleanup_tags!() - |> WandererApp.MapSystemRepo.update_visible(%{visible: true}) - end - - _ -> - {:ok, solar_system_info} = - WandererApp.CachedInfo.get_system_static_info(solar_system_id) - - @ddrt.insert( - {solar_system_id, - WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ - position_x: x, - position_y: y - })}, - rtree_name - ) - - WandererApp.MapSystemRepo.create(%{ - map_id: map_id, - solar_system_id: solar_system_id, - name: solar_system_info.solar_system_name, - position_x: x, - position_y: y - }) - end - - :ok = map_id |> WandererApp.Map.add_system(system) - - WandererApp.Cache.put( - "map_#{map_id}:system_#{system.id}:last_activity", - DateTime.utc_now(), - ttl: @system_inactive_timeout - ) - - broadcast!(map_id, :add_system, system) - - {:ok, _} = - WandererApp.User.ActivityTracker.track_map_event(:system_added, %{ - character_id: character_id, - user_id: user_id, - map_id: map_id, - solar_system_id: solar_system_id - }) - - state - end - defp save_map_state(%{map_id: map_id} = _state) do systems_last_activity = map_id @@ -1164,15 +372,7 @@ defmodule WandererApp.Map.Server.Impl do systems_last_activity: systems_last_activity, connections_eol_time: connections_eol_time }} -> - systems_last_activity - |> Enum.each(fn {system_id, last_activity} -> - WandererApp.Cache.put( - "map_#{map_id}:system_#{system_id}:last_activity", - last_activity, - ttl: @system_inactive_timeout - ) - end) - + SystemsImpl.init_last_activity_cache(map_id, systems_last_activity) ConnectionsImpl.init_eol_cache(map_id, connections_eol_time) state @@ -1215,26 +415,6 @@ defmodule WandererApp.Map.Server.Impl do %{state | map: map, map_opts: map_opts} end - defp init_map_systems(state, [] = _systems), do: state - - defp init_map_systems(%__MODULE__{map_id: map_id, rtree_name: rtree_name} = state, systems) do - systems - |> Enum.each(fn %{id: system_id, solar_system_id: solar_system_id} = system -> - @ddrt.insert( - {solar_system_id, WandererApp.Map.PositionCalculator.get_system_bounding_rect(system)}, - rtree_name - ) - - WandererApp.Cache.put( - "map_#{map_id}:system_#{system_id}:last_activity", - DateTime.utc_now(), - ttl: @system_inactive_timeout - ) - end) - - state - end - def maybe_import_systems(state, %{"systems" => systems} = _settings, user_id, character_id) do state = systems @@ -1325,19 +505,6 @@ defmodule WandererApp.Map.Server.Impl do end) end - defp update_map_system_last_activity( - map_id, - updated_system - ) do - WandererApp.Cache.put( - "map_#{map_id}:system_#{updated_system.id}:last_activity", - DateTime.utc_now(), - ttl: @system_inactive_timeout - ) - - broadcast!(map_id, :update_system, updated_system) - end - defp update_presence(map_id) do case WandererApp.Cache.lookup!("map_#{map_id}:started", false) and WandererApp.Cache.get_and_remove!("map_#{map_id}:presence_updated", false) do @@ -1356,10 +523,8 @@ defmodule WandererApp.Map.Server.Impl do not Enum.member?(presence_character_ids, character_id) end) - track_characters(presence_character_ids, map_id) - - map_id - |> _untrack_characters(not_present_character_ids) + CharactersImpl.track_characters(map_id, presence_character_ids) + CharactersImpl.untrack_characters(map_id, not_present_character_ids) broadcast!( map_id, @@ -1374,220 +539,4 @@ defmodule WandererApp.Map.Server.Impl do :ok end end - - defp track_acls([]), do: :ok - - defp track_acls([acl_id | rest]) do - track_acl(acl_id) - track_acls(rest) - end - - defp track_acl(acl_id), - do: @pubsub_client.subscribe(WandererApp.PubSub, "acls:#{acl_id}") - - defp track_characters([], _map_id), do: :ok - - defp track_characters([character_id | rest], map_id) do - track_character(character_id, map_id) - track_characters(rest, map_id) - end - - defp track_character(character_id, map_id), - do: - WandererApp.Character.TrackerManager.update_track_settings(character_id, %{ - map_id: map_id, - track: true, - track_online: true, - track_location: true, - track_ship: true - }) - - defp _update_character(map_id, character_id) do - {:ok, character} = WandererApp.Character.get_character(character_id) - broadcast!(map_id, :character_updated, character) - end - - defp _untrack_characters(map_id, character_ids) do - character_ids - |> Enum.each(fn character_id -> - WandererApp.Character.TrackerManager.update_track_settings(character_id, %{ - map_id: map_id, - track: false - }) - end) - end - - defp maybe_add_system(map_id, location, old_location, rtree_name, map_opts) - when not is_nil(location) do - case WandererApp.Map.check_location(map_id, location) do - {:ok, location} -> - {:ok, position} = calc_new_system_position(map_id, old_location, rtree_name, map_opts) - - case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id( - map_id, - location.solar_system_id - ) do - {:ok, existing_system} when not is_nil(existing_system) -> - {:ok, updated_system} = - existing_system - |> WandererApp.MapSystemRepo.update_position!(%{ - position_x: position.x, - position_y: position.y - }) - |> WandererApp.MapSystemRepo.cleanup_labels!(map_opts) - |> WandererApp.MapSystemRepo.update_visible!(%{visible: true}) - |> WandererApp.MapSystemRepo.cleanup_tags() - - @ddrt.insert( - {existing_system.solar_system_id, - WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ - position_x: position.x, - position_y: position.y - })}, - rtree_name - ) - - WandererApp.Cache.put( - "map_#{map_id}:system_#{updated_system.id}:last_activity", - DateTime.utc_now(), - ttl: @system_inactive_timeout - ) - - WandererApp.Map.add_system(map_id, updated_system) - - broadcast!(map_id, :add_system, updated_system) - :ok - - _ -> - {:ok, solar_system_info} = - WandererApp.CachedInfo.get_system_static_info(location.solar_system_id) - - WandererApp.MapSystemRepo.create(%{ - map_id: map_id, - solar_system_id: location.solar_system_id, - name: solar_system_info.solar_system_name, - position_x: position.x, - position_y: position.y - }) - |> case do - {:ok, new_system} -> - @ddrt.insert( - {new_system.solar_system_id, - WandererApp.Map.PositionCalculator.get_system_bounding_rect(new_system)}, - rtree_name - ) - - WandererApp.Cache.put( - "map_#{map_id}:system_#{new_system.id}:last_activity", - DateTime.utc_now(), - ttl: @system_inactive_timeout - ) - - WandererApp.Map.add_system(map_id, new_system) - broadcast!(map_id, :add_system, new_system) - - :ok - - error -> - @logger.warning("Failed to create system: #{inspect(error, pretty: true)}") - :ok - end - end - - error -> - @logger.debug("Skip adding system: #{inspect(error, pretty: true)}") - :ok - end - end - - defp maybe_add_system(_map_id, _location, _old_location, _rtree_name, _map_opts), do: :ok - - defp calc_new_system_position(map_id, old_location, rtree_name, opts), - do: - {:ok, - map_id - |> WandererApp.Map.find_system_by_location(old_location) - |> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)} - - defp broadcast_acl_updates( - {:ok, - %{ - eve_character_ids: eve_character_ids, - eve_corporation_ids: eve_corporation_ids, - eve_alliance_ids: eve_alliance_ids - }}, - map_id - ) do - eve_character_ids - |> Enum.uniq() - |> Enum.each(fn eve_character_id -> - @pubsub_client.broadcast( - WandererApp.PubSub, - "character:#{eve_character_id}", - :update_permissions - ) - end) - - eve_corporation_ids - |> Enum.uniq() - |> Enum.each(fn eve_corporation_id -> - @pubsub_client.broadcast( - WandererApp.PubSub, - "corporation:#{eve_corporation_id}", - :update_permissions - ) - end) - - eve_alliance_ids - |> Enum.uniq() - |> Enum.each(fn eve_alliance_id -> - @pubsub_client.broadcast( - WandererApp.PubSub, - "alliance:#{eve_alliance_id}", - :update_permissions - ) - end) - - character_ids = - map_id - |> WandererApp.Map.get_map!() - |> Map.get(:characters, []) - - WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids) - - :ok - end - - defp broadcast_acl_updates(_, _map_id), do: :ok - - defp update_acl(acl_id) do - {:ok, %{owner: owner, members: members}} = - WandererApp.AccessListRepo.get(acl_id, [:owner, :members]) - - result = - members - |> Enum.reduce( - %{eve_character_ids: [owner.eve_id], eve_corporation_ids: [], eve_alliance_ids: []}, - fn member, acc -> - case member do - %{eve_character_id: eve_character_id} when not is_nil(eve_character_id) -> - acc - |> Map.put(:eve_character_ids, [eve_character_id | acc.eve_character_ids]) - - %{eve_corporation_id: eve_corporation_id} when not is_nil(eve_corporation_id) -> - acc - |> Map.put(:eve_corporation_ids, [eve_corporation_id | acc.eve_corporation_ids]) - - %{eve_alliance_id: eve_alliance_id} when not is_nil(eve_alliance_id) -> - acc - |> Map.put(:eve_alliance_ids, [eve_alliance_id | acc.eve_alliance_ids]) - - _ -> - acc - end - end - ) - - {:ok, result} - end end diff --git a/lib/wanderer_app/map/server/map_server_acls_impl.ex b/lib/wanderer_app/map/server/map_server_acls_impl.ex new file mode 100644 index 00000000..5165828b --- /dev/null +++ b/lib/wanderer_app/map/server/map_server_acls_impl.ex @@ -0,0 +1,180 @@ +defmodule WandererApp.Map.Server.AclsImpl do + @moduledoc false + + require Logger + + alias WandererApp.Map.Server.Impl + + @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client) + + def handle_map_acl_updated(%{map_id: map_id, map: old_map} = state, added_acls, removed_acls) do + {:ok, map} = + WandererApp.MapRepo.get(map_id, + acls: [ + :owner_id, + members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id] + ] + ) + + track_acls(added_acls) + + result = + (added_acls ++ removed_acls) + |> Task.async_stream( + fn acl_id -> + update_acl(acl_id) + end, + max_concurrency: 10, + timeout: :timer.seconds(15) + ) + |> Enum.reduce( + %{ + eve_alliance_ids: [], + eve_character_ids: [], + eve_corporation_ids: [] + }, + fn result, acc -> + case result do + {:ok, val} -> + {:ok, + %{ + eve_alliance_ids: eve_alliance_ids, + eve_character_ids: eve_character_ids, + eve_corporation_ids: eve_corporation_ids + }} = val + + %{ + acc + | eve_alliance_ids: eve_alliance_ids ++ acc.eve_alliance_ids, + eve_character_ids: eve_character_ids ++ acc.eve_character_ids, + eve_corporation_ids: eve_corporation_ids ++ acc.eve_corporation_ids + } + + error -> + Logger.error("Failed to update map #{map_id} acl: #{inspect(error, pretty: true)}") + + acc + end + end + ) + + map_update = %{acls: map.acls, scope: map.scope} + + WandererApp.Map.update_map(map_id, map_update) + + broadcast_acl_updates({:ok, result}, map_id) + + %{state | map: Map.merge(old_map, map_update)} + end + + def handle_acl_updated(map_id, acl_id) do + {:ok, map} = + WandererApp.MapRepo.get(map_id, + acls: [ + :owner_id, + members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id] + ] + ) + + if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do + WandererApp.Map.update_map(map_id, %{acls: map.acls}) + + :ok = + acl_id + |> update_acl() + |> broadcast_acl_updates(map_id) + end + end + + def track_acls([]), do: :ok + + def track_acls([acl_id | rest]) do + track_acl(acl_id) + track_acls(rest) + end + + defp track_acl(acl_id), + do: @pubsub_client.subscribe(WandererApp.PubSub, "acls:#{acl_id}") + + defp broadcast_acl_updates( + {:ok, + %{ + eve_character_ids: eve_character_ids, + eve_corporation_ids: eve_corporation_ids, + eve_alliance_ids: eve_alliance_ids + }}, + map_id + ) do + eve_character_ids + |> Enum.uniq() + |> Enum.each(fn eve_character_id -> + @pubsub_client.broadcast( + WandererApp.PubSub, + "character:#{eve_character_id}", + :update_permissions + ) + end) + + eve_corporation_ids + |> Enum.uniq() + |> Enum.each(fn eve_corporation_id -> + @pubsub_client.broadcast( + WandererApp.PubSub, + "corporation:#{eve_corporation_id}", + :update_permissions + ) + end) + + eve_alliance_ids + |> Enum.uniq() + |> Enum.each(fn eve_alliance_id -> + @pubsub_client.broadcast( + WandererApp.PubSub, + "alliance:#{eve_alliance_id}", + :update_permissions + ) + end) + + character_ids = + map_id + |> WandererApp.Map.get_map!() + |> Map.get(:characters, []) + + WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids) + + :ok + end + + defp broadcast_acl_updates(_, _map_id), do: :ok + + defp update_acl(acl_id) do + {:ok, %{owner: owner, members: members}} = + WandererApp.AccessListRepo.get(acl_id, [:owner, :members]) + + result = + members + |> Enum.reduce( + %{eve_character_ids: [owner.eve_id], eve_corporation_ids: [], eve_alliance_ids: []}, + fn member, acc -> + case member do + %{eve_character_id: eve_character_id} when not is_nil(eve_character_id) -> + acc + |> Map.put(:eve_character_ids, [eve_character_id | acc.eve_character_ids]) + + %{eve_corporation_id: eve_corporation_id} when not is_nil(eve_corporation_id) -> + acc + |> Map.put(:eve_corporation_ids, [eve_corporation_id | acc.eve_corporation_ids]) + + %{eve_alliance_id: eve_alliance_id} when not is_nil(eve_alliance_id) -> + acc + |> Map.put(:eve_alliance_ids, [eve_alliance_id | acc.eve_alliance_ids]) + + _ -> + acc + end + end + ) + + {:ok, result} + end +end diff --git a/lib/wanderer_app/map/server/map_server_characters_impl.ex b/lib/wanderer_app/map/server/map_server_characters_impl.ex new file mode 100644 index 00000000..690a7313 --- /dev/null +++ b/lib/wanderer_app/map/server/map_server_characters_impl.ex @@ -0,0 +1,459 @@ +defmodule WandererApp.Map.Server.CharactersImpl do + @moduledoc false + + require Logger + + alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl} + + def get_characters(%{map_id: map_id} = _state), + do: {:ok, map_id |> WandererApp.Map.list_characters()} + + def add_character(%{map_id: map_id} = state, %{id: character_id} = character, track_character) do + Task.start_link(fn -> + with :ok <- map_id |> WandererApp.Map.add_character(character), + {:ok, _} <- + WandererApp.MapCharacterSettingsRepo.create(%{ + character_id: character_id, + map_id: map_id, + tracked: track_character + }), + {:ok, character} <- WandererApp.Character.get_character(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) + Impl.broadcast!(map_id, :character_added, character) + :ok + end + end) + + state + end + + 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 + Impl.broadcast!(map_id, :character_removed, character) + + :telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1}) + + :ok + else + {:error, _error} -> + :ok + end + end) + end + + def update_tracked_characters(map_id) do + Task.start_link(fn -> + {:ok, map_tracked_character_ids} = + map_id + |> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all() + |> case do + {:ok, settings} -> {:ok, settings |> Enum.map(&Map.get(&1, :character_id))} + _ -> {: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) + + WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters) + + :ok + end) + end + + def untrack_characters(map_id, character_ids), + do: + character_ids + |> Enum.each(fn character_id -> + WandererApp.Character.TrackerManager.update_track_settings(character_id, %{ + map_id: map_id, + track: false + }) + end) + + def cleanup_characters(map_id, owner_id) do + {:ok, invalidate_character_ids} = + WandererApp.Cache.lookup( + "map_#{map_id}:invalidate_character_ids", + [] + ) + + invalidate_character_ids + |> Task.async_stream( + fn character_id -> + character_id + |> WandererApp.Character.get_character() + |> case do + {:ok, character} -> + acls = + map_id + |> WandererApp.Map.get_map!() + |> Map.get(:acls, []) + + [character_permissions] = + WandererApp.Permissions.check_characters_access([character], acls) + + map_permissions = + WandererApp.Permissions.get_map_permissions( + character_permissions, + owner_id, + [character_id] + ) + + case map_permissions do + %{view_system: false} -> + {:remove_character, character_id} + + %{track_character: false} -> + {:remove_character, character_id} + + _ -> + :ok + end + + _ -> + :ok + end + end, + timeout: :timer.seconds(60), + max_concurrency: System.schedulers_online(), + on_timeout: :kill_task + ) + |> Enum.each(fn + {:ok, {:remove_character, character_id}} -> + remove_and_untrack_characters(map_id, [character_id]) + :ok + + {:ok, _result} -> + :ok + + {: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 + Logger.debug(fn -> + "Map #{map_id} - remove and untrack characters #{inspect(character_ids)}" + end) + + map_id + |> untrack_characters(character_ids) + + map_id + |> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(character_ids) + |> case do + {:ok, settings} -> + settings + |> Enum.each(fn s -> + WandererApp.MapCharacterSettingsRepo.untrack(s) + remove_character(map_id, s.character_id) + end) + + _ -> + :ok + end + end + + def track_characters(_map_id, []), do: :ok + + def track_characters(map_id, [character_id | rest]) do + track_character(map_id, character_id) + track_characters(map_id, rest) + end + + def update_characters(%{map_id: map_id} = state) do + WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", []) + |> Enum.map(fn character_id -> + Task.start_link(fn -> + character_updates = + maybe_update_online(map_id, character_id) ++ + maybe_update_location(map_id, character_id) ++ + maybe_update_ship(map_id, character_id) ++ + maybe_update_alliance(map_id, character_id) ++ + maybe_update_corporation(map_id, character_id) + + character_updates + |> Enum.filter(fn update -> update != :skip end) + |> Enum.map(fn update -> + update + |> case do + {:character_location, location_info, old_location_info} -> + update_location( + character_id, + location_info, + old_location_info, + state + ) + + :broadcast + + {:character_ship, _info} -> + :broadcast + + {:character_online, _info} -> + :broadcast + + {:character_alliance, _info} -> + WandererApp.Cache.insert_or_update( + "map_#{map_id}:invalidate_character_ids", + [character_id], + fn ids -> + [character_id | ids] + end + ) + + :broadcast + + {:character_corporation, _info} -> + WandererApp.Cache.insert_or_update( + "map_#{map_id}:invalidate_character_ids", + [character_id], + fn ids -> + [character_id | ids] + end + ) + + :broadcast + + _ -> + :skip + end + end) + |> Enum.filter(fn update -> update != :skip end) + |> Enum.uniq() + |> Enum.each(fn update -> + case update do + :broadcast -> + update_character(map_id, character_id) + + _ -> + :ok + end + end) + + :ok + end) + end) + end + + defp update_character(map_id, character_id) do + {:ok, character} = WandererApp.Character.get_character(character_id) + Impl.broadcast!(map_id, :character_updated, character) + end + + defp update_location( + character_id, + location, + old_location, + %{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state + ) do + case is_nil(old_location.solar_system_id) and + ConnectionsImpl.can_add_location(map.scope, location.solar_system_id) do + true -> + :ok = SystemsImpl.maybe_add_system(map_id, location, nil, rtree_name, map_opts) + + _ -> + ConnectionsImpl.is_connection_valid( + map.scope, + old_location.solar_system_id, + location.solar_system_id + ) + |> case do + true -> + :ok = + SystemsImpl.maybe_add_system(map_id, location, old_location, rtree_name, map_opts) + + :ok = + SystemsImpl.maybe_add_system(map_id, old_location, location, rtree_name, map_opts) + + :ok = + ConnectionsImpl.maybe_add_connection(map_id, location, old_location, character_id) + + _ -> + :ok + end + end + end + + defp track_character(map_id, character_id), + do: + WandererApp.Character.TrackerManager.update_track_settings(character_id, %{ + map_id: map_id, + track: true, + track_online: true, + track_location: true, + track_ship: true + }) + + defp maybe_update_online(map_id, character_id) do + with {:ok, old_online} <- + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:online"), + {:ok, %{online: online}} <- + WandererApp.Character.get_character(character_id) do + case old_online != online do + true -> + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:online", + online + ) + + [{:character_online, %{online: online}}] + + _ -> + [:skip] + end + else + error -> + Logger.error("Failed to update online: #{inspect(error, pretty: true)}") + [:skip] + end + end + + defp maybe_update_ship(map_id, character_id) do + with {:ok, old_ship_type_id} <- + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_type_id"), + {:ok, old_ship_name} <- + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_name"), + {:ok, %{ship: ship_type_id, ship_name: ship_name}} <- + WandererApp.Character.get_character(character_id) do + case old_ship_type_id != ship_type_id or + old_ship_name != ship_name do + true -> + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:ship_type_id", + ship_type_id + ) + + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:ship_name", + ship_name + ) + + [{:character_ship, %{ship: ship_type_id, ship_name: ship_name}}] + + _ -> + [:skip] + end + else + error -> + Logger.error("Failed to update ship: #{inspect(error, pretty: true)}") + [:skip] + end + end + + defp maybe_update_location(map_id, character_id) do + WandererApp.Cache.lookup!( + "character:#{character_id}:location_started", + false + ) + |> case do + true -> + {:ok, old_solar_system_id} = + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id") + + {:ok, %{solar_system_id: solar_system_id}} = + WandererApp.Character.get_character(character_id) + + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:solar_system_id", + solar_system_id + ) + + case solar_system_id != old_solar_system_id do + true -> + [ + {:character_location, %{solar_system_id: solar_system_id}, + %{solar_system_id: old_solar_system_id}} + ] + + _ -> + [:skip] + end + + false -> + {:ok, old_solar_system_id} = + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id") + + {:ok, %{solar_system_id: solar_system_id} = _character} = + WandererApp.Character.get_character(character_id) + + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:solar_system_id", + solar_system_id + ) + + if is_nil(old_solar_system_id) or solar_system_id != old_solar_system_id do + [ + {:character_location, %{solar_system_id: solar_system_id}, %{solar_system_id: nil}} + ] + else + [:skip] + end + end + end + + defp maybe_update_alliance(map_id, character_id) do + with {:ok, old_alliance_id} <- + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:alliance_id"), + {:ok, %{alliance_id: alliance_id}} <- + WandererApp.Character.get_character(character_id) do + case old_alliance_id != alliance_id do + true -> + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:alliance_id", + alliance_id + ) + + [{:character_alliance, %{alliance_id: alliance_id}}] + + _ -> + [:skip] + end + else + error -> + Logger.error("Failed to update alliance: #{inspect(error, pretty: true)}") + [:skip] + end + end + + defp maybe_update_corporation(map_id, character_id) do + with {:ok, old_corporation_id} <- + WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:corporation_id"), + {:ok, %{corporation_id: corporation_id}} <- + WandererApp.Character.get_character(character_id) do + case old_corporation_id != corporation_id do + true -> + WandererApp.Cache.insert( + "map:#{map_id}:character:#{character_id}:corporation_id", + corporation_id + ) + + [{:character_corporation, %{corporation_id: corporation_id}}] + + _ -> + [:skip] + end + else + error -> + Logger.error("Failed to update corporation: #{inspect(error, pretty: true)}") + [:skip] + end + end +end diff --git a/lib/wanderer_app/map/server/map_server_connections_impl.ex b/lib/wanderer_app/map/server/map_server_connections_impl.ex index dd717a88..1e48a90e 100644 --- a/lib/wanderer_app/map/server/map_server_connections_impl.ex +++ b/lib/wanderer_app/map/server/map_server_connections_impl.ex @@ -1,7 +1,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do - @moduledoc """ - Holds state for a map and exposes an interface to managing the map instance - """ + @moduledoc false + require Logger alias WandererApp.Map.Server.Impl diff --git a/lib/wanderer_app/map/server/map_server_systems_impl.ex b/lib/wanderer_app/map/server/map_server_systems_impl.ex new file mode 100644 index 00000000..be861cab --- /dev/null +++ b/lib/wanderer_app/map/server/map_server_systems_impl.ex @@ -0,0 +1,493 @@ +defmodule WandererApp.Map.Server.SystemsImpl do + @moduledoc false + + require Logger + + alias WandererApp.Map.Server.{Impl} + + @ddrt Application.compile_env(:wanderer_app, :ddrt) + @system_auto_expire_minutes 15 + @system_inactive_timeout :timer.minutes(15) + + def init_last_activity_cache(map_id, systems_last_activity) do + systems_last_activity + |> Enum.each(fn {system_id, last_activity} -> + WandererApp.Cache.put( + "map_#{map_id}:system_#{system_id}:last_activity", + last_activity, + ttl: @system_inactive_timeout + ) + end) + end + + def init_map_systems(state, [] = _systems), do: state + + def init_map_systems(%{map_id: map_id, rtree_name: rtree_name} = state, systems) do + systems + |> Enum.each(fn %{id: system_id, solar_system_id: solar_system_id} = system -> + @ddrt.insert( + {solar_system_id, WandererApp.Map.PositionCalculator.get_system_bounding_rect(system)}, + rtree_name + ) + + WandererApp.Cache.put( + "map_#{map_id}:system_#{system_id}:last_activity", + DateTime.utc_now(), + ttl: @system_inactive_timeout + ) + end) + + state + end + + def add_system( + %{map_id: map_id} = state, + %{ + solar_system_id: solar_system_id + } = system_info, + user_id, + character_id + ) do + case map_id |> WandererApp.Map.check_location(%{solar_system_id: solar_system_id}) do + {:ok, _location} -> + state |> _add_system(system_info, user_id, character_id) + + {:error, :already_exists} -> + state + end + end + + def cleanup_systems(%{map_id: map_id} = state) do + expired_systems = + map_id + |> WandererApp.Map.list_systems!() + |> Enum.filter(fn %{ + id: system_id, + visible: system_visible, + locked: system_locked, + solar_system_id: solar_system_id + } = _system -> + last_updated_time = + WandererApp.Cache.get("map_#{map_id}:system_#{system_id}:last_activity") + + if system_visible and not system_locked and + (is_nil(last_updated_time) or + DateTime.diff(DateTime.utc_now(), last_updated_time, :minute) >= + @system_auto_expire_minutes) do + no_active_connections? = + map_id + |> WandererApp.Map.find_connections(solar_system_id) + |> Enum.empty?() + + no_active_characters? = + map_id |> WandererApp.Map.get_system_characters(solar_system_id) |> Enum.empty?() + + no_active_connections? and no_active_characters? + else + false + end + end) + |> Enum.map(& &1.solar_system_id) + + case expired_systems |> Enum.empty?() do + false -> + state |> delete_systems(expired_systems, nil, nil) + + _ -> + state + end + end + + def update_system_name( + state, + update + ), + do: state |> update_system(:update_name, [:name], update) + + def update_system_description( + state, + update + ), + do: state |> update_system(:update_description, [:description], update) + + def update_system_status( + state, + update + ), + do: state |> update_system(:update_status, [:status], update) + + def update_system_tag( + state, + update + ), + do: state |> update_system(:update_tag, [:tag], update) + + def update_system_locked( + state, + update + ), + do: state |> update_system(:update_locked, [:locked], update) + + def update_system_labels( + state, + update + ), + do: state |> update_system(:update_labels, [:labels], update) + + def update_system_position( + %{rtree_name: rtree_name} = state, + update + ), + do: + state + |> update_system( + :update_position, + [:position_x, :position_y], + update, + fn updated_system -> + @ddrt.update( + updated_system.solar_system_id, + WandererApp.Map.PositionCalculator.get_system_bounding_rect(updated_system), + rtree_name + ) + end + ) + + def add_hub( + %{map_id: map_id} = state, + hub_info + ) do + with :ok <- WandererApp.Map.add_hub(map_id, hub_info), + {:ok, hubs} = map_id |> WandererApp.Map.list_hubs(), + {:ok, _} <- + WandererApp.MapRepo.update_hubs(map_id, hubs) do + Impl.broadcast!(map_id, :update_map, %{hubs: hubs}) + state + else + error -> + Logger.error("Failed to add hub: #{inspect(error, pretty: true)}") + state + end + end + + def remove_hub( + %{map_id: map_id} = state, + hub_info + ) do + with :ok <- WandererApp.Map.remove_hub(map_id, hub_info), + {:ok, hubs} = map_id |> WandererApp.Map.list_hubs(), + {:ok, _} <- + WandererApp.MapRepo.update_hubs(map_id, hubs) do + Impl.broadcast!(map_id, :update_map, %{hubs: hubs}) + state + else + error -> + Logger.error("Failed to remove hub: #{inspect(error, pretty: true)}") + state + end + end + + def delete_systems( + %{map_id: map_id, rtree_name: rtree_name} = state, + removed_ids, + user_id, + character_id + ) do + connections_to_remove = + removed_ids + |> Enum.map(fn solar_system_id -> + WandererApp.Map.find_connections(map_id, solar_system_id) + end) + |> List.flatten() + |> Enum.uniq_by(& &1.id) + + :ok = WandererApp.Map.remove_connections(map_id, connections_to_remove) + :ok = WandererApp.Map.remove_systems(map_id, removed_ids) + + removed_ids + |> Enum.each(fn solar_system_id -> + map_id + |> WandererApp.MapSystemRepo.remove_from_map(solar_system_id) + |> case do + {:ok, _} -> + :ok + + {:error, error} -> + Logger.error("Failed to remove system from map: #{inspect(error, pretty: true)}") + :ok + end + end) + + connections_to_remove + |> Enum.each(fn connection -> + Logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end) + WandererApp.MapConnectionRepo.destroy(map_id, connection) + end) + + @ddrt.delete(removed_ids, rtree_name) + + Impl.broadcast!(map_id, :remove_connections, connections_to_remove) + Impl.broadcast!(map_id, :systems_removed, removed_ids) + + case not is_nil(user_id) do + true -> + {:ok, _} = + WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{ + character_id: character_id, + user_id: user_id, + map_id: map_id, + solar_system_ids: removed_ids + }) + + :telemetry.execute( + [:wanderer_app, :map, :systems, :remove], + %{count: removed_ids |> Enum.count()} + ) + + :ok + + _ -> + :ok + end + + state + end + + def maybe_add_system(map_id, location, old_location, rtree_name, map_opts) + when not is_nil(location) do + case WandererApp.Map.check_location(map_id, location) do + {:ok, location} -> + {:ok, position} = calc_new_system_position(map_id, old_location, rtree_name, map_opts) + + case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id( + map_id, + location.solar_system_id + ) do + {:ok, existing_system} when not is_nil(existing_system) -> + {:ok, updated_system} = + existing_system + |> WandererApp.MapSystemRepo.update_position!(%{ + position_x: position.x, + position_y: position.y + }) + |> WandererApp.MapSystemRepo.cleanup_labels!(map_opts) + |> WandererApp.MapSystemRepo.update_visible!(%{visible: true}) + |> WandererApp.MapSystemRepo.cleanup_tags() + + @ddrt.insert( + {existing_system.solar_system_id, + WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ + position_x: position.x, + position_y: position.y + })}, + rtree_name + ) + + WandererApp.Cache.put( + "map_#{map_id}:system_#{updated_system.id}:last_activity", + DateTime.utc_now(), + ttl: @system_inactive_timeout + ) + + WandererApp.Map.add_system(map_id, updated_system) + + Impl.broadcast!(map_id, :add_system, updated_system) + :ok + + _ -> + {:ok, solar_system_info} = + WandererApp.CachedInfo.get_system_static_info(location.solar_system_id) + + WandererApp.MapSystemRepo.create(%{ + map_id: map_id, + solar_system_id: location.solar_system_id, + name: solar_system_info.solar_system_name, + position_x: position.x, + position_y: position.y + }) + |> case do + {:ok, new_system} -> + @ddrt.insert( + {new_system.solar_system_id, + WandererApp.Map.PositionCalculator.get_system_bounding_rect(new_system)}, + rtree_name + ) + + WandererApp.Cache.put( + "map_#{map_id}:system_#{new_system.id}:last_activity", + DateTime.utc_now(), + ttl: @system_inactive_timeout + ) + + WandererApp.Map.add_system(map_id, new_system) + Impl.broadcast!(map_id, :add_system, new_system) + + :ok + + error -> + Logger.warning("Failed to create system: #{inspect(error, pretty: true)}") + :ok + end + end + + error -> + Logger.debug("Skip adding system: #{inspect(error, pretty: true)}") + :ok + end + end + + def maybe_add_system(_map_id, _location, _old_location, _rtree_name, _map_opts), do: :ok + + defp _add_system( + %{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state, + %{ + solar_system_id: solar_system_id, + coordinates: coordinates + } = system_info, + user_id, + character_id + ) do + %{"x" => x, "y" => y} = + coordinates + |> case do + %{"x" => x, "y" => y} -> + %{"x" => x, "y" => y} + + _ -> + %{x: x, y: y} = + WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts) + + %{"x" => x, "y" => y} + end + + {:ok, system} = + case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do + {:ok, existing_system} when not is_nil(existing_system) -> + use_old_coordinates = Map.get(system_info, :use_old_coordinates, false) + + if use_old_coordinates do + @ddrt.insert( + {solar_system_id, + WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ + position_x: existing_system.position_x, + position_y: existing_system.position_y + })}, + rtree_name + ) + + existing_system + |> WandererApp.MapSystemRepo.update_visible(%{visible: true}) + else + @ddrt.insert( + {solar_system_id, + WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ + position_x: x, + position_y: y + })}, + rtree_name + ) + + existing_system + |> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y}) + |> WandererApp.MapSystemRepo.cleanup_labels!(map_opts) + |> WandererApp.MapSystemRepo.cleanup_tags!() + |> WandererApp.MapSystemRepo.update_visible(%{visible: true}) + end + + _ -> + {:ok, solar_system_info} = + WandererApp.CachedInfo.get_system_static_info(solar_system_id) + + @ddrt.insert( + {solar_system_id, + WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{ + position_x: x, + position_y: y + })}, + rtree_name + ) + + WandererApp.MapSystemRepo.create(%{ + map_id: map_id, + solar_system_id: solar_system_id, + name: solar_system_info.solar_system_name, + position_x: x, + position_y: y + }) + end + + :ok = map_id |> WandererApp.Map.add_system(system) + + WandererApp.Cache.put( + "map_#{map_id}:system_#{system.id}:last_activity", + DateTime.utc_now(), + ttl: @system_inactive_timeout + ) + + Impl.broadcast!(map_id, :add_system, system) + + {:ok, _} = + WandererApp.User.ActivityTracker.track_map_event(:system_added, %{ + character_id: character_id, + user_id: user_id, + map_id: map_id, + solar_system_id: solar_system_id + }) + + state + end + + defp calc_new_system_position(map_id, old_location, rtree_name, opts), + do: + {:ok, + map_id + |> WandererApp.Map.find_system_by_location(old_location) + |> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)} + + defp update_system( + %{map_id: map_id} = state, + update_method, + attributes, + update, + callback_fn \\ nil + ) do + with :ok <- WandererApp.Map.update_system_by_solar_system_id(map_id, update), + {:ok, system} <- + WandererApp.MapSystemRepo.get_by_map_and_solar_system_id( + map_id, + update.solar_system_id + ), + {:ok, update_map} <- Impl.get_update_map(update, attributes) do + {:ok, updated_system} = + apply(WandererApp.MapSystemRepo, update_method, [ + system, + update_map + ]) + + if not is_nil(callback_fn) do + callback_fn.(updated_system) + end + + update_map_system_last_activity(map_id, updated_system) + + state + else + error -> + Logger.error("Fail ed to update system: #{inspect(error, pretty: true)}") + state + end + end + + defp update_map_system_last_activity( + map_id, + updated_system + ) do + WandererApp.Cache.put( + "map_#{map_id}:system_#{updated_system.id}:last_activity", + DateTime.utc_now(), + ttl: @system_inactive_timeout + ) + + Impl.broadcast!(map_id, :update_system, updated_system) + end +end diff --git a/lib/wanderer_app/permissions.ex b/lib/wanderer_app/permissions.ex index 3e765e68..86be8ce9 100644 --- a/lib/wanderer_app/permissions.ex +++ b/lib/wanderer_app/permissions.ex @@ -15,6 +15,8 @@ defmodule WandererApp.Permissions do @add_acl 1024 @delete_acl 2048 @delete_map 4096 + @manage_map 8192 + @admin_map 16384 @viewer_role [@view_system, @view_character, @view_connection] @member_role @viewer_role ++ @@ -24,11 +26,10 @@ defmodule WandererApp.Permissions do @update_system, @track_character, @delete_connection, - @delete_system, - @lock_system + @delete_system ] - @manager_role @member_role - @admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map] + @manager_role @member_role ++ [@lock_system, @manage_map] + @admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map, @admin_map] @viewer_role_mask @viewer_role |> Enum.reduce(0, fn x, acc -> x ||| acc end) @member_role_mask @member_role |> Enum.reduce(0, fn x, acc -> x ||| acc end) @@ -72,6 +73,8 @@ defmodule WandererApp.Permissions do def get_permissions(user_permissions) do %{ + admin_map: check_permission(user_permissions, @admin_map), + manage_map: check_permission(user_permissions, @manage_map), view_system: check_permission(user_permissions, @view_system), view_character: check_permission(user_permissions, @view_character), view_connection: check_permission(user_permissions, @view_connection), diff --git a/lib/wanderer_app_web/live/access_lists/access_lists_live.ex b/lib/wanderer_app_web/live/access_lists/access_lists_live.ex index 6f8e6087..19099a84 100755 --- a/lib/wanderer_app_web/live/access_lists/access_lists_live.ex +++ b/lib/wanderer_app_web/live/access_lists/access_lists_live.ex @@ -222,7 +222,7 @@ defmodule WandererAppWeb.AccessListsLive do def handle_event( "add_members", - %{"member_id" => member_id} = _params, + %{"member_id" => [member_id]} = _params, %{assigns: assigns} = socket ) when is_binary(member_id) and member_id != "" do diff --git a/lib/wanderer_app_web/live/access_lists/access_lists_live.html.heex b/lib/wanderer_app_web/live/access_lists/access_lists_live.html.heex index 9c8d79c0..88fcf320 100644 --- a/lib/wanderer_app_web/live/access_lists/access_lists_live.html.heex +++ b/lib/wanderer_app_web/live/access_lists/access_lists_live.html.heex @@ -228,6 +228,7 @@ available_option_class="w-full" debounce={250} update_min_len={3} + mode={:tags} options={@member_search_options} placeholder="Search a character/corporation/alliance" > diff --git a/lib/wanderer_app_web/live/maps/event_handlers/map_characters_event_handler.ex b/lib/wanderer_app_web/live/maps/event_handlers/map_characters_event_handler.ex index e913d556..4961162f 100644 --- a/lib/wanderer_app_web/live/maps/event_handlers/map_characters_event_handler.ex +++ b/lib/wanderer_app_web/live/maps/event_handlers/map_characters_event_handler.ex @@ -74,23 +74,20 @@ defmodule WandererAppWeb.MapCharactersEventHandler do } } = socket ) do - {:ok, character_settings} = - case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do - {:ok, settings} -> {:ok, settings} - _ -> {:ok, []} - end - {:noreply, socket - |> assign( - show_tracking?: true, - character_settings: character_settings - ) + |> assign(show_tracking?: true) |> assign_async(:characters, fn -> {:ok, map} = map_id |> WandererApp.MapRepo.get([:acls]) + {:ok, character_settings} = + case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do + {:ok, settings} -> {:ok, settings} + _ -> {:ok, []} + end + map |> WandererApp.Maps.load_characters( character_settings, @@ -122,12 +119,17 @@ defmodule WandererAppWeb.MapCharactersEventHandler do %{ assigns: %{ map_id: map_id, - character_settings: character_settings, current_user: current_user, only_tracked_characters: only_tracked_characters } } = socket ) do + {:ok, character_settings} = + case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do + {:ok, settings} -> {:ok, settings} + _ -> {:ok, []} + end + socket = case character_settings |> Enum.find(&(&1.character_id == character_id)) do nil -> @@ -202,7 +204,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do socket |> assign(user_characters: user_character_eve_ids) |> assign(has_tracked_characters?: has_tracked_characters?(user_character_eve_ids)) - |> assign(character_settings: character_settings) |> assign_async(:characters, fn -> {:ok, %{characters: characters}} end) diff --git a/lib/wanderer_app_web/live/maps/event_handlers/map_systems_event_handler.ex b/lib/wanderer_app_web/live/maps/event_handlers/map_systems_event_handler.ex index 82b9c595..3954da2a 100644 --- a/lib/wanderer_app_web/live/maps/event_handlers/map_systems_event_handler.ex +++ b/lib/wanderer_app_web/live/maps/event_handlers/map_systems_event_handler.ex @@ -66,7 +66,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do def handle_ui_event( "add_system", - %{"system_id" => solar_system_id} = _event, + %{"system_id" => [solar_system_id]} = _event, %{ assigns: %{ @@ -217,7 +217,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do current_user: current_user, tracked_character_ids: tracked_character_ids, has_tracked_characters?: true, - user_permissions: %{update_system: true} + user_permissions: %{update_system: true} = user_permissions } } = socket @@ -244,23 +244,25 @@ defmodule WandererAppWeb.MapSystemsEventHandler do _ -> :none end - apply(WandererApp.Map.Server, method_atom, [ - map_id, - %{ - solar_system_id: "#{solar_system_id}" |> String.to_integer() - } - |> Map.put_new(key_atom, value) - ]) + if can_update_system?(key_atom, user_permissions) do + apply(WandererApp.Map.Server, method_atom, [ + map_id, + %{ + solar_system_id: "#{solar_system_id}" |> String.to_integer() + } + |> Map.put_new(key_atom, value) + ]) - {:ok, _} = - WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{ - character_id: tracked_character_ids |> List.first(), - user_id: current_user.id, - map_id: map_id, - solar_system_id: "#{solar_system_id}" |> String.to_integer(), - key: key_atom, - value: value - }) + {:ok, _} = + WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{ + character_id: tracked_character_ids |> List.first(), + user_id: current_user.id, + map_id: map_id, + solar_system_id: "#{solar_system_id}" |> String.to_integer(), + key: key_atom, + value: value + }) + end {:noreply, socket} end @@ -305,6 +307,9 @@ defmodule WandererAppWeb.MapSystemsEventHandler do def handle_ui_event(event, body, socket), do: MapCoreEventHandler.handle_ui_event(event, body, socket) + defp can_update_system?(:locked, %{lock_system: false} = _user_permissions), do: false + defp can_update_system?(_key, _user_permissions), do: true + defp update_system_positions(_map_id, []), do: :ok defp update_system_positions(map_id, [position | rest]) do diff --git a/lib/wanderer_app_web/live/maps/map_live.html.heex b/lib/wanderer_app_web/live/maps/map_live.html.heex index 07417ebe..f5ffd910 100644 --- a/lib/wanderer_app_web/live/maps/map_live.html.heex +++ b/lib/wanderer_app_web/live/maps/map_live.html.heex @@ -46,6 +46,7 @@ update_min_len={2} available_option_class="w-full text-sm" debounce={200} + mode={:tags} > <:option :let={option}>
@@ -96,7 +97,7 @@ on_cancel={JS.push("hide_tracking")} > <.async_result :let={characters} assign={@characters}> - <:loading>Loading... + <:loading>. <:failed :let={reason}><%= reason %> <.table @@ -104,7 +105,6 @@ id="characters-tracking-table" class="h-[400px] !overflow-y-auto" rows={characters} - > <:col :let={character} label="Tracked">