mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-08 16:56:03 +00:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64788e73de | ||
|
|
114fd471e8 | ||
|
|
b24a3120d3 | ||
|
|
c5f93b3d0a | ||
|
|
79290e4721 | ||
|
|
984e126f23 | ||
|
|
cd1ad31aed | ||
|
|
1e3f6cf9e7 | ||
|
|
9c6ccd9a8a | ||
|
|
681ba21d39 | ||
|
|
aef62189ee | ||
|
|
09f70ac817 | ||
|
|
1eacb22143 | ||
|
|
8524bad377 | ||
|
|
9d899243d1 | ||
|
|
9acf20a639 | ||
|
|
71ef6b2e82 | ||
|
|
5e34d95dd2 | ||
|
|
25a809c064 | ||
|
|
f760498150 | ||
|
|
328301a375 | ||
|
|
f28e7ebbbb | ||
|
|
bfa84af71e | ||
|
|
42cd1ba976 | ||
|
|
88cba866fd | ||
|
|
af2876a84b | ||
|
|
c5b15bfa78 | ||
|
|
85f00a63c2 | ||
|
|
05f427bcd7 | ||
|
|
69f4c41534 | ||
|
|
30b9239a8b | ||
|
|
2061a83c59 | ||
|
|
24e723de07 | ||
|
|
27b5694885 | ||
|
|
4093f28cee | ||
|
|
08aaf2f2dd | ||
|
|
5a927e5ba5 | ||
|
|
10fafcf59f | ||
|
|
be87591801 | ||
|
|
086d4378d3 | ||
|
|
e982275905 | ||
|
|
77c02703e9 | ||
|
|
0ef27d4f95 | ||
|
|
5edc27744e | ||
|
|
02ff887fee | ||
|
|
3a30eeb59f | ||
|
|
79af8fb601 | ||
|
|
f9f00faa0e | ||
|
|
a3c41e84e4 | ||
|
|
7f21f33351 | ||
|
|
568f682cee | ||
|
|
901c4c8ca4 | ||
|
|
3dbba97f9c | ||
|
|
3475620267 | ||
|
|
8936a5e5d8 | ||
|
|
719e34f9bc | ||
|
|
df955ff8b0 | ||
|
|
4dc74022b4 | ||
|
|
c2b7d07208 | ||
|
|
21e76a6f05 | ||
|
|
89c0d6fad6 | ||
|
|
b74d15b1ee | ||
|
|
10313438bf | ||
|
|
a764217948 | ||
|
|
d81d6fb937 | ||
|
|
4c0f7ab7f9 | ||
|
|
1c48945a96 | ||
|
|
850901f62f | ||
|
|
4822854e30 | ||
|
|
f580538331 | ||
|
|
0d70c555e6 | ||
|
|
c5f6cf0080 | ||
|
|
6ff7b3bc9a | ||
|
|
346c2c65b0 | ||
|
|
a6445fd500 | ||
|
|
358e43e508 | ||
|
|
20c5ba6b63 | ||
|
|
661658a6e8 | ||
|
|
0a6f224ed3 | ||
|
|
e7bb29693f | ||
|
|
32dfd50461 | ||
|
|
bfec385dce | ||
|
|
04278f99d7 | ||
|
|
9d3db19dc1 | ||
|
|
3953e33f37 | ||
|
|
611fdd56d0 | ||
|
|
36a4fd0f35 | ||
|
|
488984a988 | ||
|
|
1b183d6e58 | ||
|
|
3dcb6d30b5 | ||
|
|
1d11788f89 | ||
|
|
d3822128ab | ||
|
|
6ac7836505 |
235
CHANGELOG.md
235
CHANGELOG.md
@@ -2,6 +2,241 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.68.1](https://github.com/wanderer-industries/wanderer/compare/v1.68.0...v1.68.1) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed auth from welcome page if invites disabled
|
||||
|
||||
## [v1.68.0](https://github.com/wanderer-industries/wanderer/compare/v1.67.5...v1.68.0) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added invites store support
|
||||
|
||||
## [v1.67.5](https://github.com/wanderer-industries/wanderer/compare/v1.67.4...v1.67.5) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added back ARM docker image build
|
||||
|
||||
## [v1.67.4](https://github.com/wanderer-industries/wanderer/compare/v1.67.3...v1.67.4) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issue with system splash updates
|
||||
|
||||
## [v1.67.3](https://github.com/wanderer-industries/wanderer/compare/v1.67.2...v1.67.3) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issue with system splash updates
|
||||
|
||||
## [v1.67.2](https://github.com/wanderer-industries/wanderer/compare/v1.67.1...v1.67.2) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.67.1](https://github.com/wanderer-industries/wanderer/compare/v1.67.0...v1.67.1) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.67.0](https://github.com/wanderer-industries/wanderer/compare/v1.66.25...v1.67.0) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added support for WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED env variable to pause inactive character trackers
|
||||
|
||||
## [v1.66.25](https://github.com/wanderer-industries/wanderer/compare/v1.66.24...v1.66.25) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Disabled kills fetching based on env settings
|
||||
|
||||
## [v1.66.24](https://github.com/wanderer-industries/wanderer/compare/v1.66.23...v1.66.24) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.23](https://github.com/wanderer-industries/wanderer/compare/v1.66.22...v1.66.23) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.22](https://github.com/wanderer-industries/wanderer/compare/v1.66.21...v1.66.22) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.21](https://github.com/wanderer-industries/wanderer/compare/v1.66.20...v1.66.21) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed kills fetching based on env settings
|
||||
|
||||
## [v1.66.20](https://github.com/wanderer-industries/wanderer/compare/v1.66.19...v1.66.20) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.19](https://github.com/wanderer-industries/wanderer/compare/v1.66.18...v1.66.19) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added check for offline characters timeouts
|
||||
|
||||
## [v1.66.18](https://github.com/wanderer-industries/wanderer/compare/v1.66.17...v1.66.18) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.17](https://github.com/wanderer-industries/wanderer/compare/v1.66.16...v1.66.17) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Increased tracking pause timeout for offline characters up to 10 hours
|
||||
|
||||
## [v1.66.16](https://github.com/wanderer-industries/wanderer/compare/v1.66.15...v1.66.16) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Increased tracking pause timeout for offline characters up to 10 hours
|
||||
|
||||
## [v1.66.15](https://github.com/wanderer-industries/wanderer/compare/v1.66.14...v1.66.15) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added back arm docker image build
|
||||
|
||||
## [v1.66.14](https://github.com/wanderer-industries/wanderer/compare/v1.66.13...v1.66.14) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed online updates
|
||||
|
||||
## [v1.66.13](https://github.com/wanderer-industries/wanderer/compare/v1.66.12...v1.66.13) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed location tracking issues
|
||||
|
||||
## [v1.66.12](https://github.com/wanderer-industries/wanderer/compare/v1.66.11...v1.66.12) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.11](https://github.com/wanderer-industries/wanderer/compare/v1.66.10...v1.66.11) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed refresh character tokens
|
||||
|
||||
## [v1.66.10](https://github.com/wanderer-industries/wanderer/compare/v1.66.9...v1.66.10) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.9](https://github.com/wanderer-industries/wanderer/compare/v1.66.8...v1.66.9) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.8](https://github.com/wanderer-industries/wanderer/compare/v1.66.7...v1.66.8) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fixed disable detailed kills env check
|
||||
|
||||
## [v1.66.7](https://github.com/wanderer-industries/wanderer/compare/v1.66.6...v1.66.7) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fixed disable detailed kills env check
|
||||
|
||||
## [v1.66.6](https://github.com/wanderer-industries/wanderer/compare/v1.66.5...v1.66.6) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* respect error limits for ESI APIs
|
||||
|
||||
## [v1.66.5](https://github.com/wanderer-industries/wanderer/compare/v1.66.4...v1.66.5) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* respect error limits for ESI APIs
|
||||
|
||||
## [v1.66.4](https://github.com/wanderer-industries/wanderer/compare/v1.66.3...v1.66.4) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* respect error limits for ESI APIs
|
||||
|
||||
## [v1.66.3](https://github.com/wanderer-industries/wanderer/compare/v1.66.2...v1.66.3) (2025-06-05)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.2](https://github.com/wanderer-industries/wanderer/compare/v1.66.1...v1.66.2) (2025-06-05)
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import classes from './Characters.module.scss';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import classes from './Characters.module.scss';
|
||||
interface CharactersProps {
|
||||
data: CharacterTypeRaw[];
|
||||
}
|
||||
@@ -17,13 +16,22 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const {
|
||||
outCommand,
|
||||
data: { mainCharacterEveId, followingCharacterEveId },
|
||||
} = useMapRootState();
|
||||
|
||||
const handleSelect = useCallback((character: CharacterTypeRaw) => {
|
||||
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
|
||||
if (!character) {
|
||||
return;
|
||||
}
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.startTracking,
|
||||
data: { character_eve_id: character.eve_id },
|
||||
});
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
data: character?.location?.solar_system_id?.toString(),
|
||||
data: character.location?.solar_system_id?.toString(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -37,14 +45,26 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !character.online,
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.name}
|
||||
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
|
||||
>
|
||||
{character.tracking_paused && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
|
||||
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
|
||||
'pi',
|
||||
PrimeIcons.PAUSE,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
@@ -55,6 +75,7 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{followingCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
CommandCharacterAdded,
|
||||
CommandCharacterRemoved,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
CommandCharacterUpdated,
|
||||
CommandPresentCharacters,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const useCommandsCharacters = () => {
|
||||
const { update } = useMapState();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
|
||||
|
||||
export const useMapInit = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Characters } from '../characters/Characters';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { sortOnlineFunc } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import { Button } from 'primereact/button';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { Characters } from '../characters/Characters';
|
||||
|
||||
const Topbar = ({ children }: WithChildren) => {
|
||||
const {
|
||||
data: { characters, userCharacters, pings },
|
||||
data: { characters, userCharacters },
|
||||
} = useMapRootState();
|
||||
|
||||
const charsToShow = useMemo(() => {
|
||||
|
||||
@@ -33,6 +33,7 @@ export type CharacterTypeRaw = {
|
||||
corporation_id: number;
|
||||
corporation_name: string;
|
||||
corporation_ticker: string;
|
||||
tracking_paused: boolean;
|
||||
};
|
||||
|
||||
export interface TrackingCharacter {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { CommentType, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
|
||||
export enum Commands {
|
||||
init = 'init',
|
||||
@@ -260,6 +260,7 @@ export enum OutCommand {
|
||||
updateMainCharacter = 'updateMainCharacter',
|
||||
addPing = 'add_ping',
|
||||
cancelPing = 'cancel_ping',
|
||||
startTracking = 'startTracking',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
|
||||
@@ -53,16 +53,6 @@ public_api_disabled =
|
||||
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
character_api_disabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_CHARACTER_API_DISABLED", "true")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
zkill_preload_disabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
map_subscriptions_enabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
|
||||
@@ -127,9 +117,15 @@ config :wanderer_app,
|
||||
admins: admins,
|
||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1") |> String.to_integer(),
|
||||
public_api_disabled: public_api_disabled,
|
||||
character_api_disabled: character_api_disabled,
|
||||
zkill_preload_disabled: zkill_preload_disabled,
|
||||
character_tracking_pause_disabled:
|
||||
System.get_env("WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED", "true")
|
||||
|> String.to_existing_atom(),
|
||||
character_api_disabled:
|
||||
System.get_env("WANDERER_CHARACTER_API_DISABLED", "true") |> String.to_existing_atom(),
|
||||
zkill_preload_disabled:
|
||||
System.get_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false") |> String.to_existing_atom(),
|
||||
map_subscriptions_enabled: map_subscriptions_enabled,
|
||||
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
||||
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
|
||||
|
||||
@@ -29,5 +29,6 @@ defmodule WandererApp.Api do
|
||||
resource WandererApp.Api.CorpWalletTransaction
|
||||
resource WandererApp.Api.License
|
||||
resource WandererApp.Api.MapPing
|
||||
resource WandererApp.Api.MapInvite
|
||||
end
|
||||
end
|
||||
|
||||
96
lib/wanderer_app/api/map_invite.ex
Normal file
96
lib/wanderer_app/api/map_invite.ex
Normal file
@@ -0,0 +1,96 @@
|
||||
defmodule WandererApp.Api.MapInvite do
|
||||
@moduledoc false
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_invites_v1")
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define(:new, action: :new)
|
||||
define(:read, action: :read)
|
||||
define(:destroy, action: :destroy)
|
||||
|
||||
define(:by_id,
|
||||
get_by: [:id],
|
||||
action: :read
|
||||
)
|
||||
|
||||
define(:by_map,
|
||||
action: :by_map
|
||||
)
|
||||
end
|
||||
|
||||
actions do
|
||||
default_accept [
|
||||
:token
|
||||
]
|
||||
|
||||
defaults [:read, :update, :destroy]
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
:map_id,
|
||||
:token,
|
||||
:type,
|
||||
:valid_until
|
||||
]
|
||||
|
||||
primary?(true)
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: true
|
||||
|
||||
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
read :by_map do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
|
||||
filter(expr(map_id == ^arg(:map_id)))
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :token, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :type, :atom do
|
||||
default "user"
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
:user,
|
||||
:admin
|
||||
]
|
||||
)
|
||||
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
attribute :valid_until, :utc_datetime do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :map, WandererApp.Api.Map do
|
||||
attribute_writable? true
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
references do
|
||||
reference :map, on_delete: :delete
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -47,17 +47,16 @@ defmodule WandererApp.Application do
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererApp.Zkb.Supervisor,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
{WandererApp.Character.TrackerPoolSupervisor, []},
|
||||
WandererApp.Character.TrackerManager,
|
||||
WandererApp.Map.Manager,
|
||||
WandererApp.Map.ZkbDataFetcher,
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
] ++
|
||||
maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?()) ++
|
||||
maybe_start_zkb(WandererApp.Env.zkill_preload_disabled?())
|
||||
|
||||
opts = [strategy: :one_for_one, name: WandererApp.Supervisor]
|
||||
|
||||
@@ -78,6 +77,12 @@ defmodule WandererApp.Application do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp maybe_start_zkb(false),
|
||||
do: [WandererApp.Zkb.Supervisor, WandererApp.Map.ZkbDataFetcher]
|
||||
|
||||
defp maybe_start_zkb(_),
|
||||
do: []
|
||||
|
||||
defp maybe_start_corp_wallet_tracker(true),
|
||||
do: [
|
||||
WandererApp.StartCorpWalletTrackerTask
|
||||
|
||||
@@ -179,20 +179,24 @@ defmodule WandererApp.Character do
|
||||
end
|
||||
|
||||
def search(character_id, opts \\ []) do
|
||||
{:ok, %{access_token: access_token, eve_id: eve_id} = _character} =
|
||||
get_character(character_id)
|
||||
get_character(character_id)
|
||||
|> case do
|
||||
{:ok, %{access_token: access_token, eve_id: eve_id} = _character} ->
|
||||
case WandererApp.Esi.search(eve_id |> String.to_integer(),
|
||||
access_token: access_token,
|
||||
character_id: character_id,
|
||||
refresh_token?: true,
|
||||
params: opts[:params]
|
||||
) do
|
||||
{:ok, result} ->
|
||||
{:ok, result |> prepare_search_results()}
|
||||
|
||||
case WandererApp.Esi.search(eve_id |> String.to_integer(),
|
||||
access_token: access_token,
|
||||
character_id: character_id,
|
||||
refresh_token?: true,
|
||||
params: opts[:params]
|
||||
) do
|
||||
{:ok, result} ->
|
||||
{:ok, result |> prepare_search_results()}
|
||||
error ->
|
||||
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
|
||||
error ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
@@ -203,9 +207,9 @@ defmodule WandererApp.Character do
|
||||
|
||||
def can_track_wallet?(_), do: false
|
||||
|
||||
def can_track_corp_wallet?(%{scopes: scopes} = _character) when not is_nil(scopes) do
|
||||
scopes |> String.split(" ") |> Enum.member?(@read_corp_wallet_scope)
|
||||
end
|
||||
def can_track_corp_wallet?(%{scopes: scopes} = _character)
|
||||
when not is_nil(scopes),
|
||||
do: scopes |> String.split(" ") |> Enum.member?(@read_corp_wallet_scope)
|
||||
|
||||
def can_track_corp_wallet?(_), do: false
|
||||
|
||||
@@ -251,13 +255,22 @@ defmodule WandererApp.Character do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_merge_map_character_settings(character, map_id, true), do: character
|
||||
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
|
||||
{:ok, tracking_paused} =
|
||||
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
|
||||
|
||||
character
|
||||
|> Map.merge(%{tracking_paused: tracking_paused})
|
||||
end
|
||||
|
||||
defp maybe_merge_map_character_settings(
|
||||
%{id: character_id} = character,
|
||||
map_id,
|
||||
_character_is_present
|
||||
) do
|
||||
{:ok, tracking_paused} =
|
||||
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
|
||||
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character_id)
|
||||
|> case do
|
||||
{:ok, settings} when not is_nil(settings) ->
|
||||
@@ -270,6 +283,7 @@ defmodule WandererApp.Character do
|
||||
|> Map.put(:online, false)
|
||||
|> Map.merge(@default_character_tracking_data)
|
||||
end
|
||||
|> Map.merge(%{tracking_paused: tracking_paused})
|
||||
end
|
||||
|
||||
defp prepare_search_results(result) do
|
||||
|
||||
@@ -33,8 +33,16 @@ defmodule WandererApp.Character.Tracker do
|
||||
status: binary()
|
||||
}
|
||||
|
||||
@online_error_timeout :timer.minutes(3)
|
||||
@forbidden_ttl :timer.minutes(1)
|
||||
@pause_tracking_timeout :timer.minutes(60 * 10)
|
||||
@offline_timeout :timer.minutes(5)
|
||||
@online_error_timeout :timer.minutes(2)
|
||||
@ship_error_timeout :timer.minutes(2)
|
||||
@location_error_timeout :timer.minutes(2)
|
||||
@online_forbidden_ttl :timer.seconds(7)
|
||||
@online_limit_ttl :timer.seconds(7)
|
||||
@forbidden_ttl :timer.seconds(5)
|
||||
@limit_ttl :timer.seconds(5)
|
||||
@location_limit_ttl :timer.seconds(1)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
|
||||
def new(), do: __struct__()
|
||||
@@ -49,6 +57,95 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> new()
|
||||
end
|
||||
|
||||
def check_offline(character_id) do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
|
||||
|> case do
|
||||
nil ->
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:last_online_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
last_online_time ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), last_online_time, :millisecond)
|
||||
|
||||
if duration >= @offline_timeout do
|
||||
pause_tracking(character_id)
|
||||
|
||||
:ok
|
||||
else
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_online_errors(character_id),
|
||||
do: check_tracking_errors(character_id, "online", @online_error_timeout)
|
||||
|
||||
def check_ship_errors(character_id),
|
||||
do: check_tracking_errors(character_id, "ship", @ship_error_timeout)
|
||||
|
||||
def check_location_errors(character_id),
|
||||
do: check_tracking_errors(character_id, "location", @location_error_timeout)
|
||||
|
||||
defp check_tracking_errors(character_id, type, timeout) do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:#{type}_error_time")
|
||||
|> case do
|
||||
nil ->
|
||||
:skip
|
||||
|
||||
error_time ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
|
||||
|
||||
if duration >= timeout do
|
||||
pause_tracking(character_id)
|
||||
|
||||
:ok
|
||||
else
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp pause_tracking(character_id) do
|
||||
if not WandererApp.Env.character_tracking_pause_disabled?() &&
|
||||
not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
Logger.warning("[CharacterTracker] paused for #{character_id}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:tracking_paused",
|
||||
true,
|
||||
ttl: @pause_tracking_timeout
|
||||
)
|
||||
|
||||
{:ok, %{solar_system_id: solar_system_id}} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
{:ok, %{active_maps: active_maps}} =
|
||||
WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
active_maps
|
||||
|> Enum.each(fn map_id ->
|
||||
WandererApp.Cache.put(
|
||||
"map:#{map_id}:character:#{character_id}:start_solar_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def update_settings(character_id, track_settings) do
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
@@ -61,8 +158,138 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> maybe_start_ship_tracking(track_settings)}
|
||||
end
|
||||
|
||||
def update_online(character_id) when is_binary(character_id),
|
||||
do:
|
||||
character_id
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_online()
|
||||
|
||||
def update_online(%{track_online: true, character_id: character_id} = character_state) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token}}
|
||||
when not is_nil(access_token) ->
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
case WandererApp.Esi.get_character_online(eve_id,
|
||||
access_token: access_token,
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, online} ->
|
||||
online = get_online(online)
|
||||
|
||||
if online.online == true do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:last_online_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
|
||||
WandererApp.Character.update_character(character_id, online)
|
||||
|
||||
update = %{
|
||||
character_state
|
||||
| is_online: online.online,
|
||||
track_ship: online.online,
|
||||
track_location: online.online
|
||||
}
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, update)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
Logger.warning("#{__MODULE__} failed to update_online: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
ttl: @online_forbidden_ttl
|
||||
)
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
|
||||
) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:online_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
Logger.warning(".")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to update_online: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
ttl: @online_forbidden_ttl
|
||||
)
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
|
||||
) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:online_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
|
||||
def update_online(_), do: {:error, :skipped}
|
||||
|
||||
defp get_reset_timeout(_headers, _default_timeout \\ @limit_ttl)
|
||||
|
||||
defp get_reset_timeout(
|
||||
%{"x-esi-error-limit-remain" => ["0"], "x-esi-error-limit-reset" => [reset_seconds]},
|
||||
_default_timeout
|
||||
)
|
||||
when is_binary(reset_seconds),
|
||||
do: :timer.seconds((reset_seconds |> String.to_integer()) + 1)
|
||||
|
||||
defp get_reset_timeout(_headers, default_timeout), do: default_timeout
|
||||
|
||||
def update_info(character_id) do
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden")
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -79,7 +306,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :error_limited, :not_found, :timeout] ->
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
Logger.warning("#{__MODULE__} failed to get_character_info: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
@@ -90,18 +317,40 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, error}
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
Logger.warning(".")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}")
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_ship(character_id) when is_binary(character_id) do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_ship()
|
||||
end
|
||||
def update_ship(character_id) when is_binary(character_id),
|
||||
do:
|
||||
character_id
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_ship()
|
||||
|
||||
def update_ship(
|
||||
%{character_id: character_id, track_ship: true, is_online: true} = character_state
|
||||
@@ -110,7 +359,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> WandererApp.Character.get_character()
|
||||
|> case do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden")
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -118,15 +369,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
_ ->
|
||||
case WandererApp.Esi.get_character_ship(eve_id,
|
||||
access_token: access_token,
|
||||
character_id: character_id,
|
||||
refresh_token?: true
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, ship} when is_non_struct_map(ship) ->
|
||||
character_state |> maybe_update_ship(ship)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :error_limited, :not_found, :timeout] ->
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
Logger.warning("#{__MODULE__} failed to update_ship: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
@@ -135,8 +385,28 @@ defmodule WandererApp.Character.Tracker do
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
if is_nil(WandererApp.Cache.lookup!("character:#{character_id}:ship_error_time")) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:ship_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, error}
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
Logger.warning(".")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to update_ship: #{inspect(error)}")
|
||||
|
||||
@@ -146,11 +416,31 @@ defmodule WandererApp.Character.Tracker do
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
if is_nil(WandererApp.Cache.lookup!("character:#{character_id}:ship_error_time")) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:ship_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
Logger.error("#{__MODULE__} failed to update_ship: wrong response")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
if is_nil(WandererApp.Cache.lookup!("character:#{character_id}:ship_error_time")) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:ship_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
@@ -162,18 +452,18 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
def update_ship(_), do: {:error, :skipped}
|
||||
|
||||
def update_location(character_id) when is_binary(character_id) do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_location()
|
||||
end
|
||||
def update_location(character_id) when is_binary(character_id),
|
||||
do:
|
||||
character_id
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_location()
|
||||
|
||||
def update_location(
|
||||
%{track_location: true, is_online: true, character_id: character_id} = character_state
|
||||
) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden")
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused")
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -181,8 +471,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
_ ->
|
||||
case WandererApp.Esi.get_character_location(eve_id,
|
||||
access_token: access_token,
|
||||
character_id: character_id,
|
||||
refresh_token?: true
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, location} when is_non_struct_map(location) ->
|
||||
character_state
|
||||
@@ -190,31 +479,59 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :error_limited, :not_found, :timeout] ->
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
Logger.warning("#{__MODULE__} failed to update_location: #{inspect(error)}")
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
|
||||
) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:location_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
Logger.warning(".")
|
||||
|
||||
reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
|
||||
) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:location_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, error}
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
Logger.error("#{__MODULE__} failed to update_location: wrong response")
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
|
||||
) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:location_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
@@ -229,116 +546,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
def update_location(_), do: {:error, :skipped}
|
||||
|
||||
def update_online(character_id) when is_binary(character_id) do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_online()
|
||||
end
|
||||
|
||||
def update_online(%{track_online: true, character_id: character_id} = character_state) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token}}
|
||||
when not is_nil(access_token) ->
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden")
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
case WandererApp.Esi.get_character_online(eve_id,
|
||||
access_token: access_token,
|
||||
character_id: character_id,
|
||||
refresh_token?: true
|
||||
) do
|
||||
{:ok, online} ->
|
||||
online = get_online(online)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Character.update_character(character_id, online)
|
||||
|
||||
update = %{
|
||||
character_state
|
||||
| is_online: online.online,
|
||||
track_ship: online.online,
|
||||
track_location: online.online
|
||||
}
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, update)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :error_limited, :not_found, :timeout] ->
|
||||
Logger.warning("#{__MODULE__} failed to update_online: #{inspect(error)}")
|
||||
|
||||
if not WandererApp.Cache.lookup!(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
false
|
||||
) do
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup("character:#{character_id}:online_error_time")
|
||||
) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:online_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to update_online: #{inspect(error)}")
|
||||
|
||||
if is_nil(WandererApp.Cache.lookup("character:#{character_id}:online_error_time")) do
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:online_error_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
|
||||
def update_online(_), do: {:error, :skipped}
|
||||
|
||||
def check_online_errors(character_id) do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
|
||||
|> case do
|
||||
nil ->
|
||||
:skip
|
||||
|
||||
error_time ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
|
||||
|
||||
if duration >= @online_error_timeout do
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
:ok
|
||||
else
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_wallet(character_id) do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character()
|
||||
@@ -349,7 +556,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> WandererApp.Character.can_track_wallet?()
|
||||
|> case do
|
||||
true ->
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden")
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -358,8 +567,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
case WandererApp.Esi.get_character_wallet(eve_id,
|
||||
params: %{datasource: "tranquility"},
|
||||
access_token: access_token,
|
||||
character_id: character_id,
|
||||
refresh_token?: true
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, result} ->
|
||||
{:ok, state} = WandererApp.Character.get_character_state(character_id)
|
||||
@@ -367,8 +575,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error}
|
||||
when error in [:forbidden, :error_limited, :not_found, :timeout] ->
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
Logger.warning("#{__MODULE__} failed to update_wallet: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
@@ -377,11 +584,42 @@ defmodule WandererApp.Character.Tracker do
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
Logger.warning(".")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:wallet_forbidden",
|
||||
true,
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__} failed to _update_wallet: #{inspect(error)}")
|
||||
{:error, error}
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:wallet_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
{:error, :skipped}
|
||||
|
||||
error ->
|
||||
Logger.error("#{__MODULE__} failed to _update_wallet: #{inspect(error)}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:wallet_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -395,83 +633,99 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
|
||||
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
|
||||
alliance_id
|
||||
|> WandererApp.Esi.get_alliance_info()
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
alliance_id: alliance_id,
|
||||
alliance_name: alliance_name,
|
||||
alliance_ticker: alliance_ticker
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
WandererApp.Api.Character.update_alliance(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:alliance",
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
true ->
|
||||
state
|
||||
|
||||
_error ->
|
||||
Logger.error("Failed to get alliance info for #{alliance_id}")
|
||||
state
|
||||
_ ->
|
||||
alliance_id
|
||||
|> WandererApp.Esi.get_alliance_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
alliance_id: alliance_id,
|
||||
alliance_name: alliance_name,
|
||||
alliance_ticker: alliance_ticker
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
WandererApp.Api.Character.update_alliance(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:alliance",
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
state
|
||||
|
||||
_error ->
|
||||
Logger.error("Failed to get alliance info for #{alliance_id}")
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
|
||||
corporation_id
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
alliance_id = Map.get(corporation_info, "alliance_id")
|
||||
true ->
|
||||
state
|
||||
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
_ ->
|
||||
corporation_id
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
alliance_id = Map.get(corporation_info, "alliance_id")
|
||||
|
||||
character_update = %{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corporation_name,
|
||||
corporation_ticker: corporation_ticker,
|
||||
alliance_id: alliance_id
|
||||
}
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
{:ok, _character} =
|
||||
WandererApp.Api.Character.update_corporation(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:corporation",
|
||||
{:character_corporation,
|
||||
{character_id,
|
||||
%{
|
||||
character_update = %{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corporation_name,
|
||||
corporation_ticker: corporation_ticker
|
||||
}}}
|
||||
)
|
||||
corporation_ticker: corporation_ticker,
|
||||
alliance_id: alliance_id
|
||||
}
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|
||||
|> maybe_update_alliance()
|
||||
{:ok, _character} =
|
||||
WandererApp.Api.Character.update_corporation(character, character_update)
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
corporation_id: corporation_id
|
||||
)
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
state
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:corporation",
|
||||
{:character_corporation,
|
||||
{character_id,
|
||||
%{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corporation_name,
|
||||
corporation_ticker: corporation_ticker
|
||||
}}}
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|
||||
|> maybe_update_alliance()
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
corporation_id: corporation_id
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -541,13 +795,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
state
|
||||
end
|
||||
|
||||
# defp is_location_started?(character_id),
|
||||
# do:
|
||||
# WandererApp.Cache.lookup!(
|
||||
# "character:#{character_id}:location_started",
|
||||
# false
|
||||
# )
|
||||
|
||||
defp is_location_updated?(
|
||||
%{
|
||||
solar_system_id: new_solar_system_id,
|
||||
|
||||
@@ -14,7 +14,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(5)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
@@ -107,32 +108,49 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
} = track_settings
|
||||
) do
|
||||
if track do
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[],
|
||||
fn untrack_queue ->
|
||||
untrack_queue
|
||||
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||
end
|
||||
)
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, track_settings)
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
else
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
add_to_untrack_queue(map_id, character_id)
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def add_to_untrack_queue(map_id, character_id) do
|
||||
if not WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
|
||||
WandererApp.Cache.insert(
|
||||
"#{map_id}:#{character_id}:untrack_requested",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def remove_from_untrack_queue(map_id, character_id) do
|
||||
WandererApp.Cache.delete("#{map_id}:#{character_id}:untrack_requested")
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[],
|
||||
fn untrack_queue ->
|
||||
untrack_queue
|
||||
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def get_characters(
|
||||
state,
|
||||
_opts \\ []
|
||||
@@ -208,10 +226,28 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
) do
|
||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||
|
||||
WandererApp.Cache.get_and_remove!("character_untrack_queue", [])
|
||||
WandererApp.Cache.lookup!("character_untrack_queue", [])
|
||||
|> Task.async_stream(
|
||||
fn {map_id, character_id} ->
|
||||
if not character_is_present(map_id, character_id) do
|
||||
untrack_timeout_reached =
|
||||
if WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
|
||||
untrack_requested =
|
||||
WandererApp.Cache.lookup!(
|
||||
"#{map_id}:#{character_id}:untrack_requested",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), untrack_requested, :millisecond)
|
||||
duration >= @untrack_character_timeout
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
Logger.debug(fn -> "Untrack timeout reached: #{inspect(untrack_timeout_reached)}" end)
|
||||
|
||||
if untrack_timeout_reached do
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
@@ -235,6 +271,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
end
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
|
||||
@@ -16,14 +16,19 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
@registry :tracker_pool_registry
|
||||
@unique_registry :unique_tracker_pool_registry
|
||||
|
||||
@update_location_interval :timer.seconds(2)
|
||||
@update_location_interval :timer.seconds(1)
|
||||
@update_online_interval :timer.seconds(5)
|
||||
@check_online_errors_interval :timer.seconds(30)
|
||||
@check_offline_characters_interval :timer.minutes(2)
|
||||
@check_online_errors_interval :timer.minutes(1)
|
||||
@check_ship_errors_interval :timer.minutes(1)
|
||||
@check_location_errors_interval :timer.minutes(1)
|
||||
@update_ship_interval :timer.seconds(2)
|
||||
@update_info_interval :timer.minutes(1)
|
||||
@update_wallet_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(5)
|
||||
|
||||
@pause_tracking_timeout :timer.minutes(60 * 24)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
def new(), do: __struct__()
|
||||
@@ -50,6 +55,12 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
tracked_ids
|
||||
|> Enum.each(fn id ->
|
||||
# WandererApp.Cache.put(
|
||||
# "character:#{id}:tracking_paused",
|
||||
# true,
|
||||
# ttl: @pause_tracking_timeout
|
||||
# )
|
||||
|
||||
Cachex.put(@cache, id, uuid)
|
||||
end)
|
||||
|
||||
@@ -77,6 +88,12 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
[tracked_id | r_tracked_ids]
|
||||
end)
|
||||
|
||||
# WandererApp.Cache.put(
|
||||
# "character:#{tracked_id}:tracking_paused",
|
||||
# true,
|
||||
# ttl: @pause_tracking_timeout
|
||||
# )
|
||||
|
||||
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
|
||||
# {:commit, ids ++ [tracked_id]}
|
||||
# end)
|
||||
@@ -116,7 +133,10 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
)
|
||||
|
||||
Process.send_after(self(), :update_online, 100)
|
||||
Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
|
||||
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
|
||||
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
|
||||
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
|
||||
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
|
||||
Process.send_after(self(), :update_location, 300)
|
||||
Process.send_after(self(), :update_ship, 500)
|
||||
Process.send_after(self(), :update_info, 1500)
|
||||
@@ -203,6 +223,46 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_offline_characters,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_offline,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
{:error, reason} -> @logger.error("Error in check_offline: #{inspect(reason)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] check_offline => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_online_errors,
|
||||
%{
|
||||
@@ -243,6 +303,86 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_ship_errors,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_ship_errors, @check_ship_errors_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_ship_errors,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
{:error, reason} -> @logger.error("Error in check_ship_errors: #{inspect(reason)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] check_ship_errors => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_location_errors,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_location_errors, @check_location_errors_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_location_errors,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
{:error, reason} -> @logger.error("Error in check_location_errors: #{inspect(reason)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] check_location_errors => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:update_location,
|
||||
%{
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
@cache :tracked_characters
|
||||
@registry :tracker_pool_registry
|
||||
@unique_registry :unique_tracker_pool_registry
|
||||
@tracker_pool_limit 100
|
||||
@tracker_pool_limit 50
|
||||
|
||||
@name __MODULE__
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
|
||||
{:ok, latest_transactions} = WandererApp.Api.CorpWalletTransaction.latest()
|
||||
|
||||
case WandererApp.Character.can_track_corp_wallet?(character) do
|
||||
case character.eve_id == WandererApp.Env.corp_wallet_eve_id() &&
|
||||
WandererApp.Character.can_track_corp_wallet?(character) do
|
||||
true ->
|
||||
Process.send_after(self(), :update_corp_wallets, 500)
|
||||
Process.send_after(self(), :check_wallets, 500)
|
||||
@@ -145,13 +146,15 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
end
|
||||
|
||||
defp get_wallet_journal(
|
||||
%{corporation_id: corporation_id, access_token: access_token} = _character,
|
||||
%{id: character_id, corporation_id: corporation_id, access_token: access_token} =
|
||||
_character,
|
||||
division
|
||||
)
|
||||
when not is_nil(access_token) do
|
||||
case WandererApp.Esi.get_corporation_wallet_journal(corporation_id, division,
|
||||
params: %{datasource: "tranquility"},
|
||||
access_token: access_token
|
||||
access_token: access_token,
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, result} ->
|
||||
{:corporation_wallet_journal, result}
|
||||
@@ -160,8 +163,8 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
Logger.warning("#{__MODULE__} failed to get_wallet_journal: forbidden")
|
||||
{:error, :forbidden}
|
||||
|
||||
{:error, :error_limited} ->
|
||||
Logger.warning("#{__MODULE__} failed to get_wallet_journal: error_limited")
|
||||
{:error, :error_limited, _headers} ->
|
||||
Logger.warning(".")
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
@@ -173,12 +176,14 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
defp get_wallet_journal(_character, _division), do: {:error, :skipped}
|
||||
|
||||
defp update_corp_wallets(
|
||||
%{corporation_id: corporation_id, access_token: access_token} = _character
|
||||
%{id: character_id, corporation_id: corporation_id, access_token: access_token} =
|
||||
_character
|
||||
)
|
||||
when not is_nil(access_token) do
|
||||
case WandererApp.Esi.get_corporation_wallets(corporation_id,
|
||||
params: %{datasource: "tranquility"},
|
||||
access_token: access_token
|
||||
access_token: access_token,
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, result} ->
|
||||
{:corporation_wallets, result}
|
||||
@@ -187,8 +192,8 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
Logger.warning("#{__MODULE__} failed to update_corp_wallets: forbidden")
|
||||
{:error, :forbidden}
|
||||
|
||||
{:error, :error_limited} ->
|
||||
Logger.warning("#{__MODULE__} failed to update_corp_wallets: error_limited")
|
||||
{:error, :error_limited, _headers} ->
|
||||
Logger.warning(".")
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -16,6 +16,7 @@ defmodule WandererApp.Env do
|
||||
def invites, do: get_key(:invites, false)
|
||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
||||
def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
|
||||
def character_api_disabled?, do: get_key(:character_api_disabled, false)
|
||||
def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false)
|
||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||
@@ -23,6 +24,7 @@ defmodule WandererApp.Env do
|
||||
def admin_username, do: get_key(:admin_username)
|
||||
def admin_password, do: get_key(:admin_password)
|
||||
def corp_wallet, do: get_key(:corp_wallet, "")
|
||||
def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, -1)
|
||||
def corp_eve_id, do: get_key(:corp_id, -1)
|
||||
def subscription_settings, do: get_key(:subscription_settings)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@default_avoid_systems [@zarzakh_system]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [max_retries: 0, retry_log_level: :warning]
|
||||
@retry_opts [retry: false, retry_log_level: :warning]
|
||||
@timeout_opts [pool_timeout: 15_000, receive_timeout: :timer.minutes(1)]
|
||||
@api_retry_count 1
|
||||
|
||||
@@ -275,10 +275,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
"success" => true
|
||||
}
|
||||
|
||||
{:error, :not_found} ->
|
||||
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
|
||||
|
||||
{:error, error} ->
|
||||
error ->
|
||||
Logger.warning("Error getting routes: #{inspect(error)}")
|
||||
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
|
||||
end
|
||||
@@ -293,6 +290,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
case _get_alliance_info(eve_id, "", opts) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -314,6 +312,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
case _get_corporation_info(eve_id, "", opts) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -330,6 +329,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -463,10 +463,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
get(
|
||||
path,
|
||||
auth_opts,
|
||||
opts
|
||||
opts |> with_refresh_token()
|
||||
)
|
||||
else
|
||||
get_retry(path, auth_opts, opts)
|
||||
get_retry(path, auth_opts, opts |> with_refresh_token())
|
||||
end
|
||||
end
|
||||
|
||||
@@ -485,7 +485,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
[params: opts[:params] || []] ++
|
||||
(opts |> get_auth_opts()),
|
||||
opts ++ @cache_opts
|
||||
(opts |> with_refresh_token()) ++ @cache_opts
|
||||
)
|
||||
|
||||
defp with_user_agent_opts(opts) do
|
||||
@@ -495,6 +495,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
)
|
||||
end
|
||||
|
||||
defp with_refresh_token(opts) do
|
||||
opts |> Keyword.merge(refresh_token?: true)
|
||||
end
|
||||
|
||||
defp with_cache_opts(opts) do
|
||||
opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
end
|
||||
@@ -531,13 +535,12 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: 404}} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
get_retry(path, api_opts, opts)
|
||||
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
Logger.warning("error_limited error: #{inspect(headers)}")
|
||||
{:error, :error_limited}
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
@@ -588,8 +591,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: 403}} ->
|
||||
{:error, :forbidden}
|
||||
|
||||
{:ok, %{status: 420}} ->
|
||||
{:error, :error_limited}
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
@@ -608,8 +611,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
defp post_esi(url, opts) do
|
||||
try do
|
||||
req_opts =
|
||||
(opts
|
||||
|> with_user_agent_opts()) ++
|
||||
(opts |> with_user_agent_opts() |> Keyword.merge(@retry_opts)) ++
|
||||
[params: opts[:params] || []]
|
||||
|
||||
Req.new(
|
||||
@@ -627,8 +629,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: 403}} ->
|
||||
{:error, :forbidden}
|
||||
|
||||
{:ok, %{status: 420}} ->
|
||||
{:error, :error_limited}
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
|
||||
|
||||
user_hash ->
|
||||
user_hash
|
||||
|> _get_user_characters()
|
||||
|> get_user_characters()
|
||||
|> maybe_start_corp_wallet_tracker()
|
||||
end
|
||||
end
|
||||
@@ -25,7 +25,8 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
|
||||
admin_character =
|
||||
user_characters
|
||||
|> Enum.find(fn character ->
|
||||
WandererApp.Character.can_track_corp_wallet?(character)
|
||||
character.eve_id == WandererApp.Env.corp_wallet_eve_id() &&
|
||||
WandererApp.Character.can_track_corp_wallet?(character)
|
||||
end)
|
||||
|
||||
if not is_nil(admin_character) do
|
||||
@@ -41,12 +42,12 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
|
||||
|
||||
def maybe_start_corp_wallet_tracker(_), do: :ok
|
||||
|
||||
defp _get_user_characters(user_hash) when not is_nil(user_hash) and is_binary(user_hash) do
|
||||
defp get_user_characters(user_hash) when not is_nil(user_hash) and is_binary(user_hash) do
|
||||
case WandererApp.Api.User.by_hash(user_hash, load: :characters) do
|
||||
{:ok, user} -> {:ok, user.characters}
|
||||
{:error, _} -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_user_characters(_), do: {:ok, []}
|
||||
defp get_user_characters(_), do: {:ok, []}
|
||||
end
|
||||
|
||||
@@ -86,19 +86,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
def untrack_characters(map_id, character_ids),
|
||||
do:
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
if is_character_map_active?(map_id, character_id) do
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
is_character_map_active?(map_id, character_id)
|
||||
|> untrack_character(map_id, character_id)
|
||||
end)
|
||||
end
|
||||
|
||||
Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
end
|
||||
end)
|
||||
def untrack_character(true, map_id, character_id) do
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
end
|
||||
|
||||
def untrack_character(_is_character_map_active, _map_id, character_id) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def is_character_map_active?(map_id, character_id) do
|
||||
case WandererApp.Character.get_character_state(character_id) do
|
||||
@@ -219,6 +224,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
Task.start_link(fn ->
|
||||
character_updates =
|
||||
maybe_update_online(map_id, character_id) ++
|
||||
maybe_update_tracking_status(map_id, character_id) ++
|
||||
maybe_update_location(map_id, character_id) ++
|
||||
maybe_update_ship(map_id, character_id) ++
|
||||
maybe_update_alliance(map_id, character_id) ++
|
||||
@@ -245,6 +251,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:character_online, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_tracking, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_alliance, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
@@ -388,6 +397,33 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_tracking_status(map_id, character_id) do
|
||||
with {:ok, old_tracking_paused} <-
|
||||
WandererApp.Cache.lookup(
|
||||
"map:#{map_id}:character:#{character_id}:tracking_paused",
|
||||
false
|
||||
),
|
||||
{:ok, tracking_paused} <-
|
||||
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false) do
|
||||
case old_tracking_paused != tracking_paused do
|
||||
true ->
|
||||
WandererApp.Cache.insert(
|
||||
"map:#{map_id}:character:#{character_id}:tracking_paused",
|
||||
tracking_paused
|
||||
)
|
||||
|
||||
[{:character_tracking, %{tracking_paused: tracking_paused}}]
|
||||
|
||||
_ ->
|
||||
[:skip]
|
||||
end
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to update character_tracking: #{inspect(error, pretty: true)}")
|
||||
[:skip]
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_ship(map_id, character_id) do
|
||||
with {:ok, old_ship_type_id} <-
|
||||
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_type_id"),
|
||||
|
||||
@@ -30,7 +30,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@backup_state_timeout :timer.minutes(1)
|
||||
@update_presence_timeout :timer.seconds(1)
|
||||
@update_presence_timeout :timer.seconds(5)
|
||||
@update_characters_timeout :timer.seconds(1)
|
||||
@update_tracked_characters_timeout :timer.seconds(1)
|
||||
|
||||
|
||||
@@ -18,34 +18,38 @@ defmodule WandererApp.Maps do
|
||||
]
|
||||
|
||||
def find_routes(map_id, hubs, origin, routes_settings, false) do
|
||||
{:ok, routes} =
|
||||
WandererApp.Esi.find_routes(
|
||||
map_id,
|
||||
origin,
|
||||
hubs,
|
||||
routes_settings
|
||||
)
|
||||
WandererApp.Esi.find_routes(
|
||||
map_id,
|
||||
origin,
|
||||
hubs,
|
||||
routes_settings
|
||||
)
|
||||
|> case do
|
||||
{:ok, routes} ->
|
||||
systems_static_data =
|
||||
routes
|
||||
|> Enum.map(fn route_info -> route_info.systems end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> Task.async_stream(
|
||||
fn system_id ->
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, nil} ->
|
||||
nil
|
||||
|
||||
systems_static_data =
|
||||
routes
|
||||
|> Enum.map(fn route_info -> route_info.systems end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> Task.async_stream(
|
||||
fn system_id ->
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, nil} ->
|
||||
nil
|
||||
{:ok, system} ->
|
||||
system |> Map.take(@minimum_route_attrs)
|
||||
end
|
||||
end,
|
||||
max_concurrency: 10
|
||||
)
|
||||
|> Enum.map(fn {:ok, val} -> val end)
|
||||
|
||||
{:ok, system} ->
|
||||
system |> Map.take(@minimum_route_attrs)
|
||||
end
|
||||
end,
|
||||
max_concurrency: 10
|
||||
)
|
||||
|> Enum.map(fn {:ok, val} -> val end)
|
||||
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
|
||||
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
error ->
|
||||
{:ok, %{routes: [], systems_static_data: []}}
|
||||
end
|
||||
end
|
||||
|
||||
def find_routes(map_id, hubs, origin, routes_settings, true) do
|
||||
|
||||
@@ -28,8 +28,6 @@ defmodule WandererApp.Server.ServerStatusTracker do
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
def get_status(), do: GenServer.call(@name, :get_status)
|
||||
|
||||
def start_link(opts \\ []), do: GenServer.start(__MODULE__, opts, name: @name)
|
||||
|
||||
@impl true
|
||||
@@ -42,9 +40,6 @@ defmodule WandererApp.Server.ServerStatusTracker do
|
||||
@impl true
|
||||
def terminate(_reason, _state), do: :ok
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_status, _from, state), do: {:reply, {:ok, state}, state}
|
||||
|
||||
@impl true
|
||||
def handle_call(:stop, _, state), do: {:stop, :normal, :ok, state}
|
||||
|
||||
@@ -66,7 +61,7 @@ defmodule WandererApp.Server.ServerStatusTracker do
|
||||
} = state
|
||||
) do
|
||||
Process.send_after(self(), :refresh_status, @refresh_interval)
|
||||
Task.async(fn -> _get_server_status(retries) end)
|
||||
Task.async(fn -> get_server_status(retries) end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
@@ -104,7 +99,7 @@ defmodule WandererApp.Server.ServerStatusTracker do
|
||||
def handle_info(_action, state),
|
||||
do: {:noreply, state}
|
||||
|
||||
defp _get_server_status(retries) do
|
||||
defp get_server_status(retries) do
|
||||
case WandererApp.Esi.get_server_status() do
|
||||
{:ok, result} ->
|
||||
{:status, _get_status(result)}
|
||||
@@ -113,7 +108,7 @@ defmodule WandererApp.Server.ServerStatusTracker do
|
||||
if retries > 0 do
|
||||
:retry
|
||||
else
|
||||
@logger.warning("#{__MODULE__} failed to refresh server status: :timeout")
|
||||
Logger.warning("#{__MODULE__} failed to refresh server status: :timeout")
|
||||
{:status, @initial_state}
|
||||
end
|
||||
|
||||
@@ -121,9 +116,12 @@ defmodule WandererApp.Server.ServerStatusTracker do
|
||||
if retries > 0 do
|
||||
:retry
|
||||
else
|
||||
@logger.warning("#{__MODULE__} failed to refresh server status: #{inspect(error)}")
|
||||
Logger.warning("#{__MODULE__} failed to refresh server status: #{inspect(error)}")
|
||||
{:status, @initial_state}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :unknown}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,17 +20,9 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
|
||||
is_admin? = Map.get(params, "admin", "false") in ~w(true 1)
|
||||
invite_token = Map.get(params, "invite", nil)
|
||||
|
||||
invite_token_valid =
|
||||
case WandererApp.Env.invites() do
|
||||
true ->
|
||||
case invite_token do
|
||||
nil -> false
|
||||
token -> WandererApp.Cache.lookup!("invite_#{token}", false)
|
||||
end
|
||||
{invite_token_valid, invite_type} = check_invite_valid(invite_token)
|
||||
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
is_admin? = is_admin? || invite_type == :admin
|
||||
|
||||
case invite_token_valid do
|
||||
true ->
|
||||
@@ -218,4 +210,33 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
|
||||
defp option(conn, key) do
|
||||
Keyword.get(options(conn), key, Keyword.get(default_options(), key))
|
||||
end
|
||||
|
||||
defp check_invite_valid(invite_token) do
|
||||
case invite_token do
|
||||
token when not is_nil(token) and token != "" ->
|
||||
check_token_valid(token)
|
||||
|
||||
_ ->
|
||||
{not WandererApp.Env.invites(), :user}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_token_valid(token) do
|
||||
WandererApp.Cache.lookup!("invite_#{token}", false)
|
||||
|> case do
|
||||
true -> {true, :user}
|
||||
_ -> check_map_token_valid(token)
|
||||
end
|
||||
end
|
||||
|
||||
def check_map_token_valid(token) do
|
||||
{:ok, invites} = WandererApp.Api.MapInvite.read()
|
||||
|
||||
invites
|
||||
|> Enum.find(fn invite -> invite.token == token end)
|
||||
|> case do
|
||||
nil -> {false, nil}
|
||||
invite -> {true, invite.type}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,26 +15,39 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
@doc """
|
||||
Fetch killmails for multiple systems, returning a map of system_id => kills.
|
||||
"""
|
||||
def fetch_kills_for_systems(system_ids, since_hours, state, _opts \\ []) when is_list(system_ids) do
|
||||
try do
|
||||
{final_map, final_state} =
|
||||
Enum.reduce(system_ids, {%{}, state}, fn sid, {acc_map, acc_st} ->
|
||||
case fetch_kills_for_system(sid, since_hours, acc_st) do
|
||||
{:ok, kills, new_st} ->
|
||||
{Map.put(acc_map, sid, kills), new_st}
|
||||
def fetch_kills_for_systems(system_ids, since_hours, state, _opts \\ [])
|
||||
when is_list(system_ids) do
|
||||
zkill_preload_disabled = WandererApp.Env.zkill_preload_disabled?()
|
||||
|
||||
{:error, reason, new_st} ->
|
||||
Logger.debug(fn -> "[Fetcher] system=#{sid} => error=#{inspect(reason)}" end)
|
||||
{Map.put(acc_map, sid, {:error, reason}), new_st}
|
||||
end
|
||||
if not zkill_preload_disabled do
|
||||
try do
|
||||
{final_map, final_state} =
|
||||
Enum.reduce(system_ids, {%{}, state}, fn sid, {acc_map, acc_st} ->
|
||||
case fetch_kills_for_system(sid, since_hours, acc_st) do
|
||||
{:ok, kills, new_st} ->
|
||||
{Map.put(acc_map, sid, kills), new_st}
|
||||
|
||||
{:error, reason, new_st} ->
|
||||
Logger.debug(fn -> "[Fetcher] system=#{sid} => error=#{inspect(reason)}" end)
|
||||
{Map.put(acc_map, sid, {:error, reason}), new_st}
|
||||
end
|
||||
end)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[Fetcher] fetch_kills_for_systems => done, final_map_size=#{map_size(final_map)} calls=#{final_state.calls_count}"
|
||||
end)
|
||||
|
||||
Logger.debug(fn -> "[Fetcher] fetch_kills_for_systems => done, final_map_size=#{map_size(final_map)} calls=#{final_state.calls_count}" end)
|
||||
{:ok, final_map}
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("[Fetcher] EXCEPTION in fetch_kills_for_systems => #{Exception.message(e)}")
|
||||
{:error, e}
|
||||
{:ok, final_map}
|
||||
rescue
|
||||
e ->
|
||||
Logger.error(
|
||||
"[Fetcher] EXCEPTION in fetch_kills_for_systems => #{Exception.message(e)}"
|
||||
)
|
||||
|
||||
{:error, e}
|
||||
end
|
||||
else
|
||||
{:error, :kills_disabled}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,55 +62,68 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
Returns `{:ok, kills, updated_state}` on success, or `{:error, reason, updated_state}`.
|
||||
"""
|
||||
def fetch_kills_for_system(system_id, since_hours, state, opts \\ []) do
|
||||
limit = Keyword.get(opts, :limit, nil)
|
||||
force? = Keyword.get(opts, :force, false)
|
||||
zkill_preload_disabled = WandererApp.Env.zkill_preload_disabled?()
|
||||
|
||||
log_prefix = "[Fetcher] fetch_kills_for_system => system=#{system_id}"
|
||||
if not zkill_preload_disabled do
|
||||
limit = Keyword.get(opts, :limit, nil)
|
||||
force? = Keyword.get(opts, :force, false)
|
||||
|
||||
# Check the "recently fetched" cache if not forced
|
||||
if not force? and KillsCache.recently_fetched?(system_id) do
|
||||
cached_kills = KillsCache.fetch_cached_kills(system_id)
|
||||
final = maybe_take(cached_kills, limit)
|
||||
Logger.debug(fn -> "#{log_prefix}, recently_fetched?=true => returning #{length(final)} cached kills" end)
|
||||
{:ok, final, state}
|
||||
else
|
||||
Logger.debug(fn -> "#{log_prefix}, hours=#{since_hours}, limit=#{inspect(limit)}, force=#{force?}" end)
|
||||
log_prefix = "[Fetcher] fetch_kills_for_system => system=#{system_id}"
|
||||
|
||||
cutoff_dt = hours_ago(since_hours)
|
||||
# Check the "recently fetched" cache if not forced
|
||||
if not force? and KillsCache.recently_fetched?(system_id) do
|
||||
cached_kills = KillsCache.fetch_cached_kills(system_id)
|
||||
final = maybe_take(cached_kills, limit)
|
||||
|
||||
result =
|
||||
retry with: exponential_backoff(300)
|
||||
|> randomize()
|
||||
|> cap(5_000)
|
||||
|> expiry(120_000) do
|
||||
case do_multi_page_fetch(system_id, cutoff_dt, 1, 0, limit, state) do
|
||||
{:ok, new_st, total_fetched} ->
|
||||
# Mark system as fully fetched (to prevent repeated calls).
|
||||
KillsCache.put_full_fetched_timestamp(system_id)
|
||||
final_kills = KillsCache.fetch_cached_kills(system_id) |> maybe_take(limit)
|
||||
Logger.debug(fn ->
|
||||
"#{log_prefix}, recently_fetched?=true => returning #{length(final)} cached kills"
|
||||
end)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"#{log_prefix}, total_fetched=#{total_fetched}, final_cached=#{length(final_kills)}, calls_count=#{new_st.calls_count}"
|
||||
end)
|
||||
{:ok, final, state}
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"#{log_prefix}, hours=#{since_hours}, limit=#{inspect(limit)}, force=#{force?}"
|
||||
end)
|
||||
|
||||
{:ok, final_kills, new_st}
|
||||
cutoff_dt = hours_ago(since_hours)
|
||||
|
||||
{:error, :rate_limited, _new_st} ->
|
||||
raise ":rate_limited"
|
||||
result =
|
||||
retry with:
|
||||
exponential_backoff(300)
|
||||
|> randomize()
|
||||
|> cap(5_000)
|
||||
|> expiry(120_000) do
|
||||
case do_multi_page_fetch(system_id, cutoff_dt, 1, 0, limit, state) do
|
||||
{:ok, new_st, total_fetched} ->
|
||||
# Mark system as fully fetched (to prevent repeated calls).
|
||||
KillsCache.put_full_fetched_timestamp(system_id)
|
||||
final_kills = KillsCache.fetch_cached_kills(system_id) |> maybe_take(limit)
|
||||
|
||||
{:error, reason, _new_st} ->
|
||||
raise "#{log_prefix}, reason=#{inspect(reason)}"
|
||||
Logger.debug(fn ->
|
||||
"#{log_prefix}, total_fetched=#{total_fetched}, final_cached=#{length(final_kills)}, calls_count=#{new_st.calls_count}"
|
||||
end)
|
||||
|
||||
{:ok, final_kills, new_st}
|
||||
|
||||
{:error, :rate_limited, _new_st} ->
|
||||
raise ":rate_limited"
|
||||
|
||||
{:error, reason, _new_st} ->
|
||||
raise "#{log_prefix}, reason=#{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, kills, new_st} ->
|
||||
{:ok, kills, new_st}
|
||||
|
||||
error ->
|
||||
Logger.error("#{log_prefix}, EXHAUSTED => error=#{inspect(error)}")
|
||||
{:error, error, state}
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, kills, new_st} ->
|
||||
{:ok, kills, new_st}
|
||||
|
||||
error ->
|
||||
Logger.error("#{log_prefix}, EXHAUSTED => error=#{inspect(error)}")
|
||||
{:error, error, state}
|
||||
end
|
||||
else
|
||||
raise ":kills_disabled"
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@@ -118,10 +144,13 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
|
||||
with {:ok, st1} <- increment_calls_count(state),
|
||||
{:ok, st2, partials} <- ZkbApi.fetch_and_parse_page(system_id, page, st1) do
|
||||
Logger.debug(fn -> "[Fetcher] system=#{system_id}, page=#{page}, partials_count=#{length(partials)}" end)
|
||||
Logger.debug(fn ->
|
||||
"[Fetcher] system=#{system_id}, page=#{page}, partials_count=#{length(partials)}"
|
||||
end)
|
||||
|
||||
{_count_stored, older_found?, total_now} =
|
||||
Enum.reduce_while(partials, {0, false, total_so_far}, fn partial, {acc_count, had_older, acc_total} ->
|
||||
Enum.reduce_while(partials, {0, false, total_so_far}, fn partial,
|
||||
{acc_count, had_older, acc_total} ->
|
||||
# If we have a limit and reached it, stop immediately
|
||||
if reached_limit?(limit, acc_total) do
|
||||
{:halt, {acc_count, had_older, acc_total}}
|
||||
@@ -170,7 +199,10 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial, cutoff_dt) do
|
||||
defp parse_partial(
|
||||
%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial,
|
||||
cutoff_dt
|
||||
) do
|
||||
# If we've already cached this kill, skip
|
||||
if KillsCache.get_killmail(kill_id) do
|
||||
:skip
|
||||
@@ -191,7 +223,8 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
defp parse_partial(_other, _cutoff_dt), do: :skip
|
||||
|
||||
defp fetch_full_killmail(k_id, k_hash) do
|
||||
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
|
||||
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000),
|
||||
rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_killmail(k_id, k_hash) do
|
||||
{:ok, full_km} ->
|
||||
{:ok, full_km}
|
||||
@@ -206,12 +239,25 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.warning("[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}")
|
||||
Logger.warning(
|
||||
"[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
raise "ESI error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}")
|
||||
Logger.warning(
|
||||
"[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(error)}"
|
||||
)
|
||||
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -223,6 +269,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
do: {:ok, %{st | calls_count: c + 1}}
|
||||
|
||||
defp reached_limit?(nil, _count_so_far), do: false
|
||||
|
||||
defp reached_limit?(limit, count_so_far) when is_integer(limit),
|
||||
do: count_so_far >= limit
|
||||
|
||||
|
||||
@@ -126,7 +126,9 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
KillsCache.incr_system_kill_count(system_id)
|
||||
end
|
||||
else
|
||||
Logger.warning("[Parser] store_killmail => build_kill_data returned nil for kill_id=#{kill_id}")
|
||||
Logger.warning(
|
||||
"[Parser] store_killmail => build_kill_data returned nil for kill_id=#{kill_id}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -144,7 +146,6 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
"total_value" => total_value,
|
||||
"npc" => npc
|
||||
}) do
|
||||
|
||||
victim_map = extract_victim_fields(victim)
|
||||
final_blow_map = extract_final_blow_fields(attackers)
|
||||
|
||||
@@ -153,17 +154,14 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
"kill_time" => kill_time_dt,
|
||||
"solar_system_id" => sys_id,
|
||||
"zkb" => zkb,
|
||||
|
||||
"victim_char_id" => victim_map.char_id,
|
||||
"victim_corp_id" => victim_map.corp_id,
|
||||
"victim_alliance_id" => victim_map.alliance_id,
|
||||
"victim_ship_type_id" => victim_map.ship_type_id,
|
||||
|
||||
"final_blow_char_id" => final_blow_map.char_id,
|
||||
"final_blow_corp_id" => final_blow_map.corp_id,
|
||||
"final_blow_alliance_id" => final_blow_map.alliance_id,
|
||||
"final_blow_ship_type_id" => final_blow_map.ship_type_id,
|
||||
|
||||
"attacker_count" => attacker_count,
|
||||
"total_value" => total_value,
|
||||
"npc" => npc
|
||||
@@ -179,14 +177,14 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
"alliance_id" => alli,
|
||||
"ship_type_id" => st_id
|
||||
}),
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
|
||||
|
||||
defp extract_victim_fields(%{
|
||||
"character_id" => cid,
|
||||
"corporation_id" => corp,
|
||||
"ship_type_id" => st_id
|
||||
}),
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
|
||||
|
||||
defp extract_victim_fields(_),
|
||||
do: %{char_id: nil, corp_id: nil, alliance_id: nil, ship_type_id: nil}
|
||||
@@ -208,14 +206,14 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
"alliance_id" => alli,
|
||||
"ship_type_id" => st_id
|
||||
}),
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: alli, ship_type_id: st_id}
|
||||
|
||||
defp extract_attacker_fields(%{
|
||||
"character_id" => cid,
|
||||
"corporation_id" => corp,
|
||||
"ship_type_id" => st_id
|
||||
}),
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
|
||||
do: %{char_id: cid, corp_id: corp, alliance_id: nil, ship_type_id: st_id}
|
||||
|
||||
defp extract_attacker_fields(%{"ship_type_id" => st_id} = attacker) do
|
||||
%{
|
||||
@@ -235,52 +233,78 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
|> enrich_final_blow()
|
||||
end
|
||||
|
||||
|
||||
defp enrich_victim(km) do
|
||||
km
|
||||
|> maybe_put_character_name("victim_char_id", "victim_char_name")
|
||||
|> maybe_put_corp_info("victim_corp_id", "victim_corp_ticker", "victim_corp_name")
|
||||
|> maybe_put_alliance_info("victim_alliance_id", "victim_alliance_ticker", "victim_alliance_name")
|
||||
|> maybe_put_alliance_info(
|
||||
"victim_alliance_id",
|
||||
"victim_alliance_ticker",
|
||||
"victim_alliance_name"
|
||||
)
|
||||
|> maybe_put_ship_name("victim_ship_type_id", "victim_ship_name")
|
||||
end
|
||||
|
||||
|
||||
defp enrich_final_blow(km) do
|
||||
km
|
||||
|> maybe_put_character_name("final_blow_char_id", "final_blow_char_name")
|
||||
|> maybe_put_corp_info("final_blow_corp_id", "final_blow_corp_ticker", "final_blow_corp_name")
|
||||
|> maybe_put_alliance_info("final_blow_alliance_id", "final_blow_alliance_ticker", "final_blow_alliance_name")
|
||||
|> maybe_put_alliance_info(
|
||||
"final_blow_alliance_id",
|
||||
"final_blow_alliance_ticker",
|
||||
"final_blow_alliance_name"
|
||||
)
|
||||
|> maybe_put_ship_name("final_blow_ship_type_id", "final_blow_ship_name")
|
||||
end
|
||||
|
||||
defp maybe_put_character_name(km, id_key, name_key) do
|
||||
case Map.get(km, id_key) do
|
||||
nil -> km
|
||||
0 -> km
|
||||
nil ->
|
||||
km
|
||||
|
||||
0 ->
|
||||
km
|
||||
|
||||
eve_id ->
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, %{"name" => char_name}} ->
|
||||
{:ok, char_name}
|
||||
result =
|
||||
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
|
||||
rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, %{"name" => char_name}} ->
|
||||
{:ok, char_name}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
|
||||
raise "Character info timeout, will retry"
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
|
||||
:skip
|
||||
raise "Character info timeout, will retry"
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Character info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.debug(fn -> "[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}" end)
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
|
||||
:skip
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}"
|
||||
end)
|
||||
|
||||
raise "Character info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}"
|
||||
end)
|
||||
|
||||
:skip
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(error)}"
|
||||
end)
|
||||
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, char_name} -> Map.put(km, name_key, char_name)
|
||||
@@ -291,109 +315,180 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
|
||||
defp maybe_put_corp_info(km, id_key, ticker_key, name_key) do
|
||||
case Map.get(km, id_key) do
|
||||
nil -> km
|
||||
0 -> km
|
||||
nil ->
|
||||
km
|
||||
|
||||
0 ->
|
||||
km
|
||||
|
||||
corp_id ->
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_corporation_info(corp_id) do
|
||||
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
|
||||
{:ok, {ticker, corp_name}}
|
||||
result =
|
||||
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
|
||||
rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_corporation_info(corp_id) do
|
||||
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
|
||||
{:ok, {ticker, corp_name}}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Corporation info timeout, retrying => id=#{corp_id}" end)
|
||||
raise "Corporation info timeout, will retry"
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Corporation info timeout, retrying => id=#{corp_id}"
|
||||
end)
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
|
||||
:skip
|
||||
raise "Corporation info timeout, will retry"
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Corporation info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
|
||||
:skip
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}"
|
||||
end)
|
||||
|
||||
raise "Corporation info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning(
|
||||
"[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(error)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, {ticker, corp_name}} ->
|
||||
km
|
||||
|> Map.put(ticker_key, ticker)
|
||||
|> Map.put(name_key, corp_name)
|
||||
_ -> km
|
||||
|
||||
_ ->
|
||||
km
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_alliance_info(km, id_key, ticker_key, name_key) do
|
||||
case Map.get(km, id_key) do
|
||||
nil -> km
|
||||
0 -> km
|
||||
nil ->
|
||||
km
|
||||
|
||||
0 ->
|
||||
km
|
||||
|
||||
alliance_id ->
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
|
||||
{:ok, {alliance_ticker, alliance_name}}
|
||||
result =
|
||||
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
|
||||
rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
|
||||
{:ok, {alliance_ticker, alliance_name}}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Alliance info timeout, retrying => id=#{alliance_id}" end)
|
||||
raise "Alliance info timeout, will retry"
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Alliance info timeout, retrying => id=#{alliance_id}"
|
||||
end)
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
|
||||
:skip
|
||||
raise "Alliance info timeout, will retry"
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Alliance info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.debug(fn -> "[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}" end)
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
|
||||
:skip
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}"
|
||||
end)
|
||||
|
||||
raise "Alliance info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}"
|
||||
end)
|
||||
|
||||
:skip
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(error)}"
|
||||
end)
|
||||
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, {alliance_ticker, alliance_name}} ->
|
||||
km
|
||||
|> Map.put(ticker_key, alliance_ticker)
|
||||
|> Map.put(name_key, alliance_name)
|
||||
_ -> km
|
||||
|
||||
_ ->
|
||||
km
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_ship_name(km, id_key, name_key) do
|
||||
case Map.get(km, id_key) do
|
||||
nil -> km
|
||||
0 -> km
|
||||
nil ->
|
||||
km
|
||||
|
||||
0 ->
|
||||
km
|
||||
|
||||
type_id ->
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.CachedInfo.get_ship_type(type_id) do
|
||||
{:ok, nil} -> :skip
|
||||
{:ok, %{name: ship_name}} -> {:ok, ship_name}
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
|
||||
raise "Ship type timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Ship type error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
|
||||
result =
|
||||
retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000),
|
||||
rescue_only: [RuntimeError] do
|
||||
case WandererApp.CachedInfo.get_ship_type(type_id) do
|
||||
{:ok, nil} ->
|
||||
:skip
|
||||
end
|
||||
|
||||
{:ok, %{name: ship_name}} ->
|
||||
{:ok, ship_name}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
|
||||
raise "Ship type timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn ->
|
||||
"[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}"
|
||||
end)
|
||||
|
||||
raise "Ship type error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning(
|
||||
"[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(error)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, ship_name} -> Map.put(km, name_key, ship_name)
|
||||
|
||||
@@ -46,7 +46,10 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
|
||||
# Called on disconnect
|
||||
def handle_disconnect(code, reason, _old_state) do
|
||||
Logger.warning("[KillsProvider.Websocket] Disconnected => code=#{code}, reason=#{inspect(reason)} => reconnecting")
|
||||
Logger.warning(
|
||||
"[KillsProvider.Websocket] Disconnected => code=#{code}, reason=#{inspect(reason)} => reconnecting"
|
||||
)
|
||||
|
||||
:reconnect
|
||||
end
|
||||
|
||||
@@ -69,41 +72,73 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
end
|
||||
|
||||
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
|
||||
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
|
||||
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
|
||||
defp parse_and_store_zkb_partial(
|
||||
%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial
|
||||
) do
|
||||
Logger.debug(fn ->
|
||||
"[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}"
|
||||
end)
|
||||
|
||||
result = retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
|
||||
case Esi.get_killmail(kill_id, kill_hash) do
|
||||
{:ok, full_esi_data} ->
|
||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
|
||||
Parser.parse_and_store_killmail(enriched)
|
||||
:ok
|
||||
result =
|
||||
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000),
|
||||
rescue_only: [RuntimeError] do
|
||||
case Esi.get_killmail(kill_id, kill_hash) do
|
||||
{:ok, full_esi_data} ->
|
||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
|
||||
Parser.parse_and_store_killmail(enriched)
|
||||
:ok
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying...")
|
||||
raise "ESI timeout, will retry"
|
||||
{:error, :timeout} ->
|
||||
Logger.warning(
|
||||
"[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying..."
|
||||
)
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}")
|
||||
:skip
|
||||
raise "ESI timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}"
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
raise "ESI error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
:skip
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.warning(
|
||||
"[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
raise "ESI error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning(
|
||||
"[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(error)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
:ok -> :ok
|
||||
:skip -> :skip
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
:skip ->
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
Logger.error(
|
||||
"[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
:skip
|
||||
end
|
||||
end
|
||||
|
||||
@@ -104,7 +104,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
Creates a new ACL member.
|
||||
"""
|
||||
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation :create,
|
||||
operation(:create,
|
||||
summary: "Create ACL Member",
|
||||
description: "Creates a new ACL member for a given ACL.",
|
||||
parameters: [
|
||||
@@ -127,6 +127,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
@acl_member_create_response_schema
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def create(conn, %{"acl_id" => acl_id, "member" => member_params}) do
|
||||
chosen =
|
||||
cond do
|
||||
@@ -160,8 +162,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{
|
||||
error:
|
||||
"#{String.capitalize(type)} members cannot have an admin or manager role"
|
||||
error: "#{String.capitalize(type)} members cannot have an admin or manager role"
|
||||
})
|
||||
else
|
||||
info_fetcher =
|
||||
@@ -191,7 +192,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
|> json(%{error: "Creation failed: #{inspect(error)}"})
|
||||
end
|
||||
else
|
||||
{:error, error} ->
|
||||
error ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Entity lookup failed: #{inspect(error)}"})
|
||||
@@ -206,7 +207,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
Updates the role of an ACL member.
|
||||
"""
|
||||
@spec update_role(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation :update_role,
|
||||
operation(:update_role,
|
||||
summary: "Update ACL Member Role",
|
||||
description: "Updates the role of an ACL member identified by ACL ID and member external ID.",
|
||||
parameters: [
|
||||
@@ -235,6 +236,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
@acl_member_update_response_schema
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def update_role(conn, %{
|
||||
"acl_id" => acl_id,
|
||||
"member_id" => external_id,
|
||||
@@ -301,7 +304,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
Deletes an ACL member.
|
||||
"""
|
||||
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation :delete,
|
||||
operation(:delete,
|
||||
summary: "Delete ACL Member",
|
||||
description: "Deletes an ACL member identified by ACL ID and member external ID.",
|
||||
parameters: [
|
||||
@@ -325,6 +328,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
@acl_member_delete_response_schema
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def delete(conn, %{"acl_id" => acl_id, "member_id" => external_id}) do
|
||||
external_id_str = to_string(external_id)
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ defmodule WandererAppWeb.AdminLive do
|
||||
corp_wallet_character =
|
||||
socket.assigns.current_user.characters
|
||||
|> Enum.find(fn character ->
|
||||
WandererApp.Character.can_track_corp_wallet?(character)
|
||||
character.eve_id == WandererApp.Env.corp_wallet_eve_id() &&
|
||||
WandererApp.Character.can_track_corp_wallet?(character)
|
||||
end)
|
||||
|
||||
Phoenix.PubSub.subscribe(
|
||||
@@ -58,7 +59,6 @@ defmodule WandererAppWeb.AdminLive do
|
||||
socket
|
||||
|> assign(
|
||||
active_map_subscriptions: active_map_subscriptions,
|
||||
show_invites?: WandererApp.Env.invites(),
|
||||
user_character_ids: user_character_ids,
|
||||
user_id: user_id,
|
||||
invite_link: nil,
|
||||
@@ -77,21 +77,6 @@ defmodule WandererAppWeb.AdminLive do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params, uri)}
|
||||
end
|
||||
|
||||
def handle_event("generate-invite-link", _params, socket) do
|
||||
uuid = UUID.uuid4(:default)
|
||||
WandererApp.Cache.put("invite_#{uuid}", true, ttl: @invite_link_ttl)
|
||||
|
||||
invite_link =
|
||||
socket.assigns.uri
|
||||
|> Map.put(:path, "/welcome")
|
||||
|> Map.put(:query, URI.encode_query(%{invite: uuid}))
|
||||
|> URI.to_string()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(invite_link: invite_link)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update-eve-db-data", _params, socket) do
|
||||
WandererApp.EveDataService.update_eve_data()
|
||||
@@ -224,6 +209,58 @@ defmodule WandererAppWeb.AdminLive do
|
||||
|> push_navigate(to: ~p"/maps/new")}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"form" => params}, socket) do
|
||||
form = AshPhoenix.Form.validate(socket.assigns.form, params)
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("generate-invite-link", _params, socket) do
|
||||
token = UUID.uuid4()
|
||||
new_params = Map.put(socket.assigns.form.params || %{}, "token", token)
|
||||
form = AshPhoenix.Form.validate(socket.assigns.form, new_params)
|
||||
|
||||
invite_link =
|
||||
socket.assigns.uri
|
||||
|> get_invite_link(token)
|
||||
|
||||
{:noreply, assign(socket, form: form, invite_link: invite_link)}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"add_invite_link",
|
||||
%{"form" => %{"type" => type, "valid_until" => valid_until}},
|
||||
socket
|
||||
) do
|
||||
%{
|
||||
type: type |> String.to_existing_atom(),
|
||||
valid_until: get_valid_until(valid_until),
|
||||
token: UUID.uuid4(),
|
||||
map_id: nil
|
||||
}
|
||||
|> WandererApp.Api.MapInvite.new()
|
||||
|> case do
|
||||
{:ok, _invite} ->
|
||||
{:noreply, socket |> push_patch(to: ~p"/admin")}
|
||||
|
||||
error ->
|
||||
{:noreply, socket |> put_flash(:error, "Failed to add invite. Try again.")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"delete-invite",
|
||||
%{"id" => id},
|
||||
socket
|
||||
) do
|
||||
id
|
||||
|> WandererApp.Api.MapInvite.by_id!()
|
||||
|> WandererApp.Api.MapInvite.destroy!()
|
||||
|
||||
{:ok, invites} = WandererApp.Api.MapInvite.read()
|
||||
|
||||
{:noreply, socket |> assign(:invites, invites)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(event, body, socket) do
|
||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||
@@ -255,6 +292,8 @@ defmodule WandererAppWeb.AdminLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params, uri) do
|
||||
{:ok, invites} = WandererApp.Api.MapInvite.read()
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :admin)
|
||||
|> assign(:uri, URI.parse(uri))
|
||||
@@ -268,6 +307,48 @@ defmodule WandererAppWeb.AdminLive do
|
||||
])
|
||||
|> assign(:form, to_form(%{"amount" => 500_000_000}))
|
||||
|> assign(:unlink_character_form, to_form(%{}))
|
||||
|> assign(:invites, invites)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :add_invite_link, _params, uri) do
|
||||
socket
|
||||
|> assign(:active_page, :admin)
|
||||
|> assign(:uri, URI.parse(uri))
|
||||
|> assign(:page_title, "Add Invite Link")
|
||||
|> assign(:invite_types, [%{label: "User", id: :user}, %{label: "Admin", id: :admin}])
|
||||
|> assign(:valid_types, [
|
||||
%{label: "1D", id: 1},
|
||||
%{label: "1W", id: 7},
|
||||
%{label: "1M", id: 30},
|
||||
%{label: "1Y", id: 365}
|
||||
])
|
||||
|> assign(:unlink_character_form, to_form(%{}))
|
||||
|> assign(:character_search_options, [])
|
||||
|> assign(:amounts, [
|
||||
%{label: "500M", value: 500_000_000},
|
||||
%{label: "1B", value: 1_000_000_000},
|
||||
%{label: "5B", value: 5_000_000_000},
|
||||
%{label: "10B", value: 10_000_000_000}
|
||||
])
|
||||
|> assign(:form, to_form(%{"amount" => 500_000_000}))
|
||||
|> assign(:invite_token, UUID.uuid4())
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.MapInvite, :new,
|
||||
forms: [
|
||||
auto?: true
|
||||
]
|
||||
)
|
||||
|> to_form()
|
||||
)
|
||||
|> assign(:invites, [])
|
||||
end
|
||||
|
||||
defp get_invite_link(uri, token) do
|
||||
uri
|
||||
|> Map.put(:path, "/auth/eve")
|
||||
|> Map.put(:query, URI.encode_query(%{invite: token}))
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
defp search(search) do
|
||||
@@ -290,11 +371,29 @@ defmodule WandererAppWeb.AdminLive do
|
||||
</div>
|
||||
</div>
|
||||
<span :if={@option.value == :loading} <span class="loading loading-spinner loading-xs"></span>
|
||||
<%= @option.label %>
|
||||
{@option.label}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_valid_until("1") do
|
||||
DateTime.utc_now() |> DateTime.add(24 * 3600, :second)
|
||||
end
|
||||
|
||||
defp get_valid_until("7") do
|
||||
DateTime.utc_now() |> DateTime.add(24 * 3600 * 7, :second)
|
||||
end
|
||||
|
||||
defp get_valid_until("30") do
|
||||
DateTime.utc_now() |> DateTime.add(24 * 3600 * 30, :second)
|
||||
end
|
||||
|
||||
defp get_valid_until("365") do
|
||||
DateTime.utc_now() |> DateTime.add(24 * 3600 * 365, :second)
|
||||
end
|
||||
|
||||
defp get_valid_until(_), do: get_valid_until("1")
|
||||
|
||||
def search_member_icon_url(%{character: true} = option),
|
||||
do: member_icon_url(%{eve_character_id: option.value})
|
||||
|
||||
|
||||
@@ -112,34 +112,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :if={@show_invites?} class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<div class="col-span-6">
|
||||
<span class="text-gray-400 dark:text-gray-400">Invite Link</span>
|
||||
<span class="text-gray-400 dark:text-gray-400">Invites</span>
|
||||
<h4 class="my-4 font-medium text-gray-800 text-4xl dark:text-gray-100">
|
||||
<.button class="btn btn-primary" phx-click="generate-invite-link">
|
||||
Generate
|
||||
</.button>
|
||||
<.link class="btn mt-2 w-full btn-neutral rounded-none" patch={~p"/admin/invite"}>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">New Invite</h3>
|
||||
</.link>
|
||||
|
||||
<div :if={not is_nil(@invite_link)} class="join">
|
||||
<input
|
||||
class="input input-bordered join-item"
|
||||
readonly
|
||||
type="text"
|
||||
value={@invite_link}
|
||||
/>
|
||||
<.button
|
||||
phx-hook="CopyToClipboard"
|
||||
id="copy-to-clipboard"
|
||||
class="copy-link btn join-item rounded-r-full"
|
||||
data-url={@invite_link}
|
||||
>
|
||||
Copy
|
||||
<div class="absolute w-[100px] !mr-[-170px] link-copied hidden">
|
||||
Link copied
|
||||
<.table
|
||||
id="invites"
|
||||
rows={@invites}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={invite} label="Link">
|
||||
<div class="flex items-center">
|
||||
<div class="w-[200px] no-wrap truncate"><%= get_invite_link(@uri, invite.token) %></div>
|
||||
<.button
|
||||
phx-hook="CopyToClipboard"
|
||||
id="copy-to-clipboard"
|
||||
class="copy-link btn btn-neutral rounded-none"
|
||||
data-url={get_invite_link(@uri, invite.token)}
|
||||
>
|
||||
Copy
|
||||
<div class="absolute w-[100px] !mr-[-170px] link-copied hidden">
|
||||
Link copied
|
||||
</div>
|
||||
</.button>
|
||||
</div>
|
||||
</.button>
|
||||
</div>
|
||||
</:col>
|
||||
<:col :let={invite} label="Type">
|
||||
<%= invite.type %>
|
||||
</:col>
|
||||
<:col :let={invite} label="Valid Until">
|
||||
<div>
|
||||
<p class="mb-0 text-xs text-gray-600 dark:text-zinc-100 whitespace-nowrap">
|
||||
<.local_time id={invite.id} at={invite.valid_until} />
|
||||
</p>
|
||||
</div>
|
||||
</:col>
|
||||
<:action :let={invite}>
|
||||
<.button
|
||||
phx-click="delete-invite"
|
||||
phx-value-id={invite.id}
|
||||
data={[confirm: "Please confirm to delete invite!"]}
|
||||
class="hover:text-white"
|
||||
>
|
||||
<.icon name="hero-trash-solid" class="w-4 h-4" />
|
||||
</.button>
|
||||
</:action>
|
||||
</.table>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,4 +285,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.modal
|
||||
:if={@live_action in [:add_invite_link]}
|
||||
title={"New Invite"}
|
||||
class="!w-[500px]"
|
||||
id="add_invite_link_modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/admin")}
|
||||
>
|
||||
<.form :let={f} for={@form} phx-change="validate" phx-submit={@live_action}>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:type]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
wrapper_class="mt-2"
|
||||
label="Type"
|
||||
options={Enum.map(@invite_types, fn invite_type -> {invite_type.label, invite_type.id} end)}
|
||||
/>
|
||||
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:valid_until]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
wrapper_class="mt-2"
|
||||
label="Valid"
|
||||
options={Enum.map(@valid_types, fn valid_type -> {valid_type.label, valid_type.id} end)}
|
||||
/>
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="modal-action">
|
||||
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
||||
<%= (@live_action == :add_invite_link && "Add") || "Save" %>
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</.modal>
|
||||
</main>
|
||||
|
||||
@@ -32,16 +32,18 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
)
|
||||
end
|
||||
|
||||
def handle_server_event(%{event: :untrack_character, payload: character_id}, %{
|
||||
assigns: %{
|
||||
map_id: map_id
|
||||
}
|
||||
} = socket) do
|
||||
def handle_server_event(
|
||||
%{event: :untrack_character, payload: character_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
:ok = WandererApp.Character.TrackingUtils.untrack([%{id: character_id}], map_id, self())
|
||||
socket
|
||||
end
|
||||
|
||||
|
||||
def handle_server_event(
|
||||
%{event: :characters_updated},
|
||||
%{
|
||||
@@ -276,6 +278,24 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"startTracking",
|
||||
%{"character_eve_id" => character_eve_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: %{id: current_user_id}
|
||||
}
|
||||
} = socket
|
||||
)
|
||||
when not is_nil(character_eve_id) do
|
||||
{:ok, character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
|
||||
WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
@@ -295,6 +315,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|> Map.put(:alliance_ticker, Map.get(character, :alliance_ticker, ""))
|
||||
|> Map.put_new(:ship, WandererApp.Character.get_ship(character))
|
||||
|> Map.put_new(:location, get_location(character))
|
||||
|> Map.put_new(:tracking_paused, character |> Map.get(:tracking_paused, false))
|
||||
|
||||
defp get_location(character),
|
||||
do: %{
|
||||
|
||||
@@ -33,7 +33,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
"getCharactersTrackingInfo",
|
||||
"updateCharacterTracking",
|
||||
"updateFollowingCharacter",
|
||||
"updateMainCharacter"
|
||||
"updateMainCharacter",
|
||||
"startTracking"
|
||||
]
|
||||
|
||||
@map_system_events [
|
||||
|
||||
@@ -11,9 +11,7 @@ defmodule WandererAppWeb.ServerStatusLive do
|
||||
"server_status"
|
||||
)
|
||||
|
||||
{:ok, status} = WandererApp.Server.ServerStatusTracker.get_status()
|
||||
|
||||
{:ok, socket |> assign(server_online: not status.vip)}
|
||||
{:ok, socket |> assign(server_online: true)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -230,7 +230,11 @@ defmodule WandererAppWeb.Router do
|
||||
delete "/connections", MapConnectionAPIController, :delete
|
||||
delete "/systems", MapSystemAPIController, :delete
|
||||
resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
|
||||
resources "/connections", MapConnectionAPIController, only: [:index, :show, :create, :update, :delete], param: "id"
|
||||
|
||||
resources "/connections", MapConnectionAPIController,
|
||||
only: [:index, :show, :create, :update, :delete],
|
||||
param: "id"
|
||||
|
||||
resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
|
||||
@@ -238,8 +242,6 @@ defmodule WandererAppWeb.Router do
|
||||
get "/tracked-characters", MapAPIController, :show_tracked_characters
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Other API routes
|
||||
#
|
||||
@@ -359,6 +361,7 @@ defmodule WandererAppWeb.Router do
|
||||
WandererAppWeb.Nav
|
||||
] do
|
||||
live("/", AdminLive, :index)
|
||||
live("/invite", AdminLive, :add_invite_link)
|
||||
end
|
||||
|
||||
error_tracker_dashboard("/errors",
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.66.2"
|
||||
@version "1.68.1"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
40
priv/repo/migrations/20250608220906_add_map_invites.exs
Normal file
40
priv/repo/migrations/20250608220906_add_map_invites.exs
Normal file
@@ -0,0 +1,40 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapInvites do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:map_invites_v1, primary_key: false) do
|
||||
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
|
||||
add :token, :text
|
||||
add :valid_until, :utc_datetime
|
||||
|
||||
add :inserted_at, :utc_datetime_usec,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
|
||||
add :updated_at, :utc_datetime_usec,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
|
||||
add :map_id,
|
||||
references(:maps_v1,
|
||||
column: :id,
|
||||
name: "map_invites_v1_map_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public",
|
||||
on_delete: :delete_all
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop constraint(:map_invites_v1, "map_invites_v1_map_id_fkey")
|
||||
|
||||
drop table(:map_invites_v1)
|
||||
end
|
||||
end
|
||||
21
priv/repo/migrations/20250608221356_add_map_invite_type.exs
Normal file
21
priv/repo/migrations/20250608221356_add_map_invite_type.exs
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapInviteType do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:map_invites_v1) do
|
||||
add :type, :text, null: false, default: "user"
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:map_invites_v1) do
|
||||
remove :type
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "token",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "valid_until",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_invites_v1_map_id_fkey",
|
||||
"on_delete": "delete",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "maps_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "map_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "B4122C666C9DCF20E420209F604765AB1A3C4979D4134916F7EF9292162B250C",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_invites_v1"
|
||||
}
|
||||
108
priv/resource_snapshots/repo/map_invites_v1/20250608221356.json
Normal file
108
priv/resource_snapshots/repo/map_invites_v1/20250608221356.json
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "token",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"user\"",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "valid_until",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_invites_v1_map_id_fkey",
|
||||
"on_delete": "delete",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "maps_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "map_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "99662C7392D745B0B2D22445A3A703DC2287EBBA185BB7818D58F472B5D033D3",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_invites_v1"
|
||||
}
|
||||
Reference in New Issue
Block a user