Compare commits

..

93 Commits

Author SHA1 Message Date
CI
64788e73de chore: release version v1.68.1 2025-06-09 12:25:10 +00:00
Dmitry Popov
114fd471e8 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-09 14:24:37 +02:00
Dmitry Popov
b24a3120d3 fix(Core): Fixed auth from welcome page if invites disabled 2025-06-09 14:24:35 +02:00
CI
c5f93b3d0a chore: release version v1.68.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-09 08:20:31 +00:00
Dmitry Popov
79290e4721 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-09 10:19:56 +02:00
Dmitry Popov
984e126f23 feat(Core): Added invites store support 2025-06-09 10:19:53 +02:00
CI
cd1ad31aed chore: release version v1.67.5
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-08 19:05:08 +00:00
Dmitry Popov
1e3f6cf9e7 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 21:04:38 +02:00
Dmitry Popov
9c6ccd9a8a fix(Core): Added back ARM docker image build 2025-06-08 21:04:34 +02:00
CI
681ba21d39 chore: release version v1.67.4 2025-06-08 18:56:40 +00:00
Dmitry Popov
aef62189ee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 20:56:07 +02:00
Dmitry Popov
09f70ac817 fix(Core): Fixed issue with system splash updates 2025-06-08 20:55:57 +02:00
CI
1eacb22143 chore: release version v1.67.3 2025-06-08 18:55:40 +00:00
Dmitry Popov
8524bad377 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 20:55:10 +02:00
Dmitry Popov
9d899243d1 fix(Core): Fixed issue with system splash updates 2025-06-08 20:54:59 +02:00
CI
9acf20a639 chore: release version v1.67.2 2025-06-08 16:57:01 +00:00
Dmitry Popov
71ef6b2e82 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 18:56:35 +02:00
Dmitry Popov
5e34d95dd2 chore: release version v1.66.25 2025-06-08 18:56:32 +02:00
CI
25a809c064 chore: release version v1.67.1 2025-06-08 16:46:28 +00:00
Dmitry Popov
f760498150 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 18:45:56 +02:00
Dmitry Popov
328301a375 chore: release version v1.66.25 2025-06-08 18:45:53 +02:00
CI
f28e7ebbbb chore: release version v1.67.0
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-08 14:27:48 +00:00
Dmitry Popov
bfa84af71e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 16:24:32 +02:00
Dmitry Popov
42cd1ba976 feat(Core): Added support for WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED env variable to pause inactive character trackers 2025-06-08 16:24:29 +02:00
CI
88cba866fd chore: release version v1.66.25 2025-06-08 11:33:36 +00:00
Dmitry Popov
af2876a84b Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 13:33:09 +02:00
Dmitry Popov
c5b15bfa78 fix(Core): Disabled kills fetching based on env settings 2025-06-08 13:33:05 +02:00
CI
85f00a63c2 chore: release version v1.66.24 2025-06-08 09:27:32 +00:00
Dmitry Popov
05f427bcd7 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-08 11:26:53 +02:00
Dmitry Popov
69f4c41534 chore: release version v1.66.15 2025-06-08 11:26:50 +02:00
CI
30b9239a8b chore: release version v1.66.23 2025-06-08 09:15:07 +00:00
Dmitry Popov
2061a83c59 chore: release version v1.66.15 2025-06-08 11:14:30 +02:00
Dmitry Popov
24e723de07 Merge branch 'tracking-controls' 2025-06-08 11:10:22 +02:00
Dmitry Popov
27b5694885 chore: release version v1.66.15 2025-06-08 11:03:13 +02:00
Dmitry Popov
4093f28cee chore: release version v1.66.15 2025-06-08 10:45:41 +02:00
Dmitry Popov
08aaf2f2dd chore: release version v1.66.15 2025-06-08 10:19:25 +02:00
CI
5a927e5ba5 chore: release version v1.66.22
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-07 16:11:48 +00:00
Dmitry Popov
10fafcf59f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 18:11:17 +02:00
Dmitry Popov
be87591801 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 18:11:14 +02:00
CI
086d4378d3 chore: release version v1.66.21 2025-06-07 16:03:22 +00:00
Dmitry Popov
e982275905 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 18:02:37 +02:00
Dmitry Popov
77c02703e9 fix(Core): Fixed kills fetching based on env settings 2025-06-07 18:02:34 +02:00
CI
0ef27d4f95 chore: release version v1.66.20
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-07 15:41:18 +00:00
Dmitry Popov
5edc27744e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 17:40:52 +02:00
Dmitry Popov
02ff887fee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 17:40:48 +02:00
CI
3a30eeb59f chore: release version v1.66.19 2025-06-07 15:31:53 +00:00
Dmitry Popov
79af8fb601 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 17:31:09 +02:00
Dmitry Popov
f9f00faa0e fix(Core): Added check for offline characters timeouts 2025-06-07 17:31:05 +02:00
CI
a3c41e84e4 chore: release version v1.66.18 2025-06-07 14:55:06 +00:00
Dmitry Popov
7f21f33351 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 16:54:40 +02:00
Dmitry Popov
568f682cee chore: release version v1.66.16 2025-06-07 16:54:36 +02:00
CI
901c4c8ca4 chore: release version v1.66.17 2025-06-07 14:44:50 +00:00
Dmitry Popov
3dbba97f9c fix(Core): Increased tracking pause timeout for offline characters up to 10 hours 2025-06-07 16:44:18 +02:00
CI
3475620267 chore: release version v1.66.16 2025-06-07 13:41:56 +00:00
Dmitry Popov
8936a5e5d8 chore: release version v1.66.15 2025-06-07 15:41:22 +02:00
Dmitry Popov
719e34f9bc fix(Core): Increased tracking pause timeout for offline characters up to 10 hours 2025-06-07 15:41:06 +02:00
Dmitry Popov
df955ff8b0 chore: release version v1.66.15 2025-06-07 13:57:25 +02:00
CI
4dc74022b4 chore: release version v1.66.15 2025-06-07 09:01:44 +00:00
Dmitry Popov
c2b7d07208 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 10:58:35 +02:00
Dmitry Popov
21e76a6f05 fix(Core): Added back arm docker image build 2025-06-07 10:58:24 +02:00
CI
89c0d6fad6 chore: release version v1.66.14 2025-06-07 08:48:05 +00:00
Dmitry Popov
b74d15b1ee Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 10:47:31 +02:00
Dmitry Popov
10313438bf fix(Core): fixed online updates 2025-06-07 10:47:28 +02:00
CI
a764217948 chore: release version v1.66.13 2025-06-07 07:49:31 +00:00
Dmitry Popov
d81d6fb937 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 09:48:56 +02:00
Dmitry Popov
4c0f7ab7f9 fix(Core): fixed location tracking issues 2025-06-07 09:48:53 +02:00
CI
1c48945a96 chore: release version v1.66.12
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-06 23:58:04 +00:00
Dmitry Popov
850901f62f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 01:57:37 +02:00
Dmitry Popov
4822854e30 chore: release version v1.66.10 2025-06-07 01:57:32 +02:00
CI
f580538331 chore: release version v1.66.11 2025-06-06 23:47:37 +00:00
Dmitry Popov
0d70c555e6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 01:47:07 +02:00
Dmitry Popov
c5f6cf0080 fix(Core): fixed refresh character tokens 2025-06-07 01:47:03 +02:00
CI
6ff7b3bc9a chore: release version v1.66.10
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-06 22:37:51 +00:00
Dmitry Popov
346c2c65b0 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-07 00:37:23 +02:00
Dmitry Popov
a6445fd500 chore: release version v1.66.8 2025-06-07 00:37:20 +02:00
CI
358e43e508 chore: release version v1.66.9 2025-06-06 22:23:05 +00:00
Dmitry Popov
20c5ba6b63 chore: release version v1.66.8 2025-06-07 00:22:35 +02:00
CI
661658a6e8 chore: release version v1.66.8 2025-06-06 21:32:10 +00:00
Dmitry Popov
0a6f224ed3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 23:31:41 +02:00
Dmitry Popov
e7bb29693f fix: fixed disable detailed kills env check 2025-06-06 23:31:38 +02:00
CI
32dfd50461 chore: release version v1.66.7 2025-06-06 20:34:16 +00:00
Dmitry Popov
bfec385dce Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 22:33:49 +02:00
Dmitry Popov
04278f99d7 fix: fixed disable detailed kills env check 2025-06-06 22:33:46 +02:00
CI
9d3db19dc1 chore: release version v1.66.6 2025-06-06 20:03:06 +00:00
Dmitry Popov
3953e33f37 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 22:02:34 +02:00
Dmitry Popov
611fdd56d0 fix: respect error limits for ESI APIs 2025-06-06 22:02:31 +02:00
CI
36a4fd0f35 chore: release version v1.66.5 2025-06-06 17:13:49 +00:00
Dmitry Popov
488984a988 fix: respect error limits for ESI APIs 2025-06-06 19:13:07 +02:00
CI
1b183d6e58 chore: release version v1.66.4
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-06 12:52:02 +00:00
Dmitry Popov
3dcb6d30b5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-06-06 14:51:27 +02:00
Dmitry Popov
1d11788f89 fix: respect error limits for ESI APIs 2025-06-06 14:51:24 +02:00
CI
d3822128ab chore: release version v1.66.3
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-06-05 22:58:55 +00:00
Dmitry Popov
6ac7836505 chore: release version v1.66.2 2025-06-06 00:58:24 +02:00
40 changed files with 2123 additions and 631 deletions

View File

@@ -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)

View File

@@ -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(

View File

@@ -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();

View File

@@ -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();

View File

@@ -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(() => {

View File

@@ -33,6 +33,7 @@ export type CharacterTypeRaw = {
corporation_id: number;
corporation_name: string;
corporation_ticker: string;
tracking_paused: boolean;
};
export interface TrackingCharacter {

View File

@@ -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',

View File

@@ -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,

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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,
%{

View File

@@ -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__

View File

@@ -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} ->

View File

@@ -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)

View File

@@ -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}"}

View File

@@ -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

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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>
&nbsp; <%= @option.label %>
&nbsp; {@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})

View File

@@ -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>

View File

@@ -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: %{

View File

@@ -33,7 +33,8 @@ defmodule WandererAppWeb.MapEventHandler do
"getCharactersTrackingInfo",
"updateCharacterTracking",
"updateFollowingCharacter",
"updateMainCharacter"
"updateMainCharacter",
"startTracking"
]
@map_system_events [

View File

@@ -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

View File

@@ -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",

View File

@@ -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
[

View 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

View 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

View File

@@ -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"
}

View 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"
}