diff --git a/packages/types/lib/general.d.ts b/packages/types/lib/general.d.ts index 0bd1a0c62..434de68db 100644 --- a/packages/types/lib/general.d.ts +++ b/packages/types/lib/general.d.ts @@ -128,3 +128,20 @@ export interface RMSliderProps { values: number[] handleChange: RMSliderHandleChange } + +export type ScanTypes = 'scanNext' | 'scanZone' + +export interface ScanOnDemandData { + typeName: ScanTypes + type: 'scan_next' + scanCoords?: [number, number][] + scanLocation?: [number, number] + scanSize?: number + cooldown?: number +} + +export interface ScanOnDemandReq { + category: ScanTypes | 'getQueue' + method: HttpMethod + data?: ScanOnDemandData +} diff --git a/packages/types/lib/server.d.ts b/packages/types/lib/server.d.ts index 382ad0110..ce9cf387f 100644 --- a/packages/types/lib/server.d.ts +++ b/packages/types/lib/server.d.ts @@ -139,7 +139,7 @@ export interface GqlContext { user: string transaction: Transaction operation: 'query' | 'mutation' - startTime: number + startTime?: number } export interface Permissions { diff --git a/server/src/configs/default.json b/server/src/configs/default.json index 54a733e39..1c396b3a0 100644 --- a/server/src/configs/default.json +++ b/server/src/configs/default.json @@ -595,13 +595,14 @@ }, "scanner": { "backendConfig": { - "platform": "rdm/mad", + "platform": "rdm/mad/custom", "apiEndpoint": "http://ip:port/api/", "headers": [], "apiUsername": "username", "apiPassword": "password", "queueRefreshInterval": 5, - "sendDiscordMessage": true + "sendDiscordMessage": true, + "sendTelegramMessage": true }, "scanNext": { "enabled": false, @@ -681,6 +682,8 @@ { "name": "telegram", "type": "telegram", + "scanNextLogChannelId": "", + "scanZoneLogChannelId": "", "enabled": false, "botToken": "", "groups": [], diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index b8c40e586..c6c70d0f2 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -6,7 +6,6 @@ const config = require('@rm/config') const buildDefaultFilters = require('../services/filters/builder/base') const filterComponents = require('../services/functions/filterComponents') -const { filterRTree } = require('../services/functions/filterRTree') const validateSelectedWebhook = require('../services/functions/validateSelectedWebhook') const PoracleAPI = require('../services/api/Poracle') const { geocoder } = require('../services/geocoder') @@ -14,6 +13,7 @@ const scannerApi = require('../services/api/scannerApi') const getPolyVector = require('../services/functions/getPolyVector') const getPlacementCells = require('../services/functions/getPlacementCells') const getTypeCells = require('../services/functions/getTypeCells') +const { getValidCoords } = require('../services/functions/getValidCoords') /** @type {import("@apollo/server").ApolloServerOptions['resolvers']} */ const resolvers = { @@ -106,22 +106,8 @@ const resolvers = { return !!results.length }, /** @param {unknown} _ @param {{ mode: 'scanNext' | 'scanZone' }} args */ - checkValidScan: (_, { mode, points }, { perms }) => { - if (perms?.scanner.includes(mode)) { - const areaRestrictions = - config.getSafe(`scanner.${mode}.${mode}AreaRestriction`) || [] - - const validPoints = points.map((point) => - filterRTree( - { lat: point[0], lon: point[1] }, - perms.areaRestrictions, - areaRestrictions, - ), - ) - return validPoints - } - return [] - }, + checkValidScan: (_, { mode, points }, { perms }) => + getValidCoords(mode, points, perms), /** @param {unknown} _ @param {{ component: 'loginPage' | 'donationPage' | 'messageOfTheDay' }} args */ customComponent: (_, { component }, { perms, req, user }) => { switch (component) { @@ -598,20 +584,33 @@ const resolvers = { } return {} }, + /** @param {unknown} _ @param {import('@rm/types').ScanOnDemandReq} args */ scanner: (_, args, { req, perms }) => { const { category, method, data } = args - if (data?.cooldown) { - req.session.cooldown = Math.max( - req.session.cooldown || 0, - data.cooldown || 0, - ) - req.session.save() - } if (category === 'getQueue') { return scannerApi(category, method, data, req?.user) } - if (perms?.scanner?.includes(category)) { - return scannerApi(category, method, data, req?.user) + if ( + perms?.scanner?.includes(category) && + (!req.session.cooldown || req.session.cooldown < Date.now()) + ) { + const validCoords = getValidCoords(category, data?.scanCoords, perms) + + const cooldown = + config.getSafe(`scanner.${category}.userCooldownSeconds`) * + validCoords.filter(Boolean).length * + 1000 + + Date.now() + req.session.cooldown = cooldown + return scannerApi( + category, + method, + { + ...data, + scanCoords: data.scanCoords?.filter((__, i) => validCoords[i]), + }, + req?.user, + ) } return {} }, diff --git a/server/src/graphql/server.js b/server/src/graphql/server.js index 9b850c21f..8fc5682c2 100644 --- a/server/src/graphql/server.js +++ b/server/src/graphql/server.js @@ -28,7 +28,7 @@ async function startApollo(httpServer) { const apolloServer = new ApolloServer({ typeDefs, resolvers, - introspection: config.getSafe('devOptions.enabled'), + introspection: config.getSafe('devOptions.graphiql'), formatError: (e) => { let customMessage = '' if ( @@ -80,8 +80,9 @@ async function startApollo(httpServer) { { async requestDidStart(requestContext) { requestContext.contextValue.startTime = Date.now() - - log.debug(requestContext.request?.variables?.filters) + if (requestContext.request?.variables?.filters) { + log.debug(requestContext.request?.variables?.filters) + } return { async willSendResponse(context) { const filterCount = Object.keys( diff --git a/server/src/index.js b/server/src/index.js index 578dcee3e..15ececc5d 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -62,9 +62,8 @@ if (sentry.enabled || process.env.SENTRY_DSN) { ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), ], tracesSampleRate: - parseFloat( - process.env.SENTRY_TRACES_SAMPLE_RATE || sentry.tracesSampleRate, - ) || 0.1, + +(process.env.SENTRY_TRACES_SAMPLE_RATE || sentry.tracesSampleRate) || + 0.1, release: pkg.version, }) @@ -215,10 +214,10 @@ app.use((err, req, res, next) => { startApollo(httpServer).then((server) => { app.use( '/graphql', - cors(), + cors({ origin: '/' }), json(), expressMiddleware(server, { - context: ({ req, res }) => { + context: async ({ req, res }) => { const perms = req.user ? req.user.perms : req.session.perms const user = req?.user?.username || '' const id = req?.user?.id || 0 diff --git a/server/src/services/DiscordClient.js b/server/src/services/DiscordClient.js index 75c4d4007..4c18a90a3 100644 --- a/server/src/services/DiscordClient.js +++ b/server/src/services/DiscordClient.js @@ -219,7 +219,10 @@ class DiscordClient { } } - /** @param {import('discord.js').APIEmbed} embed @param {string} channel */ + /** + * @param {import('discord.js').APIEmbed} embed + * @param {keyof DiscordClient['loggingChannels']} channel + */ async sendMessage(embed, channel = 'main') { const safeChannel = this.loggingChannels[channel] if (!safeChannel || typeof embed !== 'object') { diff --git a/server/src/services/TelegramClient.js b/server/src/services/TelegramClient.js index 37d1f8235..fe898f724 100644 --- a/server/src/services/TelegramClient.js +++ b/server/src/services/TelegramClient.js @@ -27,6 +27,12 @@ class TelegramClient { this.alwaysEnabledPerms = config.getSafe( 'authentication.alwaysEnabledPerms', ) + this.loggingChannels = { + main: strategy.logChannelId, + event: strategy.eventLogChannelId, + scanNext: strategy.scanNextLogChannelId, + scanZone: strategy.scanZoneLogChannelId, + } } /** @param {TGUser} user */ @@ -194,6 +200,45 @@ class TelegramClient { } } + /** + * + * @param {string} text + * @param {keyof TelegramClient['loggingChannels']} channel + */ + async sendMessage(text, channel = 'main') { + if (!this.loggingChannels[channel]) return + try { + const response = await fetch( + `https://api.telegram.org/bot${this.strategy.botToken}/sendMessage`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: this.loggingChannels[channel], + parse_mode: 'HTML', + disable_web_page_preview: true, + text, + }), + }, + ) + if (!response.ok) { + throw new Error( + `Telegram API error: ${response.status} ${response.statusText}`, + ) + } + log.info( + HELPERS.custom(this.rmStrategy, '#26A8EA'), + `${channel} Log Sent`, + ) + } catch (e) { + log.error( + HELPERS.custom(this.rmStrategy, '#26A8EA'), + `Error sending ${channel} Log`, + e, + ) + } + } + initPassport() { passport.use( this.rmStrategy, diff --git a/server/src/services/api/scannerApi.js b/server/src/services/api/scannerApi.js index 85d443ff8..296507f28 100644 --- a/server/src/services/api/scannerApi.js +++ b/server/src/services/api/scannerApi.js @@ -1,10 +1,14 @@ -/* eslint-disable no-nested-ternary */ +// @ts-check const { default: fetch } = require('node-fetch') const NodeCache = require('node-cache') const config = require('@rm/config') const { log, HELPERS } = require('@rm/logger') + +const { getCache, setCache } = require('../cache') const Clients = require('../Clients') +const TelegramClient = require('../TelegramClient') +const DiscordClient = require('../DiscordClient') const scannerQueue = { scanNext: {}, @@ -13,6 +17,52 @@ const scannerQueue = { const userCache = new NodeCache({ stdTTL: 60 * 60 * 24 }) +const onShutdown = async () => { + const cacheObj = {} + userCache.keys().forEach((key) => { + cacheObj[key] = userCache.get(key) + }) + await setCache('scanUserHistory.json', cacheObj) +} +process.on('SIGINT', async () => { + await onShutdown() + process.exit(0) +}) +process.on('SIGTERM', async () => { + await onShutdown() + process.exit(0) +}) +process.on('SIGUSR1', async () => { + await onShutdown() + process.exit(0) +}) +process.on('SIGUSR2', async () => { + await onShutdown() + process.exit(0) +}) + +Object.entries(getCache('scanUserHistory.json', {})).forEach(([k, v]) => + userCache.set(k, v), +) + +const backendConfig = config.getSafe('scanner.backendConfig') + +const dateFormat = new Intl.DateTimeFormat(undefined, { + dateStyle: 'short', + timeStyle: 'medium', +}) +const timeFormat = new Intl.DateTimeFormat(undefined, { + timeStyle: 'medium', +}) + +/** + * + * @param {import('packages/types/lib').ScanOnDemandReq['category']} category + * @param {import('packages/types/lib').ScanOnDemandReq['method']} method + * @param {import('packages/types/lib').ScanOnDemandReq['data']} data + * @param {Partial} user + * @returns + */ async function scannerApi( category, method, @@ -26,12 +76,12 @@ async function scannerApi( }, config.api.fetchTimeoutMs) const coords = - config.scanner.backendConfig.platform === 'mad' + backendConfig.platform === 'mad' ? [ parseFloat(data.scanCoords[0][0].toFixed(5)), parseFloat(data.scanCoords[0][1].toFixed(5)), ] - : config.scanner.backendConfig.platform === 'custom' + : backendConfig.platform === 'custom' ? data.scanCoords?.map((coord) => [ parseFloat(coord[0].toFixed(5)), parseFloat(coord[1].toFixed(5)), @@ -43,28 +93,25 @@ async function scannerApi( try { const headers = Object.fromEntries( - config.scanner.backendConfig.headers.map((header) => [ + backendConfig.headers.map((header) => [ typeof header === 'string' ? header : header.key || header.name, typeof header === 'string' ? header : header.value, ]), ) - switch (config.scanner.backendConfig.platform) { + switch (backendConfig.platform) { case 'mad': case 'rdm': Object.assign(headers, { Authorization: `Basic ${Buffer.from( - `${config.scanner.backendConfig.apiUsername}:${config.scanner.backendConfig.apiPassword}`, + `${backendConfig.apiUsername}:${backendConfig.apiPassword}`, ).toString('base64')}`, }) break case 'custom': - if ( - config.scanner.backendConfig.apiUsername || - config.scanner.backendConfig.apiPassword - ) { + if (backendConfig.apiUsername || backendConfig.apiPassword) { Object.assign(headers, { Authorization: `Basic ${Buffer.from( - `${config.scanner.backendConfig.apiUsername}:${config.scanner.backendConfig.apiPassword}`, + `${backendConfig.apiUsername}:${backendConfig.apiPassword}`, ).toString('base64')}`, }) } @@ -72,10 +119,11 @@ async function scannerApi( default: break } - const payloadObj = /** @type {{ url: string, options: RequestInit }} */ ({ - url: '', - options: {}, - }) + const payloadObj = + /** @type {{ url: string, options: import('node-fetch').RequestInit }} */ ({ + url: '', + options: {}, + }) const cache = userCache.has(user.id) ? userCache.get(user.id) : { coordinates: 0, requests: 0 } @@ -94,11 +142,11 @@ async function scannerApi( 5, )},${data.scanLocation[1].toFixed(5)}`, ) - switch (config.scanner.backendConfig.platform) { + switch (backendConfig.platform) { case 'mad': Object.assign(payloadObj, { url: `${ - config.scanner.backendConfig.apiEndpoint + backendConfig.apiEndpoint }/send_gps?origin=${encodeURIComponent( config.scanner.scanNext.scanNextDevice, )}&coords=${JSON.stringify(coords)}&sleeptime=${ @@ -110,7 +158,7 @@ async function scannerApi( case 'rdm': Object.assign(payloadObj, { url: `${ - config.scanner.backendConfig.apiEndpoint + backendConfig.apiEndpoint }/set_data?scan_next=true&instance=${encodeURIComponent( config.scanner.scanNext.scanNextInstance, )}&coords=${JSON.stringify(coords)}`, @@ -119,7 +167,7 @@ async function scannerApi( break case 'custom': Object.assign(payloadObj, { - url: config.scanner.backendConfig.apiEndpoint, + url: backendConfig.apiEndpoint, options: { method: 'POST', headers, @@ -144,10 +192,10 @@ async function scannerApi( 5, )},${data.scanLocation[1].toFixed(5)}`, ) - switch (config.scanner.backendConfig.platform) { + switch (backendConfig.platform) { case 'custom': Object.assign(payloadObj, { - url: config.scanner.backendConfig.apiEndpoint, + url: backendConfig.apiEndpoint, options: { method: 'POST', headers, @@ -158,7 +206,7 @@ async function scannerApi( default: Object.assign(payloadObj, { url: `${ - config.scanner.backendConfig.apiEndpoint + backendConfig.apiEndpoint }/set_data?scan_next=true&instance=${encodeURIComponent( config.scanner.scanZone.scanZoneInstance, )}&coords=${JSON.stringify(coords)}`, @@ -170,7 +218,7 @@ async function scannerApi( case 'getQueue': if ( scannerQueue[data.typeName].timestamp > - Date.now() - config.scanner.backendConfig.queueRefreshInterval * 1000 + Date.now() - backendConfig.queueRefreshInterval * 1000 ) { log.info( HELPERS.scanner, @@ -181,16 +229,16 @@ async function scannerApi( return { status: 'ok', message: scannerQueue[data.typeName].queue } } log.info(HELPERS.scanner, `Getting queue for method ${data.typeName}`) - switch (config.scanner.backendConfig.platform) { + switch (backendConfig.platform) { case 'custom': Object.assign(payloadObj, { - url: `${config.scanner.backendConfig.apiEndpoint}/queue`, + url: `${backendConfig.apiEndpoint}/queue`, options: { method, headers }, }) break default: Object.assign(payloadObj, { - url: `${config.scanner.backendConfig.apiEndpoint}/get_data?${ + url: `${backendConfig.apiEndpoint}/get_data?${ data.type }=true&queue_size=true&instance=${encodeURIComponent( config.scanner[data.typeName][`${data.typeName}Instance`], @@ -232,7 +280,7 @@ async function scannerApi( (scannerResponse.status === 200 || scannerResponse.status === 201) && category === 'getQueue' ) { - if (config.scanner.backendConfig.platform === 'custom') { + if (backendConfig.platform === 'custom') { const { queue } = await scannerResponse.json() log.info( HELPERS.scanner, @@ -256,70 +304,90 @@ async function scannerApi( return { status: 'ok', message: queueData.size } } - if ( - Clients[user.rmStrategy]?.sendMessage && - config.scanner.backendConfig.sendDiscordMessage - ) { + if (backendConfig.sendTelegramMessage || backendConfig.sendDiscordMessage) { const capitalized = category.replace('scan', 'Scan ') const updatedCache = userCache.get(user.id) const trimmed = coords .filter((_c, i) => i < 25) .map((c) => - config.scanner.backendConfig.platform === 'custom' + backendConfig.platform === 'custom' ? `${c[0]}, ${c[1]}` - : `${c.lat}, ${c.lon}`, + : typeof c === 'object' + ? `${'lat' in c && c.lat}, ${'lon' in c && c.lon}` + : c, ) .join('\n') - switch (user.strategy) { - case 'discord': - Clients[user.rmStrategy].sendMessage( - { - title: `${capitalized} Request`, - author: { - name: user.username, - icon_url: `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png`, + const client = Clients[user.rmStrategy] + if ( + client instanceof TelegramClient && + backendConfig.sendTelegramMessage + ) { + client.sendMessage( + `${capitalized} Request\nSize: ${ + data.scanSize + }\nCoordinates: ${coords.length}\nCenter: ${data.scanLocation + ?.map((c) => c.toFixed(5)) + .join(', ')}\n\nUser History\nUsername: ${ + user.username || user.telegramId + }\nTotal Requests: ${ + updatedCache?.requests || 0 + }\nTotal Coordinates: ${ + updatedCache?.coordinates || 0 + }\n\n${dateFormat.format(Date.now())}`, + category === 'getQueue' ? 'main' : category, + ) + } else if ( + client instanceof DiscordClient && + backendConfig.sendDiscordMessage + ) { + client.sendMessage( + { + title: `${capitalized} Request`, + author: { + name: user.username, + icon_url: `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png`, + }, + thumbnail: { + url: + config.authentication.strategies.find( + (strategy) => strategy.name === user.rmStrategy, + )?.thumbnailUrl ?? + `https://user-images.githubusercontent.com/58572875/167069223-745a139d-f485-45e3-a25c-93ec4d09779c.png`, + }, + footer: { + text: timeFormat.format(Date.now()), + }, + description: `<@${user.discordId}>\n${capitalized} Size: ${data.scanSize}\nCoordinates: ${coords.length}\n`, + fields: [ + { + name: `User History`, + value: `Total Requests: ${ + updatedCache?.requests || 0 + }\nTotal Coordinates: ${updatedCache?.coordinates || 0}`, + inline: true, }, - thumbnail: { - url: - config.authentication.strategies.find( - (strategy) => strategy.name === user.rmStrategy, - )?.thumbnailUrl ?? - `https://user-images.githubusercontent.com/58572875/167069223-745a139d-f485-45e3-a25c-93ec4d09779c.png`, + { + name: 'Instance', + value: `${ + backendConfig.platform === 'mad' + ? `Device: ${config.scanner.scanNext.scanNextDevice}` + : '' + }\nName: ${ + config.scanner[category]?.[`${category}Instance`] || '-' + }\nQueue: ${scannerQueue[category]?.queue || 0}`, + inline: true, }, - timestamp: new Date(), - description: `<@${user.discordId}>\n${capitalized} Size: ${data.scanSize}\nCoordinates: ${coords.length}\n`, - fields: [ - { - name: `User History`, - value: `Total Requests: ${ - updatedCache?.requests || 0 - }\nTotal Coordinates: ${updatedCache?.coordinates || 0}`, - inline: true, - }, - { - name: 'Instance', - value: `${ - config.scanner.backendConfig.platform === 'mad' - ? `Device: ${config.scanner.scanNext.scanNextDevice}` - : '' - }\nName: ${ - config.scanner[category]?.[`${category}Instance`] || '-' - }\nQueue: ${scannerQueue[category]?.queue || 0}`, - inline: true, - }, - { - name: `Coordinates (${coords.length})`, - value: - coords.length > 25 - ? `${trimmed}\n...${coords.length - 25} more` - : trimmed, - }, - ], - }, - category, - ) - break - default: + { + name: `Coordinates (${coords.length})`, + value: + coords.length > 25 + ? `${trimmed}\n...${coords.length - 25} more` + : trimmed, + }, + ], + }, + category === 'getQueue' ? 'main' : category, + ) } } diff --git a/server/src/services/functions/getValidCoords.js b/server/src/services/functions/getValidCoords.js new file mode 100644 index 000000000..4c3cbcf53 --- /dev/null +++ b/server/src/services/functions/getValidCoords.js @@ -0,0 +1,34 @@ +// @ts-check +const config = require('@rm/config') + +const { filterRTree } = require('./filterRTree') + +/** + * + * @param {'scanNext' | 'scanZone'} mode + * @param {[number, number][]} points + * @param {import('@rm/types').Permissions} perms + */ +function getValidCoords(mode, points, perms) { + if (perms?.scanner?.includes(mode) && points?.length) { + const configString = + mode === 'scanNext' + ? 'scanner.scanNext.scanNextAreaRestriction' + : 'scanner.scanZone.scanZoneAreaRestriction' + const areaRestrictions = config.getSafe(configString) || [] + + const validPoints = points.map((point) => + filterRTree( + { lat: point[0], lon: point[1] }, + perms.areaRestrictions, + areaRestrictions, + ), + ) + return validPoints + } + return [] +} + +module.exports = { + getValidCoords, +} diff --git a/src/components/layout/dialogs/scanner/ScanDialog.jsx b/src/components/layout/dialogs/scanner/ScanDialog.jsx index 641e9433d..73092d48f 100644 --- a/src/components/layout/dialogs/scanner/ScanDialog.jsx +++ b/src/components/layout/dialogs/scanner/ScanDialog.jsx @@ -26,6 +26,20 @@ export default function ScanDialog() { if (scanZone) return setScanMode('scanZoneMode') }, [scanNext, scanZone]) + const footerOptions = React.useMemo( + () => + /** @type {import('@components/layout/general/Footer').FooterButton[]} */ ([ + { + name: 'close', + icon: 'Clear', + color: 'primary', + align: 'right', + action: handleClose, + }, + ]), + [handleClose], + ) + return ( - {t(`scan_${scanMode}`)} + {scanMode && t(`scan_${scanMode}`)} - ) } diff --git a/src/services/apollo/index.js b/src/services/apollo/index.js index 56c3f38d5..8f445e874 100644 --- a/src/services/apollo/index.js +++ b/src/services/apollo/index.js @@ -7,50 +7,7 @@ const abortableLink = new AbortableLink() export const apolloCache = new InMemoryCache({ typePolicies: { - Query: { - // fields: { - // badges: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // devices: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // gyms: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // nests: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // pokemon: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // pokestops: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // portals: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // spawnpoints: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // }, - }, + Query: {}, SearchQuest: { keyFields: ['id', 'with_ar'], }, @@ -84,20 +41,6 @@ export const apolloCache = new InMemoryCache({ PoracleWeather: { keyFields: ['uid'], }, - // Pokestop: { - // fields: { - // quests: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // invasions: { - // merge(existing, incoming) { - // return incoming - // }, - // }, - // }, - // }, }, })