diff --git a/package.json b/package.json index ed0c823da..b4e19cf40 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@sentry/cli": "^1.73.0", "dotenv": "^10.0.0", "esbuild": "^0.14.22", - "esbuild-plugin-eslinter": "^0.1.1", + "esbuild-plugin-eslinter": "^0.1.2", "esbuild-plugin-mxn-copy": "^1.0.1", "esbuild-plugin-path-alias": "^1.0.3", "eslint": "^8.9.0", @@ -54,6 +54,7 @@ "@sentry/tracing": "^6.18.2", "@turf/boolean-point-in-polygon": "^6.5.0", "@turf/center": "^6.3.0", + "@turf/destination": "^6.5.0", "@turf/helpers": "^6.5.0", "apollo-link-timeout": "^4.0.0", "apollo-server-core": "^3.5.0", diff --git a/public/base-locales/de.json b/public/base-locales/de.json index 22d7b3d85..6cb8f3c5e 100644 --- a/public/base-locales/de.json +++ b/public/base-locales/de.json @@ -490,6 +490,24 @@ "loading_icons": "Icons abrufen", "loading_invasions": "Rocket-Lineup abrufen", "pvp_ranking_cap": "Level", + "scan_next": "Standort scannen", + "scan_next_choose": "Ziehen Sie die Markierung per Drag & Drop um die Scanposition festzulegen", + "scan_zone": "Scanne ein Gebiet", + "scan_zone_choose": "Ziehen Sie die Markierung per Drag & Drop um die Scanposition festzulegen und die Größe zu wählen", + "scan_zone_size": "Größe", + "scan_zone_range": "Reichweite", + "scan_zone_spacing": "Abstand", + "scan_zone_radius": "Radius", + "scan_requests": "Scan Anfrage", + "scan_queue": "Aktuelle Warteschlange", + "click_to_scan": "Hier scannen", + "scan_confirmed_title": "Scan-Anfrage bestätigt", + "scan_confirmed": "Gerät wurde an den Standort geschickt, das Ergebnis wird bald auf der Karte erscheinen!", + "scan_loading_title": "Sende Scan-Anfrage", + "scan_loading": "Deine Scan-Anfrage wird bearbeitet und wurde erfolgreich abgeschickt!", + "scan_error_title": "Fehler", + "scan_error": "Es ist ein Fehler bei der Verarbeitung der Scan-Anfrage aufgetreten...", + "scan_outside_area": "Dieser Standort liegt außerhalb der Grenzen der zugelassenen Gebiete", "device_icons": "Gerätesymbole", "spawnpoint_icons": "Spawnpunkt-Symbole", "disabled": "Deaktiviert", diff --git a/public/base-locales/en.json b/public/base-locales/en.json index db2d7666e..bd14284e3 100644 --- a/public/base-locales/en.json +++ b/public/base-locales/en.json @@ -493,6 +493,24 @@ "loading": "Loading {{category}}", "loading_icons": "Fetching Icons", "loading_invasions": "Fetching Invasions", + "scan_next": "Scan Location", + "scan_next_choose": "Drag and Drop the Marker to Set the Scan Location", + "scan_zone": "Scan an Area", + "scan_zone_choose": "Drag and Drop the Marker to Set the Scan Location and Choose the Size", + "scan_zone_size": "Size", + "scan_zone_range": "Range", + "scan_zone_spacing": "Spacing", + "scan_zone_radius": "Radius", + "scan_requests": "Scan Requests", + "scan_queue": "Current Queue", + "click_to_scan": "Scan Here", + "scan_confirmed_title": "Scan demand confirmed", + "scan_confirmed": "Worker has been sent to location, result will soon appear on the map!", + "scan_loading_title": "Sending scan request", + "scan_loading": "Your scan request is being processed and sent to the system!", + "scan_error_title": "Error", + "scan_error": "There has been an error while processing the scan request...", + "scan_outside_area": "This location is outside the boundaries of authorized areas", "pvp_ranking_cap": "Level", "lc_title": "Follow Your Location", "lc_metersUnit": "meters", diff --git a/public/base-locales/fr.json b/public/base-locales/fr.json index 28f03c13f..25f6f0636 100644 --- a/public/base-locales/fr.json +++ b/public/base-locales/fr.json @@ -484,6 +484,24 @@ "loading_invasions": "Récupération des Invasions", "login_button": 12, "join_button": 12, + "scan_next": "Scanner un emplacement", + "scan_next_choose": "Glisser et déposer le marqueur pour définir l'emplacement de scan", + "scan_zone": "Scanner une zone", + "scan_zone_choose": "Glisser et déposer le marqueur pour définir l'emplacement et choisissez la taille du scan", + "scan_zone_size": "Taille", + "scan_zone_range": "Portée", + "scan_zone_spacing": "Espacement", + "scan_zone_radius": "Rayon", + "scan_requests": "Demandes de Scan ", + "scan_queue": "File d'attente ", + "click_to_scan": "Scanner ici", + "scan_confirmed_title": "Demande de scan confirmée", + "scan_confirmed": "L'appareil a été envoyé à la position de scan, le résultat sera bientôt visible sur la map !", + "scan_loading_title": "Envoi de la demande de scan", + "scan_loading": "Votre demande de scan est analysée et transmise au système !", + "scan_error_title": "Erreur", + "scan_error": "Il y a eu une erreur lors du traitement de la demande de scan...", + "scan_outside_area": "Cet emplacement est en dehors des zones autorisées", "pvp_ranking_cap": "Niveau", "device_icons": "Icônes d'appareils", "spawnpoint_icons": "Icônes de points d'apparition", diff --git a/server/src/configs/default.json b/server/src/configs/default.json index 20ab2d7f4..e26123c37 100644 --- a/server/src/configs/default.json +++ b/server/src/configs/default.json @@ -369,6 +369,39 @@ } ] }, + "scanner": { + "backendConfig": { + "platform": "rdm/mad", + "apiEndpoint": "http://ip:port/api/", + "apiUsername": "username", + "apiPassword": "password", + "queueRefreshInterval": 5 + }, + "scanNext": { + "enabled": false, + "showScanCount": false, + "showScanQueue": false, + "scanNextInstance": "scanNext", + "scanNextDevice": "Device01", + "scanNextSleeptime": 5, + "scanNextAreaRestriction": [], + "discordRoles": [], + "telegramGroups": [] + }, + "scanZone": { + "enabled": false, + "showScanCount": false, + "showScanQueue": false, + "scanZoneMaxSize": 10, + "advancedScanZoneOptions": false, + "scanZoneRadius": { "pokemon": 70, "gym": 750 }, + "scanZoneSpacing": 1, + "scanZoneInstance": "scanZone", + "scanZoneAreaRestriction": [], + "discordRoles": [], + "telegramGroups": [] + } + }, "webhooks": [], "authentication": { "strategies": [ diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index 9ae2dcb77..c2c034e52 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -284,6 +284,17 @@ module.exports = { } return {} }, + scanner: (parent, args, { req }) => { + const perms = req.user ? req.user.perms : req.session.perms + const { category, method, data } = args + if (category === 'getQueue') { + return Fetch.scannerApi(category, method, data) + } + if (perms?.scanner?.includes(category)) { + return Fetch.scannerApi(category, method, data) + } + return {} + }, }, Mutation: { webhook: (_, args, { req }) => { diff --git a/server/src/graphql/scannerTypes.js b/server/src/graphql/scannerTypes.js index 0313e70ff..fe3f5bd78 100644 --- a/server/src/graphql/scannerTypes.js +++ b/server/src/graphql/scannerTypes.js @@ -195,4 +195,9 @@ module.exports = gql` updated: Int polygon: [[Float]] } + + type ScannerApi { + status: String + message: String + } ` diff --git a/server/src/graphql/typeDefs.js b/server/src/graphql/typeDefs.js index b2651340a..5241cdd3a 100644 --- a/server/src/graphql/typeDefs.js +++ b/server/src/graphql/typeDefs.js @@ -32,6 +32,7 @@ module.exports = gql` submissionCells(minLat: Float, maxLat: Float, minLon: Float, maxLon: Float, ts: Int, zoom: Int): [SubmissionCell] weather: [Weather] webhook(category: String, status: String, name: String): Poracle + scanner(category: String, method: String, data: JSON): ScannerApi } type Mutation { diff --git a/server/src/routes/rootRouter.js b/server/src/routes/rootRouter.js index 300c8eea6..5e977ff2d 100644 --- a/server/src/routes/rootRouter.js +++ b/server/src/routes/rootRouter.js @@ -60,6 +60,7 @@ rootRouter.get('/settings', async (req, res) => { req.session.perms = { areaRestrictions: Utility.areaPerms(['none']), webhooks: [], + scanner: [], } config.authentication.alwaysEnabledPerms.forEach(perm => { if (config.authentication.perms[perm]) { @@ -115,6 +116,21 @@ rootRouter.get('/settings', async (req, res) => { }, manualAreas: config.manualAreas || {}, icons: config.icons, + scanner: { + scannerType: config.scanner.backendConfig.platform, + enableScanNext: config.scanner.scanNext.enabled, + scanNextShowScanCount: config.scanner.scanNext.showScanCount, + scanNextShowScanQueue: config.scanner.scanNext.showScanQueue, + scanNextAreaRestriction: config.scanner.scanNext.scanNextAreaRestriction, + enableScanZone: config.scanner.scanZone.enabled, + scanZoneShowScanCount: config.scanner.scanZone.showScanCount, + scanZoneShowScanQueue: config.scanner.scanZone.showScanQueue, + advancedScanZoneOptions: config.scanner.scanZone.advancedScanZoneOptions, + scanZoneRadius: config.scanner.scanZone.scanZoneRadius, + scanZoneSpacing: config.scanner.scanZone.scanZoneSpacing, + scanZoneMaxSize: config.scanner.scanZone.scanZoneMaxSize, + scanZoneAreaRestriction: config.scanner.scanZone.scanZoneAreaRestriction, + }, gymValidDataLimit: Date.now() / 1000 - (config.api.gymValidDataLimit * 86400), }, available: {}, @@ -125,7 +141,7 @@ rootRouter.get('/settings', async (req, res) => { serverSettings.loggedIn = req.user // keys that are being sent to the frontend but are not options - const ignoreKeys = ['map', 'manualAreas', 'limit', 'icons', 'gymValidDataLimit'] + const ignoreKeys = ['map', 'manualAreas', 'limit', 'icons', 'scanner', 'gymValidDataLimit'] Object.keys(serverSettings.config).forEach(setting => { try { diff --git a/server/src/services/DiscordClient.js b/server/src/services/DiscordClient.js index 41929697e..c4b5f182a 100644 --- a/server/src/services/DiscordClient.js +++ b/server/src/services/DiscordClient.js @@ -6,7 +6,7 @@ /* eslint-disable import/no-dynamic-require */ /* global BigInt */ const fs = require('fs') -const { authentication: { alwaysEnabledPerms }, webhooks } = require('./config') +const { authentication: { alwaysEnabledPerms }, scanner, webhooks } = require('./config') const Utility = require('./Utility') module.exports = class DiscordMapClient { @@ -58,6 +58,7 @@ module.exports = class DiscordMapClient { const perms = Object.fromEntries(Object.keys(this.config.perms).map(x => [x, false])) perms.areaRestrictions = [] perms.webhooks = [] + perms.scanner = [] try { const { guildsFull } = user const guilds = user.guilds.map(guild => guild.id) @@ -65,6 +66,7 @@ module.exports = class DiscordMapClient { Object.keys(perms).forEach((key) => perms[key] = true) perms.areaRestrictions = [] perms.webhooks = webhooks.map(x => x.name) + perms.scanner = Object.keys(scanner).map(x => x !== 'backendConfig' && scanner[x].enabled && x).filter(x => x !== false) console.log(`User ${user.username}#${user.discriminator} (${user.id}) in allowed users list, skipping guild and role check.`) return perms } @@ -98,6 +100,7 @@ module.exports = class DiscordMapClient { } perms.areaRestrictions.push(...Utility.areaPerms(userRoles, 'discord')) perms.webhooks.push(...Utility.webhookPerms(userRoles, 'discordRoles')) + perms.scanner.push(...Utility.scannerPerms(userRoles, 'discordRoles')) } } if (perms.areaRestrictions.length) { diff --git a/server/src/services/Fetch.js b/server/src/services/Fetch.js index 9e4cd8652..f3532261a 100644 --- a/server/src/services/Fetch.js +++ b/server/src/services/Fetch.js @@ -3,6 +3,7 @@ const fetchRaids = require('./api/fetchRaids') const fetchQuests = require('./api/fetchQuests') const fetchNests = require('./api/fetchNests') const webhookApi = require('./api/webhookApi') +const scannerApi = require('./api/scannerApi') module.exports = class Fetch { static async json(url, options) { @@ -24,4 +25,8 @@ module.exports = class Fetch { static async webhookApi(category, discordId, method, name, data) { return webhookApi(category, discordId, method, name, data) } + + static async scannerApi(category, method, data) { + return scannerApi(category, method, data) + } } diff --git a/server/src/services/Utility.js b/server/src/services/Utility.js index bdf5f7441..f9ac78887 100644 --- a/server/src/services/Utility.js +++ b/server/src/services/Utility.js @@ -11,6 +11,7 @@ const webhook = require('./ui/webhook') const geocoder = require('./geocoder') const areaPerms = require('./functions/areaPerms') const webhookPerms = require('./functions/webhookPerms') +const scannerPerms = require('./functions/scannerPerms') const mergePerms = require('./functions/mergePerms') const evalWebhookId = require('./functions/evalWebhookId') @@ -67,6 +68,10 @@ module.exports = class Utility { return webhookPerms(roles, provider) } + static scannerPerms(roles, provider) { + return scannerPerms(roles, provider) + } + static mergePerms(existingPerms, incomingPerms = {}) { return mergePerms(existingPerms, incomingPerms) } diff --git a/server/src/services/api/scannerApi.js b/server/src/services/api/scannerApi.js new file mode 100644 index 000000000..895d329de --- /dev/null +++ b/server/src/services/api/scannerApi.js @@ -0,0 +1,93 @@ +/* eslint-disable no-console */ +const fetch = require('node-fetch') +const config = require('../config') + +const scannerQueue = { + scanNext: {}, + scanZone: {}, +} + +module.exports = async function scannerApi(category, method, data = null) { + try { + const headers = {} + switch (config.scanner.backendConfig.platform) { + case 'mad': + case 'rdm': Object.assign(headers, { Authorization: `Basic ${Buffer.from(`${config.scanner.backendConfig.apiUsername}:${config.scanner.backendConfig.apiPassword}`).toString('base64')}` }); break + default: break + } + const payloadObj = {} + switch (category) { + case 'scanNext': { + console.log(`[scannerApi] Request to scan new location by ${data.username}${data.userId ? ` (${data.userId})` : ''} - type ${data.scanNextType}: ${data.scanNextLocation[0].toFixed(5)},${data.scanNextLocation[1].toFixed(5)}`) + const coords = config.scanner.backendConfig.platform === 'mad' ? `${parseFloat(data.scanNextCoords[0][0].toFixed(5))},${parseFloat(data.scanNextCoords[0][1].toFixed(5))}` + : JSON.stringify(data.scanNextCoords.map(coord => ( + { lat: parseFloat(coord[0].toFixed(5)), lon: parseFloat(coord[1].toFixed(5)) }))) + Object.assign(payloadObj, { + url: config.scanner.backendConfig.platform === 'mad' ? `${config.scanner.backendConfig.apiEndpoint}/send_gps?origin=${encodeURIComponent(config.scanner.scanNext.scanNextDevice)}&coords=${coords}&sleeptime=${config.scanner.scanNext.scanNextSleeptime}` + : `${config.scanner.backendConfig.apiEndpoint}/set_data?scan_next=true&instance=${encodeURIComponent(config.scanner.scanNext.scanNextInstance)}&coords=${coords}`, + options: { method, headers }, + }) + } break + case 'scanZone': { + console.log(`[scannerApi] Request to scan new zone by ${data.username}${data.userId ? ` (${data.userId})` : ''} - size ${data.scanZoneSize}: ${data.scanZoneLocation[0].toFixed(5)},${data.scanZoneLocation[1].toFixed(5)}`) + const coords = JSON.stringify(data.scanZoneCoords.map(coord => ( + { lat: parseFloat(coord[0].toFixed(5)), lon: parseFloat(coord[1].toFixed(5)) }))) + Object.assign(payloadObj, { + url: `${config.scanner.backendConfig.apiEndpoint}/set_data?scan_next=true&instance=${encodeURIComponent(config.scanner.scanZone.scanZoneInstance)}&coords=${coords}`, + options: { method, headers }, + }) + } break + case 'getQueue': + if (scannerQueue[data.typeName].timestamp > (Date.now() - config.scanner.backendConfig.queueRefreshInterval * 1000)) { + console.log(`[scannerApi] Returning queue from memory for method ${data.typeName}: ${scannerQueue[data.typeName].queue}`) + return { status: 'ok', message: scannerQueue[data.typeName].queue } + } + console.log(`[scannerApi] Getting queue for method ${data.typeName}`) + Object.assign(payloadObj, { + url: `${config.scanner.backendConfig.apiEndpoint}/get_data?${data.type}=true&queue_size=true&instance=${encodeURIComponent(config.scanner[data.typeName][`${data.typeName}Instance`])}`, + options: { method, headers }, + }); break + default: + console.warn('[scannerApi] Api call without category'); break + } + + if (payloadObj.options.body) { + Object.assign(payloadObj.options.headers, { Accept: 'application/json', 'Content-Type': 'application/json' }) + } + const scannerResponse = await fetch(payloadObj.url, payloadObj.options) + + if (!scannerResponse) { + throw new Error('[scannerApi] No data returned from server') + } + + if (scannerResponse.status === 200 && category === 'getQueue') { + const { data: queueData } = await scannerResponse.json() + console.log(`[scannerApi] Returning received queue for method ${data.typeName}: ${queueData.size}`) + scannerQueue[data.typeName] = { queue: queueData.size, timestamp: Date.now() } + return { status: 'ok', message: queueData.size } + } + + switch (scannerResponse.status) { + case 200: + console.log(`[scannerApi] Request from ${data.username}${data.userId ? ` (${data.userId})` : ''} successful`) + return { status: 'ok', message: 'scanner_ok' } + case 401: + console.log('[scannerApi] Wrong credentials - check your scanner API settings in config') + return { status: 'error', message: 'scanner_wrong_credentials' } + case 404: + console.log(`[scannerApi] Error: instance ${config.scanner[category][`${category}Instance`]} does not exist`) + return { status: 'error', message: 'scanner_no_instance' } + case 416: + console.log(`[scannerApi] Error: instance ${config.scanner[category][`${category}Instance`]} has no device assigned`) + return { status: 'error', message: 'scanner_no_device_assigned' } + case 500: + console.log(`[scannerApi] Error: device ${config.scanner[category][`${category}Device`]} does not exist`) + return { status: 'error', message: 'scanner_no_device' } + default: + return { status: 'error', message: 'scanner_error' } + } + } catch (e) { + console.log('[scannerApi] There was a problem processing that scanner request') + return { status: 'error', message: 'scanner_error' } + } +} diff --git a/server/src/services/functions/scannerPerms.js b/server/src/services/functions/scannerPerms.js new file mode 100644 index 000000000..212f9bb3a --- /dev/null +++ b/server/src/services/functions/scannerPerms.js @@ -0,0 +1,18 @@ +const { scanner } = require('../config') + +module.exports = function scannerPerms(roles, provider) { + let perms = [] + if (Object.keys(scanner).length) { + roles.forEach(role => { + Object.keys(scanner).forEach(mode => { + if (scanner[mode][provider]?.includes(role)) { + perms.push(mode) + } + }) + }) + } + if (perms.length) { + perms = [...new Set(perms)] + } + return perms +} diff --git a/server/src/services/sessionStore.js b/server/src/services/sessionStore.js index cc01e7443..5992c4445 100644 --- a/server/src/services/sessionStore.js +++ b/server/src/services/sessionStore.js @@ -44,12 +44,12 @@ const isValidSession = async (userId) => { return results.length < maxSessions } -const clearOtherSessions = async (userId, currentSessionId, botName) => { +const clearOtherSessions = async (userId, currentSessionId) => { const results = await Session.query() .whereRaw(`json_extract(data, '$.passport.user.id') = ${userId}`) .andWhere('session_id', '!=', currentSessionId || '') .delete() - console.log(`[Session${botName && ` - ${botName}`}] Clear Result:`, results) + console.log('[Session] Clear Result:', results) } const clearDiscordSessions = async (discordId, botName) => { diff --git a/server/src/strategies/local.js b/server/src/strategies/local.js index 47b75742b..b85ee0f94 100644 --- a/server/src/strategies/local.js +++ b/server/src/strategies/local.js @@ -25,6 +25,7 @@ const authHandler = async (req, username, password, done) => { ), areaRestrictions: Utility.areaPerms(localPerms, 'local'), webhooks: [], + scanner: [], }, } diff --git a/server/src/strategies/telegram.js b/server/src/strategies/telegram.js index 9a08e3c42..ba5ad5462 100644 --- a/server/src/strategies/telegram.js +++ b/server/src/strategies/telegram.js @@ -49,6 +49,7 @@ const authHandler = async (req, profile, done) => { user.perms.areaRestrictions = Utility.areaPerms(groupInfo, 'telegram') user.perms.webhooks = Utility.webhookPerms(groupInfo, 'telegramGroups') + user.perms.scanner = Utility.scannerPerms(groupInfo, 'telegramGroups') try { await User.query() diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 4ca9224f3..2f0cfd1c1 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -9,6 +9,8 @@ import { useStatic, useStore } from '@hooks/useStore' import Nav from './layout/Nav' import QueryData from './QueryData' import Webhook from './layout/dialogs/webhooks/Webhook' +import ScanNext from './layout/dialogs/scanner/ScanNext' +import ScanZone from './layout/dialogs/scanner/ScanZone' const userSettingsCategory = category => { switch (category) { @@ -32,7 +34,8 @@ const getTileServer = (tileServers, settings, isNight) => { return tileServers[settings.tileServers] || fallbackTs } -export default function Map({ serverSettings: { config: { map: config, tileServers }, Icons, webhooks }, params }) { +export default function Map({ serverSettings: + { config: { map: config, tileServers, scanner }, Icons, webhooks }, params }) { Utility.analytics(window.location.pathname) const map = useMap() @@ -58,6 +61,8 @@ export default function Map({ serverSettings: { config: { map: config, tileServe const userSettings = useStore(state => state.userSettings) const [webhookMode, setWebhookMode] = useState(false) + const [scanNextMode, setScanNextMode] = useState(false) + const [scanZoneMode, setScanZoneMode] = useState(false) const [manualParams, setManualParams] = useState(params) const [lc] = useState(L.control.locate({ position: 'bottomright', @@ -176,6 +181,23 @@ export default function Map({ serverSettings: { config: { map: config, tileServe }) ) } + {scanNextMode && ( + + )} + {scanZoneMode && ( + + )}