Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83801c9063 | ||
|
|
0f34350c58 | ||
|
|
1c4c0f0715 | ||
|
|
3825fc831a | ||
|
|
654670cbc8 | ||
|
|
947570072c | ||
|
|
01b6b45380 | ||
|
|
b9dc1f8357 | ||
|
|
b4bd810c9d | ||
|
|
490b037920 | ||
|
|
cdff5458bc | ||
|
|
09314a09e9 | ||
|
|
49ea8edb27 |
@@ -1,6 +1,27 @@
|
||||
FROM elixir:1.17-otp-27
|
||||
|
||||
RUN apt install -yq curl gnupg
|
||||
# Install OS packages and Node.js (via nodesource),
|
||||
# plus inotify-tools and yarn
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
sudo \
|
||||
curl \
|
||||
make \
|
||||
git \
|
||||
bash \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
jq \
|
||||
vim \
|
||||
net-tools \
|
||||
procps \
|
||||
# Optionally add any other tools you need, e.g. vim, wget...
|
||||
&& curl -sL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs inotify-tools \
|
||||
&& npm install -g yarn \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt --fix-broken install
|
||||
|
||||
RUN mix local.hex --force
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
{
|
||||
"name": "wanderer-dev",
|
||||
"dockerComposeFile": ["./docker-compose.yml"],
|
||||
"extensions": [
|
||||
"jakebecker.elixir-ls",
|
||||
"JakeBecker.elixir-ls",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"jakebecker.elixir-ls",
|
||||
"JakeBecker.elixir-ls",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": true,
|
||||
"search.exclude": {
|
||||
"**/doc": true
|
||||
},
|
||||
"elixirLS.dialyzerEnabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"service": "wanderer",
|
||||
"workspaceFolder": "/app",
|
||||
"shutdownAction": "stopCompose",
|
||||
"settings": {
|
||||
"editor.formatOnSave": true,
|
||||
"search.exclude": {
|
||||
"**/doc": true
|
||||
},
|
||||
"elixirLS.dialyzerEnabled": false
|
||||
}
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||
"networkArgs": ["--add-host=host.docker.internal:host-gateway"]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [4444]
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ services:
|
||||
|
||||
wanderer:
|
||||
environment:
|
||||
PORT: 8000
|
||||
PORT: 4444
|
||||
DB_HOST: db
|
||||
WEB_APP_URL: "http://localhost:8000"
|
||||
WEB_APP_URL: "http://localhost:4444"
|
||||
ERL_AFLAGS: "-kernel shell_history enabled"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 8000:8000
|
||||
- 4444:4444
|
||||
volumes:
|
||||
- ..:/app:delegated
|
||||
- ~/.gitconfig:/root/.gitconfig
|
||||
|
||||
32
CHANGELOG.md
@@ -2,6 +2,38 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.56.1](https://github.com/wanderer-industries/wanderer/compare/v1.56.0...v1.56.1) (2025-03-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* update activity api (#284)
|
||||
|
||||
* qol updates for dev (#283)
|
||||
|
||||
## [v1.56.0](https://github.com/wanderer-industries/wanderer/compare/v1.55.2...v1.56.0) (2025-03-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add static wh info (#262)
|
||||
|
||||
* add static wh info
|
||||
|
||||
* api: add character activity api (#263)
|
||||
|
||||
* api: add character activity api
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* character activity hide error
|
||||
|
||||
* character added to map on follow (#272)
|
||||
|
||||
## [v1.55.2](https://github.com/wanderer-industries/wanderer/compare/v1.55.1...v1.55.2) (2025-03-16)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { VirtualScroller } from 'primereact/virtualscroller';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TrackingCharacter } from './types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
|
||||
import { TrackingCharacter } from './types';
|
||||
|
||||
interface TrackAndFollowProps {
|
||||
visible: boolean;
|
||||
@@ -12,80 +12,50 @@ interface TrackAndFollowProps {
|
||||
}
|
||||
|
||||
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
|
||||
const [trackedCharacters, setTrackedCharacters] = useState<string[]>([]);
|
||||
const [followedCharacter, setFollowedCharacter] = useState<string | null>(null);
|
||||
const { outCommand, data } = useMapRootState();
|
||||
const { trackingCharactersData } = data;
|
||||
|
||||
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (trackingCharactersData) {
|
||||
const newTrackedCharacters = trackingCharactersData.filter(tc => tc.tracked).map(tc => tc.character.eve_id);
|
||||
|
||||
setTrackedCharacters(newTrackedCharacters);
|
||||
|
||||
const followedChar = trackingCharactersData.find(tc => tc.followed);
|
||||
|
||||
if (followedChar?.character?.eve_id !== followedCharacter) {
|
||||
setFollowedCharacter(followedChar?.character?.eve_id || null);
|
||||
}
|
||||
}
|
||||
}, [followedCharacter, trackingCharactersData]);
|
||||
|
||||
const handleTrackToggle = (characterId: string) => {
|
||||
const isCurrentlyTracked = trackedCharacters.includes(characterId);
|
||||
|
||||
if (isCurrentlyTracked) {
|
||||
setTrackedCharacters(prev => prev.filter(id => id !== characterId));
|
||||
} else {
|
||||
setTrackedCharacters(prev => [...prev, characterId]);
|
||||
}
|
||||
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
};
|
||||
|
||||
const handleFollowToggle = async (characterEveId: string) => {
|
||||
const isCurrentlyFollowed = followedCharacter === characterEveId;
|
||||
const isCurrentlyTracked = trackedCharacters.includes(characterEveId);
|
||||
|
||||
// If not followed and not tracked, we need to track it first
|
||||
if (!isCurrentlyFollowed && !isCurrentlyTracked) {
|
||||
setTrackedCharacters(prev => [...prev, characterEveId]);
|
||||
|
||||
// Send track command first
|
||||
await outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterEveId },
|
||||
});
|
||||
|
||||
// Then send follow command after a short delay
|
||||
setTimeout(() => {
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterEveId },
|
||||
const handleTrackToggle = useCallback(
|
||||
async (characterId: string) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { character_id: characterId },
|
||||
});
|
||||
}, 100);
|
||||
} else {
|
||||
// Otherwise just toggle follow
|
||||
await outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterEveId },
|
||||
});
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error toggling track:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const handleFollowToggle = useCallback(
|
||||
async (characterId: string) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { character_id: characterId },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling follow:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const rowTemplate = (tc: TrackingCharacter) => {
|
||||
const characterEveId = tc.character.eve_id;
|
||||
|
||||
return (
|
||||
<TrackingCharacterWrapper
|
||||
key={tc.character.eve_id}
|
||||
key={characterEveId}
|
||||
character={tc.character}
|
||||
isTracked={trackedCharacters.includes(tc.character.eve_id)}
|
||||
isFollowed={followedCharacter === tc.character.eve_id}
|
||||
onTrackToggle={() => handleTrackToggle(tc.character.eve_id)}
|
||||
onFollowToggle={() => handleFollowToggle(tc.character.eve_id)}
|
||||
isTracked={tc.tracked}
|
||||
isFollowed={tc.followed}
|
||||
onTrackToggle={() => handleTrackToggle(characterEveId)}
|
||||
onFollowToggle={() => handleFollowToggle(characterEveId)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
|
||||
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
|
||||
import { CharacterCard, TooltipPosition, WdTooltipWrapper } from '../../../ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip/WdTooltip';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
|
||||
|
||||
interface TrackingCharacterWrapperProps {
|
||||
character: CharacterTypeRaw;
|
||||
@@ -26,7 +28,7 @@ export const TrackingCharacterWrapper = ({
|
||||
<div className="flex justify-center items-center p-0.5 text-center">
|
||||
<WdTooltipWrapper content="Track this character on the map" position={TooltipPosition.top}>
|
||||
<div className="flex justify-center items-center w-full">
|
||||
<WdCheckbox id={trackCheckboxId} label="" value={isTracked} onChange={() => onTrackToggle()} />
|
||||
<WdCheckbox id={trackCheckboxId} label="" value={isTracked} onChange={onTrackToggle} />
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
|
||||
@@ -18,13 +18,7 @@ export const useCharacterActivityHandlers = () => {
|
||||
...state,
|
||||
showCharacterActivity: false,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.hideActivity,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
}, [update]);
|
||||
|
||||
/**
|
||||
* Handle showing the character activity dialog
|
||||
@@ -56,7 +50,7 @@ export const useCharacterActivityHandlers = () => {
|
||||
// Update local state with the activity data
|
||||
update(state => ({
|
||||
...state,
|
||||
characterActivityData: activityData.activity,
|
||||
characterActivityData: activityData,
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -13,18 +13,12 @@ export const useTrackAndFollowHandlers = () => {
|
||||
* Handle hiding the track and follow dialog
|
||||
*/
|
||||
const handleHideTracking = useCallback(() => {
|
||||
// Send the command to the server first
|
||||
outCommand({
|
||||
type: OutCommand.hideTracking,
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Then update local state to hide the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: false,
|
||||
}));
|
||||
}, [outCommand, update]);
|
||||
}, [update]);
|
||||
|
||||
/**
|
||||
* Handle showing the track and follow dialog
|
||||
@@ -101,7 +95,6 @@ export const useTrackAndFollowHandlers = () => {
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Handle user settings updates
|
||||
*/
|
||||
|
||||
@@ -45,16 +45,9 @@ export const useCommandsActivity = () => {
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const hideTracking = useCallback(() => {
|
||||
ref.current.update((state: MapRootData) => ({
|
||||
...state,
|
||||
showTrackAndFollow: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const userSettingsUpdated = useCallback((data: CommandUserSettingsUpdated) => {
|
||||
emitMapEvent({ name: Commands.userSettingsUpdated, data });
|
||||
}, []);
|
||||
|
||||
return { characterActivityData, trackingCharactersData, userSettingsUpdated, hideActivity, hideTracking };
|
||||
return { characterActivityData, trackingCharactersData, userSettingsUpdated, hideActivity };
|
||||
};
|
||||
|
||||
@@ -241,7 +241,6 @@ export enum OutCommand {
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
showActivity = 'show_activity',
|
||||
hideTracking = 'hide_tracking',
|
||||
showTracking = 'show_tracking',
|
||||
getUserSettings = 'get_user_settings',
|
||||
updateUserSettings = 'update_user_settings',
|
||||
|
||||
18175
assets/static/images/news/03-18-bots/bot.svg
Executable file
|
After Width: | Height: | Size: 524 KiB |
BIN
assets/static/images/news/03-18-bots/dashboard.png
Executable file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/static/images/news/03-18-bots/free-character.png
Executable file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/static/images/news/03-18-bots/free-kill.png
Executable file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/static/images/news/03-18-bots/free-system.png
Executable file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/static/images/news/03-18-bots/paid-character.png
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/static/images/news/03-18-bots/paid-kill.png
Executable file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/static/images/news/03-18-bots/paid-system.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
@@ -112,6 +112,31 @@ defmodule WandererApp.CachedInfo do
|
||||
end
|
||||
end
|
||||
|
||||
def get_wormhole_classes() do
|
||||
case WandererApp.Cache.lookup(:wormhole_classes) do
|
||||
{:ok, nil} ->
|
||||
wormhole_classes = WandererApp.EveDataService.load_wormhole_classes()
|
||||
|
||||
cache_items(wormhole_classes, :wormhole_classes)
|
||||
|
||||
{:ok, wormhole_classes}
|
||||
|
||||
{:ok, wormhole_classes} ->
|
||||
{:ok, wormhole_classes}
|
||||
end
|
||||
end
|
||||
|
||||
def get_wormhole_classes!() do
|
||||
case get_wormhole_classes() do
|
||||
{:ok, wormhole_classes} ->
|
||||
wormhole_classes
|
||||
|
||||
error ->
|
||||
Logger.error("Error loading wormhole classes: #{inspect(error)}")
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def get_effects() do
|
||||
case WandererApp.Cache.lookup(:effects) do
|
||||
{:ok, nil} ->
|
||||
|
||||
374
lib/wanderer_app/character/tracking_utils.ex
Normal file
@@ -0,0 +1,374 @@
|
||||
defmodule WandererApp.Character.TrackingUtils do
|
||||
@moduledoc """
|
||||
Utility functions for handling character tracking and following operations.
|
||||
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias WandererApp.MapCharacterSettingsRepo
|
||||
|
||||
@doc """
|
||||
Toggles the tracking state for a character on a map.
|
||||
Returns the updated tracking data for all characters with access to the map.
|
||||
"""
|
||||
def toggle_track(map_id, character_id, current_user_id, caller_pid \\ nil, only_tracked_characters \\ false) do
|
||||
with current_user when not is_nil(current_user) <- WandererApp.User.load(current_user_id),
|
||||
{:ok, character} <- WandererApp.Character.find_character_by_eve_id(current_user, character_id),
|
||||
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||
map <- Ash.load!(map, :acls) do
|
||||
|
||||
# Check if the character is currently tracked before toggling
|
||||
{:ok, existing_settings} = MapCharacterSettingsRepo.get_by_map(map_id, character.id)
|
||||
was_tracked = existing_settings && existing_settings.tracked
|
||||
|
||||
# Toggle the tracking state
|
||||
with {:ok, _updated_settings} <- do_toggle_character_tracking(character, map_id, caller_pid) do
|
||||
# Get updated tracking data
|
||||
{:ok, tracking_data} = build_tracking_data(map_id, current_user_id)
|
||||
|
||||
# Determine which event to send based on tracking mode and previous state
|
||||
event = case {only_tracked_characters, was_tracked} do
|
||||
{true, true} -> :not_all_characters_tracked # Untracking in tracked-only mode
|
||||
_ -> %{event: :refresh_user_characters} # All other cases
|
||||
end
|
||||
|
||||
{:ok, tracking_data, event}
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to toggle tracking: #{inspect(error)}")
|
||||
{:error, "Failed to toggle tracking"}
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
Logger.error("User not found when toggling track")
|
||||
{:error, "User not found"}
|
||||
error ->
|
||||
Logger.error("Failed to toggle track: #{inspect(error)}")
|
||||
{:error, "Failed to toggle track"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Toggles the follow state for a character on a map.
|
||||
Returns the updated tracking data for all characters with access to the map.
|
||||
"""
|
||||
def toggle_follow(map_id, character_id, current_user_id, caller_pid \\ nil) do
|
||||
# Get all settings before the operation to see the followed state
|
||||
{:ok, all_settings_before} = MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
# Check if the clicked character is already followed
|
||||
is_already_followed =
|
||||
all_settings_before
|
||||
|> Enum.any?(fn setting ->
|
||||
setting.followed && "#{setting.character_id}" == "#{character_id}"
|
||||
end)
|
||||
|
||||
with current_user when not is_nil(current_user) <- WandererApp.User.load(current_user_id),
|
||||
{:ok, clicked_char} <- WandererApp.Character.find_character_by_eve_id(current_user, character_id),
|
||||
{:ok, _updated_settings} <- do_toggle_character_follow(map_id, clicked_char, is_already_followed, caller_pid) do
|
||||
|
||||
# Get updated tracking data
|
||||
{:ok, tracking_data} = build_tracking_data(map_id, current_user_id)
|
||||
|
||||
# Always send refresh_user_characters for follow operations
|
||||
{:ok, tracking_data, %{event: :refresh_user_characters}}
|
||||
else
|
||||
nil ->
|
||||
Logger.error("User not found when toggling follow")
|
||||
{:error, "User not found"}
|
||||
error ->
|
||||
Logger.error("Failed to toggle follow: #{inspect(error)}")
|
||||
{:error, "Failed to toggle follow"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds tracking data for all characters with access to a map.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with current_user when not is_nil(current_user) <- WandererApp.User.load(current_user_id),
|
||||
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||
map <- Ash.load!(map, :acls),
|
||||
{:ok, character_settings} <- MapCharacterSettingsRepo.get_all_by_map(map_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user.id) do
|
||||
|
||||
# Map characters to tracking data
|
||||
tracking_data = build_character_tracking_data(characters_with_access, character_settings)
|
||||
|
||||
# Check for inconsistent state
|
||||
check_tracking_consistency(tracking_data)
|
||||
|
||||
{:ok, tracking_data}
|
||||
else
|
||||
nil ->
|
||||
Logger.warning("User not found when building tracking data", %{user_id: current_user_id})
|
||||
{:error, "User not found"}
|
||||
|
||||
error ->
|
||||
Logger.error("Error building tracking data: #{inspect(error)}")
|
||||
{:error, "Failed to build tracking data"}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to build tracking data for each character
|
||||
defp build_character_tracking_data(characters, character_settings) do
|
||||
Enum.map(characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
|
||||
%{
|
||||
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
|
||||
tracked: setting && setting.tracked || false,
|
||||
followed: setting && setting.followed || false
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
# Helper to check for inconsistent tracking state
|
||||
defp check_tracking_consistency(tracking_data) do
|
||||
followed_in_data = Enum.find(tracking_data, &(&1.followed))
|
||||
followed_but_not_tracked = followed_in_data && !Enum.find(tracking_data, &(&1.followed && &1.tracked))
|
||||
|
||||
if followed_but_not_tracked do
|
||||
Logger.warning("Inconsistent state detected: Character is followed but not tracked", %{
|
||||
character_id: followed_in_data.character.eve_id
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# Private implementation of toggle character tracking
|
||||
defp do_toggle_character_tracking(character, map_id, caller_pid) do
|
||||
with false <- is_nil(caller_pid),
|
||||
{:ok, existing_settings} <- MapCharacterSettingsRepo.get_by_map(map_id, character.id) do
|
||||
|
||||
case existing_settings.tracked do
|
||||
# Untracking flow
|
||||
true ->
|
||||
with {:ok, updated_settings} <- untrack_character_settings(existing_settings),
|
||||
:ok <- untrack_characters([character], map_id, caller_pid),
|
||||
:ok <- remove_characters([character], map_id) do
|
||||
{:ok, updated_settings}
|
||||
end
|
||||
|
||||
# Tracking flow
|
||||
false ->
|
||||
with {:ok, updated_settings} <- MapCharacterSettingsRepo.track(existing_settings),
|
||||
:ok <- track_characters([character], map_id, true, caller_pid),
|
||||
:ok <- add_characters([character], map_id, true) do
|
||||
{:ok, updated_settings}
|
||||
end
|
||||
end
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for toggling character tracking")
|
||||
{:error, "caller_pid is required"}
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Create new settings with tracking enabled
|
||||
create_character_settings(character.id, map_id, true, false, caller_pid)
|
||||
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to untrack character settings, handling the followed state
|
||||
defp untrack_character_settings(settings) do
|
||||
case settings.followed do
|
||||
true ->
|
||||
# First unfollow, then untrack
|
||||
with {:ok, unfollowed_settings} <- MapCharacterSettingsRepo.unfollow(settings) do
|
||||
MapCharacterSettingsRepo.untrack(unfollowed_settings)
|
||||
end
|
||||
|
||||
false ->
|
||||
# Just untrack
|
||||
MapCharacterSettingsRepo.untrack(settings)
|
||||
end
|
||||
end
|
||||
|
||||
# Private implementation of toggle character follow
|
||||
defp do_toggle_character_follow(map_id, clicked_char, is_already_followed, caller_pid) do
|
||||
with false <- is_nil(caller_pid),
|
||||
{:ok, clicked_char_settings} <- MapCharacterSettingsRepo.get_by_map(map_id, clicked_char.id) do
|
||||
|
||||
case {is_already_followed, clicked_char_settings.tracked} do
|
||||
# Case 1: Already followed and not tracked - unfollow and remove
|
||||
{true, false} ->
|
||||
with {:ok, _} <- MapCharacterSettingsRepo.unfollow(clicked_char_settings),
|
||||
:ok <- remove_characters([clicked_char], map_id) do
|
||||
{:ok, clicked_char_settings}
|
||||
end
|
||||
|
||||
# Case 2: Already followed and tracked - just unfollow
|
||||
{true, true} ->
|
||||
MapCharacterSettingsRepo.unfollow(clicked_char_settings)
|
||||
|
||||
# Case 3: Not followed - ensure tracked and followed
|
||||
{false, _} ->
|
||||
ensure_character_tracked_and_followed(map_id, clicked_char, clicked_char_settings, caller_pid)
|
||||
end
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for toggling character following")
|
||||
{:error, "caller_pid is required"}
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Create new settings with both tracking and following enabled
|
||||
create_character_settings(clicked_char.id, map_id, true, true, caller_pid, clicked_char)
|
||||
end
|
||||
end
|
||||
|
||||
# Consolidated helper function to ensure a character is tracked when followed
|
||||
defp ensure_character_tracked_and_followed(map_id, character, settings, caller_pid) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
# Toggle the followed state
|
||||
followed = !settings.followed
|
||||
|
||||
case {followed, settings.tracked} do
|
||||
# Case 1: Following and not tracked - need to track and follow
|
||||
{true, false} ->
|
||||
# Unfollow all others first
|
||||
:ok = maybe_unfollow_others(map_id, character.id, true)
|
||||
|
||||
# Track and follow
|
||||
with {:ok, tracked_settings} <- MapCharacterSettingsRepo.track(settings),
|
||||
{:ok, updated_settings} <- update_follow(tracked_settings, true),
|
||||
:ok <- track_characters([character], map_id, true, caller_pid),
|
||||
:ok <- add_characters([character], map_id, true) do
|
||||
{:ok, updated_settings}
|
||||
end
|
||||
|
||||
# Case 2: Following and already tracked - just follow
|
||||
{true, true} ->
|
||||
# Unfollow all others first
|
||||
:ok = maybe_unfollow_others(map_id, character.id, true)
|
||||
# Update follow status
|
||||
update_follow(settings, true)
|
||||
|
||||
# Case 3: Unfollowing - just update follow status
|
||||
{false, _} ->
|
||||
update_follow(settings, false)
|
||||
end
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for ensuring character is tracked and followed")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to create character settings with specified tracking and following states
|
||||
defp create_character_settings(character_id, map_id, tracked, followed, caller_pid, character \\ nil) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
# Unfollow others if needed
|
||||
case followed do
|
||||
true -> :ok = maybe_unfollow_others(map_id, character_id, true)
|
||||
false -> :ok
|
||||
end
|
||||
|
||||
# Create the settings
|
||||
{:ok, settings} = MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
map_id: map_id,
|
||||
tracked: tracked,
|
||||
followed: followed
|
||||
})
|
||||
|
||||
# Handle tracking based on character presence and tracking flag
|
||||
case {character, tracked} do
|
||||
{nil, _} ->
|
||||
# No character provided, just return settings
|
||||
{:ok, settings}
|
||||
|
||||
{_, false} ->
|
||||
# Character provided but not tracking
|
||||
{:ok, settings}
|
||||
|
||||
{_, true} ->
|
||||
# Character provided and tracking enabled
|
||||
with :ok <- track_characters([character], map_id, true, caller_pid),
|
||||
:ok <- add_characters([character], map_id, true) do
|
||||
{:ok, settings}
|
||||
end
|
||||
end
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for creating character settings")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_unfollow_others(_map_id, _char_id, false), do: :ok
|
||||
defp maybe_unfollow_others(map_id, char_id, true) do
|
||||
# Unfollow all other characters when setting a character as followed
|
||||
with {:ok, all_settings} <- MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
all_settings
|
||||
|> Enum.filter(&(&1.character_id != char_id && &1.followed))
|
||||
|> Enum.each(&MapCharacterSettingsRepo.unfollow/1)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp update_follow(settings, true), do: MapCharacterSettingsRepo.follow(settings)
|
||||
defp update_follow(settings, false), do: MapCharacterSettingsRepo.unfollow(settings)
|
||||
|
||||
# Helper functions for character tracking
|
||||
def track_characters(_, _, false, _), do: :ok
|
||||
def track_characters([], _map_id, _is_track_character?, _), do: :ok
|
||||
def track_characters([character | characters], map_id, true, caller_pid) do
|
||||
with :ok <- track_character(character, map_id, caller_pid) do
|
||||
track_characters(characters, map_id, true, caller_pid)
|
||||
end
|
||||
end
|
||||
|
||||
def track_character(%{id: character_id, eve_id: eve_id, corporation_id: corporation_id, alliance_id: alliance_id}, map_id, caller_pid) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{})
|
||||
|
||||
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
|
||||
|
||||
case WandererApp.Cache.lookup!(cache_key, false) do
|
||||
true ->
|
||||
:ok
|
||||
_ ->
|
||||
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
|
||||
:ok = WandererApp.Cache.put(cache_key, true)
|
||||
end
|
||||
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for tracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
end
|
||||
|
||||
def untrack_characters(characters, map_id, caller_pid) do
|
||||
with false <- is_nil(caller_pid) do
|
||||
characters
|
||||
|> Enum.each(fn character ->
|
||||
WandererAppWeb.Presence.untrack(caller_pid, map_id, character.id)
|
||||
WandererApp.Cache.put("#{inspect(caller_pid)}_map_#{map_id}:character_#{character.id}:tracked", false)
|
||||
:ok = Phoenix.PubSub.unsubscribe(WandererApp.PubSub, "character:#{character.eve_id}")
|
||||
end)
|
||||
|
||||
:ok
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for untracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
end
|
||||
|
||||
def add_characters([], _map_id, _track_character), do: :ok
|
||||
def add_characters([character | characters], map_id, track_character) do
|
||||
:ok = WandererApp.Map.Server.add_character(map_id, character, track_character)
|
||||
add_characters(characters, map_id, track_character)
|
||||
end
|
||||
|
||||
def remove_characters([], _map_id), do: :ok
|
||||
def remove_characters([character | characters], map_id) do
|
||||
:ok = WandererApp.Map.Server.remove_character(map_id, character.id)
|
||||
remove_characters(characters, map_id)
|
||||
end
|
||||
end
|
||||
@@ -15,13 +15,14 @@ defmodule WandererAppWeb.CharactersAPIController do
|
||||
items: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_name: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_name: %OpenApiSpex.Schema{type: :string}
|
||||
corporation_id: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_ticker: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_id: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_ticker: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["id", "eve_id", "name"]
|
||||
required: ["eve_id", "name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -47,13 +48,7 @@ defmodule WandererAppWeb.CharactersAPIController do
|
||||
{:ok, characters} ->
|
||||
result =
|
||||
characters
|
||||
|> Enum.map(&%{
|
||||
id: &1.id,
|
||||
eve_id: &1.eve_id,
|
||||
name: &1.name,
|
||||
corporation_name: &1.corporation_name,
|
||||
alliance_name: &1.alliance_name
|
||||
})
|
||||
|> Enum.map(&WandererAppWeb.MapEventHandler.map_ui_character_stat/1)
|
||||
|
||||
json(conn, %{data: result})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
|
||||
alias WandererApp.CachedInfo
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
alias WandererApp.EveDataService
|
||||
|
||||
@system_static_response_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
@@ -26,6 +27,32 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
effect_name: %OpenApiSpex.Schema{type: :string},
|
||||
effect_power: %OpenApiSpex.Schema{type: :integer},
|
||||
statics: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
|
||||
static_details: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %OpenApiSpex.Schema{type: :string},
|
||||
destination: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string},
|
||||
short_name: %OpenApiSpex.Schema{type: :string}
|
||||
}
|
||||
},
|
||||
properties: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
lifetime: %OpenApiSpex.Schema{type: :string},
|
||||
max_mass: %OpenApiSpex.Schema{type: :integer},
|
||||
max_jump_mass: %OpenApiSpex.Schema{type: :integer},
|
||||
mass_regeneration: %OpenApiSpex.Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
wandering: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
|
||||
triglavian_invasion_status: %OpenApiSpex.Schema{type: :string},
|
||||
sun_type_id: %OpenApiSpex.Schema{type: :integer}
|
||||
@@ -64,8 +91,14 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
{:ok, solar_system_id} <- Util.parse_int(solar_system_str) do
|
||||
case CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, system} ->
|
||||
# Get basic system data
|
||||
data = static_system_to_json(system)
|
||||
json(conn, %{data: data})
|
||||
|
||||
# Enhance with wormhole type information if statics exist
|
||||
enhanced_data = enhance_with_static_details(data)
|
||||
|
||||
# Return the enhanced data
|
||||
json(conn, %{data: enhanced_data})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
@@ -80,6 +113,11 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a system map to a JSON-friendly format.
|
||||
|
||||
Takes only the fields that are needed for the API response.
|
||||
"""
|
||||
defp static_system_to_json(system) do
|
||||
system
|
||||
|> Map.take([
|
||||
@@ -103,4 +141,97 @@ defmodule WandererAppWeb.CommonAPIController do
|
||||
:sun_type_id
|
||||
])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enhances system data with wormhole type information.
|
||||
|
||||
If the system has static wormholes, adds detailed information about each static.
|
||||
Otherwise, returns the original data unchanged.
|
||||
"""
|
||||
defp enhance_with_static_details(data) do
|
||||
if data[:statics] && length(data[:statics]) > 0 do
|
||||
# Add the enhanced static details to the response
|
||||
Map.put(data, :static_details, get_static_details(data[:statics]))
|
||||
else
|
||||
# No statics, return the original data
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets detailed information for each static wormhole.
|
||||
|
||||
Uses the CachedInfo to get both wormhole type data and wormhole class data.
|
||||
"""
|
||||
defp get_static_details(statics) do
|
||||
# Get wormhole data from CachedInfo
|
||||
{:ok, wormhole_types} = CachedInfo.get_wormhole_types()
|
||||
wormhole_classes = CachedInfo.get_wormhole_classes!()
|
||||
|
||||
# Create a map of wormhole classes by ID for quick lookup
|
||||
classes_by_id = Enum.reduce(wormhole_classes, %{}, fn class, acc ->
|
||||
Map.put(acc, class.id, class)
|
||||
end)
|
||||
|
||||
# Find detailed information for each static
|
||||
Enum.map(statics, fn static_name ->
|
||||
# Find the wormhole type by name
|
||||
wh_type = Enum.find(wormhole_types, fn type -> type.name == static_name end)
|
||||
|
||||
if wh_type do
|
||||
create_wormhole_details(wh_type, classes_by_id)
|
||||
else
|
||||
create_fallback_wormhole_details(static_name)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates detailed wormhole information when the wormhole type is found.
|
||||
|
||||
Includes information about the destination and properties of the wormhole.
|
||||
Ensures that destination.id is always a string to match the OpenAPI schema.
|
||||
"""
|
||||
defp create_wormhole_details(wh_type, classes_by_id) do
|
||||
# Get destination class info
|
||||
dest_class = Map.get(classes_by_id, wh_type.dest)
|
||||
|
||||
# Create enhanced static info
|
||||
%{
|
||||
name: wh_type.name,
|
||||
destination: %{
|
||||
id: to_string(wh_type.dest),
|
||||
name: (if dest_class, do: dest_class.title, else: wh_type.dest),
|
||||
short_name: (if dest_class, do: dest_class.short_name, else: wh_type.dest)
|
||||
},
|
||||
properties: %{
|
||||
lifetime: wh_type.lifetime,
|
||||
max_mass: wh_type.total_mass,
|
||||
max_jump_mass: wh_type.max_mass_per_jump,
|
||||
mass_regeneration: wh_type.mass_regen
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates fallback information when a wormhole type is not found.
|
||||
|
||||
Provides a placeholder structure with nil values for unknown wormhole types.
|
||||
"""
|
||||
defp create_fallback_wormhole_details(static_name) do
|
||||
%{
|
||||
name: static_name,
|
||||
destination: %{
|
||||
id: nil,
|
||||
name: "Unknown",
|
||||
short_name: "?"
|
||||
},
|
||||
properties: %{
|
||||
lifetime: nil,
|
||||
max_mass: nil,
|
||||
max_jump_mass: nil,
|
||||
mass_regeneration: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,19 +65,14 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
@character_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_id: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_name: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_ticker: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_id: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_name: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_ticker: %OpenApiSpex.Schema{type: :string},
|
||||
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
|
||||
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
|
||||
alliance_ticker: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["id", "eve_id", "name"]
|
||||
required: ["eve_id", "name"]
|
||||
}
|
||||
|
||||
@tracked_char_schema %OpenApiSpex.Schema{
|
||||
@@ -174,6 +169,31 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
required: ["data"]
|
||||
}
|
||||
|
||||
# For operation :character_activity
|
||||
@character_activity_item_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
description: "Character activity data",
|
||||
properties: %{
|
||||
character: @character_schema,
|
||||
passages: %OpenApiSpex.Schema{type: :integer, description: "Number of passages through systems"},
|
||||
connections: %OpenApiSpex.Schema{type: :integer, description: "Number of connections created"},
|
||||
signatures: %OpenApiSpex.Schema{type: :integer, description: "Number of signatures added"},
|
||||
timestamp: %OpenApiSpex.Schema{type: :string, format: :date_time, description: "Timestamp of the activity"}
|
||||
},
|
||||
required: ["character", "passages", "connections", "signatures"]
|
||||
}
|
||||
|
||||
@character_activity_response_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @character_activity_item_schema
|
||||
}
|
||||
},
|
||||
required: ["data"]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# MAP endpoints
|
||||
# -----------------------------------------------------------------
|
||||
@@ -622,6 +642,107 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
GET /api/map/character_activity
|
||||
|
||||
Returns character activity data for a map.
|
||||
|
||||
Requires either `?map_id=<UUID>` or `?slug=<map-slug>`.
|
||||
|
||||
Example:
|
||||
GET /api/map/character_activity?map_id=<uuid>
|
||||
GET /api/map/character_activity?slug=<map-slug>
|
||||
"""
|
||||
@spec character_activity(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation :character_activity,
|
||||
summary: "Get Character Activity",
|
||||
description: "Returns character activity data for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: ""
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "map-name"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"Character activity data",
|
||||
"application/json",
|
||||
@character_activity_response_schema
|
||||
},
|
||||
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
|
||||
}
|
||||
}}
|
||||
]
|
||||
def character_activity(conn, params) do
|
||||
with {:ok, map_id} <- Util.fetch_map_id(params) do
|
||||
# Get raw activity data directly from the Map module instead of the Activity processor
|
||||
raw_activity = WandererApp.Map.get_character_activity(map_id)
|
||||
|
||||
# Group activities by user_id and summarize
|
||||
summarized_result =
|
||||
if raw_activity == [] do
|
||||
# Return empty list if there's no data
|
||||
[]
|
||||
else
|
||||
raw_activity
|
||||
|> Enum.group_by(fn activity ->
|
||||
# Get user_id from the character
|
||||
activity.character.user_id
|
||||
end)
|
||||
|> Enum.map(fn {_user_id, user_activities} ->
|
||||
# Get the most active or followed character for this user
|
||||
representative_activity =
|
||||
user_activities
|
||||
|> Enum.max_by(fn activity ->
|
||||
activity.passages + activity.connections + activity.signatures
|
||||
end)
|
||||
|
||||
# Sum up all activities for this user
|
||||
total_passages = Enum.sum(Enum.map(user_activities, & &1.passages))
|
||||
total_connections = Enum.sum(Enum.map(user_activities, & &1.connections))
|
||||
total_signatures = Enum.sum(Enum.map(user_activities, & &1.signatures))
|
||||
|
||||
# Return summarized activity with the mapped character
|
||||
%{
|
||||
character: character_to_json(representative_activity.character),
|
||||
passages: total_passages,
|
||||
connections: total_connections,
|
||||
signatures: total_signatures,
|
||||
timestamp: representative_activity.timestamp
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
json(conn, %{data: summarized_result})
|
||||
else
|
||||
{:error, msg} when is_binary(msg) ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: msg})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Could not fetch character activity: #{inspect(reason)}"})
|
||||
end
|
||||
end
|
||||
|
||||
# If hours_str is present and valid, parse it. Otherwise return nil (no filter).
|
||||
defp parse_hours_ago(nil), do: nil
|
||||
@@ -830,18 +951,6 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
end
|
||||
|
||||
defp character_to_json(ch) do
|
||||
Map.take(ch, [
|
||||
:id,
|
||||
:eve_id,
|
||||
:name,
|
||||
:corporation_id,
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
])
|
||||
WandererAppWeb.MapEventHandler.map_ui_character_stat(ch)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,11 +73,11 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_track_" <> character_id, _, socket) do
|
||||
handle_event("toggle_track", %{"character-id" => character_id}, socket)
|
||||
handle_event("toggle_track", %{"character_id" => character_id}, socket)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_track", %{"character-id" => character_id}, socket) do
|
||||
def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
|
||||
selected_map = socket.assigns.selected_map
|
||||
character_settings = socket.assigns.character_settings
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
phx-click="toggle_track"
|
||||
phx-value-character-id={character.id}
|
||||
phx-value-character_id={character.id}
|
||||
id={"character-track-#{character.id}"}
|
||||
checked={character.tracked}
|
||||
/>
|
||||
|
||||
@@ -117,7 +117,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|
||||
def handle_ui_event(
|
||||
"toggle_track",
|
||||
%{"character-id" => character_eve_id},
|
||||
%{"character_id" => character_eve_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
@@ -126,54 +126,22 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
# First, get all existing settings to preserve states
|
||||
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
case WandererApp.Character.TrackingUtils.toggle_track(map_id, character_eve_id, current_user.id, self(), only_tracked_characters) do
|
||||
{:ok, tracking_data, event} ->
|
||||
# Send the appropriate event based on the result from toggle_track
|
||||
Process.send_after(self(), event, 10)
|
||||
|
||||
# Save the followed character ID before making any changes
|
||||
followed_character_id =
|
||||
all_settings
|
||||
|> Enum.find(& &1.followed)
|
||||
|> case do
|
||||
nil -> nil
|
||||
setting -> setting.character_id
|
||||
end
|
||||
# Send the updated tracking data to the client
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"tracking_characters_data",
|
||||
%{characters: tracking_data}
|
||||
)}
|
||||
|
||||
# Find the character we're toggling
|
||||
with {:ok, character} <-
|
||||
WandererApp.Character.find_character_by_eve_id(current_user, character_eve_id),
|
||||
{:ok, _updated_settings} <-
|
||||
toggle_character_tracking(character, map_id, only_tracked_characters) do
|
||||
# Get the map with ACLs
|
||||
{:ok, map} = WandererApp.Api.Map.by_id(map_id)
|
||||
map = Ash.load!(map, :acls)
|
||||
|
||||
# If there was a followed character before, check if it's still followed
|
||||
# Only check if we're not toggling the followed character itself
|
||||
if followed_character_id && followed_character_id != character.id do
|
||||
# Get the current settings for the followed character
|
||||
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, followed_character_id) do
|
||||
{:ok, current_settings} ->
|
||||
# If it's not followed anymore, follow it again
|
||||
if !current_settings.followed do
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.follow(current_settings)
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, tracking_data} = build_tracking_data(map_id, current_user)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"tracking_characters_data",
|
||||
%{characters: tracking_data}
|
||||
)}
|
||||
else
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to toggle track: #{inspect(reason)}")
|
||||
{:noreply, socket |> put_flash(:error, "Failed to toggle character tracking")}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -183,48 +151,38 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
|
||||
) do
|
||||
# Create tracking data for characters with access to the map
|
||||
{:ok, tracking_data} = build_tracking_data(map_id, current_user)
|
||||
case WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user.id) do
|
||||
{:ok, tracking_data} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"tracking_characters_data",
|
||||
%{characters: tracking_data}
|
||||
)}
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"show_tracking",
|
||||
%{}
|
||||
)
|
||||
|> MapEventHandler.push_map_event(
|
||||
"tracking_characters_data",
|
||||
%{characters: tracking_data}
|
||||
)}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to load tracking data: #{inspect(reason)}")
|
||||
{:noreply, socket |> put_flash(:error, "Failed to load tracking data")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"toggle_follow",
|
||||
%{"character-id" => clicked_char_id},
|
||||
%{"character_id" => clicked_char_id},
|
||||
%{assigns: %{current_user: current_user, map_id: map_id}} = socket
|
||||
) do
|
||||
# Get all settings before the operation to see the followed state
|
||||
{:ok, all_settings_before} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
followed_before = all_settings_before |> Enum.find(& &1.followed)
|
||||
case WandererApp.Character.TrackingUtils.toggle_follow(map_id, clicked_char_id, current_user.id, self()) do
|
||||
{:ok, tracking_data, event} ->
|
||||
# Send the appropriate event based on the result from toggle_follow
|
||||
Process.send_after(self(), event, 10)
|
||||
|
||||
# Check if the clicked character is already followed
|
||||
is_already_followed =
|
||||
followed_before && "#{followed_before.character_id}" == "#{clicked_char_id}"
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("tracking_characters_data", %{characters: tracking_data})}
|
||||
|
||||
# Use find_character_by_eve_id from WandererApp.Character
|
||||
with {:ok, clicked_char} <-
|
||||
WandererApp.Character.find_character_by_eve_id(current_user, clicked_char_id),
|
||||
{:ok, _updated_settings} <-
|
||||
toggle_character_follow(map_id, clicked_char, is_already_followed) do
|
||||
# Build tracking data
|
||||
{:ok, tracking_data} = build_tracking_data(map_id, current_user)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("tracking_characters_data", %{characters: tracking_data})}
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to toggle follow: #{inspect(error)}")
|
||||
{:noreply, socket}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to toggle follow: #{inspect(reason)}")
|
||||
{:noreply, socket |> put_flash(:error, "Failed to toggle character following")}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -251,133 +209,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|> Map.put_new(:ship, WandererApp.Character.get_ship(character))
|
||||
|> Map.put_new(:location, get_location(character))
|
||||
|
||||
def add_characters([], _map_id, _track_character), do: :ok
|
||||
|
||||
def add_characters([character | characters], map_id, track_character) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.add_character(character, track_character)
|
||||
|
||||
add_characters(characters, map_id, track_character)
|
||||
end
|
||||
|
||||
def remove_characters([], _map_id), do: :ok
|
||||
|
||||
def remove_characters([character | characters], map_id) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.remove_character(character.id)
|
||||
|
||||
remove_characters(characters, map_id)
|
||||
end
|
||||
|
||||
def untrack_characters(characters, map_id) do
|
||||
characters
|
||||
|> Enum.each(fn character ->
|
||||
WandererAppWeb.Presence.untrack(self(), map_id, character.id)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"#{inspect(self())}_map_#{map_id}:character_#{character.id}:tracked",
|
||||
false
|
||||
)
|
||||
|
||||
:ok =
|
||||
Phoenix.PubSub.unsubscribe(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}"
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def track_characters(_, _, false), do: :ok
|
||||
|
||||
def track_characters([], _map_id, _is_track_character?), do: :ok
|
||||
|
||||
def track_characters(
|
||||
[character | characters],
|
||||
map_id,
|
||||
true
|
||||
) do
|
||||
track_character(character, map_id)
|
||||
|
||||
track_characters(characters, map_id, true)
|
||||
end
|
||||
|
||||
def track_character(
|
||||
%{
|
||||
id: character_id,
|
||||
eve_id: eve_id,
|
||||
corporation_id: corporation_id,
|
||||
alliance_id: alliance_id
|
||||
},
|
||||
map_id
|
||||
) do
|
||||
WandererAppWeb.Presence.track(self(), map_id, character_id, %{})
|
||||
|
||||
case WandererApp.Cache.lookup!(
|
||||
"#{inspect(self())}_map_#{map_id}:character_#{character_id}:tracked",
|
||||
false
|
||||
) do
|
||||
true ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
:ok =
|
||||
Phoenix.PubSub.subscribe(
|
||||
WandererApp.PubSub,
|
||||
"character:#{eve_id}"
|
||||
)
|
||||
|
||||
:ok =
|
||||
WandererApp.Cache.put(
|
||||
"#{inspect(self())}_map_#{map_id}:character_#{character_id}:tracked",
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
case WandererApp.Cache.lookup(
|
||||
"#{inspect(self())}_map_#{map_id}:corporation_#{corporation_id}:tracked",
|
||||
false
|
||||
) do
|
||||
{:ok, true} ->
|
||||
:ok
|
||||
|
||||
{:ok, false} ->
|
||||
:ok =
|
||||
Phoenix.PubSub.subscribe(
|
||||
WandererApp.PubSub,
|
||||
"corporation:#{corporation_id}"
|
||||
)
|
||||
|
||||
:ok =
|
||||
WandererApp.Cache.put(
|
||||
"#{inspect(self())}_map_#{map_id}:corporation_#{corporation_id}:tracked",
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
case WandererApp.Cache.lookup(
|
||||
"#{inspect(self())}_map_#{map_id}:alliance_#{alliance_id}:tracked",
|
||||
false
|
||||
) do
|
||||
{:ok, true} ->
|
||||
:ok
|
||||
|
||||
{:ok, false} ->
|
||||
:ok =
|
||||
Phoenix.PubSub.subscribe(
|
||||
WandererApp.PubSub,
|
||||
"alliance:#{alliance_id}"
|
||||
)
|
||||
|
||||
:ok =
|
||||
WandererApp.Cache.put(
|
||||
"#{inspect(self())}_map_#{map_id}:alliance_#{alliance_id}:tracked",
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
end
|
||||
|
||||
defp get_location(character),
|
||||
do: %{solar_system_id: character.solar_system_id, structure_id: character.structure_id}
|
||||
|
||||
@@ -461,8 +292,8 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
end
|
||||
|
||||
defp handle_tracking_event({:track_characters, map_characters, track_character}, socket, map_id) do
|
||||
:ok = track_characters(map_characters, map_id, track_character)
|
||||
:ok = add_characters(map_characters, map_id, track_character)
|
||||
:ok = WandererApp.Character.TrackingUtils.track_characters(map_characters, map_id, track_character, self())
|
||||
:ok = WandererApp.Character.TrackingUtils.add_characters(map_characters, map_id, track_character)
|
||||
socket
|
||||
end
|
||||
|
||||
@@ -555,165 +386,4 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
:character_fleet -> handle_fleet_result(socket, result)
|
||||
end
|
||||
end
|
||||
|
||||
defp toggle_character_follow(map_id, clicked_char, is_already_followed) do
|
||||
with {:ok, clicked_char_settings} <-
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, clicked_char.id) do
|
||||
if is_already_followed do
|
||||
# If already followed, just unfollow without affecting other characters
|
||||
WandererApp.MapCharacterSettingsRepo.unfollow(clicked_char_settings)
|
||||
else
|
||||
# Normal follow toggle
|
||||
update_follow_status(map_id, clicked_char, clicked_char_settings)
|
||||
end
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
# Character not found in settings, create new settings
|
||||
update_follow_status(map_id, clicked_char, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp update_follow_status(map_id, clicked_char, nil) do
|
||||
# Create new settings with tracked=true and followed=true
|
||||
# If we're following this character, unfollow all others first
|
||||
:ok = maybe_unfollow_others(map_id, clicked_char.id, true)
|
||||
|
||||
result =
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: clicked_char.id,
|
||||
map_id: map_id,
|
||||
tracked: true,
|
||||
followed: true
|
||||
})
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp update_follow_status(map_id, clicked_char, clicked_char_settings) do
|
||||
# Toggle the followed state
|
||||
followed = !clicked_char_settings.followed
|
||||
|
||||
# Only unfollow other characters if we're explicitly following this character
|
||||
# This prevents unfollowing other characters when just tracking a character
|
||||
if followed do
|
||||
# We're following this character, so unfollow all others
|
||||
:ok = maybe_unfollow_others(map_id, clicked_char.id, followed)
|
||||
end
|
||||
|
||||
# If we're following, make sure the character is also tracked
|
||||
:ok = maybe_track_character(clicked_char_settings, followed)
|
||||
|
||||
# Update the follow status
|
||||
{:ok, settings} = update_follow(clicked_char_settings, followed)
|
||||
|
||||
{:ok, settings}
|
||||
end
|
||||
|
||||
defp maybe_unfollow_others(_map_id, _char_id, false), do: :ok
|
||||
|
||||
defp maybe_unfollow_others(map_id, char_id, true) do
|
||||
# This function should only be called when explicitly following a character,
|
||||
# not when tracking a character. It unfollows all other characters when
|
||||
# setting a character as followed.
|
||||
|
||||
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
# Unfollow other characters
|
||||
all_settings
|
||||
|> Enum.filter(&(&1.character_id != char_id && &1.followed))
|
||||
|> Enum.each(fn setting ->
|
||||
WandererApp.MapCharacterSettingsRepo.unfollow(setting)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp maybe_track_character(_settings, false), do: :ok
|
||||
|
||||
defp maybe_track_character(settings, true) do
|
||||
if not settings.tracked do
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.track(settings)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_follow(settings, true), do: WandererApp.MapCharacterSettingsRepo.follow(settings)
|
||||
defp update_follow(settings, false), do: WandererApp.MapCharacterSettingsRepo.unfollow(settings)
|
||||
|
||||
defp build_tracking_data(map_id, current_user) do
|
||||
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||
map <- Ash.load!(map, :acls),
|
||||
{:ok, character_settings} <- WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user.id) do
|
||||
tracking_data =
|
||||
Enum.map(characters_with_access, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
tracked = if setting, do: setting.tracked, else: false
|
||||
# Important: Preserve the followed state
|
||||
followed = if setting, do: setting.followed, else: false
|
||||
|
||||
%{
|
||||
character: char |> MapEventHandler.map_ui_character_stat(),
|
||||
tracked: tracked,
|
||||
followed: followed
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, tracking_data}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to toggle character tracking
|
||||
defp toggle_character_tracking(character, map_id, only_tracked_characters) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id) do
|
||||
{:ok, existing_settings} ->
|
||||
if existing_settings.tracked do
|
||||
# Untrack the character
|
||||
{:ok, updated_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(existing_settings)
|
||||
|
||||
:ok = untrack_characters([character], map_id)
|
||||
:ok = remove_characters([character], map_id)
|
||||
|
||||
if only_tracked_characters do
|
||||
Process.send_after(self(), :not_all_characters_tracked, 10)
|
||||
end
|
||||
|
||||
# If the character was followed, we need to unfollow it too
|
||||
# But we should NOT unfollow other characters
|
||||
if existing_settings.followed do
|
||||
{:ok, final_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.unfollow(updated_settings)
|
||||
|
||||
{:ok, final_settings}
|
||||
else
|
||||
{:ok, updated_settings}
|
||||
end
|
||||
else
|
||||
# Track the character
|
||||
{:ok, updated_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.track(existing_settings)
|
||||
|
||||
:ok = track_characters([character], map_id, true)
|
||||
:ok = add_characters([character], map_id, true)
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 10)
|
||||
|
||||
{:ok, updated_settings}
|
||||
end
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Create new settings
|
||||
result =
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character.id,
|
||||
map_id: map_id,
|
||||
tracked: true,
|
||||
followed: false
|
||||
})
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,14 +56,14 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
case track_character do
|
||||
false ->
|
||||
:ok = MapCharactersEventHandler.untrack_characters(map_characters, map_id)
|
||||
:ok = MapCharactersEventHandler.remove_characters(map_characters, map_id)
|
||||
:ok = WandererApp.Character.TrackingUtils.untrack_characters(map_characters, map_id, self())
|
||||
:ok = WandererApp.Character.TrackingUtils.remove_characters(map_characters, map_id)
|
||||
|
||||
_ ->
|
||||
:ok = MapCharactersEventHandler.track_characters(map_characters, map_id, true)
|
||||
:ok = WandererApp.Character.TrackingUtils.track_characters(map_characters, map_id, true, self())
|
||||
|
||||
:ok =
|
||||
MapCharactersEventHandler.add_characters(map_characters, map_id, track_character)
|
||||
WandererApp.Character.TrackingUtils.add_characters(map_characters, map_id, track_character)
|
||||
end
|
||||
|
||||
socket
|
||||
|
||||
@@ -29,7 +29,6 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
@map_characters_ui_events [
|
||||
"toggle_track",
|
||||
"toggle_follow",
|
||||
"hide_tracking",
|
||||
"show_tracking",
|
||||
"getCharacterInfo"
|
||||
]
|
||||
@@ -304,13 +303,13 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
} = socket,
|
||||
type,
|
||||
body
|
||||
),
|
||||
do:
|
||||
socket
|
||||
|> Phoenix.LiveView.Utils.push_event("map_event", %{
|
||||
type: type,
|
||||
body: body
|
||||
})
|
||||
) do
|
||||
socket
|
||||
|> Phoenix.LiveView.Utils.push_event("map_event", %{
|
||||
type: type,
|
||||
body: body
|
||||
})
|
||||
end
|
||||
|
||||
def push_map_event(socket, _type, _body), do: socket
|
||||
|
||||
|
||||
@@ -210,6 +210,7 @@ defmodule WandererAppWeb.Router do
|
||||
get "/system", MapAPIController, :show_system
|
||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||
get "/character-activity", MapAPIController, :character_activity
|
||||
get "/acls", MapAccessListAPIController, :index
|
||||
post "/acls", MapAccessListAPIController, :create
|
||||
end
|
||||
|
||||
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.55.2"
|
||||
@version "1.56.1"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -19,6 +19,7 @@ With these APIs, you can:
|
||||
- Retrieve map data, including systems and their properties
|
||||
- Access system static information
|
||||
- Track character locations and activities
|
||||
- View character activity metrics (passages, connections, signatures)
|
||||
- Monitor kill activity in systems
|
||||
- Manage Access Control Lists (ACLs) for permissions
|
||||
- Add, update, and remove ACL members
|
||||
@@ -245,17 +246,12 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
{
|
||||
"id": "<REDACTED_ID>",
|
||||
"character": {
|
||||
"id": "<REDACTED_ID>",
|
||||
"eve_id": "<REDACTED_EVE_ID>",
|
||||
"name": "<REDACTED_NAME>",
|
||||
"inserted_at": "2025-01-01T05:24:18.461721Z",
|
||||
"updated_at": "2025-01-03T07:45:52.294052Z",
|
||||
"alliance_id": "<REDACTED>",
|
||||
"alliance_name": "<REDACTED>",
|
||||
"alliance_ticker": "<REDACTED>",
|
||||
"corporation_id": "<REDACTED>",
|
||||
"corporation_name": "<REDACTED>",
|
||||
"corporation_ticker": "<REDACTED>",
|
||||
"eve_id": "<REDACTED>"
|
||||
"corporation_id": "<REDACTED_CORP_ID>",
|
||||
"corporation_ticker": "<REDACTED_CORP_TICKER>",
|
||||
"alliance_id": "<REDACTED_ALLIANCE_ID>",
|
||||
"alliance_ticker": "<REDACTED_ALLIANCE_TICKER>"
|
||||
},
|
||||
"tracked": true,
|
||||
"map_id": "<REDACTED_ID>"
|
||||
@@ -337,7 +333,51 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Structure Timers
|
||||
### 6. Character Activity
|
||||
|
||||
```bash
|
||||
GET /api/map/character-activity?map_id=<UUID>
|
||||
GET /api/map/character-activity?slug=<map-slug>
|
||||
```
|
||||
|
||||
- **Description:** Retrieves character activity data for a map, including passages, connections, and signatures.
|
||||
- **Authentication:** Requires Map API Token.
|
||||
- **Parameters:**
|
||||
- `map_id` (optional if `slug` is provided) — the UUID of the map.
|
||||
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
"https://wanderer.example.com/api/map/character-activity?slug=some-slug"
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"character": {
|
||||
"eve_id": "<REDACTED_EVE_ID>",
|
||||
"name": "<REDACTED_NAME>",
|
||||
"corporation_id": "<REDACTED_CORP_ID>",
|
||||
"corporation_ticker": "<REDACTED_CORP_TICKER>",
|
||||
"alliance_id": "<REDACTED_ALLIANCE_ID>",
|
||||
"alliance_ticker": "<REDACTED_ALLIANCE_TICKER>"
|
||||
},
|
||||
"passages": 42,
|
||||
"connections": 15,
|
||||
"signatures": 23,
|
||||
"timestamp": "2025-03-01T14:30:22Z"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Structure Timers
|
||||
|
||||
```bash
|
||||
GET /api/map/structure-timers?map_id=<UUID>
|
||||
@@ -377,18 +417,20 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "b374d9e6-47a7-4e20-85ad-d608809827b5",
|
||||
"name": "Some Character",
|
||||
"eve_id": "2122825111",
|
||||
"corporation_name": "School of Applied Knowledge",
|
||||
"alliance_name": null
|
||||
"name": "Some Character",
|
||||
"corporation_id": "1000044",
|
||||
"corporation_ticker": "SAOK",
|
||||
"alliance_id": null,
|
||||
"alliance_ticker": null
|
||||
},
|
||||
{
|
||||
"id": "6963bee6-eaa1-40e2-8200-4bc2fcbd7350",
|
||||
"name": "Other Character",
|
||||
"eve_id": "2122019111",
|
||||
"corporation_name": "Some Corporation",
|
||||
"alliance_name": null
|
||||
"name": "Other Character",
|
||||
"corporation_id": "98140648",
|
||||
"corporation_ticker": "CORP",
|
||||
"alliance_id": "99013806",
|
||||
"alliance_ticker": "ALLY"
|
||||
},
|
||||
...
|
||||
]
|
||||
@@ -828,6 +870,7 @@ This guide provides a comprehensive overview of Wanderer's API capabilities. Wit
|
||||
3. **Access system information** with or without authentication
|
||||
4. **Manage Access Control Lists (ACLs)** for permissions
|
||||
5. **Add, update, and remove ACL members** with different roles
|
||||
6. **View character activity metrics** including passages, connections, and signatures
|
||||
|
||||
For the most up-to-date and interactive documentation, we recommend using the Swagger UI at `/swaggerui` which allows you to explore and test endpoints directly from your browser.
|
||||
|
||||
|
||||
366
priv/posts/2025/03-18-bots.md
Normal file
@@ -0,0 +1,366 @@
|
||||
%{
|
||||
title: "Get Real-Time Notifications with Wanderer Notifier",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/03-18-bots/dashboard.png",
|
||||
tags: ~w(notifier discord notifications docker user-guide),
|
||||
description: "Download and run Wanderer Notifier to receive real-time notifications in your Discord channel. Learn how to get started with our Docker image and discover the different alerts you'll receive."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# Get Real-Time Notifications with Wanderer Notifier
|
||||
|
||||
Wanderer Notifier delivers real-time alerts directly to your Discord channel, ensuring you never miss critical in-game events. Whether it's a significant kill, a newly tracked character, or a fresh system discovery, our notifier keeps you informed with rich, detailed notifications.
|
||||
|
||||
In the fast-paced universe of EVE Online, timely information can mean the difference between success and failure. When a hostile fleet enters your territory, when a high-value target appears in your hunting grounds, or when a new wormhole connection opens up valuable opportunities - knowing immediately gives you the edge. Wanderer Notifier bridges this information gap, bringing critical intel directly to your Discord where your team is already coordinating.
|
||||
|
||||
## Table of Contents
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [How to Get Started](#how-to-get-started)
|
||||
- [Quick Install Option](#quick-install-option)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Notification Types](#notification-types)
|
||||
- [Kill Notifications](#kill-notifications)
|
||||
- [Character Tracking Notifications](#character-tracking-notifications)
|
||||
- [System Notifications](#system-notifications)
|
||||
- [Map Subscription Features & Limitations](#map-subscription-features--limitations)
|
||||
- [Free Version Features](#free-version-features)
|
||||
- [Premium Map Subscription Enhancements](#premium-map-subscription-enhancements)
|
||||
- [How to Subscribe](#how-to-subscribe)
|
||||
- [Feature Comparison](#feature-comparison)
|
||||
- [Web Dashboard](#web-dashboard)
|
||||
- [Configuration Options](#configuration-options)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Updating Wanderer Notifier](#updating-wanderer-notifier)
|
||||
- [Conclusion](#conclusion)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Wanderer Notifier, ensure you have the following:
|
||||
|
||||
- A Discord server where you have administrator permissions
|
||||
- Docker and Docker Compose installed on your system
|
||||
- Basic knowledge of terminal/command line operations
|
||||
- Your Wanderer map URL and API token
|
||||
- A Discord bot token (see our [guide on creating a Discord bot](https://gist.github.com/guarzo/a4d238b932b6a168ad1c5f0375c4a561))
|
||||
|
||||
## How to Get Started
|
||||
|
||||
There are two ways to install Wanderer Notifier: a **Quick Install** option using a one-liner, or a **Manual Setup** for those who prefer step-by-step control.
|
||||
|
||||
### Quick Install Option
|
||||
|
||||
For a streamlined installation that creates the necessary directory and files automatically, run:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gist.githubusercontent.com/guarzo/3f05f3c57005c3cf3585869212caecfe/raw/wanderer-notifier-setup.sh | bash
|
||||
```
|
||||
|
||||
Once the script finishes, update the `wanderer-notifier/.env` file with your configuration values, then proceed to [Step 4](#4-run-it).
|
||||
|
||||
### Manual Setup
|
||||
|
||||
If you'd rather set up everything manually, follow these steps:
|
||||
|
||||
#### 1. Download the Docker Image
|
||||
|
||||
Pull the latest Docker image:
|
||||
|
||||
```bash
|
||||
docker pull guarzo/wanderer-notifier:latest
|
||||
```
|
||||
|
||||
#### 2. Configure Your Environment
|
||||
|
||||
Create a `.env` file in your working directory with the following content. Replace the placeholder values with your actual credentials:
|
||||
|
||||
```dotenv
|
||||
# Required Configuration
|
||||
DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||
DISCORD_CHANNEL_ID=your_discord_channel_id
|
||||
MAP_URL_WITH_NAME="https://wanderer.ltd/<yourmap>"
|
||||
MAP_TOKEN=your_map_api_token
|
||||
|
||||
# Map Subscription Configuration (for enhanced features)
|
||||
# Note: Premium features are enabled with your map subscription
|
||||
LICENSE_KEY=your_map_license_key # Provided with your map subscription
|
||||
|
||||
# Notification Control (all enabled by default)
|
||||
# ENABLE_KILL_NOTIFICATIONS=true
|
||||
# ENABLE_CHARACTER_TRACKING=true
|
||||
# ENABLE_CHARACTER_NOTIFICATIONS=true
|
||||
# ENABLE_SYSTEM_NOTIFICATIONS=true
|
||||
# TRACK_ALL_SYSTEMS=false
|
||||
```
|
||||
|
||||
> **Note:** If you don't have a Discord bot yet, follow our [guide on creating a Discord bot](https://gist.github.com/guarzo/a4d238b932b6a168ad1c5f0375c4a561) or search the web for more information.
|
||||
|
||||
#### 3. Create the Docker Compose Configuration
|
||||
|
||||
Create a file named `docker-compose.yml` with the following content:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wanderer_notifier:
|
||||
image: guarzo/wanderer-notifier:latest
|
||||
container_name: wanderer_notifier
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID}
|
||||
- MAP_URL_WITH_NAME=${MAP_URL_WITH_NAME}
|
||||
- MAP_TOKEN=${MAP_TOKEN}
|
||||
- LICENSE_KEY=${LICENSE_KEY}
|
||||
ports:
|
||||
- 4000:4000
|
||||
volumes:
|
||||
- wanderer_data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${PORT:-4000}/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
wanderer_data:
|
||||
```
|
||||
|
||||
> **Note:** If you used the quick install option, these files have already been created for you.
|
||||
|
||||
#### 4. Run It
|
||||
|
||||
Start the service with Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Your notifier is now up and running, delivering alerts to your Discord channel automatically!
|
||||
|
||||
---
|
||||
|
||||
## Notification Types
|
||||
|
||||
Wanderer Notifier supports three main notification types, each tailored based on your map subscription status.
|
||||
|
||||
### Kill Notifications
|
||||
|
||||
When a kill occurs in a tracked system or involves a tracked character:
|
||||
|
||||
- **With Premium Map Subscription:**
|
||||
Receives a rich embed that includes:
|
||||
- Ship thumbnail image
|
||||
- Detailed information about both victim and attacker
|
||||
- Links to zKillboard profiles
|
||||
- Ship type details
|
||||
- ISK value of the kill
|
||||
- Corporation logos
|
||||
- A clickable link on the final blow character to zKillboard
|
||||
|
||||

|
||||
|
||||
- **With Free Map:**
|
||||
Displays a basic text notification containing:
|
||||
- Victim name
|
||||
- Ship type lost
|
||||
- System name
|
||||
|
||||

|
||||
|
||||
### Character Tracking Notifications
|
||||
|
||||
When a new character is added to your tracked list:
|
||||
|
||||
- **With Premium Map Subscription:**
|
||||
You get a rich embed featuring:
|
||||
- Character portrait
|
||||
- Corporation details
|
||||
- Direct link to the zKillboard profile
|
||||
- Formatted timestamp
|
||||
|
||||

|
||||
|
||||
- **With Free Map:**
|
||||
Receives a simple text notification that includes:
|
||||
- Character name
|
||||
- Corporation name (if available)
|
||||
|
||||

|
||||
|
||||
### System Notifications
|
||||
|
||||
When a new system is discovered or added to your map:
|
||||
|
||||
- **With Premium Map Subscription:**
|
||||
Shows a rich embed with:
|
||||
- System name (including aliases/temporary names)
|
||||
- System type icon
|
||||
- Region information or wormhole statics
|
||||
- Security status
|
||||
- Recent kills in the system
|
||||
- Links to zKillboard and Dotlan
|
||||
|
||||

|
||||
|
||||
- **With Free Map:**
|
||||
Provides a basic text notification including:
|
||||
- Original system name (for wormholes)
|
||||
- System name (for k-space)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Map Subscription Features & Limitations
|
||||
|
||||
Wanderer Notifier offers enhanced functionality with a premium map subscription while still providing robust features for free maps.
|
||||
|
||||
### Free Version Features
|
||||
|
||||
- **Core Notifications:** Basic text notifications for systems and characters.
|
||||
- **Web Dashboard:** View system status and subscription information.
|
||||
- **Unlimited Tracking:** Track an unlimited number of systems and characters.
|
||||
- **Notification History:** 24-hour retention of notification history.
|
||||
|
||||
### Premium Map Subscription Enhancements
|
||||
|
||||
- **Rich Notifications:** Enhanced embeds with images, links, and detailed data.
|
||||
- **Interactive Elements:** Clickable links to zKillboard profiles and additional resources.
|
||||
- **Enhanced System Information:** Comprehensive data including region details, security status, and wormhole statics.
|
||||
- **Recent Activity:** Access to recent kill data in newly mapped systems.
|
||||
- **Upcoming Features:** Daily reporting on tracked character activity, structure notifications, ACL notifications, and Slack notifications.
|
||||
|
||||
### How to Subscribe
|
||||
|
||||
To unlock the enhanced features of Wanderer Notifier:
|
||||
|
||||
1. Visit our [Map Subscriptions page](/map-subscriptions) to learn about subscription options
|
||||
2. Subscribe to any premium map tier to receive your map subscription key
|
||||
3. Add your map subscription key to the LICENSE_KEY field in your `.env` file
|
||||
4. Restart the notifier to apply your subscription benefits
|
||||
|
||||
For more details on map subscription tiers and pricing, see our [complete guide to map subscriptions](/map-subscriptions).
|
||||
|
||||
### Feature Comparison
|
||||
|
||||
| Feature | Free Map | Premium Map Subscription |
|
||||
|--------------------------|----------|--------------------------|
|
||||
| Kill Tracking | Unlimited| Unlimited |
|
||||
| System Tracking | Unlimited| Unlimited |
|
||||
| Character Tracking | Unlimited| Unlimited |
|
||||
| Notification Format | Basic Text| Rich Embeds |
|
||||
|
||||
---
|
||||
|
||||
## Web Dashboard
|
||||
|
||||
Wanderer Notifier includes a web dashboard that provides real-time insights into your notification system:
|
||||
|
||||
- **Access:** Visit `http://localhost:4000` to view the dashboard.
|
||||
- **System Status:** Monitor system details, subscription information, and notification statistics.
|
||||
- **Resource Monitoring:** Keep an eye on resource usage and feature availability.
|
||||
- **Notification Testing:** Test notifications directly from the dashboard.
|
||||
|
||||
Premium map subscribers also gain access to detailed statistics and advanced visualization tools.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Customize your notification experience with several configuration options available through environment variables.
|
||||
|
||||
### Notification Control Variables
|
||||
|
||||
- **ENABLE_KILL_NOTIFICATIONS:** Enable/disable kill notifications (default: true).
|
||||
- **ENABLE_CHARACTER_TRACKING:** Enable/disable character tracking (default: true).
|
||||
- **ENABLE_CHARACTER_NOTIFICATIONS:** Enable/disable notifications when new characters are added (default: true).
|
||||
- **ENABLE_SYSTEM_NOTIFICATIONS:** Enable/disable system notifications (default: true).
|
||||
|
||||
> **Note:**
|
||||
> - **Character Tracking:** Determines whether the application monitors characters.
|
||||
> - **Character Notifications:** Controls whether you receive Discord alerts when new characters are added.
|
||||
|
||||
To disable a notification type, set the corresponding variable to `false` or `0` in your `.env` file:
|
||||
|
||||
```dotenv
|
||||
# Example: Disable kill notifications while keeping other notifications enabled
|
||||
ENABLE_KILL_NOTIFICATIONS=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues with Wanderer Notifier, here are solutions to common problems:
|
||||
|
||||
### No Notifications Appearing
|
||||
|
||||
1. **Check Bot Permissions:** Ensure your bot has the "Send Messages" and "Embed Links" permissions in the Discord channel.
|
||||
2. **Verify Channel ID:** Double-check your DISCORD_CHANNEL_ID in the .env file.
|
||||
3. **Check Container Logs:** Run `docker logs wanderer_notifier` to see if there are any error messages.
|
||||
4. **Test API Connection:** Visit `http://localhost:4000/health` to verify the service is running.
|
||||
|
||||
### Connection Issues
|
||||
|
||||
1. **Network Configuration:** Ensure port 4000 is not blocked by your firewall.
|
||||
2. **Docker Status:** Run `docker ps` to verify the container is running.
|
||||
3. **Restart Service:** Try `docker-compose restart` to refresh the connection.
|
||||
|
||||
### Subscription Not Recognized
|
||||
|
||||
1. **Check Map Token:** Ensure your MAP_TOKEN is correct and associated with your map.
|
||||
2. **Verify LICENSE_KEY:** Make sure you've entered the correct map subscription key in your .env file.
|
||||
3. **Verify Status:** Check the dashboard at `http://localhost:4000` to see subscription status.
|
||||
4. **Restart After Subscribing:** If you've recently subscribed, restart the notifier with `docker-compose restart`.
|
||||
|
||||
For additional support, join our [Discord community](https://discord.gg/wanderer) or email support@wanderer.ltd.
|
||||
|
||||
## Updating Wanderer Notifier
|
||||
|
||||
To ensure you have the latest features and security updates, periodically update your Wanderer Notifier installation:
|
||||
|
||||
### Automatic Updates
|
||||
|
||||
The Docker image is configured to check for updates daily. To manually trigger an update:
|
||||
|
||||
```bash
|
||||
# Navigate to your wanderer-notifier directory
|
||||
cd wanderer-notifier
|
||||
|
||||
# Pull the latest image
|
||||
docker-compose pull
|
||||
|
||||
# Restart the container with the new image
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Update Notifications
|
||||
|
||||
When significant updates are available, you'll receive a notification in your Discord channel. These updates may include:
|
||||
|
||||
- New notification types
|
||||
- Enhanced visualization features
|
||||
- Security improvements
|
||||
- Bug fixes
|
||||
|
||||
### Preserving Your Configuration
|
||||
|
||||
Updates preserve your existing configuration and data. Your `.env` file and tracked entities will remain intact through the update process.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Wanderer Notifier is engineered to keep you informed of crucial in-game events effortlessly. The free version provides unlimited tracking with basic notifications, while premium map subscribers receive rich, detailed alerts with enhanced features.
|
||||
|
||||
By following either the quick install or manual setup process, you'll have the notifier running in no time—delivering real-time alerts directly to your Discord channel so you can focus on what matters most in your gameplay.
|
||||
|
||||
For further support or questions, please contact the Wanderer Team.
|
||||
|
||||
Stay vigilant and enjoy your real-time alerts!
|
||||