mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-04-30 22:40:30 +00:00
693 lines
18 KiB
Elixir
693 lines
18 KiB
Elixir
defmodule WandererApp.Map do
|
|
@moduledoc """
|
|
Represents the map structure and exposes actions that can be taken to update
|
|
it
|
|
"""
|
|
import Ecto.Query
|
|
|
|
require Logger
|
|
|
|
defstruct map_id: nil,
|
|
name: nil,
|
|
scope: :none,
|
|
owner_id: nil,
|
|
characters: [],
|
|
systems: Map.new(),
|
|
hubs: [],
|
|
connections: Map.new(),
|
|
acls: [],
|
|
options: Map.new(),
|
|
characters_limit: nil,
|
|
hubs_limit: nil
|
|
|
|
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
|
|
map =
|
|
struct!(__MODULE__,
|
|
map_id: map_id,
|
|
scope: scope,
|
|
owner_id: owner_id,
|
|
name: name,
|
|
acls: acls,
|
|
hubs: hubs
|
|
)
|
|
|
|
update_map(map_id, map)
|
|
|
|
map
|
|
end
|
|
|
|
def get_map(map_id) do
|
|
case Cachex.get(:map_cache, map_id) do
|
|
{:ok, nil} ->
|
|
{:error, :not_found}
|
|
|
|
{:ok, map} ->
|
|
{:ok, map}
|
|
end
|
|
end
|
|
|
|
def get_map!(map_id) do
|
|
case get_map(map_id) do
|
|
{:ok, map} ->
|
|
map
|
|
|
|
_ ->
|
|
Logger.error(fn -> "Failed to get map #{map_id}" end)
|
|
%{}
|
|
end
|
|
end
|
|
|
|
def update_map(map_id, map_update) do
|
|
Cachex.get_and_update(:map_cache, map_id, fn map ->
|
|
case map do
|
|
nil ->
|
|
{:commit, map_update}
|
|
|
|
_ ->
|
|
{:commit, Map.merge(map, map_update)}
|
|
end
|
|
end)
|
|
end
|
|
|
|
def get_characters_limit(map_id),
|
|
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 50)}
|
|
|
|
def get_hubs_limit(map_id),
|
|
do: {:ok, map_id |> get_map!() |> Map.get(:hubs_limit, 20)}
|
|
|
|
def is_subscription_active?(map_id),
|
|
do: is_subscription_active?(map_id, WandererApp.Env.map_subscriptions_enabled?())
|
|
|
|
def is_subscription_active?(_map_id, false), do: {:ok, true}
|
|
|
|
def is_subscription_active?(map_id, _map_subscriptions_enabled) do
|
|
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
|
{:ok, plan != :alpha}
|
|
end
|
|
|
|
def get_options(map_id),
|
|
do: {:ok, map_id |> get_map!() |> Map.get(:options, Map.new())}
|
|
|
|
@doc """
|
|
Returns a full list of characters in the map
|
|
"""
|
|
def list_characters(map_id),
|
|
do:
|
|
map_id
|
|
|> get_map!()
|
|
|> Map.get(:characters, [])
|
|
|> 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()}
|
|
|
|
def list_systems!(map_id) do
|
|
{:ok, systems} = list_systems(map_id)
|
|
systems
|
|
end
|
|
|
|
def list_hubs(map_id) do
|
|
{:ok, map} = map_id |> get_map()
|
|
|
|
{:ok, map |> Map.get(:hubs, [])}
|
|
end
|
|
|
|
def list_hubs(map_id, hubs) do
|
|
{:ok, map} = map_id |> get_map()
|
|
|
|
{:ok, hubs}
|
|
end
|
|
|
|
def list_connections(map_id),
|
|
do: {:ok, map_id |> get_map!() |> Map.get(:connections, Map.new()) |> Map.values()}
|
|
|
|
def list_connections!(map_id) do
|
|
{:ok, connections} = list_connections(map_id)
|
|
connections
|
|
end
|
|
|
|
def get_connection(map_id, solar_system_source, solar_system_target),
|
|
do:
|
|
map_id
|
|
|> get_map!()
|
|
|> Map.get(:connections, Map.new())
|
|
|> Map.get("#{solar_system_source}_#{solar_system_target}")
|
|
|
|
def add_characters!(map, []), do: map
|
|
|
|
def add_characters!(%{map_id: map_id} = map, [character | rest]) do
|
|
add_character(map_id, character)
|
|
add_characters!(map, rest)
|
|
end
|
|
|
|
def add_character(
|
|
map_id,
|
|
%{
|
|
id: character_id
|
|
} = _character
|
|
) do
|
|
characters = map_id |> get_map!() |> Map.get(:characters, [])
|
|
|
|
case not (characters |> Enum.member?(character_id)) do
|
|
true ->
|
|
WandererApp.Character.get_map_character(map_id, character_id)
|
|
|> case do
|
|
{:ok,
|
|
%{
|
|
alliance_id: alliance_id,
|
|
corporation_id: corporation_id,
|
|
solar_system_id: solar_system_id,
|
|
structure_id: structure_id,
|
|
station_id: station_id,
|
|
ship: ship_type_id,
|
|
ship_name: ship_name
|
|
}} ->
|
|
map_id
|
|
|> update_map(%{characters: [character_id | characters]})
|
|
|
|
# WandererApp.Cache.insert(
|
|
# "map:#{map_id}:character:#{character_id}:alliance_id",
|
|
# alliance_id
|
|
# )
|
|
|
|
# WandererApp.Cache.insert(
|
|
# "map:#{map_id}:character:#{character_id}:corporation_id",
|
|
# corporation_id
|
|
# )
|
|
|
|
# WandererApp.Cache.insert(
|
|
# "map:#{map_id}:character:#{character_id}:solar_system_id",
|
|
# solar_system_id
|
|
# )
|
|
|
|
# WandererApp.Cache.insert(
|
|
# "map:#{map_id}:character:#{character_id}:structure_id",
|
|
# structure_id
|
|
# )
|
|
|
|
# WandererApp.Cache.insert(
|
|
# "map:#{map_id}:character:#{character_id}:station_id",
|
|
# station_id
|
|
# )
|
|
|
|
# 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
|
|
# )
|
|
|
|
:ok
|
|
|
|
error ->
|
|
error
|
|
end
|
|
|
|
_ ->
|
|
{:error, :already_exists}
|
|
end
|
|
end
|
|
|
|
def get_system_characters(map_id, solar_system_id),
|
|
do:
|
|
map_id
|
|
|> list_characters()
|
|
|> filter(%{solar_system_id: solar_system_id}, match: :any)
|
|
|
|
@doc """
|
|
Removes a character with a given id
|
|
"""
|
|
def remove_character(map_id, character_id) do
|
|
characters = map_id |> get_map!() |> Map.get(:characters, [])
|
|
|
|
case characters |> Enum.member?(character_id) do
|
|
true ->
|
|
map_id
|
|
|> update_map(%{characters: characters |> Enum.reject(fn id -> id == character_id end)})
|
|
|
|
:ok
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def check_location(map_id, location) do
|
|
case find_system_by_location(map_id, location) do
|
|
nil ->
|
|
{:ok, location}
|
|
|
|
%{} ->
|
|
{:error, :already_exists}
|
|
end
|
|
end
|
|
|
|
def find_system_by_location(_map_id, nil), do: nil
|
|
|
|
def find_system_by_location(map_id, %{solar_system_id: solar_system_id} = _location) do
|
|
case map_id |> get_map!() |> Map.get(:systems, Map.new()) |> Map.get(solar_system_id) do
|
|
nil ->
|
|
nil
|
|
|
|
%{visible: true} = system ->
|
|
system
|
|
|
|
_system ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
def check_connection(
|
|
map_id,
|
|
%{solar_system_id: solar_system_id} = _location,
|
|
%{solar_system_id: old_solar_system_id} = _old_location
|
|
) do
|
|
case map_id
|
|
|> get_map!()
|
|
|> Map.get(:connections, Map.new())
|
|
|> is_connection_exist?(%{
|
|
solar_system_source: solar_system_id,
|
|
solar_system_target: old_solar_system_id
|
|
}) do
|
|
true ->
|
|
{:error, :already_exists}
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def update_subscription_settings!(%{map_id: map_id} = map, %{
|
|
characters_limit: characters_limit,
|
|
hubs_limit: hubs_limit
|
|
}) do
|
|
map_id
|
|
|> update_map(%{characters_limit: characters_limit, hubs_limit: hubs_limit})
|
|
|
|
map_id
|
|
|> get_map!()
|
|
end
|
|
|
|
def update_options!(%{map_id: map_id} = map, options) do
|
|
map_id
|
|
|> update_map(%{options: options})
|
|
|
|
map_id
|
|
|> get_map!()
|
|
end
|
|
|
|
def add_systems!(map, []), do: map
|
|
|
|
def add_systems!(%{map_id: map_id} = map, [system | rest]) do
|
|
:ok = add_system(map_id, system)
|
|
add_systems!(map, rest)
|
|
end
|
|
|
|
def add_system(map_id, %{solar_system_id: solar_system_id} = system) do
|
|
systems = map_id |> get_map!() |> Map.get(:systems, Map.new())
|
|
|
|
case not Map.has_key?(systems, solar_system_id) do
|
|
true ->
|
|
map_id
|
|
|> update_map(%{systems: Map.put(systems, solar_system_id, system)})
|
|
|
|
:ok
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def update_system_by_solar_system_id(
|
|
map_id,
|
|
update
|
|
) do
|
|
updated_systems =
|
|
map_id |> get_map!() |> Map.get(:systems, Map.new()) |> update_by_solar_system_id(update)
|
|
|
|
map_id
|
|
|> update_map(%{systems: updated_systems})
|
|
|
|
:ok
|
|
end
|
|
|
|
def remove_system(map_id, solar_system_id) do
|
|
systems = map_id |> get_map!() |> Map.get(:systems, Map.new())
|
|
|
|
case systems |> Map.get(solar_system_id) do
|
|
nil ->
|
|
:ok
|
|
|
|
_system ->
|
|
map_id
|
|
|> update_map(%{systems: Map.drop(systems, [solar_system_id])})
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def remove_systems(_map_id, []), do: :ok
|
|
|
|
def remove_systems(map_id, [solar_system_id | rest]) do
|
|
:ok = remove_system(map_id, solar_system_id)
|
|
remove_systems(map_id, rest)
|
|
end
|
|
|
|
def add_hub(map_id, %{solar_system_id: solar_system_id} = _hub_info) do
|
|
hubs = map_id |> get_map!() |> Map.get(:hubs, [])
|
|
|
|
case hubs |> Enum.member?("#{solar_system_id}") do
|
|
false ->
|
|
map_id
|
|
|> update_map(%{hubs: ["#{solar_system_id}" | hubs]})
|
|
|
|
:ok
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def remove_hub(map_id, %{solar_system_id: solar_system_id} = _hub_info) do
|
|
hubs = map_id |> get_map!() |> Map.get(:hubs, [])
|
|
|
|
case hubs |> Enum.member?("#{solar_system_id}") do
|
|
true ->
|
|
map_id
|
|
|> update_map(%{hubs: Enum.reject(hubs, fn hub -> hub == "#{solar_system_id}" end)})
|
|
|
|
:ok
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def add_connections!(map, []), do: map
|
|
|
|
def add_connections!(%{map_id: map_id} = map, [connection | rest]) do
|
|
case add_connection(map_id, connection) do
|
|
:ok ->
|
|
add_connections!(map, rest)
|
|
|
|
{:error, :already_exists} ->
|
|
connection
|
|
|> WandererApp.MapConnectionRepo.destroy!()
|
|
|
|
add_connections!(map, rest)
|
|
end
|
|
end
|
|
|
|
def add_connection(
|
|
map_id,
|
|
%{solar_system_source: solar_system_source, solar_system_target: solar_system_target} =
|
|
connection
|
|
) do
|
|
connections = map_id |> get_map!() |> Map.get(:connections, Map.new())
|
|
|
|
case not (connections |> is_connection_exist?(connection)) do
|
|
true ->
|
|
map_id
|
|
|> update_map(%{
|
|
connections:
|
|
Map.put_new(connections, "#{solar_system_source}_#{solar_system_target}", connection)
|
|
})
|
|
|
|
:ok
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def is_connection_exist?(
|
|
connections,
|
|
%{solar_system_source: solar_system_source, solar_system_target: solar_system_target} =
|
|
_connection
|
|
) do
|
|
connections |> Map.has_key?("#{solar_system_source}_#{solar_system_target}") or
|
|
connections |> Map.has_key?("#{solar_system_target}_#{solar_system_source}")
|
|
end
|
|
|
|
def update_connection(
|
|
map_id,
|
|
%{solar_system_source: solar_system_source, solar_system_target: solar_system_target} =
|
|
connection
|
|
) do
|
|
connections = map_id |> get_map!() |> Map.get(:connections, Map.new())
|
|
|
|
map_id
|
|
|> update_map(%{
|
|
connections:
|
|
Map.put(connections, "#{solar_system_source}_#{solar_system_target}", connection)
|
|
})
|
|
|
|
:ok
|
|
end
|
|
|
|
def remove_connection(
|
|
map_id,
|
|
%{solar_system_source: solar_system_source, solar_system_target: solar_system_target} =
|
|
_connection
|
|
) do
|
|
connections = map_id |> get_map!() |> Map.get(:connections, Map.new())
|
|
|
|
map_id
|
|
|> update_map(%{
|
|
connections: Map.drop(connections, ["#{solar_system_source}_#{solar_system_target}"])
|
|
})
|
|
|
|
:ok
|
|
end
|
|
|
|
def remove_connections(_map_id, []), do: :ok
|
|
|
|
def remove_connections(map_id, [connection | rest]) do
|
|
:ok = remove_connection(map_id, connection)
|
|
remove_connections(map_id, rest)
|
|
end
|
|
|
|
def find_connections(map_id, solar_system_id),
|
|
do:
|
|
map_id
|
|
|> list_connections!()
|
|
|> filter(
|
|
%{solar_system_source: solar_system_id, solar_system_target: solar_system_id},
|
|
match: :any
|
|
)
|
|
|
|
def find_connection(
|
|
map_id,
|
|
solar_system_source,
|
|
solar_system_target
|
|
) do
|
|
case map_id
|
|
|> get_map!()
|
|
|> Map.get(:connections, Map.new())
|
|
|> Map.get("#{solar_system_source}_#{solar_system_target}") do
|
|
nil ->
|
|
{:ok,
|
|
map_id
|
|
|> get_map!()
|
|
|> Map.get(:connections, Map.new())
|
|
|> Map.get("#{solar_system_target}_#{solar_system_source}")}
|
|
|
|
connection ->
|
|
{:ok, connection}
|
|
end
|
|
end
|
|
|
|
def get_by_id(list, id) do
|
|
case find(list, %{id: id}, match: :any) do
|
|
%{} = item -> {:ok, item}
|
|
nil -> {:error, :item_not_found}
|
|
end
|
|
end
|
|
|
|
def find(list, %{} = attrs, match: :any) do
|
|
list
|
|
|> Enum.find(fn item ->
|
|
Enum.any?(attrs, &has_equal_attribute?(item, &1))
|
|
end)
|
|
end
|
|
|
|
def find(list, %{} = attrs, match: :all) do
|
|
list
|
|
|> Enum.find(fn item ->
|
|
Enum.all?(attrs, &has_equal_attribute?(item, &1))
|
|
end)
|
|
end
|
|
|
|
def filter(list, %{} = attrs, match: :any) do
|
|
list
|
|
|> Enum.filter(fn item ->
|
|
Enum.any?(attrs, &has_equal_attribute?(item, &1))
|
|
end)
|
|
end
|
|
|
|
defp has_equal_attribute?(%{} = map, {key, {:case_insensitive, value}}) when is_binary(value) do
|
|
String.downcase(Map.get(map, key, "")) == String.downcase(value)
|
|
end
|
|
|
|
defp has_equal_attribute?(%{} = map, {key, value}) do
|
|
Map.get(map, key) == value
|
|
end
|
|
|
|
defp update_by_solar_system_id(systems, %{solar_system_id: solar_system_id} = item) do
|
|
case systems |> Map.get(solar_system_id) do
|
|
nil ->
|
|
systems
|
|
|
|
system ->
|
|
systems |> Map.put(solar_system_id, system |> Map.merge(item))
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns the raw activity data that can be processed by WandererApp.Character.Activity.
|
|
Only includes characters that are on the map's ACL.
|
|
If days parameter is provided, filters activity to that time period.
|
|
"""
|
|
def get_character_activity(map_id, days \\ nil) do
|
|
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id) do
|
|
_map_with_acls = Ash.load!(map, :acls)
|
|
|
|
# Calculate cutoff date if days is provided
|
|
cutoff_date =
|
|
if days, do: DateTime.utc_now() |> DateTime.add(-days * 24 * 3600, :second), else: nil
|
|
|
|
# Get activity data
|
|
passages_activity = get_passages_activity(map_id, cutoff_date)
|
|
connections_activity = get_connections_activity(map_id, cutoff_date)
|
|
signatures_activity = get_signatures_activity(map_id, cutoff_date)
|
|
|
|
# Return activity data
|
|
result =
|
|
passages_activity
|
|
|> Enum.map(fn passage ->
|
|
%{
|
|
character: passage.character,
|
|
passages: passage.count,
|
|
connections: Map.get(connections_activity, passage.character.id, 0),
|
|
signatures: Map.get(signatures_activity, passage.character.id, 0),
|
|
timestamp: DateTime.utc_now(),
|
|
character_id: passage.character.id,
|
|
user_id: passage.character.user_id
|
|
}
|
|
end)
|
|
|
|
{:ok, result}
|
|
end
|
|
end
|
|
|
|
defp get_passages_activity(map_id, nil) do
|
|
# Query all map chain passages without time filter
|
|
from(p in WandererApp.Api.MapChainPassages,
|
|
join: c in assoc(p, :character),
|
|
where: p.map_id == ^map_id,
|
|
group_by: [c.id],
|
|
select: {c, count(p.id)}
|
|
)
|
|
|> WandererApp.Repo.all()
|
|
|> Enum.map(fn {character, count} -> %{character: character, count: count} end)
|
|
end
|
|
|
|
defp get_passages_activity(map_id, cutoff_date) do
|
|
# Query map chain passages with time filter
|
|
from(p in WandererApp.Api.MapChainPassages,
|
|
join: c in assoc(p, :character),
|
|
where:
|
|
p.map_id == ^map_id and
|
|
p.inserted_at > ^cutoff_date,
|
|
group_by: [c.id],
|
|
select: {c, count(p.id)}
|
|
)
|
|
|> WandererApp.Repo.all()
|
|
|> Enum.map(fn {character, count} -> %{character: character, count: count} end)
|
|
end
|
|
|
|
defp get_connections_activity(map_id, nil) do
|
|
# Query all connection activity without time filter
|
|
from(ua in WandererApp.Api.UserActivity,
|
|
join: c in assoc(ua, :character),
|
|
where:
|
|
ua.entity_id == ^map_id and
|
|
ua.entity_type == :map and
|
|
ua.event_type == :map_connection_added,
|
|
group_by: [c.id],
|
|
select: {c.id, count(ua.id)}
|
|
)
|
|
|> WandererApp.Repo.all()
|
|
|> Map.new()
|
|
end
|
|
|
|
defp get_connections_activity(map_id, cutoff_date) do
|
|
from(ua in WandererApp.Api.UserActivity,
|
|
join: c in assoc(ua, :character),
|
|
where:
|
|
ua.entity_id == ^map_id and
|
|
ua.entity_type == :map and
|
|
ua.event_type == :map_connection_added and
|
|
ua.inserted_at > ^cutoff_date,
|
|
group_by: [c.id],
|
|
select: {c.id, count(ua.id)}
|
|
)
|
|
|> WandererApp.Repo.all()
|
|
|> Map.new()
|
|
end
|
|
|
|
defp get_signatures_activity(map_id, nil) do
|
|
# Query all signature activity without time filter
|
|
from(ua in WandererApp.Api.UserActivity,
|
|
join: c in assoc(ua, :character),
|
|
where:
|
|
ua.entity_id == ^map_id and
|
|
ua.entity_type == :map and
|
|
ua.event_type == :signatures_added,
|
|
select: {ua.character_id, ua.event_data}
|
|
)
|
|
|> WandererApp.Repo.all()
|
|
|> process_signatures_data()
|
|
end
|
|
|
|
defp get_signatures_activity(map_id, cutoff_date) do
|
|
from(ua in WandererApp.Api.UserActivity,
|
|
join: c in assoc(ua, :character),
|
|
where:
|
|
ua.entity_id == ^map_id and
|
|
ua.entity_type == :map and
|
|
ua.event_type == :signatures_added and
|
|
ua.inserted_at > ^cutoff_date,
|
|
select: {ua.character_id, ua.event_data}
|
|
)
|
|
|> WandererApp.Repo.all()
|
|
|> process_signatures_data()
|
|
end
|
|
|
|
defp process_signatures_data(signatures_data) do
|
|
signatures_data
|
|
|> Enum.group_by(fn {character_id, _} -> character_id end)
|
|
|> Enum.map(&process_character_signatures/1)
|
|
|> Map.new()
|
|
end
|
|
|
|
defp process_character_signatures({character_id, activities}) do
|
|
signature_count =
|
|
activities
|
|
|> Enum.map(fn {_, event_data} ->
|
|
case Jason.decode(event_data) do
|
|
{:ok, data} -> length(Map.get(data, "signatures", []))
|
|
_ -> 0
|
|
end
|
|
end)
|
|
|> Enum.sum()
|
|
|
|
{character_id, signature_count}
|
|
end
|
|
end
|