diff --git a/.gitignore b/.gitignore index 727e4d5a7..dbbc19e20 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,6 @@ server/src/models/queries/* # Cache server/.cache/* !/server/.cache/.gitkeep + +# Misc +ts-check.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f538fd7c..1b1727a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,107 @@ +# [1.35.0-develop.3](https://github.com/WatWowMap/ReactMap/compare/v1.35.0-develop.2...v1.35.0-develop.3) (2024-09-23) + + +### Bug Fixes + +* uicons.js 2.0.2 ([1153035](https://github.com/WatWowMap/ReactMap/commit/1153035919ba0eec3a47035dc9b7153ab6d1b7a0)) + +# [1.35.0-develop.2](https://github.com/WatWowMap/ReactMap/compare/v1.35.0-develop.1...v1.35.0-develop.2) (2024-09-23) + + +### Bug Fixes + +* `battleOpacity` => `stationsOpacity` ([f720aa3](https://github.com/WatWowMap/ReactMap/commit/f720aa3ed58b4c97277486dd78861d3917148bf4)) +* a slightly better temporary image hack ([29b9eb6](https://github.com/WatWowMap/ReactMap/commit/29b9eb670dcd7f83b18ac998d4def81d042b2104)) +* a slightly better temporary image hack ([fd2e7c4](https://github.com/WatWowMap/ReactMap/commit/fd2e7c4f18925f7982915f26619b39fde7ebf415)) +* add missing copy coords menu item ([32b22ff](https://github.com/WatWowMap/ReactMap/commit/32b22ff23a3f1bd4699afa2659351b6fbc43b9d7)) +* add types & onClose callback to `CopyCoords` ([7eaf43d](https://github.com/WatWowMap/ReactMap/commit/7eaf43d036520a8a202be7fc9312ee688f81a5de)) +* encode title in apollo client ([5b7b58f](https://github.com/WatWowMap/ReactMap/commit/5b7b58f7a07d0eebc436fa7535359cd031208fdf)) +* forgot to rename from `battleTimers` => `stationTimers` ([cd32477](https://github.com/WatWowMap/ReactMap/commit/cd32477cfe82e681029497f3f5e70a72c2eb8567)) +* just a lot of things :dango: ([421b06e](https://github.com/WatWowMap/ReactMap/commit/421b06e90fa1704c37fafc51389c3983f1194088)) +* just a lot of things :dango: ([7879a7f](https://github.com/WatWowMap/ReactMap/commit/7879a7f01975325192ea0a173ea255f6a9e7ee15)) +* local.example.json additions ([89706eb](https://github.com/WatWowMap/ReactMap/commit/89706ebaeca95dc887b6adc5623a60e6f61b6c0a)) +* local.example.json additions ([a200e50](https://github.com/WatWowMap/ReactMap/commit/a200e50243432953d8a1240e7e4709d3b4ea524e)) +* memo comparison ([dd66d03](https://github.com/WatWowMap/ReactMap/commit/dd66d039cf91af9034b90a3915b197863befea17)) +* missing > ([0352204](https://github.com/WatWowMap/ReactMap/commit/0352204608526d99759b3f9c01fca1fe73bc6864)) +* missing translation ([d676875](https://github.com/WatWowMap/ReactMap/commit/d676875cb26c2b017220922cc144ee93f567c898)) +* missing translations ([abb047a](https://github.com/WatWowMap/ReactMap/commit/abb047a64303deca2231972d033f6d26025a5e3f)) +* more consistent `is_battle_available` usage ([0a565ed](https://github.com/WatWowMap/ReactMap/commit/0a565ed265db65bf0a5668bc47e0103165792796)) +* optimize dynamax code ([a1cfb57](https://github.com/WatWowMap/ReactMap/commit/a1cfb570f2022eb6d3195e5a59949e90e673b5fd)) +* remove log ([6e35170](https://github.com/WatWowMap/ReactMap/commit/6e3517006f3cf7459681a8774dd6b3ceefff6a6d)) +* select query ([cac8eab](https://github.com/WatWowMap/ReactMap/commit/cac8eab24fcce4830bc97cca89e9d2153b2f4f7e)) +* select query ([4dd8b2c](https://github.com/WatWowMap/ReactMap/commit/4dd8b2c7988c50185d9fd216f25d8973795fd185)) +* some client side perm stuff ([73fd35a](https://github.com/WatWowMap/ReactMap/commit/73fd35abaac4f16a0a4b905396bc2de91060dd1a)) +* some type imports ([19ae836](https://github.com/WatWowMap/ReactMap/commit/19ae8361256165021a46c91836a4411b6b483773)) +* station and dynamax perms on profile page ([dcd994a](https://github.com/WatWowMap/ReactMap/commit/dcd994a7ecc39aef6509cd811efb4143f0db562e)) +* station clustering in config ([d87dd0e](https://github.com/WatWowMap/ReactMap/commit/d87dd0e647b4b977bd3d8092807660232ea0c038)) +* station clustering in config ([7037c46](https://github.com/WatWowMap/ReactMap/commit/7037c469ac15cfbcf60a2ef6f0b4d888110fecff)) +* uicons 1.4.1 ([a1776f1](https://github.com/WatWowMap/ReactMap/commit/a1776f16e3a51b6be38047a5432e0a791e65b887)) +* undefined/null issue in marker ([48ee917](https://github.com/WatWowMap/ReactMap/commit/48ee917a1e2c8e8f0ec6f1b6b537935a8c41383c)) +* undefined/null issue in marker ([9f7532f](https://github.com/WatWowMap/ReactMap/commit/9f7532f6330762b260c3ba9e473153b6ece3469a)) + + +### Features + +* bread mode uicons support ([4c5dfcc](https://github.com/WatWowMap/ReactMap/commit/4c5dfcc311b90f31faf4ca80f4a98a498e79748b)) +* categories in options menus ([3bffd7e](https://github.com/WatWowMap/ReactMap/commit/3bffd7e455cd43c46809aed16dde7ba1a89bd94c)) +* german translations for dynamax ([a1f527a](https://github.com/WatWowMap/ReactMap/commit/a1f527ac46ffbbb6239018f5faf3cce5929f2a51)) +* station override dropdown ([5ba34c9](https://github.com/WatWowMap/ReactMap/commit/5ba34c9d373bbb233fc657aabfcdd72f46b931e3)) +* station searching ([fbf3b6d](https://github.com/WatWowMap/ReactMap/commit/fbf3b6d078a559cc09532f28955a8f106a49a44c)) +* station searching ([7bd4c50](https://github.com/WatWowMap/ReactMap/commit/7bd4c50e2b592e59d6b2b40925e2942e7292392b)) +* station timers + bread mon moves ([70316d6](https://github.com/WatWowMap/ReactMap/commit/70316d6f86b1ac287103daf61e3ba89e30255a3a)) +* stationed pokemon ([553ab86](https://github.com/WatWowMap/ReactMap/commit/553ab864785ab3bab67f03ac4f463a50a645e2a1)) +* stations ([6e61dca](https://github.com/WatWowMap/ReactMap/commit/6e61dca891c7dd4fb5e120119159d66ba608e6d5)) +* stations ([f6a479b](https://github.com/WatWowMap/ReactMap/commit/f6a479b4ae02088461cea7570fc48e18a6423d7b)) + +# [1.35.0-develop.1](https://github.com/WatWowMap/ReactMap/compare/v1.34.1-develop.5...v1.35.0-develop.1) (2024-09-23) + + +### Bug Fixes + +* jsdoc types ([4316fff](https://github.com/WatWowMap/ReactMap/commit/4316fff7b13f98390d4d4be6f98002a4d84f9957)) + + +### Features + +* support pokemon display in showcases ([d39ebab](https://github.com/WatWowMap/ReactMap/commit/d39ebab569d8d3cba7ceadf95d7b2179f0663673)) + +## [1.34.1-develop.5](https://github.com/WatWowMap/ReactMap/compare/v1.34.1-develop.4...v1.34.1-develop.5) (2024-09-04) + + +### Bug Fixes + +* properly support gym defender temp evolutions ([4595820](https://github.com/WatWowMap/ReactMap/commit/45958204c9de795d12cb0a0e241982edcd1f5973)) +* set proper type for compat golbat ([0d5b5f7](https://github.com/WatWowMap/ReactMap/commit/0d5b5f79128887fca18e3032773504784e5365d0)) + +## [1.34.1-develop.4](https://github.com/WatWowMap/ReactMap/compare/v1.34.1-develop.3...v1.34.1-develop.4) (2024-08-30) + + +### Bug Fixes + +* revert url fallback in navigation ([eddf8bd](https://github.com/WatWowMap/ReactMap/commit/eddf8bd478d47221eb85ae2b50b7a8e709dbe6b7)) + +## [1.34.1-develop.3](https://github.com/WatWowMap/ReactMap/compare/v1.34.1-develop.2...v1.34.1-develop.3) (2024-08-29) + + +### Bug Fixes + +* backup if a user has an invalid nav option ([744ca90](https://github.com/WatWowMap/ReactMap/commit/744ca90a86dcd89d679c8e605f0b1061e8d611cb)) +* the travesty that was the nest popup ([d19fc74](https://github.com/WatWowMap/ReactMap/commit/d19fc74edbd4f83f736273f38e2da08bb3739dec)) + +## [1.34.1-develop.2](https://github.com/WatWowMap/ReactMap/compare/v1.34.1-develop.1...v1.34.1-develop.2) (2024-08-29) + + +### Bug Fixes + +* order of items for the `scanAreas` section of the drawer ([6be6b3b](https://github.com/WatWowMap/ReactMap/commit/6be6b3b339daf01d7c953053755ffdd65f246123)) + +## [1.34.1-develop.1](https://github.com/WatWowMap/ReactMap/compare/v1.34.0...v1.34.1-develop.1) (2024-08-29) + + +### Bug Fixes + +* various type adjustments ([ab51750](https://github.com/WatWowMap/ReactMap/commit/ab51750d93411cf959af8a3b556c81f4da50f91d)) + # [1.34.0](https://github.com/WatWowMap/ReactMap/compare/v1.33.1...v1.34.0) (2024-08-23) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 0467cc210..5247ed242 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -111,6 +111,10 @@ "__name": "API_POLLING_SUBMISSION_CELLS", "__format": "number" }, + "stations": { + "__name": "API_POLLING_STATIONS", + "__format": "number" + }, "weather": { "__name": "API_POLLING_WEATHER", "__format": "number" @@ -152,6 +156,10 @@ "invasions": { "__name": "API_SEARCHABLE_INVASIONS", "__format": "boolean" + }, + "stations": { + "__name": "API_SEARCHABLE_STATIONS", + "__format": "boolean" } }, "queryUpdateHours": { @@ -174,6 +182,10 @@ "historicalRarity": { "__name": "API_QUERY_UPDATE_HOURS_HISTORICAL_RARITY", "__format": "number" + }, + "stations": { + "__name": "API_QUERY_UPDATE_HOURS_STATIONS", + "__format": "number" } }, "queryOnSessionInit": { @@ -192,6 +204,10 @@ "nests": { "__name": "API_QUERY_ON_SESSION_INIT_NESTS", "__format": "boolean" + }, + "stations": { + "__name": "API_QUERY_ON_SESSION_INIT_STATIONS", + "__format": "boolean" } }, "dataRequestLimits": { @@ -227,6 +243,10 @@ "weather": { "__name": "API_DATA_REQUEST_LIMITS_CATEGORIES_WEATHER", "__format": "number" + }, + "stations": { + "__name": "API_DATA_REQUEST_LIMITS_CATEGORIES_STATIONS", + "__format": "number" } }, "time": { @@ -266,6 +286,10 @@ "scanCells": { "__name": "API_QUERY_LIMITS_SCAN_CELLS", "__format": "number" + }, + "stations": { + "__name": "API_QUERY_LIMITS_STATIONS", + "__format": "number" } }, "pvp": { @@ -304,6 +328,10 @@ "__name": "API_WEATHER_CELL_LIMIT", "__format": "number" }, + "stationUpdateLimit": { + "__name": "API_STATION_UPDATE_LIMIT", + "__format": "number" + }, "searchResultsLimit": { "__name": "API_SEARCH_RESULTS_LIMIT", "__format": "number" @@ -496,6 +524,10 @@ "__name": "MAP_MISC_ENABLE_PORTAL_POPUP_COORDS_SELECTOR", "__format": "boolean" }, + "enableStationPopupCoordsSelector": { + "__name": "MAP_MISC_ENABLE_STATION_POPUP_COORDS_SELECTOR", + "__format": "boolean" + }, "customFloatingIcons": { "__name": "MAP_MISC_CUSTOM_FLOATING_ICONS", "__format": "json" @@ -564,6 +596,16 @@ "__name": "MAP_CLUSTERING_SPAWNPOINTS_FORCED_LIMIT", "__format": "number" } + }, + "stations": { + "zoomLevel": { + "__name": "MAP_CLUSTERING_STATIONS_ZOOM_LEVEL", + "__format": "number" + }, + "forcedLimit": { + "__name": "MAP_CLUSTERING_STATIONS_FORCED_LIMIT", + "__format": "number" + } } }, "messageOfTheDay": { @@ -782,6 +824,36 @@ "__format": "number" } }, + "stations": { + "clustering": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_CLUSTERING", + "__format": "boolean" + }, + "stationTimers": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_STATION_TIMERS", + "__format": "boolean" + }, + "stationsOpacity": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_STATIONS_OPACITY", + "__format": "boolean" + }, + "enableStationPopupCoords": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_ENABLE_STATION_POPUP_COORDS", + "__format": "boolean" + }, + "opacityTenMinutes": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_OPACITY_TEN_MINUTES", + "__format": "number" + }, + "opacityFiveMinutes": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_OPACITY_FIVE_MINUTES", + "__format": "number" + }, + "opacityOneMinute": { + "__name": "CLIENT_SIDE_OPTIONS_STATIONS_OPACITY_ONE_MINUTE", + "__format": "number" + } + }, "pokemon": { "clustering": { "__name": "CLIENT_SIDE_OPTIONS_POKEMON_CLUSTERING", @@ -1247,6 +1319,21 @@ "__format": "boolean" } }, + "stations": { + "enabled": { + "__name": "DEFAULT_FILTERS_STATIONS_ENABLED", + "__format": "boolean" + }, + "pokemon": { + "__name": "DEFAULT_FILTERS_STATIONS_POKEMON", + "__format": "boolean" + }, + "battleTier": "DEFAULT_FILTERS_STATIONS_BATTLE_TIER", + "battles": { + "__name": "DEFAULT_FILTERS_STATIONS_BATTLES", + "__format": "boolean" + } + }, "s2cells": { "enabled": { "__name": "DEFAULT_FILTERS_S2CELLS_ENABLED", @@ -1847,6 +1934,34 @@ "__name": "AUTHENTICATION_PERMS_SHOWCASE_RANKINGS_ROLES", "__format": "json" } + }, + "stations": { + "enabled": { + "__name": "AUTHENTICATION_PERMS_STATIONS_ENABLED", + "__format": "boolean" + }, + "trialPeriodEligible": { + "__name": "AUTHENTICATION_PERMS_STATIONS_TRIAL_PERIOD_ELIGIBLE", + "__format": "boolean" + }, + "roles": { + "__name": "AUTHENTICATION_PERMS_STATIONS_ROLES", + "__format": "json" + } + }, + "dynamax": { + "enabled": { + "__name": "AUTHENTICATION_PERMS_DYNAMAX_ENABLED", + "__format": "boolean" + }, + "trialPeriodEligible": { + "__name": "AUTHENTICATION_PERMS_DYNAMAX_TRIAL_PERIOD_ELIGIBLE", + "__format": "boolean" + }, + "roles": { + "__name": "AUTHENTICATION_PERMS_DYNAMAX_ROLES", + "__format": "json" + } } } }, @@ -2054,6 +2169,42 @@ "__name": "ICONS_SIZES_EVENT_XL", "__format": "number" } + }, + "station": { + "sm": { + "__name": "ICONS_SIZES_STATION_SM", + "__format": "number" + }, + "md": { + "__name": "ICONS_SIZES_STATION_MD", + "__format": "number" + }, + "lg": { + "__name": "ICONS_SIZES_STATION_LG", + "__format": "number" + }, + "xl": { + "__name": "ICONS_SIZES_STATION_XL", + "__format": "number" + } + }, + "dynamax": { + "sm": { + "__name": "ICONS_SIZES_DYNAMAX_SM", + "__format": "number" + }, + "md": { + "__name": "ICONS_SIZES_DYNAMAX_MD", + "__format": "number" + }, + "lg": { + "__name": "ICONS_SIZES_DYNAMAX_LG", + "__format": "number" + }, + "xl": { + "__name": "ICONS_SIZES_DYNAMAX_XL", + "__format": "number" + } } } }, diff --git a/config/default.json b/config/default.json index c8301a8f9..ea0376867 100644 --- a/config/default.json +++ b/config/default.json @@ -36,6 +36,7 @@ "scanAreas": 10000, "scanCells": 10, "submissionCells": 500, + "stations": 15, "weather": 30 }, "searchable": { @@ -47,20 +48,23 @@ "portals": true, "nests": true, "pokemon": true, - "invasions": true + "invasions": true, + "stations": true }, "queryUpdateHours": { "pokemon": 0.17, "quests": 0.25, "raids": 0.05, "nests": 0.5, - "historicalRarity": 6 + "historicalRarity": 6, + "stations": 0.05 }, "queryOnSessionInit": { "pokemon": false, "quests": false, "raids": true, - "nests": false + "nests": false, + "stations": false }, "dataRequestLimits": { "categories": { @@ -71,7 +75,8 @@ "pokestops": 0, "portals": 0, "routes": 0, - "weather": 0 + "weather": 0, + "stations": 0 }, "time": 60 }, @@ -83,7 +88,8 @@ "portals": 5000, "spawnpoints": 10000, "nests": 2500, - "scanCells": 5000 + "scanCells": 5000, + "stations": 5000 }, "pvp": { "leagues": [ @@ -110,6 +116,7 @@ }, "portalUpdateLimit": 30, "weatherCellLimit": 3, + "stationUpdateLimit": 30, "searchResultsLimit": 15, "searchSoftKmLimit": 10, "searchHardKmLimit": 100, @@ -150,6 +157,7 @@ "gyms", "nests", "pokestops", + "stations", "pokemon", "routes", "wayfarer", @@ -255,6 +263,7 @@ "enableGymPopupCoordsSelector": false, "enablePokestopPopupCoordsSelector": false, "enablePortalPopupCoordsSelector": false, + "enableStationPopupCoordsSelector": false, "customFloatingIcons": [], "expandAllScanAreas": false, "enableRouteDownload": false @@ -284,6 +293,10 @@ "spawnpoints": { "zoomLevel": 12, "forcedLimit": 5000 + }, + "stations": { + "zoomLevel": 14, + "forcedLimit": 2500 } }, "messageOfTheDay": { @@ -364,6 +377,15 @@ "opacityFiveMinutes": 0.5, "opacityOneMinute": 0.25 }, + "stations": { + "clustering": true, + "stationTimers": false, + "stationsOpacity": true, + "enableStationPopupCoords": false, + "opacityTenMinutes": 0.75, + "opacityFiveMinutes": 0.5, + "opacityOneMinute": 0.25 + }, "pokemon": { "clustering": true, "pokemonTimers": false, @@ -553,6 +575,12 @@ "s14Cells": true, "includeSponsored": true }, + "stations": { + "enabled": false, + "pokemon": false, + "battleTier": "all", + "battles": false + }, "s2cells": { "enabled": false, "cells": [] @@ -839,6 +867,16 @@ "enabled": true, "trialPeriodEligible": false, "roles": [] + }, + "stations": { + "enabled": true, + "trialPeriodEligible": false, + "roles": [] + }, + "dynamax": { + "enabled": true, + "trialPeriodEligible": false, + "roles": [] } } }, @@ -960,6 +998,18 @@ "md": 25, "lg": 35, "xl": 45 + }, + "station": { + "sm": 15, + "md": 25, + "lg": 35, + "xl": 45 + }, + "dynamax": { + "sm": 15, + "md": 25, + "lg": 35, + "xl": 45 } } }, diff --git a/config/local.example.json b/config/local.example.json index b974ee4f2..385b3bdf5 100644 --- a/config/local.example.json +++ b/config/local.example.json @@ -37,7 +37,8 @@ "spawnpoint", "weather", "route", - "nest" + "nest", + "station" ] }, { @@ -236,6 +237,16 @@ "enabled": true, "trialPeriodEligible": false, "roles": [] + }, + "stations": { + "enabled": true, + "trialPeriodEligible": false, + "roles": [] + }, + "dynamax": { + "enabled": true, + "trialPeriodEligible": false, + "roles": [] } } }, diff --git a/package.json b/package.json index 96ecd5845..9ea781699 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactmap", - "version": "1.34.0", + "version": "1.35.0-develop.3", "private": true, "description": "React based frontend map.", "license": "MIT", @@ -128,7 +128,7 @@ "@turf/helpers": "7.1.0", "@turtlesocks/react-leaflet.locatecontrol": "^0.1.1", "bcrypt": "^5.0.1", - "body-parser": "^1.20.2", + "body-parser": "2.0.1", "bytes": "3.1.2", "chalkercli": "https://github.com/TurtIeSocks/chalkercli.git", "chokidar": "^3.5.3", @@ -139,7 +139,7 @@ "discord.js": "14.15.3", "dlv": "^1.1.3", "dotenv": "^16.3.1", - "express": "^4.19.2", + "express": "4.21.0", "express-mysql-session": "3.0.3", "express-rate-limit": "7.4.0", "express-session": "1.18.0", @@ -158,7 +158,7 @@ "moment-timezone": "^0.5.43", "mysql2": "3.11.0", "node-cache": "^5.1.2", - "node-fetch": "2.6.7", + "node-fetch": "2.7.0", "node-geocoder": "^4.2.0", "nodes2ts": "3.0.0", "objection": "3.1.4", @@ -172,12 +172,12 @@ "react-i18next": "15.0.1", "react-leaflet": "4.2.1", "react-router-dom": "^6.15.0", - "react-virtuoso": "^4.6.2", + "react-virtuoso": "4.10.1", "rtree": "^1.4.2", "source-map": "^0.7.4", "suncalc": "^1.9.0", "supercluster": "^8.0.1", - "uicons.js": "^1.1.4", + "uicons.js": "2.0.2", "zustand": "4.4.6" }, "devDependencies": { @@ -190,7 +190,7 @@ "@sentry/vite-plugin": "2.10.3", "@types/dlv": "^1.1.2", "@types/node": "^18", - "@types/node-fetch": "2.6.1", + "@types/node-fetch": "2.6.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "3.7.0", @@ -212,7 +212,7 @@ "rollup-plugin-delete": "^2.0.0", "semantic-release": "^22", "typescript": "5.5.4", - "vite": "5.4.0", + "vite": "5.4.6", "vite-plugin-checker": "0.7.2" }, "engines": { diff --git a/packages/config/.configref b/packages/config/.configref index 8036da96b..e8639de87 100644 --- a/packages/config/.configref +++ b/packages/config/.configref @@ -1 +1 @@ -24258 \ No newline at end of file +25375 \ No newline at end of file diff --git a/packages/config/lib/mutations.js b/packages/config/lib/mutations.js index 31a9f4e11..a079c397d 100644 --- a/packages/config/lib/mutations.js +++ b/packages/config/lib/mutations.js @@ -7,29 +7,28 @@ const { log, TAGS } = require('@rm/logger') const { validateJsons } = require('./validateJsons') +let firstRun = true + /** @param {import('config').IConfig} config */ const applyMutations = (config) => { + const defaults = /** @type {import('@rm/types').Config} */ ( + config.util + .getConfigSources() + .find(({ name }) => name.endsWith('default.json'))?.parsed + ) + if (!defaults) { + log.error(TAGS.config, 'Could not find default.json') + return + } + if (process.env.NODE_CONFIG_ENV) { - log.info(TAGS.config, `Using config for ${process.env.NODE_CONFIG_ENV}`) + if (firstRun) + log.info(TAGS.config, `Using config for ${process.env.NODE_CONFIG_ENV}`) } const [rootConfigDir, serverConfigDir] = ( process.env.NODE_CONFIG_DIR || '' ).split(path.delimiter) - const allowedMenuItems = [ - 'gyms', - 'nests', - 'pokestops', - 'pokemon', - 'routes', - 'wayfarer', - 's2cells', - 'scanAreas', - 'weather', - 'admin', - 'settings', - ] - try { const refLength = +fs.readFileSync( path.join(__dirname, '../.configref'), @@ -40,7 +39,7 @@ const applyMutations = (config) => { 'utf8', ).length - if (refLength !== defaultLength) { + if (refLength !== defaultLength && firstRun) { log.warn( TAGS.config, 'It looks like you have modified the `default.json` file, you should not do this! Make all of your config changes in your `local.json` file.', @@ -187,10 +186,11 @@ const applyMutations = (config) => { merged.misc.distanceUnit !== 'kilometers' && merged.misc.distanceUnit !== 'miles' ) { - log.warn( - TAGS.config, - `Invalid distanceUnit: ${merged.misc.distanceUnit}, only 'kilometers' OR 'miles' are allowed.`, - ) + if (firstRun) + log.warn( + TAGS.config, + `Invalid distanceUnit: ${merged.misc.distanceUnit}, only 'kilometers' OR 'miles' are allowed.`, + ) if (merged.misc.distance === 'km') { merged.misc.distanceUnit = 'kilometers' } else if (merged.misc.distance === 'mi') { @@ -200,9 +200,21 @@ const applyMutations = (config) => { } } merged.general.menuOrder = merged?.general?.menuOrder - ? merged.general.menuOrder.filter((x) => allowedMenuItems.includes(x)) + ? merged.general.menuOrder.filter((x) => + defaults.map.general.menuOrder.includes(x), + ) : [] + defaults.map.general.menuOrder.forEach((item) => { + if (!merged.general.menuOrder.includes(item)) { + log.warn( + TAGS.config, + `Missing menu item: ${item} in map.general.menuOrder, adding it to the end of the list.`, + ) + merged.general.menuOrder.push(item) + } + }) + merged.loginPage = config.util.extendDeep( {}, merged.loginPage, @@ -225,10 +237,11 @@ const applyMutations = (config) => { config.map = mergeMapConfig() if (config.has('multiDomains')) { - log.warn( - TAGS.config, - '`multiDomains` has been deprecated and will be removed in the next major release. Please switch to the new format that makes use of `NODE_CONFIG_ENV`', - ) + if (firstRun) + log.warn( + TAGS.config, + '`multiDomains` has been deprecated and will be removed in the next major release. Please switch to the new format that makes use of `NODE_CONFIG_ENV`', + ) // Create multiDomain Objects config.multiDomainsObj = Object.fromEntries( config.multiDomains.map((d) => [ @@ -343,13 +356,15 @@ const applyMutations = (config) => { (perm) => config.authentication.perms[perm].enabled, ) ) - log.warn( - TAGS.config, - 'No authentication strategies enabled, adding the following perms to alwaysEnabledPerms array:\n', - enabled, - ) + if (firstRun) + log.warn( + TAGS.config, + 'No authentication strategies enabled, adding the following perms to alwaysEnabledPerms array:\n', + enabled, + ) config.authentication.alwaysEnabledPerms = enabled } + firstRun = false } module.exports = { applyMutations } diff --git a/packages/config/lib/scripts/configCheck.js b/packages/config/lib/scripts/configCheck.js index ba6c71bd2..30d3e5a01 100644 --- a/packages/config/lib/scripts/configCheck.js +++ b/packages/config/lib/scripts/configCheck.js @@ -1,3 +1,5 @@ +// @ts-check + const fs = require('fs') const path = require('path') diff --git a/packages/config/lib/scripts/genEnvConfig.js b/packages/config/lib/scripts/genEnvConfig.js index 605646a62..eb7669056 100644 --- a/packages/config/lib/scripts/genEnvConfig.js +++ b/packages/config/lib/scripts/genEnvConfig.js @@ -1,3 +1,5 @@ +// @ts-check + const fs = require('fs') const { resolve } = require('path') diff --git a/packages/locales/lib/generate.js b/packages/locales/lib/generate.js index 7a8d628a8..406d2f762 100644 --- a/packages/locales/lib/generate.js +++ b/packages/locales/lib/generate.js @@ -98,7 +98,7 @@ function matchJSON(str) { */ async function sendToGPT(locale, missingKeys) { return openAI.chat.completions.create({ - model: 'gpt-4-turbo-preview', + model: 'gpt-4o-mini', messages: [ { role: 'system', diff --git a/packages/locales/lib/human/de.json b/packages/locales/lib/human/de.json index 1779c3c3e..16e5abea3 100644 --- a/packages/locales/lib/human/de.json +++ b/packages/locales/lib/human/de.json @@ -18,10 +18,10 @@ "legacy": "Legendär", "id": "ID", "location": "Standort", - "search_pokemon": "Suche Pokemon", - "search_nests": "Suche Nester", - "search_gyms": "Suche Arena", - "search_pokestops": "Suche Pokestops", + "search_pokemon": "Pokemon durchsuchen", + "search_nests": "Nester durchsuchen", + "search_gyms": "Arenen durchsuchen", + "search_pokestops": "Pokestops durchsuchen", "sm": "S", "md": "M", "lg": "L", @@ -66,7 +66,7 @@ "settings": "Einstellungen", "advanced": "erweitert", "icons": "Symbole", - "navigation": "navigation", + "navigation": "Navigation", "drawer": "Drawer", "iconsDefault": "Standard", "drawer_temporary": "Vorübergehend", @@ -422,7 +422,7 @@ "poke_global": "Alle", "amount": "Anzahl", "specific_gym": "Spezifische Arena", - "search_location": "Suche Standorte", + "search_location": "Standorte durchsuchen", "search_specific": "Suche nach spezifischer {{category}}", "webhook_success_gym": "Raids, Eier, und Team Wechsel Alarme wurden hinzugefügt!", "slot_changes": "Platzänderungen", @@ -533,11 +533,11 @@ "early_old_client_title": "veraltet", "early_old_client_body": "Eine alte Version dieser Map könnte im Browser zwischengespeichert sein, lösche den Cache, bevor diese Map verwendet werden kann.", "refresh": "Aktualisieren", - "search_raids": "Suche nach Raids", - "search_eggs": "Suche nach Eier", - "search_quests": "Suche nach Quests", - "search_lures": "Suche nach Lockmodulen", - "search_invasions": "Suche nach Invasionen", + "search_raids": "Raids durchsuchen", + "search_eggs": "Eier durchsuchen", + "search_quests": "Quests durchsuchen", + "search_lures": "Lockmodule durchsuchen", + "search_invasions": "Invasionen durchsuchen", "no_alerts": "Keine Benachrichtigungen gefunden", "points": "Punkte", "day": "Tag", @@ -763,5 +763,30 @@ "enter_translation": "Übersetzung eingeben", "individual_filters": "individuell gefiltert", "best_buddy": "Bester Kumpel", - "data_limit_reached": "Du hast kürzlich zu viele Daten angefordert und befindest dich im Cooldown bis {{until}}" + "data_limit_reached": "Du hast kürzlich zu viele Daten angefordert und befindest dich im Cooldown bis {{until}}", + "unknown_station": "unbekannte Kraftquelle", + "exclude_battle": "Dyna-Raids ausschließen", + "station": "Kraftquelle", + "stations": "Kraftquellen", + "stations_filters": "Dynamax Filter Einstellungen", + "stations_options": "Kraftquellen Optionen", + "all_stations": "alle Kraftquellen", + "search_battles": "Dyna-Raids durchsuchen", + "started": "gestartet", + "ended": "beendet", + "search_stations": "Kraftquellen durchsuchen", + "global_search_stations": "Füge den Namen der Kraftquelle ein...", + "station_timers": "Kraftquellen Timer", + "stations_opacity": "Kraftquellen Durchsichtigkeit", + "max_battles": "Dyna-Raids", + "dynamax": "Dynamax", + "stations_subtitle": "Kraftquellen auf der Map anzeigen", + "dynamax_subtitle": "Dyna-Raids auf der Map anzeigen", + "override": "Dyna-Raid Override", + "station_icons": "Kraftquellen Icons", + "stationed_pokemon": "platzierte Pokémon", + "attack_bonus": "Angriffsbonus", + "no_stationed_pokemon": "Keine Pokémon platziert", + "battle_bonus": "Kampfbonus", + "copy_coordinates": "Koordinaten kopieren" } diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index dd6003ea2..ae5826030 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -786,5 +786,34 @@ "locale_instructions_8": "Wait for the pull request to be reviewed and merged", "enter_translation": "Enter Translation", "individual_filters": "Partially Filtered", - "data_limit_reached": "You have requested too much data recently and are on cooldown until {{until}}" -} + "data_limit_reached": "You have requested too much data recently and are on cooldown until {{until}}", + "unknown_station": "Unknown Power Spot", + "exclude_battle": "Exclude Max Battle", + "station": "Power Spot", + "stations": "Power Spots", + "stations_filters": "Power Spots Filter Settings", + "stations_options": "Power Spot Options", + "all_stations": "All Power Spots", + "search_battles": "Search Max Battles", + "started": "Started", + "ended": "Ended", + "search_stations": "Search Power Spots", + "global_search_stations": "Enter Power Spot Name or Dynamax Pokémon...", + "station_timers": "Power Spot Timers", + "stations_opacity": "Dynamic Power Spot Opacity", + "max_battles": "Max Battles", + "dynamax": "Dynamax", + "stations_subtitle": "Displays Power Spots on the map", + "dynamax_subtitle": "Displays Dynamax Battles on the map", + "override": "Override", + "placed_pokemon": "Placed Pokémon", + "attack_bonus": "Attack Bonus", + "battle_bonus": "Battle Bonus", + "copy_coordinates": "Copy Coordinates", + "enable_station_popup_coords": "Show Power Spot Coords", + "station_icons": "Power Spot Icons", + "dynamic_opacity": "Dynamic Opacity", + "tooltips": "Tooltips", + "markers": "Markers", + "filters": "Filters" +} \ No newline at end of file diff --git a/packages/locales/lib/human/ja.json b/packages/locales/lib/human/ja.json index 8ac6fb2d2..393989dfe 100644 --- a/packages/locales/lib/human/ja.json +++ b/packages/locales/lib/human/ja.json @@ -1,16 +1,32 @@ { + "slider_sta_iv": "HP", "locale_selection": "言語選択", - "invasions": "ロケット団", "total_cp": "合計CP", "selected": "選択済み", "alt_forms": "他のフォルム", "wayfarer_options": "Wayfarerオプション", "raid_timers": "レイドタイマー", - "invasion_timers": "ロケット団タイマー", "lure_timers": "ルアータイマー", "candy": "アメ", + "boosted_types": "ブーストされるタイプ", + "exclude_invasion": "ロケット団を除外", + "global_search_invasions": "したっぱのタイプ、名前、またはポケモンの報酬名を入力...", "invasion": "ロケット団", + "invasions": "ロケット団", + "invasion_audio": "ロケット団の音", + "invasion_icons": "ロケット団アイコン", + "invasion_opacity": "動的ロケット団の不透明度", + "invasion_timers": "すべてのロケット団タイマー", + "loading_invasions": "ロケット団を取得中", + "search_invasions": "ロケット団を探す", + "invasions_subtitle": "ロケット団の情報を表示、バトルラインナップ、失効時間、および可能な報酬を含む", + "only_exclude_grunts": "したっぱを除外する", + "decoy": "おとり", "nest": "巣", + "global_search_nests": "巣のポケモン名を入力...", + "nest_audio": "巣の音", + "nest_icons": "巣アイコン", + "reset_nests": "巣をリセット", "stardust": "ほしのすな", "all_gyms": "全てのジム", "event_stops_subtitle": "カクレオンのような特別なイベントポケストップを表示", diff --git a/packages/locales/lib/index.js b/packages/locales/lib/index.js index 900005dde..32cb828e0 100644 --- a/packages/locales/lib/index.js +++ b/packages/locales/lib/index.js @@ -1,3 +1,5 @@ +// @ts-check + const { create } = require('./create') const { missing } = require('./missing') const { generate } = require('./generate') diff --git a/packages/locales/package.json b/packages/locales/package.json index 8a932ba42..0ab8d6405 100644 --- a/packages/locales/package.json +++ b/packages/locales/package.json @@ -18,11 +18,11 @@ "@rm/config": "*", "@rm/logger": "*", "dotenv": "^16.3.1", - "gpt-tokenizer": "^2.1.2", - "node-fetch": "2.6.7", - "openai": "4.29.0" + "gpt-tokenizer": "2.2.1", + "node-fetch": "2.7.0", + "openai": "4.58.2" }, "devDependencies": { - "@types/node-fetch": "2.6.1" + "@types/node-fetch": "2.6.11" } } diff --git a/packages/masterfile/lib/index.d.ts b/packages/masterfile/lib/index.d.ts index 842591da6..6614cabb1 100644 --- a/packages/masterfile/lib/index.d.ts +++ b/packages/masterfile/lib/index.d.ts @@ -76,8 +76,8 @@ export interface Masterfile { moves: Record invasions: Record weather: Record - teams: mfjson['teams'] - raids: mfjson['raids'] + teams: (typeof mfjson)['teams'] + raids: (typeof mfjson)['raids'] } export declare function generate( diff --git a/packages/masterfile/package.json b/packages/masterfile/package.json index 9c5dfb769..537e9889c 100644 --- a/packages/masterfile/package.json +++ b/packages/masterfile/package.json @@ -17,9 +17,9 @@ "@rm/config": "*", "@rm/logger": "*", "@rm/types": "*", - "node-fetch": "2.6.7" + "node-fetch": "2.7.0" }, "devDependencies": { - "@types/node-fetch": "2.6.1" + "@types/node-fetch": "2.6.11" } } diff --git a/packages/types/lib/augmentations.d.ts b/packages/types/lib/augmentations.d.ts index f775d0f40..43f40a704 100644 --- a/packages/types/lib/augmentations.d.ts +++ b/packages/types/lib/augmentations.d.ts @@ -29,6 +29,12 @@ declare global { } } +declare module 'express-session' { + interface SessionData { + tutorial: boolean + } +} + declare module 'passport-discord' { interface StrategyOptionsWithRequest { prompt?: string | undefined @@ -70,15 +76,6 @@ declare module 'http' { } } -// declare module '@apollo/server' { -// interface GraphQLInProgressResponse { -// __sentry_transaction?: string -// } -// } - -// TODO -// declare module '@mui/material/Button' { -// interface ExtendButtonTypeMap { -// bgcolor?: string -// } -// } +declare module 'ohbem' { + export = Ohbem +} diff --git a/packages/types/lib/client.d.ts b/packages/types/lib/client.d.ts index fcbb7c9f0..96c5c8862 100644 --- a/packages/types/lib/client.d.ts +++ b/packages/types/lib/client.d.ts @@ -1,10 +1,25 @@ import * as React from 'react' -import type { ButtonProps, SxProps, Theme } from '@mui/material' +import type { + ButtonProps, + FormControlProps, + SxProps, + Theme, + BaseSelectProps, +} from '@mui/material' +import type { SelectInputProps } from '@mui/material/Select/SelectInput' import { SystemStyleObject } from '@mui/system' import { UAssets } from '@services/Assets' import { Config } from './config' -import { AdvCategories, Permissions } from '@rm/types' +import { + AdvCategories, + BaseFilter, + Categories, + ObjectPathValue, + Permissions, + PokemonFilter, +} from '@rm/types' +import { UseStorage, UseStoragePaths } from '@store/useStorage' declare global { declare const CONFIG: Config @@ -15,7 +30,7 @@ declare global { } } -export interface CustomI extends React.HTMLProps { +export interface CustomI extends React.HTMLAttributes { size?: ButtonProps['size'] } @@ -25,7 +40,7 @@ export type Theme = 'light' | 'dark' export type TileLayer = { name: string - style: import('@rm/types').Theme + style?: import('@rm/types').Theme attribution?: string url?: string background?: string @@ -86,3 +101,54 @@ export interface FilterObj { } export type ClientFilterObj = Record> + +export type useGetDeepStore = ( + field: T, + defaultValue?: ObjectPathValue, +) => ObjectPathValue + +export type useSetDeepStore = ( + field: T, + value: ObjectPathValue, +) => void + +export type useDeepStore = < + T extends UseStoragePaths, + U extends ObjectPathValue, + V extends U | ((prevValue: U) => U) | (U extends object ? keyof U : never), +>( + field: T, + defaultValue?: ObjectPathValue, +) => [ + U, + (arg1: V, ...rest: V extends keyof U ? [arg2: U[V]] : [arg2?: never]) => void, +] + +export interface FCSelectProps extends BaseSelectProps { + fcSx?: SxProps + setWidth?: (width: number) => void +} + +export interface FCSelectListItemProps + extends FCSelectProps { + icon?: React.ReactElement +} + +export type ClientFilters = T extends 'pokemon' + ? ClassToObjectType + : ClassToObjectType + +type ClassToObjectType = Partial< + Omit< + { + [K in keyof T]: T[K] + }, + keyof T extends infer K + ? K extends keyof T + ? T[K] extends Function + ? K + : never + : never + : never + > +> diff --git a/packages/types/lib/config.d.ts b/packages/types/lib/config.d.ts index be7d9bb3f..a6dce9935 100644 --- a/packages/types/lib/config.d.ts +++ b/packages/types/lib/config.d.ts @@ -17,6 +17,7 @@ import { ObjectPathValue, } from './utility' import { Strategy } from './general' +import { TileLayer } from './client' type BaseConfig = typeof config type ExampleConfig = typeof example @@ -40,10 +41,16 @@ export type Config = DeepMerge< } : never webhooks: Webhook[] + tileServers: TileLayer[] devOptions: { logLevel: LogLevelNames skipUpdateCheck?: boolean } + defaultFilters: { + s2cells: { + cells: number[] + } + } areas: ConfigAreas authentication: { areaRestrictions: { roles: string[]; areas: string[] }[] @@ -53,7 +60,7 @@ export type Config = DeepMerge< excludeFromTutorial: string[] alwaysEnabledPerms: string[] aliases: { role: string; name: string }[] - methods: string[] + methods: Strategy[] strategies: { type: Strategy trialPeriod: { @@ -65,6 +72,12 @@ export type Config = DeepMerge< blockedGuilds: string[] allowedUsers: string[] }[] + perms: { + [K in keyof BaseConfig['authentication']['perms']]: Omit< + BaseConfig['authentication']['perms'][K], + 'roles' + > & { roles: string[] } + } } api: { pvp: { diff --git a/packages/types/lib/general.d.ts b/packages/types/lib/general.d.ts index 4b7d9e2e3..018db69d8 100644 --- a/packages/types/lib/general.d.ts +++ b/packages/types/lib/general.d.ts @@ -1,5 +1,5 @@ import type { SliderProps } from '@mui/material' -import type { Feature, Polygon, MultiPolygon } from '@turf/helpers' +import type { Feature, Polygon, MultiPolygon } from 'geojson' import { Config } from './config' export type HttpMethod = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE' @@ -75,6 +75,7 @@ export type UiconImage = `${string}.${ImageExt}` export interface UICONS { device: UiconImage[] gym: UiconImage[] + station: UiconImage[] invasion: UiconImage[] misc: UiconImage[] nest: UiconImage[] diff --git a/packages/types/lib/models.d.ts b/packages/types/lib/models.d.ts index 3185af533..5e56ac527 100644 --- a/packages/types/lib/models.d.ts +++ b/packages/types/lib/models.d.ts @@ -29,6 +29,7 @@ export interface User { strategy?: Strategy data?: string | object selectedWebhook?: string + tutorial?: boolean } export type FullUser = FullModel diff --git a/packages/types/lib/poracle.d.ts b/packages/types/lib/poracle.d.ts index c76ecc64a..86c78bf89 100644 --- a/packages/types/lib/poracle.d.ts +++ b/packages/types/lib/poracle.d.ts @@ -291,14 +291,16 @@ export interface PoracleAPIRef { } export type PoracleUI = ReturnType< - import('server/src/services/Poracle')['generateUi'] + import('server/src/services/Poracle').PoracleAPI['generateUi'] > export type PoracleDefault> = PoracleUI[T]['defaults'] export type PoracleClientContext = Omit< - ReturnType, + ReturnType< + import('server/src/services/Poracle').PoracleAPI['getClientContext'] + >, 'ui' > & { ui: PoracleUI diff --git a/packages/types/lib/scanner.d.ts b/packages/types/lib/scanner.d.ts index e5ff3533a..b07ef6df1 100644 --- a/packages/types/lib/scanner.d.ts +++ b/packages/types/lib/scanner.d.ts @@ -9,6 +9,8 @@ import ScanCellModel = require('server/src/models/ScanCell') import SpawnpointModel = require('server/src/models/Spawnpoint') import WeatherModel = require('server/src/models/Weather') import RouteModel = require('server/src/models/Route') +import StationModel = require('server/src/models/Station') + import { S2Polygon } from './general' export type Gender = 0 | 1 | 2 | 3 @@ -25,7 +27,7 @@ export interface Device { radius: number } -export type FullDevice = FullModel +export type FullDevice = FullModel export interface PokemonDisplay { form: number @@ -33,6 +35,7 @@ export interface PokemonDisplay { gender: number shiny: boolean temp_evolution: number + temp_evolution_finish_ms?: number alignment: number badge: number location_card: number @@ -79,7 +82,7 @@ export interface Gym { enabled: boolean } -export type FullGym = FullModel +export type FullGym = FullModel export interface Nest { id: number @@ -98,7 +101,7 @@ export interface Nest { submitted_by: string } -export type FullNest = FullModel +export type FullNest = FullModel export interface Quest { quest_type: number @@ -141,12 +144,9 @@ export interface Invasion { slot_3_form: number } -export interface ShowcaseEntry { +export interface ShowcaseEntry extends PokemonDisplay { rank: number pokemon_id: number - form: number - costume: number - gender: Gender score: number } @@ -194,22 +194,9 @@ export interface Pokestop { hasShowcase: boolean } -export type FullPokestop = FullModel - -export interface PvpEntry { - pokemon: number - form: number - cap: number - value: number - level: number - cp: number - percentage: number - rank: number - capped: boolean - evolution: number -} +export type FullPokestop = FullModel -export type CleanPvp = Record +export type CleanPvp = Record export interface Pokemon { id: string @@ -247,13 +234,13 @@ export interface Pokemon { expire_timestamp_verified: boolean updated: number pvp: CleanPvp - pvp_rankings_great_league?: PvpEntry[] - pvp_rankings_ultra_league?: PvpEntry[] + pvp_rankings_great_league?: import('ohbem').PvPRankEntry[] + pvp_rankings_ultra_league?: import('ohbem').PvPRankEntry[] distance?: number shiny?: boolean } -export type FullPokemon = FullModel +export type FullPokemon = FullModel export interface Portal { id: string @@ -266,7 +253,7 @@ export interface Portal { updated: number } -export type FullPortal = FullModel +export type FullPortal = FullModel export interface ScanCell { id?: string @@ -277,7 +264,7 @@ export interface ScanCell { polygon?: S2Polygon } -export type FullScanCell = FullModel +export type FullScanCell = FullModel export interface Spawnpoint { id: string @@ -287,7 +274,7 @@ export interface Spawnpoint { despawn_sec: number } -export type FullSpawnpoint = FullModel +export type FullSpawnpoint = FullModel export interface Weather { id: string @@ -308,7 +295,7 @@ export interface Weather { polygon: S2Polygon } -export type FullWeather = FullModel +export type FullWeather = FullModel export interface ScannerApi { status: string @@ -345,4 +332,46 @@ export interface Route { waypoints: Waypoint[] } -export type FullRoute = FullModel +export type FullRoute = FullModel + +export interface StationPokemon { + pokemon_id: number + form: number + costume: number + gender: number + bread_mode: number +} + +export interface Station { + id: string + lat: number + lon: number + name: string + // cell_id: BIGINT + start_time: number + end_time: number + cooldown_complete: number + is_battle_available: boolean + is_inactive: boolean + + battle_level: number + battle_start?: number + battle_end?: number + + battle_pokemon_id: number + battle_pokemon_form: number + battle_pokemon_costume: number + battle_pokemon_gender: Gender + battle_pokemon_alignment: number + battle_pokemon_bread_mode: number + battle_pokemon_move_1: number + battle_pokemon_move_2: number + + total_stationed_pokemon: number + stationed_pokemon: Parsed extends true + ? StationPokemon[] + : string | StationPokemon[] + updated: number +} + +export type FullStation = FullModel diff --git a/packages/types/lib/server.d.ts b/packages/types/lib/server.d.ts index 3b9fbc990..6ee4cdc94 100644 --- a/packages/types/lib/server.d.ts +++ b/packages/types/lib/server.d.ts @@ -4,21 +4,21 @@ import type { RmModels, RmModelKeys, ModelKeys, + Station, + Backup, + Nest, + NestSubmission, + Pokestop, + Gym, + Pokemon, } from 'server/src/models' import { Knex } from 'knex' import { Model } from 'objection' import { NextFunction, Request, Response } from 'express' import { VerifyCallback } from 'passport-oauth2' -import DbCheck = require('server/src/services/DbCheck') -import EventManager = require('server/src/services/EventManager') -import Pokemon = require('server/src/models/Pokemon') -import Gym = require('server/src/models/Gym') -import Badge = require('server/src/models/Badge') -import Backup = require('server/src/models/Backup') -import Nest = require('server/src/models/Nest') -import NestSubmission = require('server/src/models/NestSubmission') -import Pokestop = require('server/src/models/Pokestop') +import type { DbManager } from 'server/src/services/DbManager' +import type { EventManager } from 'server/src/services/EventManager' import { ModelReturn, OnlyType } from './utility' import { Profile } from 'passport-discord' import { User } from './models' @@ -44,7 +44,7 @@ export interface DbContext { hasAlignment: boolean hasShowcaseData: boolean hasShowcaseForm: boolean - hasShowcaseTypes: boolean + hasShowcaseType: boolean } export interface ExpressUser extends User { @@ -66,6 +66,7 @@ export interface Available { gyms: ModelReturn pokestops: ModelReturn nests: ModelReturn + stations: ModelReturn } export interface ApiEndpoint { @@ -86,7 +87,7 @@ export interface DbConnection { export type Schema = ApiEndpoint | DbConnection -export interface DbCheckClass { +export interface DbManagerClass { models: { [key in ScannerModelKeys]?: (DbContext & { connection: number @@ -133,7 +134,7 @@ export interface GqlContext { userId: number req: Request res: Response - Db: DbCheck + Db: DbManager Event: EventManager perms: Permissions username: string @@ -141,34 +142,9 @@ export interface GqlContext { startTime?: number } -export interface Permissions { - map: boolean - pokemon: boolean - iv: boolean - pvp: boolean - gyms: boolean - raids: boolean - pokestops: boolean - eventStops: boolean - quests: boolean - lures: boolean - portals: boolean - submissionCells: boolean - invasions: boolean - nests: boolean - nestSubmissions: boolean - scanAreas: boolean - weather: boolean - spawnpoints: boolean - s2cells: boolean - scanCells: boolean - devices: boolean - donor: boolean - gymBadges: boolean - backups: boolean - routes: boolean - blocked: boolean - admin: boolean +type BasePerms = { [K in keyof Config['authentication']['perms']]: boolean } + +export interface Permissions extends BasePerms { blockedGuildNames: string[] scanner: string[] areaRestrictions: string[] @@ -176,36 +152,6 @@ export interface Permissions { trial: boolean } -export interface Waypoint { - lat_degrees: number - lng_degrees: number - elevation_in_meters: number -} - -export interface Route { - id: string - name: string - description: string - distance_meters: number - duration_seconds: number - start_fort_id: string - start_lat: number - start_lon: number - start_image: string - end_fort_id: string - end_lat: number - end_lon: number - end_image: string - image: string - image_border_color: string - reversible: boolean - tags?: string[] - type: number - updated: number - version: number - waypoints: Waypoint[] -} - export interface FilterId { id: number form?: number @@ -216,7 +162,7 @@ export interface DnfMinMax { } export interface DnfFilter { - pokemon?: FilterId + pokemon?: FilterId | FilterId[] iv?: DnfMinMax level?: DnfMinMax cp?: DnfMinMax @@ -238,19 +184,27 @@ export type DiscordVerifyFunction = ( done: VerifyCallback, ) => void -export type BaseFilter = import('server/src/filters/Base') +export type BaseFilter = import('server/src/filters/Base').BaseFilter -export type PokemonFilter = import('server/src/filters/pokemon/Frontend') +export type PokemonFilter = + import('server/src/filters/pokemon/Frontend').PokemonFilter export type AllFilters = ReturnType< - typeof import('server/src/filters/builder/base') + (typeof import('server/src/filters/builder/base'))['buildDefaultFilters'] > export type Categories = keyof AllFilters -export type AdvCategories = 'pokemon' | 'gyms' | 'pokestops' | 'nests' +export type AdvCategories = + | 'pokemon' + | 'gyms' + | 'pokestops' + | 'nests' + | 'stations' -export type UIObject = ReturnType +export type UIObject = ReturnType< + (typeof import('server/src/ui/drawer'))['drawer'] +> export interface PokemonGlow extends Partial> { diff --git a/packages/types/lib/utility.d.ts b/packages/types/lib/utility.d.ts index bc8277df8..d3b18f7b8 100644 --- a/packages/types/lib/utility.d.ts +++ b/packages/types/lib/utility.d.ts @@ -80,67 +80,27 @@ export type ComparisonReport = } : boolean -export type DeepKeys = { - [K in keyof T]-?: K extends string - ? P extends '' - ? `${K}` | `${K}.${DeepKeys}` - : `${P}.${K}.${DeepKeys}` - : never -}[keyof T] - -export type PathValue = P extends `${infer K}.${infer Rest}` - ? K extends keyof T - ? Rest extends DeepKeys - ? PathValue - : never - : never - : P extends keyof T - ? T[P] - : never - -export type ObjectPathValue> = PathValue< - T, - P -> - export type Join = K extends string | number ? P extends string | number ? `${K}${'' extends P ? '' : '.'}${P}` : never : never -export type Prev = [ - never, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - ...0[], -] - -export type Paths = [D] extends [never] - ? never - : T extends object - ? { - [K in keyof T]-?: K extends string | number - ? `${K}` | Join> - : never - }[keyof T] - : '' +export type Paths = T extends object + ? { + [K in keyof T]-?: K extends string | number + ? `${K}` | Join> + : never + }[keyof T] + : '' + +export type ObjectPathValue< + O, + P extends string, +> = P extends `${infer K}.${infer Rest}` + ? K extends keyof O + ? ObjectPathValue + : never + : P extends keyof O + ? O[P] + : never diff --git a/packages/types/package.json b/packages/types/package.json index 6612770a1..615584f5b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -12,19 +12,19 @@ "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"" }, "devDependencies": { - "@apollo/client": "^3.7.15", - "@mui/material": "^5.14.0", - "@mui/system": "^5.15.2", - "@sentry/node": "^7.48.0", - "@types/config": "^3.3.0", - "@types/node": "^20.5.1", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.0.9", - "@turf/helpers": "^6.5.0", + "@apollo/client": "3.11.4", + "@mui/material": "5.16.7", + "@mui/system": "5.16.7", + "@sentry/node": "^7.65.0", + "@types/config": "^3.3.4", + "@types/node": "^18", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "express": "^4.19.2", - "knex": "^2.4.2", + "geojson": "0.5.0", + "knex": "3.1.0", "loglevel": "^1.8.1", - "objection": "^3.0.1", + "objection": "3.1.4", "passport-discord": "https://github.com/tonestrike/passport-discord.git", "passport-oauth2": "^1.7.0" } diff --git a/packages/vite-plugins/lib/favicon.js b/packages/vite-plugins/lib/favicon.js index 37fe21d13..53fb5513c 100644 --- a/packages/vite-plugins/lib/favicon.js +++ b/packages/vite-plugins/lib/favicon.js @@ -1,5 +1,5 @@ // @ts-check -const { resolve } = require('path') +const path = require('path') const fs = require('fs') const { log, TAGS } = require('@rm/logger') @@ -9,15 +9,24 @@ const { log, TAGS } = require('@rm/logger') * @returns {import('vite').Plugin} */ const faviconPlugin = (isDevelopment) => { - const basePath = resolve(__dirname, '../../../public/favicon') - const fallback = resolve(basePath, `fallback.ico`) + const basePath = path.join(__dirname, '../../../public/favicon') + const markerPath = path.join( + __dirname, + '../../../node_modules/leaflet/dist/images/marker-icon.png', + ) + const fallback = path.join(basePath, `fallback.ico`) const custom = process.env.NODE_CONFIG_ENV - ? resolve(basePath, `${process.env.NODE_CONFIG_ENV}.ico`) - : resolve(basePath, `favicon.ico`) + ? path.join(basePath, `${process.env.NODE_CONFIG_ENV}.ico`) + : path.join(basePath, `favicon.ico`) const favicon = fs.existsSync(custom) ? custom : fallback return { name: 'vite-plugin-favicon', generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'images/fallback-marker.png', + source: fs.readFileSync(markerPath), + }) if (isDevelopment) return try { this.emitFile({ @@ -36,6 +45,11 @@ const faviconPlugin = (isDevelopment) => { res.end(fs.readFileSync(favicon)) return } + if (req.url === '/images/fallback-marker.png') { + res.writeHead(200, { 'Content-Type': 'image/png' }) + res.end(fs.readFileSync(markerPath)) + return + } next() }) }, diff --git a/packages/vite-plugins/lib/index.js b/packages/vite-plugins/lib/index.js index 70b29276b..e3a3de15b 100644 --- a/packages/vite-plugins/lib/index.js +++ b/packages/vite-plugins/lib/index.js @@ -1,3 +1,5 @@ +// @ts-check + const { localePlugin } = require('./locale') const { faviconPlugin } = require('./favicon') const { muteWarningsPlugin } = require('./muteWarnings') diff --git a/public/images/perms/dynamax.png b/public/images/perms/dynamax.png new file mode 100644 index 000000000..37f376ead Binary files /dev/null and b/public/images/perms/dynamax.png differ diff --git a/public/images/perms/stations.png b/public/images/perms/stations.png new file mode 100644 index 000000000..423c21b77 Binary files /dev/null and b/public/images/perms/stations.png differ diff --git a/server/src/filters/Base.js b/server/src/filters/Base.js index 4289cebdd..8c109c6bd 100644 --- a/server/src/filters/Base.js +++ b/server/src/filters/Base.js @@ -14,4 +14,4 @@ class BaseFilter { } } -module.exports = BaseFilter +module.exports = { BaseFilter } diff --git a/server/src/filters/builder/base.js b/server/src/filters/builder/base.js index 6eb9f973c..61585baaa 100644 --- a/server/src/filters/builder/base.js +++ b/server/src/filters/builder/base.js @@ -1,12 +1,12 @@ // @ts-check const config = require('@rm/config') -const state = require('../../services/state') -const buildPokemon = require('./pokemon') -const buildPokestops = require('./pokestop') -const buildGyms = require('./gym') -const BaseFilter = require('../Base') -const PokemonFilter = require('../pokemon/Frontend') +const { state } = require('../../services/state') +const { buildPokemon } = require('./pokemon') +const { buildPokestops } = require('./pokestop') +const { buildGyms } = require('./gym') +const { BaseFilter } = require('../Base') +const { PokemonFilter } = require('../pokemon/Frontend') /** * @param {import("@rm/types").Permissions} perms @@ -35,6 +35,7 @@ function buildDefaultFilters(perms) { perms.pokestops || perms.lures || perms.quests || perms.invasions const gymReducer = perms.gyms || perms.raids const pokemonReducer = perms.iv || perms.pvp + const stationReducer = perms.stations || perms.dynamax const pokemon = buildPokemon(defaultFilters, base, custom) return { @@ -110,6 +111,23 @@ function buildDefaultFilters(perms) { }, } : undefined, + stations: + stationReducer && state.db.models.Station + ? { + enabled: defaultFilters.stations.enabled, + allStations: perms.stations + ? defaultFilters.stations.enabled + : undefined, + standard: new BaseFilter(), + battleTier: perms.dynamax + ? defaultFilters.stations.battleTier + : undefined, + maxBattles: perms.stations + ? defaultFilters.stations.battles + : undefined, + filter: pokemon.stations, + } + : undefined, pokemon: perms.pokemon && state.db.models.Pokemon ? { @@ -230,4 +248,4 @@ function buildDefaultFilters(perms) { } } -module.exports = buildDefaultFilters +module.exports = { buildDefaultFilters } diff --git a/server/src/filters/builder/gym.js b/server/src/filters/builder/gym.js index b10522af0..1ad0fd7d5 100644 --- a/server/src/filters/builder/gym.js +++ b/server/src/filters/builder/gym.js @@ -1,6 +1,6 @@ // @ts-check -const state = require('../../services/state') -const BaseFilter = require('../Base') +const { state } = require('../../services/state') +const { BaseFilter } = require('../Base') /** * @@ -44,4 +44,4 @@ function buildGyms(perms, defaults) { return gymFilters } -module.exports = buildGyms +module.exports = { buildGyms } diff --git a/server/src/filters/builder/pokemon.js b/server/src/filters/builder/pokemon.js index 71ff21142..b2436c899 100644 --- a/server/src/filters/builder/pokemon.js +++ b/server/src/filters/builder/pokemon.js @@ -1,11 +1,11 @@ // @ts-check -const state = require('../../services/state') -const BaseFilter = require('../Base') +const { state } = require('../../services/state') +const { BaseFilter } = require('../Base') /** * * @param {import("@rm/types").Config['defaultFilters']} defaults - * @param {import('../pokemon/Frontend')} base + * @param {import('../pokemon/Frontend').PokemonFilter} base * @param {import('@rm/types').PokemonFilter} custom * @returns {{ * full: { [key: string]: import('@rm/types').PokemonFilter }, @@ -13,12 +13,14 @@ const BaseFilter = require('../Base') * quests: { [key: string]: BaseFilter }, * nests: { [key: string]: BaseFilter }, * rocket: { [key: string]: BaseFilter }, + * stations: { [key: string]: BaseFilter }, * }} */ function buildPokemon(defaults, base, custom) { const pokemon = { full: { global: custom }, raids: { global: new BaseFilter() }, + stations: { global: new BaseFilter() }, quests: { global: new BaseFilter() }, nests: { global: new BaseFilter() }, rocket: { global: new BaseFilter() }, @@ -35,6 +37,9 @@ function buildPokemon(defaults, base, custom) { Object.keys(pkmn.forms).forEach((form) => { pokemon.full[`${id}-${form}`] = base pokemon.raids[`${id}-${form}`] = new BaseFilter(defaults.gyms.pokemon) + pokemon.stations[`${id}-${form}`] = new BaseFilter( + defaults.stations.pokemon, + ) pokemon.quests[`${id}-${form}`] = new BaseFilter( defaults.pokestops.pokemon, ) @@ -66,4 +71,4 @@ function buildPokemon(defaults, base, custom) { return pokemon } -module.exports = buildPokemon +module.exports = { buildPokemon } diff --git a/server/src/filters/builder/pokestop.js b/server/src/filters/builder/pokestop.js index 103805d53..b4b1ff870 100644 --- a/server/src/filters/builder/pokestop.js +++ b/server/src/filters/builder/pokestop.js @@ -1,8 +1,8 @@ // @ts-check const { log, TAGS } = require('@rm/logger') -const BaseFilter = require('../Base') -const state = require('../../services/state') +const { state } = require('../../services/state') +const { BaseFilter } = require('../Base') /** * @@ -110,4 +110,4 @@ function buildPokestops(perms, defaults) { return quests } -module.exports = buildPokestops +module.exports = { buildPokestops } diff --git a/server/src/filters/pokemon/Backend.js b/server/src/filters/pokemon/Backend.js index 952b8418e..021a98141 100644 --- a/server/src/filters/pokemon/Backend.js +++ b/server/src/filters/pokemon/Backend.js @@ -1,3 +1,5 @@ +// @ts-check + /* eslint-disable no-unused-vars */ const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') @@ -11,14 +13,14 @@ const { dnfifyIvFilter, } = require('./functions') const { filterRTree } = require('../../utils/filterRTree') -const state = require('../../services/state') -const PokemonFilter = require('./Frontend') +const { PokemonFilter } = require('./Frontend') +const { state } = require('../../services/state') -module.exports = class PkmnBackend { +class PkmnBackend { /** * @param {`${number}-${number}` | 'global'} id - * @param {import("./Frontend")} filter - * @param {import("./Frontend")} global + * @param {import("./Frontend").PokemonFilter} filter + * @param {import("./Frontend").PokemonFilter} global * @param {object} perms * @param {boolean} perms.pokemon * @param {boolean} perms.iv @@ -160,7 +162,7 @@ module.exports = class PkmnBackend { } /** - * @param {import("./Frontend")} filter + * @param {import("./Frontend").PokemonFilter} filter * @returns {Set} */ getRelevantKeys(filter = this.filter) { @@ -194,7 +196,7 @@ module.exports = class PkmnBackend { } /** - * @param {import("@rm/types").PvpEntry} entry + * @param {import("ohbem").PvPRankEntry} entry * @param {string} league * @returns {boolean} */ @@ -224,8 +226,8 @@ module.exports = class PkmnBackend { /** * @param {string} league - * @param {import("@rm/types").PvpEntry[]} data - * @returns {{ best: number; filtered: import("@rm/types").PvpEntry[]}} + * @param {import("ohbem").PvPRankEntry[]} data + * @returns {{ best: number; filtered: import("ohbem").PvPRankEntry[]}} */ getRanks(league, data) { const filtered = @@ -243,7 +245,7 @@ module.exports = class PkmnBackend { /** * - * @param {[number, number]} filter + * @param {number[]} filter * @param {number} [limit] * @returns DnfMinMax */ @@ -474,3 +476,5 @@ module.exports = class PkmnBackend { return result } } + +module.exports = { PkmnBackend } diff --git a/server/src/filters/pokemon/Frontend.js b/server/src/filters/pokemon/Frontend.js index 16c5007a4..2b5ebbe41 100644 --- a/server/src/filters/pokemon/Frontend.js +++ b/server/src/filters/pokemon/Frontend.js @@ -1,6 +1,6 @@ // @ts-check const config = require('@rm/config') -const BaseFilter = require('../Base') +const { BaseFilter } = require('../Base') class PokemonFilter extends BaseFilter { /** @@ -53,4 +53,4 @@ class PokemonFilter extends BaseFilter { } } -module.exports = PokemonFilter +module.exports = { PokemonFilter } diff --git a/server/src/filters/pokemon/functions.js b/server/src/filters/pokemon/functions.js index cfdb19de9..1b56d44c0 100644 --- a/server/src/filters/pokemon/functions.js +++ b/server/src/filters/pokemon/functions.js @@ -11,7 +11,7 @@ const { log, TAGS } = require('@rm/logger') /** * @param {object} pokemon - * @returns {Record} + * @returns {Record} */ function getParsedPvp(pokemon) { if (pokemon.pvp) @@ -53,11 +53,10 @@ function deepCompare(incoming, reference) { /** * @param {number} value - * @param {number} min - * @param {number} max + * @param {...number} args */ -function between(value, min, max) { - return value >= min && value <= max +function between(value, ...args) { + return value >= args[0] && value <= args[1] } /** @@ -203,7 +202,7 @@ function jsifyIvFilter(filter) { /** * * @param {string} filter - * @param {import('@rm/types').FilterId} pokemon + * @param {import('@rm/types').FilterId[]} pokemon * @returns */ function dnfifyIvFilter(filter, pokemon) { diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index 9b17f60cc..8185a56cd 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -1,3 +1,5 @@ +// @ts-check + const fs = require('fs') const { resolve } = require('path') const { GraphQLJSON } = require('graphql-type-json') @@ -6,15 +8,15 @@ const { S2LatLng, S2RegionCoverer, S2LatLngRect } = require('nodes2ts') const config = require('@rm/config') const { missing, readAndParseJson } = require('@rm/locales') -const buildDefaultFilters = require('../filters/builder/base') -const filterComponents = require('../utils/filterComponents') -const validateSelectedWebhook = require('../utils/validateSelectedWebhook') -const PoracleAPI = require('../services/Poracle') +const { buildDefaultFilters } = require('../filters/builder/base') +const { filterComponents } = require('../utils/filterComponents') +const { validateSelectedWebhook } = require('../utils/validateSelectedWebhook') +const { PoracleAPI } = require('../services/Poracle') const { geocoder } = require('../services/geocoder') -const scannerApi = require('../services/scannerApi') -const getPolyVector = require('../utils/getPolyVector') -const getPlacementCells = require('../utils/getPlacementCells') -const getTypeCells = require('../utils/getTypeCells') +const { scannerApi } = require('../services/scannerApi') +const { getPolyVector } = require('../utils/getPolyVector') +const { getPlacementCells } = require('../utils/getPlacementCells') +const { getTypeCells } = require('../utils/getTypeCells') const { getValidCoords } = require('../utils/getValidCoords') /** @type {import("@apollo/server").ApolloServerOptions['resolvers']} */ @@ -79,6 +81,8 @@ const resolvers = { } return perms?.pokestops }), + availableStations: (_, _args, { Event, perms }) => + perms?.dynamax ? Event.available.stations : [], backup: (_, args, { req, perms, Db }) => { if (perms?.backups && req?.user?.id) { return Db.models.Backup.getOne(args.id, req?.user?.id) @@ -107,7 +111,7 @@ const resolvers = { ) return !!results.length }, - /** @param {unknown} _ @param {{ mode: 'scanNext' | 'scanZone' }} args */ + /** @param {unknown} _ @param {{ mode: 'scanNext' | 'scanZone', points: [number, number][] }} args */ checkValidScan: (_, { mode, points }, { perms }) => getValidCoords(mode, points, perms), /** @param {unknown} _ @param {{ component: 'loginPage' | 'donationPage' | 'messageOfTheDay' }} args */ @@ -436,6 +440,8 @@ const resolvers = { return perms.portals ? Db.search('Portal', perms, args) : [] case 'nests': return perms.nests ? Db.search('Nest', perms, args) : [] + case 'stations': + return perms.stations ? Db.search('Station', perms, args) : [] default: return [] } @@ -480,6 +486,18 @@ const resolvers = { } return [] }, + stations: (_, args, { perms, Db }) => { + if (perms?.stations || perms?.dynamax) { + return Db.query('Station', 'getAll', perms, args) + } + return [] + }, + stationPokemon: (_, { id }, { perms, Db }) => { + if (perms?.stations) { + return Db.query('Station', 'getDynamaxMons', id) + } + return [] + }, submissionCells: async (_, args, { req, perms, Db }) => { const { submissionZoom } = config.getMapConfig(req).general if (perms?.submissionCells && args.zoom >= submissionZoom - 1) { @@ -778,23 +796,7 @@ const resolvers = { }, setGymBadge: async (_, args, { req, Db, perms }) => { if (perms?.gymBadges && req?.user?.id) { - if ( - await Db.models.Badge.query() - .where('gymId', args.gymId) - .andWhere('userId', req.user.id) - .first() - ) { - await Db.models.Badge.query() - .where('gymId', args.gymId) - .andWhere('userId', req.user.id) - .update({ badge: args.badge }) - } else { - await Db.models.Badge.query().insert({ - badge: args.badge, - gymId: args.gymId, - userId: req.user.id, - }) - } + await Db.models.Badge.insert(args.badge, args.gymId, req.user.id) return true } return false @@ -802,4 +804,4 @@ const resolvers = { }, } -module.exports = resolvers +module.exports = { resolvers } diff --git a/server/src/graphql/server.js b/server/src/graphql/server.js index dab2175fa..af1d0bb66 100644 --- a/server/src/graphql/server.js +++ b/server/src/graphql/server.js @@ -15,8 +15,8 @@ const { const config = require('@rm/config') const { Logger, TAGS, log } = require('@rm/logger') -const resolvers = require('./resolvers') -const state = require('../services/state') +const { resolvers } = require('./resolvers') +const { state } = require('../services/state') /** @param {import('http').Server} httpServer */ async function startApollo(httpServer) { @@ -167,4 +167,4 @@ async function startApollo(httpServer) { return apolloServer } -module.exports = startApollo +module.exports = { startApollo } diff --git a/server/src/graphql/typeDefs/index.graphql b/server/src/graphql/typeDefs/index.graphql index 6afeb5b2a..eff77b33b 100644 --- a/server/src/graphql/typeDefs/index.graphql +++ b/server/src/graphql/typeDefs/index.graphql @@ -6,6 +6,7 @@ type Query { availablePokestops: [String] availableGyms: [String] availableNests: [String] + availableStations: [String] badges: [Badge] backup(id: ID): Backup backups: [Backup] @@ -57,6 +58,14 @@ type Query { filters: JSON ): [Portal] portalsSingle(id: ID, perm: String): Portal + stations( + minLat: Float + maxLat: Float + minLon: Float + maxLon: Float + filters: JSON + ): [Station] + stationPokemon(id: ID): [StationPokemon] s2cells( minLat: Float maxLat: Float diff --git a/server/src/graphql/typeDefs/map.graphql b/server/src/graphql/typeDefs/map.graphql index 5840c517e..7468a6d78 100644 --- a/server/src/graphql/typeDefs/map.graphql +++ b/server/src/graphql/typeDefs/map.graphql @@ -70,6 +70,13 @@ type Search { costume: Int shiny: Int iv: Float + battle_pokemon_id: Int + battle_pokemon_form: Int + battle_pokemon_gender: Int + battle_pokemon_costume: Int + # battle_pokemon_evolution: Int + battle_pokemon_alignment: Int + battle_pokemon_bread_mode: Int } type SearchLure { diff --git a/server/src/graphql/typeDefs/scanner.graphql b/server/src/graphql/typeDefs/scanner.graphql index bb9a8ecc9..477b9765c 100644 --- a/server/src/graphql/typeDefs/scanner.graphql +++ b/server/src/graphql/typeDefs/scanner.graphql @@ -273,3 +273,36 @@ type Route { version: Int waypoints: [Waypoint] } + +type Station { + id: ID + name: String + lat: Float + lon: Float + updated: Int + start_time: Int + end_time: Int + cooldown_complete: Int + total_stationed_pokemon: Int + is_battle_available: Boolean + is_inactive: Boolean + battle_level: Int + battle_start: Int + battle_end: Int + battle_pokemon_id: Int + battle_pokemon_form: Int + battle_pokemon_costume: Int + battle_pokemon_gender: Int + battle_pokemon_alignment: Int + battle_pokemon_bread_mode: Int + battle_pokemon_move_1: Int + battle_pokemon_move_2: Int +} + +type StationPokemon { + pokemon_id: Int + form: Int + costume: Int + gender: Int + bread_mode: Int +} diff --git a/server/src/index.js b/server/src/index.js index d9ca40c57..857103fb3 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -12,12 +12,11 @@ const { rainbow } = require('chalkercli') const cors = require('cors') const { json } = require('body-parser') const http = require('http') -const { default: helmet } = require('helmet') const { log, TAGS, Logger } = require('@rm/logger') const config = require('@rm/config') -const state = require('./services/state') +const { state } = require('./services/state') const { starti18n } = require('./services/i18n') const { checkForUpdates } = require('./services/checkForUpdates') const { loadLatestAreas, loadCachedAreas } = require('./services/areas') @@ -31,11 +30,12 @@ const { initPassport } = require('./middleware/passport') const { errorMiddleware } = require('./middleware/error') const { sessionMiddleware } = require('./middleware/session') const { apolloMiddleware } = require('./middleware/apollo') +const { helmetMiddleware } = require('./middleware/helmet') -const startApollo = require('./graphql/server') +const { startApollo } = require('./graphql/server') const { bindConnections } = require('./models') const { migrate } = require('./db/migrate') -const rootRouter = require('./routes/rootRouter') +const { rootRouter } = require('./routes/rootRouter') const startServer = async () => { if (!config.getSafe('devOptions.skipUpdateCheck')) { @@ -77,23 +77,7 @@ const startServer = async () => { ) if (config.getSafe('api.enableHelmet')) { - app.use( - helmet({ - hidePoweredBy: true, - contentSecurityPolicy: { - directives: { - scriptSrc: [ - "'self'", - 'https://cdn.jsdelivr.net', - 'https://telegram.org', - 'https://static.cloudflareinsights.com', - ], - frameSrc: ["'self'", 'https://*.telegram.org'], - workerSrc: ["'self'", 'blob:'], - }, - }, - }), - ) + app.use(helmetMiddleware()) } initPassport(app) diff --git a/server/src/middleware/apollo.js b/server/src/middleware/apollo.js index 47399cf75..5e9ac9431 100644 --- a/server/src/middleware/apollo.js +++ b/server/src/middleware/apollo.js @@ -4,13 +4,13 @@ const { ApolloServerErrorCode } = require('@apollo/server/errors') const { GraphQLError } = require('graphql') const { parse } = require('graphql') -const state = require('../services/state') -const pkg = require('../../../package.json') +const { state } = require('../services/state') +const { version } = require('../../../package.json') const { DataLimitCheck } = require('../services/DataLimitCheck') /** * - * @param {Awaited>} server + * @param {Awaited>} server * @returns */ function apolloMiddleware(server) { @@ -23,9 +23,9 @@ function apolloMiddleware(server) { const clientVHeader = req.headers['apollographql-client-version'] const clientV = (typeof clientVHeader === 'string' && clientVHeader.trim()) || - pkg.version || + version || 1 - const serverV = pkg.version || 1 + const serverV = version || 1 const definition = /** @type {import('graphql').OperationDefinitionNode} */ ( diff --git a/server/src/middleware/helmet.js b/server/src/middleware/helmet.js new file mode 100644 index 000000000..2601c46f8 --- /dev/null +++ b/server/src/middleware/helmet.js @@ -0,0 +1,21 @@ +const { default: helmet } = require('helmet') + +function helmetMiddleware() { + return helmet({ + hidePoweredBy: true, + contentSecurityPolicy: { + directives: { + scriptSrc: [ + "'self'", + 'https://cdn.jsdelivr.net', + 'https://telegram.org', + 'https://static.cloudflareinsights.com', + ], + frameSrc: ["'self'", 'https://*.telegram.org'], + workerSrc: ["'self'", 'blob:'], + }, + }, + }) +} + +module.exports = { helmetMiddleware } diff --git a/server/src/middleware/passport.js b/server/src/middleware/passport.js index de26e4c53..8e3287608 100644 --- a/server/src/middleware/passport.js +++ b/server/src/middleware/passport.js @@ -1,3 +1,5 @@ +// @ts-check + const passport = require('passport') /** diff --git a/server/src/middleware/sentry.js b/server/src/middleware/sentry.js index 775a6d63c..a4ada969c 100644 --- a/server/src/middleware/sentry.js +++ b/server/src/middleware/sentry.js @@ -3,7 +3,7 @@ const Sentry = require('@sentry/node') const config = require('@rm/config') -const pkg = require('../../../package.json') +const { version } = require('../../../package.json') /** * Inits Sentry and returns the error handler middleware that should then be applied last @@ -32,7 +32,7 @@ function initSentry(app) { tracesSampleRate: +(process.env.SENTRY_TRACES_SAMPLE_RATE || sentry.tracesSampleRate) || 0.1, - release: pkg.version, + release: version, }) // RequestHandler creates a separate execution context, so that all diff --git a/server/src/models/Backup.js b/server/src/models/Backup.js index 32c41733e..f0747d90c 100644 --- a/server/src/models/Backup.js +++ b/server/src/models/Backup.js @@ -22,7 +22,7 @@ class Backup extends Model { } static get relationMappings() { - const state = require('../services/state') + const { state } = require('../services/state') return { user: { relation: Model.BelongsToOneRelation, @@ -75,7 +75,7 @@ class Backup extends Model { count['count(*)'] < config.getSafe('database.settings.userBackupLimits') ) { // @ts-ignore - return this.query().insert({ + await this.query().insert({ // @ts-ignore userId, name: backup.name, @@ -116,4 +116,4 @@ class Backup extends Model { } } -module.exports = Backup +module.exports = { Backup } diff --git a/server/src/models/Badge.js b/server/src/models/Badge.js index 3e21f1c4b..7edfac32a 100644 --- a/server/src/models/Badge.js +++ b/server/src/models/Badge.js @@ -17,7 +17,7 @@ class Badge extends Model { } static get relationMappings() { - const state = require('../services/state') + const { state } = require('../services/state') return { user: { relation: Model.BelongsToOneRelation, @@ -44,6 +44,36 @@ class Badge extends Model { .where('userId', userId) .andWhere('badge', operator, badge) } + + /** + * Returns all badges for a gym + * @param {number} badge + * @param {number} gymId + * @param {number} userId + */ + static async insert(badge, gymId, userId) { + // @ts-ignore + if ( + await this.query() + .where('gymId', gymId) + .andWhere('userId', userId) + .first() + ) { + await this.query() + .where('gymId', gymId) + .andWhere('userId', userId) + // @ts-ignore + .update({ badge }) + } else { + // @ts-ignore + await this.query().insert({ + // @ts-ignore + badge, + gymId, + userId, + }) + } + } } -module.exports = Badge +module.exports = { Badge } diff --git a/server/src/models/Device.js b/server/src/models/Device.js index a15120cf1..347d3a427 100644 --- a/server/src/models/Device.js +++ b/server/src/models/Device.js @@ -1,7 +1,7 @@ // @ts-check const { Model, raw } = require('objection') -const getAreaSql = require('../utils/getAreaSql') -const fetchJson = require('../utils/fetchJson') +const { getAreaSql } = require('../utils/getAreaSql') +const { fetchJson } = require('../utils/fetchJson') const { filterRTree } = require('../utils/filterRTree') class Device extends Model { @@ -77,4 +77,4 @@ class Device extends Model { } } -module.exports = Device +module.exports = { Device } diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index f0d1f4e6a..29787eda6 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -1,11 +1,13 @@ -/* eslint-disable no-nested-ternary */ +// @ts-check + /* eslint-disable no-restricted-syntax */ const { Model, raw } = require('objection') const i18next = require('i18next') + const config = require('@rm/config') -const state = require('../services/state') -const getAreaSql = require('../utils/getAreaSql') +const { getAreaSql } = require('../utils/getAreaSql') +const { state } = require('../services/state') const coreFields = [ 'id', @@ -643,4 +645,4 @@ class Gym extends Model { } } -module.exports = Gym +module.exports = { Gym } diff --git a/server/src/models/Nest.js b/server/src/models/Nest.js index 4c2cc8c16..4a7020fe4 100644 --- a/server/src/models/Nest.js +++ b/server/src/models/Nest.js @@ -3,8 +3,8 @@ const { Model } = require('objection') const i18next = require('i18next') const config = require('@rm/config') -const state = require('../services/state') -const getAreaSql = require('../utils/getAreaSql') +const { state } = require('../services/state') +const { getAreaSql } = require('../utils/getAreaSql') /** @typedef {Nest & Partial} FullNest */ @@ -221,4 +221,4 @@ class Nest extends Model { } } -module.exports = Nest +module.exports = { Nest } diff --git a/server/src/models/NestSubmission.js b/server/src/models/NestSubmission.js index 66f284953..444e55515 100644 --- a/server/src/models/NestSubmission.js +++ b/server/src/models/NestSubmission.js @@ -1,5 +1,6 @@ // @ts-check const { Model } = require('objection') + const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') @@ -14,7 +15,7 @@ class NestSubmission extends Model { static get relationMappings() { // eslint-disable-next-line global-require - const state = require('../services/state') + const { state } = require('../services/state') return { user: { relation: Model.BelongsToOneRelation, @@ -92,4 +93,4 @@ class NestSubmission extends Model { } } -module.exports = NestSubmission +module.exports = { NestSubmission } diff --git a/server/src/models/PoI.js b/server/src/models/PoI.js index 201ad4168..1adaeec57 100644 --- a/server/src/models/PoI.js +++ b/server/src/models/PoI.js @@ -1,14 +1,14 @@ // @ts-check class PoI { /** - * @param {string} id + * @param {string| number} id * @param {number} lat * @param {number} lon * @param {boolean} [partner] * @param {boolean} [showcase] */ constructor(id, lat, lon, partner = false, showcase = false) { - this.id = id + this.id = id.toString() this.lat = lat this.lon = lon this.partner = partner @@ -16,4 +16,4 @@ class PoI { } } -module.exports = PoI +module.exports = { PoI } diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index fe4f0d135..f78fc9141 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -1,3 +1,5 @@ +// @ts-check + /* eslint-disable no-restricted-syntax */ const { Model, raw, ref } = require('objection') const i18next = require('i18next') @@ -9,17 +11,17 @@ const { point } = require('@turf/helpers') const { log, TAGS } = require('@rm/logger') const config = require('@rm/config') -const state = require('../services/state') -const getAreaSql = require('../utils/getAreaSql') +const { getAreaSql } = require('../utils/getAreaSql') const { filterRTree } = require('../utils/filterRTree') -const fetchJson = require('../utils/fetchJson') +const { fetchJson } = require('../utils/fetchJson') const { IV_CALC, LEVEL_CALC, MAD_KEY_MAP, BASE_KEYS, } = require('../filters/pokemon/constants') -const PkmnFilter = require('../filters/pokemon/Backend') +const { PkmnBackend } = require('../filters/pokemon/Backend') +const { state } = require('../services/state') class Pokemon extends Model { static get tableName() { @@ -66,7 +68,7 @@ class Pokemon extends Model { * @param {import("@rm/types").Permissions} perms * @param {object} args * @param {import("@rm/types").DbContext} ctx - * @returns {{ filterMap: Record, globalFilter: PkmnFilter }} + * @returns {{ filterMap: Record, globalFilter: PkmnBackend }} */ static getFilters(perms, args, ctx) { const mods = { @@ -78,12 +80,12 @@ class Pokemon extends Model { .map((x) => [`onlyPvp${x}`, args.filters[`onlyPvp${x}`]]), ), } - /** @type {Record} */ + /** @type {Record} */ const filterMap = {} Object.entries(args.filters).forEach(([key, filter]) => { if (key.includes('-')) { - filterMap[key] = new PkmnFilter( + filterMap[key] = new PkmnBackend( key, filter, args.filters.onlyIvOr, @@ -99,7 +101,7 @@ class Pokemon extends Model { // if no pokemon are present we want global filters to apply still mods.onlyLinkGlobal = false } - const globalFilter = new PkmnFilter( + const globalFilter = new PkmnBackend( 'global', args.filters.onlyIvOr, args.filters.onlyIvOr, @@ -265,7 +267,7 @@ class Pokemon extends Model { if (onlyHundoIv) filters.push({ iv: { min: 100, max: 100 }, pokemon: globalPokes }) } - /** @type {import("../types").Pokemon[]} */ + /** @type {import("@rm/types").Pokemon[]} */ const results = await this.evalQuery( mem ? `${mem}/api/pokemon/v2/scan` : null, mem @@ -377,7 +379,7 @@ class Pokemon extends Model { } /** - * @template [T=import("@rm/types").Pokemon[]] + * @template T * @param {string} mem * @param {string | import("objection").QueryBuilder} query * @param {'GET' | 'POST' | 'PATCH' | 'DELETE'} method @@ -665,4 +667,4 @@ class Pokemon extends Model { } } -module.exports = Pokemon +module.exports = { Pokemon } diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index ef4fc6761..217df4a71 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -1,11 +1,13 @@ +// @ts-check + /* eslint-disable no-continue */ const { Model, raw } = require('objection') const i18next = require('i18next') const config = require('@rm/config') -const state = require('../services/state') -const getAreaSql = require('../utils/getAreaSql') +const { getAreaSql } = require('../utils/getAreaSql') const { getUserMidnight } = require('../utils/getClientTime') +const { state } = require('../services/state') const questProps = { quest_type: true, @@ -2006,4 +2008,4 @@ class Pokestop extends Model { } } -module.exports = Pokestop +module.exports = { Pokestop } diff --git a/server/src/models/Portal.js b/server/src/models/Portal.js index 103e2d35c..79a9d3aa1 100644 --- a/server/src/models/Portal.js +++ b/server/src/models/Portal.js @@ -2,7 +2,7 @@ const { Model } = require('objection') const config = require('@rm/config') -const getAreaSql = require('../utils/getAreaSql') +const { getAreaSql } = require('../utils/getAreaSql') class Portal extends Model { static get tableName() { @@ -84,4 +84,4 @@ class Portal extends Model { } } -module.exports = Portal +module.exports = { Portal } diff --git a/server/src/models/Route.js b/server/src/models/Route.js index 45bbb08a3..b3bc3e1db 100644 --- a/server/src/models/Route.js +++ b/server/src/models/Route.js @@ -2,7 +2,7 @@ const { Model, raw } = require('objection') const config = require('@rm/config') -const getAreaSql = require('../utils/getAreaSql') +const { getAreaSql } = require('../utils/getAreaSql') const { getEpoch } = require('../utils/getClientTime') const GET_ALL_SELECT = /** @type {const} */ ([ @@ -176,4 +176,4 @@ class Route extends Model { } } -module.exports = Route +module.exports = { Route } diff --git a/server/src/models/ScanCell.js b/server/src/models/ScanCell.js index fe21c7bcc..8193d1d95 100644 --- a/server/src/models/ScanCell.js +++ b/server/src/models/ScanCell.js @@ -2,8 +2,8 @@ const { Model, ref } = require('objection') const config = require('@rm/config') -const getPolyVector = require('../utils/getPolyVector') -const getAreaSql = require('../utils/getAreaSql') +const { getPolyVector } = require('../utils/getPolyVector') +const { getAreaSql } = require('../utils/getAreaSql') class ScanCell extends Model { static get tableName() { @@ -51,4 +51,4 @@ class ScanCell extends Model { } } -module.exports = ScanCell +module.exports = { ScanCell } diff --git a/server/src/models/Session.js b/server/src/models/Session.js index cbc396550..22950140b 100644 --- a/server/src/models/Session.js +++ b/server/src/models/Session.js @@ -109,4 +109,4 @@ class Session extends Model { } } -module.exports = Session +module.exports = { Session } diff --git a/server/src/models/Spawnpoint.js b/server/src/models/Spawnpoint.js index b0973cf18..6da5de679 100644 --- a/server/src/models/Spawnpoint.js +++ b/server/src/models/Spawnpoint.js @@ -3,7 +3,7 @@ const { Model, raw } = require('objection') const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') -const getAreaSql = require('../utils/getAreaSql') +const { getAreaSql } = require('../utils/getAreaSql') class Spawnpoint extends Model { static get tableName() { @@ -60,4 +60,4 @@ class Spawnpoint extends Model { } } -module.exports = Spawnpoint +module.exports = { Spawnpoint } diff --git a/server/src/models/Station.js b/server/src/models/Station.js new file mode 100644 index 000000000..bd2a92bb0 --- /dev/null +++ b/server/src/models/Station.js @@ -0,0 +1,246 @@ +// @ts-check +const { Model } = require('objection') +const config = require('@rm/config') +const i18next = require('i18next') + +const { getAreaSql } = require('../utils/getAreaSql') +const { getEpoch } = require('../utils/getClientTime') +const { state } = require('../services/state') + +class Station extends Model { + static get tableName() { + return 'station' + } + + /** + * Returns the bare essentials for displaying on the map + * @param {import("@rm/types").Permissions} perms + * @param {object} args + * @param {import("@rm/types").DbContext} ctx + * @returns {Promise} + */ + static async getAll(perms, args, { isMad }) { + const { areaRestrictions } = perms + const { onlyAreas, onlyAllStations, onlyMaxBattles, onlyBattleTier } = + args.filters + const ts = getEpoch() + + const select = [ + 'id', + 'name', + 'lat', + 'lon', + 'updated', + 'start_time', + 'end_time', + 'total_stationed_pokemon', + ] + + const query = this.query() + .whereBetween('lat', [args.minLat, args.maxLat]) + .andWhereBetween('lon', [args.minLon, args.maxLon]) + .andWhere('end_time', '>', ts) + // .where('is_inactive', false) + + if (perms.dynamax && onlyMaxBattles) { + select.push( + 'is_battle_available', + 'battle_level', + 'battle_start', + 'battle_end', + 'battle_pokemon_id', + 'battle_pokemon_form', + 'battle_pokemon_costume', + 'battle_pokemon_gender', + 'battle_pokemon_alignment', + 'battle_pokemon_bread_mode', + 'battle_pokemon_move_1', + 'battle_pokemon_move_2', + ) + + if (!onlyAllStations) { + query + .whereNotNull('battle_pokemon_id') + .andWhere('is_battle_available', true) + + if (onlyBattleTier === 'all') { + const battleBosses = new Set() + const battleForms = new Set() + const battleLevels = new Set() + + Object.keys(args.filters).forEach((key) => { + switch (key.charAt(0)) { + case 'o': + break + case 'j': + battleLevels.add(key.slice(1)) + break + default: + { + const [id, form] = key.split('-') + if (id) battleBosses.add(id) + if (form) battleForms.add(form) + } + break + } + }) + + if (battleBosses.size) { + query.andWhere('battle_pokemon_id', 'in', [...battleBosses]) + } + if (battleForms.size) { + query.andWhere('battle_pokemon_form', 'in', [...battleForms]) + } + if (battleLevels.size) { + query.andWhere('battle_level', 'in', [...battleLevels]) + } + } else { + query.andWhere('battle_level', onlyBattleTier) + } + } + } + + if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad)) { + return [] + } + /** @type {import("@rm/types").FullStation[]} */ + const results = await query.select(select) + + return results + .filter( + (station) => + onlyAllStations || + !perms.dynamax || + args.filters[`j${station.battle_level}`] || + args.filters[ + `${station.battle_pokemon_id}-${station.battle_pokemon_form}` + ] || + onlyBattleTier === 'all' || + onlyBattleTier === station.battle_level, + ) + .map((station) => { + if (station.is_battle_available && station.battle_pokemon_id === null) { + station.is_battle_available = false + } + if (station.total_stationed_pokemon === null) { + station.total_stationed_pokemon = 0 + } + return station + }) + } + + /** + * Returns the full station after querying it by ID + * @param {number} id + */ + static async getOne(id) { + /** @type {import('@rm/types').FullStation} */ + const result = await this.query().findById(id) + return result + } + + /** + * Returns the stationed mons for a given station + * @param {number} id + * @param {import('@rm/types').DbContext} _ctx + * @returns {Promise} + */ + // eslint-disable-next-line no-unused-vars + static async getDynamaxMons(id, _ctx) { + /** @type {import('@rm/types').FullStation} */ + const result = await this.query().findById(id).select('stationed_pokemon') + if (!result) { + return [] + } + return typeof result.stationed_pokemon === 'string' + ? JSON.parse(result.stationed_pokemon) + : result.stationed_pokemon || [] + } + + static async getAvailable() { + /** @type {import('@rm/types').FullStation[]} */ + const results = await this.query() + .distinct(['battle_pokemon_id', 'battle_pokemon_form', 'battle_level']) + .where('is_inactive', false) + .groupBy(['battle_pokemon_id', 'battle_pokemon_form', 'battle_level']) + .orderBy('battle_pokemon_id', 'asc') + return { + available: [ + ...new Set( + results + .filter(({ battle_level }) => !!battle_level) + .flatMap((station) => [ + `${station.battle_pokemon_id}-${station.battle_pokemon_form}`, + `j${station.battle_level}`, + ]), + ), + ], + } + } + + /** + * + * @param {import("@rm/types").Permissions} perms + * @param {object} args + * @param {import("@rm/types").DbContext} context + * @param {ReturnType} distance + * @param {ReturnType} bbox + * @returns {Promise} + */ + static async search(perms, args, { isMad }, distance, bbox) { + const { areaRestrictions } = perms + const { onlyAreas = [], search = '', locale } = args + const { searchResultsLimit, stationUpdateLimit } = config.getSafe('api') + + const pokemonIds = Object.keys(state.event.masterfile.pokemon).filter( + (pkmn) => + i18next + .t(`poke_${pkmn}`, { lng: locale }) + .toLowerCase() + .includes(search), + ) + + const select = ['id', 'name', 'lat', 'lon', distance] + if (perms.dynamax) { + select.push( + 'battle_level', + 'battle_pokemon_id', + 'battle_pokemon_form', + 'battle_pokemon_costume', + 'battle_pokemon_gender', + 'battle_pokemon_alignment', + 'battle_pokemon_bread_mode', + ) + } + + const query = this.query() + .select(select) + .whereBetween('lat', [bbox.minLat, bbox.maxLat]) + .andWhereBetween('lon', [bbox.minLon, bbox.maxLon]) + .andWhere( + 'updated', + '>', + Date.now() / 1000 - stationUpdateLimit * 60 * 60 * 24, + ) + .andWhere((builder) => { + if (perms.stations) { + builder.orWhereILike('name', `%${search}%`) + } + if (perms.dynamax) { + builder.orWhere((builder2) => { + builder2 + .whereIn('battle_pokemon_id', pokemonIds) + .andWhere('is_battle_available', true) + }) + } + }) + .limit(searchResultsLimit) + .orderBy('distance') + if (!getAreaSql(query, areaRestrictions, onlyAreas, isMad)) { + return [] + } + return query + } +} + +module.exports = { Station } diff --git a/server/src/models/User.js b/server/src/models/User.js index 9938b1dcc..d02434ee8 100644 --- a/server/src/models/User.js +++ b/server/src/models/User.js @@ -11,7 +11,7 @@ class User extends Model { static get relationMappings() { // eslint-disable-next-line global-require - const state = require('../services/state') + const { state } = require('../services/state') return { badges: { relation: Model.HasManyRelation, @@ -71,4 +71,4 @@ class User extends Model { } } -module.exports = User +module.exports = { User } diff --git a/server/src/models/Weather.js b/server/src/models/Weather.js index 2a2754b5e..4c882f001 100644 --- a/server/src/models/Weather.js +++ b/server/src/models/Weather.js @@ -6,7 +6,7 @@ const { default: pointInPolygon } = require('@turf/boolean-point-in-polygon') const { default: booleanContains } = require('@turf/boolean-contains') const config = require('@rm/config') -const getPolyVector = require('../utils/getPolyVector') +const { getPolyVector } = require('../utils/getPolyVector') const { getPolygonBbox } = require('../utils/getBbox') class Weather extends Model { @@ -88,4 +88,4 @@ class Weather extends Model { } } -module.exports = Weather +module.exports = { Weather } diff --git a/server/src/models/index.js b/server/src/models/index.js index edf9f5470..3dd967fae 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -1,20 +1,21 @@ // @ts-check -const Backup = require('./Backup') -const Badge = require('./Badge') -const Device = require('./Device') -const Gym = require('./Gym') -const Nest = require('./Nest') -const NestSubmission = require('./NestSubmission') -const Pokestop = require('./Pokestop') -const Pokemon = require('./Pokemon') -const Portal = require('./Portal') -const PoI = require('./PoI') -const Route = require('./Route') -const ScanCell = require('./ScanCell') -const Session = require('./Session') -const Spawnpoint = require('./Spawnpoint') -const User = require('./User') -const Weather = require('./Weather') +const { Backup } = require('./Backup') +const { Badge } = require('./Badge') +const { Device } = require('./Device') +const { Gym } = require('./Gym') +const { Nest } = require('./Nest') +const { NestSubmission } = require('./NestSubmission') +const { Pokestop } = require('./Pokestop') +const { Pokemon } = require('./Pokemon') +const { Portal } = require('./Portal') +const { PoI } = require('./PoI') +const { Route } = require('./Route') +const { ScanCell } = require('./ScanCell') +const { Session } = require('./Session') +const { Spawnpoint } = require('./Spawnpoint') +const { Station } = require('./Station') +const { User } = require('./User') +const { Weather } = require('./Weather') const rmModels = { Backup, @@ -34,6 +35,7 @@ const scannerModels = { Route, ScanCell, Spawnpoint, + Station, Weather, } @@ -46,7 +48,7 @@ const scannerModels = { * @typedef {keyof Models} ModelKeys */ -/** @param {import('../services/DbCheck')} db */ +/** @param {import('../services/DbManager').DbManager} db */ const bindConnections = (db) => db.bindConnections({ ...rmModels, ...scannerModels }) diff --git a/server/src/routes/api/index.js b/server/src/routes/api/index.js index f0cc87196..728ea046b 100644 --- a/server/src/routes/api/index.js +++ b/server/src/routes/api/index.js @@ -1,3 +1,5 @@ +// @ts-check + const express = require('express') const fs = require('fs') const { resolve } = require('path') @@ -20,4 +22,4 @@ fs.readdir(resolve(__dirname, './v1/'), (e, files) => { }) }) -module.exports = apiRouter +module.exports = { apiRouter } diff --git a/server/src/routes/api/v1/available.js b/server/src/routes/api/v1/available.js index 36325a8a9..e79d1dd89 100644 --- a/server/src/routes/api/v1/available.js +++ b/server/src/routes/api/v1/available.js @@ -2,7 +2,7 @@ const router = require('express').Router() const { log, TAGS } = require('@rm/logger') -const state = require('../../../services/state') +const { state } = require('../../../services/state') const queryObj = /** @type {const} */ ({ pokemon: { model: 'Pokemon', category: 'pokemon' }, @@ -117,6 +117,7 @@ router.put('/:category', async (req, res) => { state.event.setAvailable('pokestops', 'Pokestop', state.db), state.event.setAvailable('gyms', 'Gym', state.db), state.event.setAvailable('nests', 'Nest', state.db), + state.event.setAvailable('stations', 'Station', state.db), ]) } res diff --git a/server/src/routes/api/v1/sessions.js b/server/src/routes/api/v1/sessions.js index 3c7d6c664..c6ee9d71f 100644 --- a/server/src/routes/api/v1/sessions.js +++ b/server/src/routes/api/v1/sessions.js @@ -3,7 +3,7 @@ const router = require('express').Router() const { log, TAGS } = require('@rm/logger') -const state = require('../../../services/state') +const { state } = require('../../../services/state') router.get('/', async (req, res) => { try { diff --git a/server/src/routes/api/v1/trial.js b/server/src/routes/api/v1/trial.js index 7c7b068c6..da36166c4 100644 --- a/server/src/routes/api/v1/trial.js +++ b/server/src/routes/api/v1/trial.js @@ -3,7 +3,7 @@ const router = require('express').Router() const { log, TAGS } = require('@rm/logger') -const state = require('../../../services/state') +const { state } = require('../../../services/state') router.get('/start', async (req, res) => { state.setTrials(true) diff --git a/server/src/routes/api/v1/users.js b/server/src/routes/api/v1/users.js index 9987751e4..d3d03eb7e 100644 --- a/server/src/routes/api/v1/users.js +++ b/server/src/routes/api/v1/users.js @@ -3,7 +3,7 @@ // @ts-check const router = require('express').Router() const { log, TAGS } = require('@rm/logger') -const state = require('../../../services/state') +const { state } = require('../../../services/state') router.get('/', async (req, res) => { try { diff --git a/server/src/routes/authRouter.js b/server/src/routes/authRouter.js index c7dd3905e..34cd61a0c 100644 --- a/server/src/routes/authRouter.js +++ b/server/src/routes/authRouter.js @@ -1,16 +1,16 @@ // @ts-check -const router = require('express').Router() +const authRouter = require('express').Router() const passport = require('passport') const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') -const state = require('../services/state') +const { state } = require('../services/state') // Loads up the base auth routes and any custom ones const loadAuthStrategies = () => { - router.stack = [] + authRouter.stack = [] config.getSafe('authentication.strategies').forEach((strategy, i) => { const method = strategy.type === 'discord' || strategy.type === 'telegram' @@ -26,11 +26,11 @@ const loadAuthStrategies = () => { if (strategy.type === 'discord') { callbackOptions.prompt = strategy.clientPrompt } - router[method]( + authRouter[method]( `/${name}`, passport.authenticate(name, authenticateOptions), ) - router[method](`/${name}/callback`, async (req, res, next) => + authRouter[method](`/${name}/callback`, async (req, res, next) => passport.authenticate( name, callbackOptions, @@ -76,7 +76,7 @@ const loadAuthStrategies = () => { } }) - router.get('/logout', (req, res) => { + authRouter.get('/logout', (req, res) => { req.logout((err) => { if (err) log.error(TAGS.auth, 'Unable to logout', err) }) @@ -84,9 +84,9 @@ const loadAuthStrategies = () => { res.redirect('/') }) - log.debug(TAGS.auth, 'Auth Router Stack Size:', router.stack.length) + log.debug(TAGS.auth, 'Auth Router Stack Size:', authRouter.stack.length) } loadAuthStrategies() -module.exports = { loadAuthStrategies, authRouter: router } +module.exports = { loadAuthStrategies, authRouter } diff --git a/server/src/routes/clientRouter.js b/server/src/routes/clientRouter.js index 7c0b3525d..4451940ef 100644 --- a/server/src/routes/clientRouter.js +++ b/server/src/routes/clientRouter.js @@ -2,7 +2,7 @@ const express = require('express') const path = require('path') -const router = express.Router() +const clientRouter = express.Router() const CLIENT_ROUTES = [ '/', @@ -21,7 +21,7 @@ const CLIENT_ROUTES = [ '/error/:message', ] -router.get(CLIENT_ROUTES, (req, res) => { +clientRouter.get(CLIENT_ROUTES, (req, res) => { res.sendFile( path.join( __dirname, @@ -32,4 +32,4 @@ router.get(CLIENT_ROUTES, (req, res) => { ) }) -module.exports = router +module.exports = { clientRouter } diff --git a/server/src/routes/rootRouter.js b/server/src/routes/rootRouter.js index a6318fd09..3a8ce000b 100644 --- a/server/src/routes/rootRouter.js +++ b/server/src/routes/rootRouter.js @@ -1,3 +1,5 @@ +// @ts-check + const express = require('express') const fs = require('fs') const { join } = require('path') @@ -7,13 +9,13 @@ const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') const { authRouter } = require('./authRouter') -const clientRouter = require('./clientRouter') -const apiRouter = require('./api') -const state = require('../services/state') -const areaPerms = require('../utils/areaPerms') -const getServerSettings = require('../utils/getServerSettings') +const { clientRouter } = require('./clientRouter') +const { apiRouter } = require('./api') +const { areaPerms } = require('../utils/areaPerms') +const { getServerSettings } = require('../utils/getServerSettings') const { secretMiddleware } = require('../middleware/secret') const { version } = require('../../../package.json') +const { state } = require('../services/state') const rootRouter = express.Router() @@ -132,7 +134,7 @@ rootRouter.get('/api/settings', async (req, res, next) => { !authentication.methods.length ) { if (req.session.tutorial === undefined) { - req.session.tutorial = !mapConfig.forceTutorial + req.session.tutorial = !mapConfig.misc.forceTutorial } req.session.perms = { ...Object.fromEntries( @@ -187,13 +189,13 @@ rootRouter.get('/api/settings', async (req, res, next) => { if ('perms' in settings.user) { if (settings.user.perms.pokemon && api.queryOnSessionInit.pokemon) { - state.event.setAvailable('pokemon', 'Pokemon', state.db, false) + state.event.setAvailable('pokemon', 'Pokemon', state.db) } if ( api.queryOnSessionInit.raids && (settings.user.perms.raids || settings.user.perms.gyms) ) { - state.event.setAvailable('gyms', 'Gym', state.db, false) + state.event.setAvailable('gyms', 'Gym', state.db) } if ( api.queryOnSessionInit.quests && @@ -202,10 +204,13 @@ rootRouter.get('/api/settings', async (req, res, next) => { settings.user.perms.invasions || settings.user.perms.lures) ) { - state.event.setAvailable('pokestops', 'Pokestop', state.db, false) + state.event.setAvailable('pokestops', 'Pokestop', state.db) } if (settings.user.perms.nests && api.queryOnSessionInit.nests) { - state.event.setAvailable('nests', 'Nest', state.db, false) + state.event.setAvailable('nests', 'Nest', state.db) + } + if (settings.user.perms.stations && api.queryOnSessionInit.stations) { + state.event.setAvailable('stations', 'Station', state.db) } } @@ -216,4 +221,4 @@ rootRouter.get('/api/settings', async (req, res, next) => { } }) -module.exports = rootRouter +module.exports = { rootRouter } diff --git a/server/src/services/AuthClient.js b/server/src/services/AuthClient.js index 2b7483167..6384a9f06 100644 --- a/server/src/services/AuthClient.js +++ b/server/src/services/AuthClient.js @@ -3,7 +3,7 @@ const { Logger } = require('@rm/logger') const config = require('@rm/config') -const { Trial: TrialManager } = require('./Trial') +const { Trial } = require('./Trial') /** * @typedef {(rmStrategy: string, strategy: import("@rm/types").StrategyConfig) => void} ClientConstructor @@ -20,7 +20,7 @@ class AuthClient extends Logger { constructor(rmStrategy, strategy) { super(strategy.type, rmStrategy) this.rmStrategy = rmStrategy || 'custom' - this.trialManager = new TrialManager(strategy) + this.trialManager = new Trial(strategy) this.loggingChannels = { main: strategy.logChannelId, event: strategy.eventLogChannelId, @@ -99,4 +99,4 @@ class AuthClient extends Logger { } } -module.exports = AuthClient +module.exports = { AuthClient } diff --git a/server/src/services/DataLimitCheck.js b/server/src/services/DataLimitCheck.js index 5542a0552..27f2e5808 100644 --- a/server/src/services/DataLimitCheck.js +++ b/server/src/services/DataLimitCheck.js @@ -2,7 +2,7 @@ const config = require('@rm/config') const { Logger } = require('@rm/logger') -const state = require('./state') +const { state } = require('./state') class DataLimitCheck extends Logger { /** diff --git a/server/src/services/DbCheck.js b/server/src/services/DbManager.js similarity index 93% rename from server/src/services/DbCheck.js rename to server/src/services/DbManager.js index 97db63b16..ae3e3d407 100644 --- a/server/src/services/DbCheck.js +++ b/server/src/services/DbManager.js @@ -1,3 +1,5 @@ +// @ts-check + /* eslint-disable no-await-in-loop */ const { knex } = require('knex') const { raw } = require('objection') @@ -8,9 +10,9 @@ const { getBboxFromCenter } = require('../utils/getBbox') const { getCache } = require('./cache') /** - * @type {import("@rm/types").DbCheckClass} + * @type {import("@rm/types").DbManagerClass} */ -module.exports = class DbCheck extends Logger { +class DbManager extends Logger { static validModels = /** @type {const} */ ([ 'Device', 'Gym', @@ -21,6 +23,7 @@ module.exports = class DbCheck extends Logger { 'Route', 'ScanCell', 'Spawnpoint', + 'Station', 'Weather', ]) @@ -50,11 +53,10 @@ module.exports = class DbCheck extends Logger { .filter((s) => s.useFor.length) .map((schema, i) => { schema.useFor.forEach((category) => { - /** @type {import('../models').ModelKeys} */ - const capital = `${category.charAt(0).toUpperCase()}${category.slice( - 1, - )}` - if (DbCheck.singleModels.includes(capital)) { + const capital = /** @type {import('../models').ModelKeys} */ ( + `${category.charAt(0).toUpperCase()}${category.slice(1)}` + ) + if (DbManager.singleModels.includes(capital)) { this.models[capital] = {} if (capital === 'User') { this.reactMapDb = i @@ -211,7 +213,7 @@ module.exports = class DbCheck extends Logger { this.connections.map(async (schema, i) => { try { const schemaContext = schema - ? await DbCheck.schemaCheck(schema) + ? await DbManager.schemaCheck(schema) : { mem: this.endpoints[i].endpoint, secret: this.endpoints[i].secret, @@ -305,9 +307,8 @@ module.exports = class DbCheck extends Logger { bindConnections(models) { try { Object.entries(this.models).forEach(([modelName, sources]) => { - if (DbCheck.singleModels.includes(modelName)) { - /** @type {import('../models').RmModelKeys} */ - const model = modelName + const model = /** @type {import('../models').RmModelKeys} */ (modelName) + if (DbManager.singleModels.includes(model)) { if (sources.length > 1) { this.log.error(model, `only supports one database connection`) process.exit(0) @@ -325,8 +326,6 @@ module.exports = class DbCheck extends Logger { this.models[model] = models[model] this.models[model].knex(this.connections[this.reactMapDb]) } else if (Array.isArray(sources)) { - /** @type {import('../models').ScannerModelKeys} */ - const model = modelName sources.forEach((source, i) => { if (this.connections[source.connection]) { this.models[model][i].SubModel = models[model].bindKnex( @@ -348,7 +347,7 @@ module.exports = class DbCheck extends Logger { } catch (e) { this.log.error( e, - `\n\nOnly ${[...DbCheck.validModels, ...DbCheck.singleModels].join( + `\n\nOnly ${[...DbManager.validModels, ...DbManager.singleModels].join( ', ', )} are valid options in the useFor arrays`, ) @@ -395,7 +394,7 @@ module.exports = class DbCheck extends Logger { SubModel[method](perms, args, source, userId), ), ) - return DbCheck.deDupeResults(data) + return DbManager.deDupeResults(data) } catch (e) { this.log.error(TAGS[model.toLowerCase()], e) throw e @@ -414,22 +413,19 @@ module.exports = class DbCheck extends Logger { SubModel.getOne(id, source), ), ) - const cleaned = DbCheck.deDupeResults(data.filter(Boolean)) + const cleaned = DbManager.deDupeResults(data.filter(Boolean)) return cleaned || {} } /** * @template {import("@rm/types").BaseRecord} T - * @template {import("../models").ScannerModelKeys} U + * @template {Exclude} U * @param {U} model * @param {import("@rm/types").Permissions} perms * @param {object} args * @param {U extends 'Gym' ? 'searchRaids' | 'search' * : U extends 'Pokestop' ? 'searchInvasions' | 'searchQuests' | 'searchLures' | 'search' - * : U extends 'Pokemon' ? 'search' - * : U extends 'Portal' ? 'search' - * : U extends 'Nest' ? 'search' - * : never} method + * : 'search'} method * @returns {Promise} */ async search(model, perms, args, method = 'search') { @@ -452,12 +448,12 @@ module.exports = class DbCheck extends Logger { perms, args, source, - DbCheck.getDistance(args, source.isMad), + DbManager.getDistance(args, source.isMad), bbox, ), ), ) - const results = DbCheck.deDupeResults(data) + const results = DbManager.deDupeResults(data) if (results.length > deDuped.length) { deDuped = results } @@ -510,8 +506,8 @@ module.exports = class DbCheck extends Logger { * @param {import("@rm/types").Permissions} perms * @param {object} args * @returns {Promise<[ - * import("@rm/types").BaseRecord[], - * import("@rm/types").BaseRecord[] + * import("@rm/types").Pokestop[], + * import("@rm/types").Gym[] * ]>} */ async submissionCells(perms, args) { @@ -525,7 +521,7 @@ module.exports = class DbCheck extends Logger { SubModel.getSubmissions(perms, args, source), ), ) - return [DbCheck.deDupeResults(stopData), DbCheck.deDupeResults(gymData)] + return [DbManager.deDupeResults(stopData), DbManager.deDupeResults(gymData)] } /** @@ -553,7 +549,7 @@ module.exports = class DbCheck extends Logger { SubModel[method](...args, source), ), ) - return DbCheck.deDupeResults(data.filter(Boolean)) + return DbManager.deDupeResults(data.filter(Boolean)) } return this.models[model][method](...args) } @@ -647,3 +643,5 @@ module.exports = class DbCheck extends Logger { } } } + +module.exports = { DbManager } diff --git a/server/src/services/DiscordClient.js b/server/src/services/DiscordClient.js index ac8797046..a9f10cc6e 100644 --- a/server/src/services/DiscordClient.js +++ b/server/src/services/DiscordClient.js @@ -5,13 +5,13 @@ const passport = require('passport') const config = require('@rm/config') -const state = require('./state') -const logUserAuth = require('./logUserAuth') -const areaPerms = require('../utils/areaPerms') -const webhookPerms = require('../utils/webhookPerms') -const scannerPerms = require('../utils/scannerPerms') -const mergePerms = require('../utils/mergePerms') -const AuthClient = require('./AuthClient') +const { logUserAuth } = require('./logUserAuth') +const { areaPerms } = require('../utils/areaPerms') +const { webhookPerms } = require('../utils/webhookPerms') +const { scannerPerms } = require('../utils/scannerPerms') +const { mergePerms } = require('../utils/mergePerms') +const { AuthClient } = require('./AuthClient') +const { state } = require('./state') class DiscordClient extends AuthClient { /** @type {import('./AuthClient').ClientConstructor} */ @@ -397,4 +397,4 @@ class DiscordClient extends AuthClient { } } -module.exports = DiscordClient +module.exports = { DiscordClient } diff --git a/server/src/services/EventManager.js b/server/src/services/EventManager.js index 054b1cdb1..b996b4777 100644 --- a/server/src/services/EventManager.js +++ b/server/src/services/EventManager.js @@ -7,12 +7,11 @@ const config = require('@rm/config') const { Logger } = require('@rm/logger') const { generate, read } = require('@rm/masterfile') -const PoracleAPI = require('./Poracle') +const { PoracleAPI } = require('./Poracle') const { getCache } = require('./cache') /** - * @typedef {import('./DiscordClient') | import('./TelegramClient') | null} Client - * @typedef {Record} ClientObject + * @typedef {Record} ClientObject */ class EventManager extends Logger { @@ -30,6 +29,7 @@ class EventManager extends Logger { pokestops: [], pokemon: [], nests: [], + stations: [], }) this.uicons = getCache('uicons.json', []) this.uaudio = getCache('uaudio.json', []) @@ -73,7 +73,7 @@ class EventManager extends Logger { * * @param {keyof EventManager['available']} category * @param {import('../models').ScannerModelKeys} model - * @param {import('./DbCheck')} Db + * @param {import('./DbManager').DbManager} Db */ async setAvailable(category, model, Db) { this.available[category] = await Db.getAvailable(model) @@ -118,7 +118,7 @@ class EventManager extends Logger { /** * - * @param {keyof import('./AuthClient')['loggingChannels']} channel + * @param {keyof (import('./AuthClient').AuthClient['loggingChannels'])} channel * @param {import('discord.js').APIEmbed} embed * @param {keyof EventManager['authClients']} [strategy] */ @@ -166,7 +166,7 @@ class EventManager extends Logger { /** * - * @param {import('./DbCheck')} Db + * @param {import('./DbManager').DbManager} Db * @param {import('./PvpWrapper').PvpWrapper | null} Pvp */ startIntervals(Db, Pvp) { @@ -217,6 +217,17 @@ class EventManager extends Logger { 1000 * 60 * 60 * (config.getSafe('api.queryUpdateHours.quests') || 3), ) } + if (!config.getSafe('api.queryOnSessionInit.stations')) { + this.intervals.stationUpdate = setInterval( + async () => { + await this.setAvailable('stations', 'Station', Db) + await this.chatLog('event', { + description: 'Refreshed available stations', + }) + }, + 1000 * 60 * 60 * (config.getSafe('api.queryUpdateHours.stations') || 1), + ) + } this.intervals.uicons = setInterval( async () => { await this.getUniversalAssets('uicons') @@ -411,7 +422,7 @@ class EventManager extends Logger { } if (!this.masterfile.pokemon[id].forms[form]) { this.masterfile.pokemon[id].forms[form] = { name: '*', category } - this.log.info( + this.log.debug( `Added ${this.masterfile.pokemon[id].name} Key: ${item} to masterfile. (${category})`, ) } @@ -446,4 +457,4 @@ class EventManager extends Logger { } } -module.exports = EventManager +module.exports = { EventManager } diff --git a/server/src/services/LocalClient.js b/server/src/services/LocalClient.js new file mode 100644 index 000000000..b0c3639a3 --- /dev/null +++ b/server/src/services/LocalClient.js @@ -0,0 +1,148 @@ +// @ts-check +const { Strategy } = require('passport-local') +const passport = require('passport') +const bcrypt = require('bcrypt') + +const config = require('@rm/config') + +const { areaPerms } = require('../utils/areaPerms') +const { webhookPerms } = require('../utils/webhookPerms') +const { scannerPerms } = require('../utils/scannerPerms') +const { mergePerms } = require('../utils/mergePerms') +const { AuthClient } = require('./AuthClient') +const { state } = require('./state') + +class LocalClient extends AuthClient { + getPerms(trialActive = false) { + return Object.fromEntries( + Object.entries(this.perms).map(([perm, info]) => { + if (info.enabled) { + if ( + this.alwaysEnabledPerms.includes(perm) || + info.roles.includes('local') || + (trialActive && + info.trialPeriodEligible && + this.strategy.trialPeriod.roles.includes('local')) + ) { + return [perm, true] + } + } + return [perm, false] + }), + ) + } + + /** @type {import('passport-local').VerifyFunctionWithRequest} */ + async authHandler(_req, username, password, done) { + const forceTutorial = config.getSafe('map.misc.forceTutorial') + const trialActive = this.trialManager.active() + const localPerms = Object.keys(this.perms).filter((key) => + this.perms[key].roles.includes('local'), + ) + const user = { + perms: /** @type {import('@rm/types').Permissions} */ ({ + ...Object.fromEntries(Object.keys(this.perms).map((x) => [x, false])), + areaRestrictions: areaPerms(localPerms), + webhooks: [], + scanner: [], + }), + rmStrategy: this.rmStrategy, + } + + try { + await state.db.models.User.query() + .findOne({ username }) + .then( + async (/** @type {import('@rm/types').FullUser} */ userExists) => { + if (!userExists) { + try { + /** @type {import('@rm/types').FullUser} */ + const newUser = + await state.db.models.User.query().insertAndFetch({ + username, + password: await bcrypt.hash(password, 10), + strategy: 'local', + tutorial: !forceTutorial, + }) + user.id = newUser.id + user.username = newUser.username + user.perms = { ...user.perms, ...this.getPerms(trialActive) } + + this.log.info( + user.username, + `(${user.id})`, + 'Authenticated successfully.', + ) + return done(null, user) + } catch (e) { + return done(null, user, { message: 'error_creating_user' }) + } + } + if (bcrypt.compareSync(password, userExists.password)) { + ;['discordPerms', 'telegramPerms'].forEach((permSet) => { + if (userExists[permSet]) { + user.perms = mergePerms( + user.perms, + typeof userExists[permSet] === 'string' + ? JSON.parse(userExists[permSet]) + : userExists[permSet], + ) + } + }) + if (userExists.strategy !== 'local') { + await state.db.models.User.query() + .update({ strategy: 'local' }) + .where('id', userExists.id) + userExists.strategy = 'local' + } + user.id = userExists.id + user.username = userExists.username + user.discordId = userExists.discordId + user.telegramId = userExists.telegramId + user.webhookStrategy = userExists.webhookStrategy + user.data = userExists.data + user.status = userExists.data + ? (typeof userExists.data === 'string' + ? JSON.parse(userExists.data).status + : userExists.data.status) || 'local' + : 'local' + + user.perms = { ...user.perms, ...this.getPerms(trialActive) } + + webhookPerms([user.status], 'local', trialActive).forEach((x) => + user.perms.webhooks.push(x), + ) + scannerPerms([user.status], 'local', trialActive).forEach((x) => + user.perms.scanner.push(x), + ) + this.log.info( + user.username, + `(${user.id})`, + 'Authenticated successfully.', + ) + return done(null, user) + } + return done(null, false, { message: 'invalid_credentials' }) + }, + ) + } catch (e) { + this.log.error('User has failed authentication.', e) + } + } + + initPassport() { + passport.use( + this.rmStrategy, + new Strategy( + { + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true, + }, + (...args) => this.authHandler(...args), + ), + ) + } +} + +module.exports = { LocalClient } diff --git a/server/src/services/Poracle.js b/server/src/services/Poracle.js index f5bdaf19d..4b14f1f9a 100644 --- a/server/src/services/Poracle.js +++ b/server/src/services/Poracle.js @@ -1,5 +1,7 @@ +// @ts-check + const { log, TAGS } = require('@rm/logger') -const fetchJson = require('../utils/fetchJson') +const { fetchJson } = require('../utils/fetchJson') const { setCache, getCache } = require('./cache') const PLATFORMS = /** @type {const} */ (['discord', 'telegram']) @@ -1250,4 +1252,4 @@ class PoracleAPI { } } -module.exports = PoracleAPI +module.exports = { PoracleAPI } diff --git a/server/src/services/PvpWrapper.js b/server/src/services/PvpWrapper.js index 7f471a3c5..5d3de174d 100644 --- a/server/src/services/PvpWrapper.js +++ b/server/src/services/PvpWrapper.js @@ -1,3 +1,5 @@ +// @ts-check + const Ohbem = require('ohbem') const NodeCache = require('node-cache') const config = require('@rm/config') @@ -7,7 +9,6 @@ class PvpWrapper extends Ohbem { constructor() { super({ leagues: config.getSafe('api.pvp.leagueObj'), - pokemonData: {}, levelCaps: config.getSafe('api.pvp.levels'), cachingStrategy: Ohbem.cachingStrategies.memoryHeavy, }) @@ -22,7 +23,7 @@ class PvpWrapper extends Ohbem { /** * @param {import("@rm/types").Pokemon} pokemon * @param {number} currentTs - * @returns {import("@rm/types").CleanPvp} + * @returns {Record} */ resultWithCache(pokemon, currentTs) { if (pokemon.pokemon_id === 132) return {} diff --git a/server/src/services/TelegramClient.js b/server/src/services/TelegramClient.js index 7d5777ec7..ea951f48e 100644 --- a/server/src/services/TelegramClient.js +++ b/server/src/services/TelegramClient.js @@ -5,12 +5,12 @@ const passport = require('passport') const config = require('@rm/config') -const state = require('./state') -const areaPerms = require('../utils/areaPerms') -const webhookPerms = require('../utils/webhookPerms') -const scannerPerms = require('../utils/scannerPerms') -const mergePerms = require('../utils/mergePerms') -const AuthClient = require('./AuthClient') +const { state } = require('./state') +const { areaPerms } = require('../utils/areaPerms') +const { webhookPerms } = require('../utils/webhookPerms') +const { scannerPerms } = require('../utils/scannerPerms') +const { mergePerms } = require('../utils/mergePerms') +const { AuthClient } = require('./AuthClient') /** * @typedef {import('@rainb0w-clwn/passport-telegram-official/dist/types').PassportTelegramUser} TGUser @@ -250,4 +250,4 @@ class TelegramClient extends AuthClient { } } -module.exports = TelegramClient +module.exports = { TelegramClient } diff --git a/server/src/services/Trial.js b/server/src/services/Trial.js index 1b9e826e9..1ef701bbe 100644 --- a/server/src/services/Trial.js +++ b/server/src/services/Trial.js @@ -3,7 +3,7 @@ const { Logger, log } = require('@rm/logger') const config = require('@rm/config') const { Timer } = require('./Timer') -const state = require('./state') +const { state } = require('./state') class Trial extends Logger { /** @param {import("@rm/types").StrategyConfig} strategy */ diff --git a/server/src/services/geocoder.js b/server/src/services/geocoder.js index 6ff73e25a..1b15d3dc6 100644 --- a/server/src/services/geocoder.js +++ b/server/src/services/geocoder.js @@ -1,3 +1,5 @@ +// @ts-check + const NodeGeocoder = require('node-geocoder') const { log, TAGS } = require('@rm/logger') diff --git a/server/src/services/logUserAuth.js b/server/src/services/logUserAuth.js index 60e926fec..0245c7a15 100644 --- a/server/src/services/logUserAuth.js +++ b/server/src/services/logUserAuth.js @@ -42,7 +42,7 @@ const mapPerms = (perms, userPerms) => * @param {boolean} hidePii * @returns {Promise} */ -async function getAuthInfo(req, user, strategy = 'custom', hidePii = false) { +async function logUserAuth(req, user, strategy = 'custom', hidePii = false) { const ip = req.headers['cf-connecting-ip'] || `${req.headers['x-forwarded-for'] || ''}`.split(', ')[0] || @@ -216,4 +216,4 @@ async function getAuthInfo(req, user, strategy = 'custom', hidePii = false) { return embed } -module.exports = getAuthInfo +module.exports = { logUserAuth } diff --git a/server/src/services/scannerApi.js b/server/src/services/scannerApi.js index 6b7ea8593..3d328ffe8 100644 --- a/server/src/services/scannerApi.js +++ b/server/src/services/scannerApi.js @@ -4,7 +4,7 @@ const { default: fetch } = require('node-fetch') const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') -const state = require('./state') +const { state } = require('./state') const scannerQueue = { scanNext: {}, @@ -412,4 +412,4 @@ async function scannerApi( } } -module.exports = scannerApi +module.exports = { scannerApi } diff --git a/server/src/services/state.js b/server/src/services/state.js index 74613f322..e4464b157 100644 --- a/server/src/services/state.js +++ b/server/src/services/state.js @@ -5,15 +5,15 @@ const path = require('path') const config = require('@rm/config') const { log, TAGS } = require('@rm/logger') -const DbCheck = require('./DbCheck') -const EventManager = require('./EventManager') +const { DbManager } = require('./DbManager') +const { EventManager } = require('./EventManager') const { PvpWrapper } = require('./PvpWrapper') const { setCache } = require('./cache') const { migrate } = require('../db/migrate') const { Stats } = require('./Stats') -const serverState = { - db: new DbCheck(), +const state = { + db: new DbManager(), pvp: config.getSafe('api.pvp.reactMapHandlesPvp') ? new PvpWrapper() : null, event: new EventManager(), stats: new Stats(), @@ -104,6 +104,7 @@ const serverState = { this.event.setAvailable('pokestops', 'Pokestop', this.db), this.event.setAvailable('pokemon', 'Pokemon', this.db), this.event.setAvailable('nests', 'Nest', this.db), + this.event.setAvailable('stations', 'Station', this.db), ) } await Promise.all(promises) @@ -138,7 +139,7 @@ const serverState = { if (reloadReport.database) { await this.writeCache() await migrate() - this.db = new DbCheck() + this.db = new DbManager() } if (reloadReport.pvp) { this.pvp = config.getSafe('api.pvp.reactMapHandlesPvp') @@ -180,25 +181,25 @@ const serverState = { } process.on('SIGINT', async (e) => { - await serverState.writeCache(e) + await state.writeCache(e) process.exit(0) }) process.on('SIGTERM', async (e) => { - await serverState.writeCache(e) + await state.writeCache(e) process.exit(0) }) process.on('SIGUSR1', async (e) => { - await serverState.writeCache(e) + await state.writeCache(e) process.exit(0) }) process.on('SIGUSR2', async (e) => { - await serverState.writeCache(e) + await state.writeCache(e) process.exit(0) }) process.on('uncaughtException', async (e) => { log.error(TAGS.ReactMap, e) - await serverState.writeCache('SIGBREAK') + await state.writeCache('SIGBREAK') process.exit(99) }) -module.exports = serverState +module.exports = { state } diff --git a/server/src/strategies/discord.js b/server/src/strategies/discord.js index abc2b2ae5..9d911ebc8 100644 --- a/server/src/strategies/discord.js +++ b/server/src/strategies/discord.js @@ -1,6 +1,6 @@ // @ts-check const config = require('@rm/config') -const DiscordClient = require('../services/DiscordClient') +const { DiscordClient } = require('../services/DiscordClient') /** * diff --git a/server/src/strategies/local.js b/server/src/strategies/local.js index ceeb051ab..2a2d8cc68 100644 --- a/server/src/strategies/local.js +++ b/server/src/strategies/local.js @@ -1,155 +1,20 @@ -const passport = require('passport') -const Strategy = require('passport-local') -const bcrypt = require('bcrypt') -const path = require('path') - -const { name } = path.parse(__filename) - -const { log, TAGS } = require('@rm/logger') +// @ts-check const config = require('@rm/config') - -const state = require('../services/state') -const areaPerms = require('../utils/areaPerms') -const mergePerms = require('../utils/mergePerms') -const webhookPerms = require('../utils/webhookPerms') -const scannerPerms = require('../utils/scannerPerms') - -const authHandler = async (_req, username, password, done) => { - const forceTutorial = config.getSafe('map.misc.forceTutorial') - const { - [name]: strategyConfig, - alwaysEnabledPerms, - perms, - } = config.getSafe('authentication') - - const date = new Date() - const trialActive = - strategyConfig.trialPeriod && - date >= strategyConfig.trialPeriod.start.js && - date <= strategyConfig.trialPeriod.end.js - const localPerms = Object.keys(perms).filter((key) => - perms[key].roles.includes('local'), - ) - const user = { - perms: { - ...Object.fromEntries(Object.keys(perms).map((x) => [x, false])), - areaRestrictions: areaPerms(localPerms, 'local'), - webhooks: [], - scanner: [], - }, - rmStrategy: path.parse(__filename).name, - } - - try { - await state.db.models.User.query() - .findOne({ username }) - .then(async (userExists) => { - if (!userExists) { - try { - const newUser = await state.db.models.User.query().insertAndFetch({ - username, - password: await bcrypt.hash(password, 10), - strategy: 'local', - tutorial: !forceTutorial, - }) - user.id = newUser.id - user.username = newUser.username - Object.entries(perms).forEach(([perm, info]) => { - if (info.enabled) { - if ( - alwaysEnabledPerms.includes(perm) || - info.roles.includes('local') || - (trialActive && - info.trialPeriodEligible && - strategyConfig.trialPeriod.roles.includes('local')) - ) { - user.perms[perm] = true - } - } - }) - log.info( - TAGS.custom(name), - user.username, - `(${user.id})`, - 'Authenticated successfully.', - ) - return done(null, user) - } catch (e) { - return done(null, user, { message: 'error_creating_user' }) - } - } - if (bcrypt.compareSync(password, userExists.password)) { - ;['discordPerms', 'telegramPerms'].forEach((permSet) => { - if (userExists[permSet]) { - user.perms = mergePerms( - user.perms, - typeof userExists[permSet] === 'string' - ? JSON.parse(userExists[permSet]) - : userExists[permSet], - ) - } - }) - if (userExists.strategy !== 'local') { - await state.db.models.User.query() - .update({ strategy: 'local' }) - .where('id', userExists.id) - userExists.strategy = 'local' - } - user.id = userExists.id - user.username = userExists.username - user.discordId = userExists.discordId - user.telegramId = userExists.telegramId - user.webhookStrategy = userExists.webhookStrategy - user.data = userExists.data - user.status = userExists.data - ? (typeof userExists.data === 'string' - ? JSON.parse(userExists.data).status - : userExists.data.status) || 'local' - : 'local' - Object.entries(perms).forEach(([perm, info]) => { - if (info.enabled) { - if ( - alwaysEnabledPerms.includes(perm) || - info.roles.includes(user.status) || - (trialActive && - info.trialPeriodEligible && - strategyConfig.trialPeriod.roles.includes(user.status)) - ) { - user.perms[perm] = true - } - } - }) - webhookPerms([user.status], 'local', trialActive).forEach((x) => - user.perms.webhooks.push(x), - ) - scannerPerms([user.status], 'local', trialActive).forEach((x) => - user.perms.scanner.push(x), - ) - log.info( - TAGS.custom(name), - user.username, - `(${user.id})`, - 'Authenticated successfully.', - ) - return done(null, user) - } - return done(null, false, { message: 'invalid_credentials' }) - }) - } catch (e) { - log.error(TAGS.custom(name), 'User has failed authentication.', e) +const { LocalClient } = require('../services/LocalClient') + +/** + * + * @param {string} strategy + * @returns + */ +module.exports = (strategy) => { + const strategyConfig = config + .getSafe('authentication.strategies') + .find((s) => s.name === strategy) + if (strategyConfig) { + const Client = new LocalClient(strategy, strategyConfig) + Client.initPassport() + + return Client } } - -passport.use( - path.parse(__filename).name, - new Strategy( - { - usernameField: 'username', - passwordField: 'password', - passReqToCallback: true, - }, - authHandler, - ), -) - -module.exports = null diff --git a/server/src/strategies/telegram.js b/server/src/strategies/telegram.js index 16bdaeb55..b2464e339 100644 --- a/server/src/strategies/telegram.js +++ b/server/src/strategies/telegram.js @@ -1,6 +1,6 @@ // @ts-check const config = require('@rm/config') -const TelegramClient = require('../services/TelegramClient') +const { TelegramClient } = require('../services/TelegramClient') /** * diff --git a/server/src/ui/advMenus.js b/server/src/ui/advMenus.js index ae921a37a..d296d9127 100644 --- a/server/src/ui/advMenus.js +++ b/server/src/ui/advMenus.js @@ -1,5 +1,5 @@ // @ts-check -const state = require('../services/state') +const { state } = require('../services/state') const CATEGORIES = /** @type {const} */ ({ gyms: ['teams', 'eggs', 'raids', 'pokemon'], @@ -17,11 +17,17 @@ const CATEGORIES = /** @type {const} */ ({ 'quest_reward_1', 'general', ], + stations: ['pokemon'], pokemon: ['pokemon'], nests: ['pokemon'], }) -function buildMenus() { +/** + * + * @param {import('@rm/types').Permissions} perms + * @returns + */ +function advMenus(perms) { const rarityTiers = new Set( Object.values(state.event.masterfile.pokemon).map((val) => val.rarity), ) @@ -70,7 +76,7 @@ function buildMenus() { reverse: false, selected: false, unselected: false, - onlyAvailable: false, + onlyAvailable: true, }, } @@ -79,10 +85,6 @@ function buildMenus() { categories: CATEGORIES.gyms, filters: { ...pokemonFilters, - others: { - ...pokemonFilters.others, - onlyAvailable: true, - }, categories: Object.fromEntries( CATEGORIES.gyms.map((item) => [item, false]), ), @@ -92,23 +94,26 @@ function buildMenus() { categories: CATEGORIES.pokestops, filters: { ...pokemonFilters, - others: { - ...pokemonFilters.others, - onlyAvailable: true, - }, categories: Object.fromEntries( CATEGORIES.pokestops.map((item) => [item, false]), ), }, }, + stations: { + categories: perms?.dynamax ? CATEGORIES.stations : [], + filters: perms.dynamax + ? { + ...pokemonFilters, + categories: Object.fromEntries( + CATEGORIES.stations.map((item) => [item, false]), + ), + } + : {}, + }, pokemon: { categories: CATEGORIES.pokemon, filters: { ...pokemonFilters, - others: { - ...pokemonFilters.others, - onlyAvailable: true, - }, categories: Object.fromEntries( CATEGORIES.pokemon.map((item) => [item, false]), ), @@ -118,10 +123,6 @@ function buildMenus() { categories: CATEGORIES.nests, filters: { ...pokemonFilters, - others: { - ...pokemonFilters.others, - onlyAvailable: true, - }, categories: Object.fromEntries( CATEGORIES.nests.map((item) => [item, false]), ), @@ -132,4 +133,4 @@ function buildMenus() { return returnObj } -module.exports = buildMenus +module.exports = { advMenus } diff --git a/server/src/ui/clientOptions.js b/server/src/ui/clientOptions.js index 463135794..4e6cb25e6 100644 --- a/server/src/ui/clientOptions.js +++ b/server/src/ui/clientOptions.js @@ -13,80 +13,230 @@ function clientOptions(perms) { devicePathColor: { type: 'color', perm: ['devices'] }, }, gyms: { - clustering: { type: 'bool', perm: ['gyms', 'raids'] }, - raidTimers: { type: 'bool', perm: ['raids'] }, - interactionRanges: { type: 'bool', perm: ['gyms', 'raids'] }, - '300mRange': { type: 'bool', perm: ['raids'] }, + clustering: { + type: 'bool', + perm: ['gyms', 'raids'], + category: 'markers', + }, + raidTimers: { type: 'bool', perm: ['raids'], category: 'tooltips' }, + interactionRanges: { + type: 'bool', + perm: ['gyms', 'raids'], + category: 'markers', + }, + '300mRange': { type: 'bool', perm: ['raids'], category: 'markers' }, customRange: { type: 'number', perm: ['raids', 'gyms'], min: 0, max: 5000, + category: 'markers', + }, + showExBadge: { type: 'bool', perm: ['gyms'], category: 'markers' }, + showArBadge: { type: 'bool', perm: ['gyms'], category: 'markers' }, + raidLevelBadges: { type: 'bool', perm: ['raids'], category: 'markers' }, + gymBadgeDiamonds: { + type: 'bool', + perm: ['gymBadges'], + category: 'markers', + }, + raidOpacity: { + type: 'bool', + perm: ['raids'], + category: 'dynamic_opacity', + }, + opacityTenMinutes: { + type: 'number', + perm: ['raids'], + category: 'dynamic_opacity', + }, + opacityFiveMinutes: { + type: 'number', + perm: ['raids'], + category: 'dynamic_opacity', + }, + opacityOneMinute: { + type: 'number', + perm: ['raids'], + category: 'dynamic_opacity', }, - showExBadge: { type: 'bool', perm: ['gyms'] }, - showArBadge: { type: 'bool', perm: ['gyms'] }, - raidLevelBadges: { type: 'bool', perm: ['raids'] }, - gymBadgeDiamonds: { type: 'bool', perm: ['gymBadges'] }, - raidOpacity: { type: 'bool', perm: ['raids'] }, - opacityTenMinutes: { type: 'number', perm: ['raids'] }, - opacityFiveMinutes: { type: 'number', perm: ['raids'] }, - opacityOneMinute: { type: 'number', perm: ['raids'] }, enableGymPopupCoords: map.misc.enableGymPopupCoordsSelector - ? { type: 'bool', perm: ['gyms'] } + ? { type: 'bool', perm: ['gyms'], category: 'popups' } : undefined, }, pokestops: { - clustering: { type: 'bool', perm: ['pokestops', 'quests', 'invasions'] }, - invasionTimers: { type: 'bool', perm: ['invasions'] }, - lureTimers: { type: 'bool', perm: ['lures'] }, - eventStopTimers: { type: 'bool', perm: ['pokestops'] }, - interactionRanges: { type: 'bool', perm: ['pokestops'] }, - lureRange: { type: 'bool', perm: ['lures'] }, + clustering: { + type: 'bool', + perm: ['pokestops', 'quests', 'invasions'], + category: 'markers', + }, + invasionTimers: { + type: 'bool', + perm: ['invasions'], + category: 'tooltips', + }, + lureTimers: { type: 'bool', perm: ['lures'], category: 'tooltips' }, + eventStopTimers: { + type: 'bool', + perm: ['pokestops'], + category: 'tooltips', + }, + interactionRanges: { + type: 'bool', + perm: ['pokestops'], + category: 'markers', + }, + lureRange: { type: 'bool', perm: ['lures'], category: 'markers' }, customRange: { type: 'number', perm: ['raids', 'gyms'], min: 0, max: 5000, + category: 'markers', + }, + hasQuestIndicator: { + type: 'bool', + perm: ['quests'], + category: 'markers', + }, + showArBadge: { type: 'bool', perm: ['pokestops'], category: 'markers' }, + invasionOpacity: { + type: 'bool', + perm: ['invasions'], + category: 'dynamic_opacity', + }, + opacityTenMinutes: { + type: 'number', + perm: ['invasions'], + category: 'dynamic_opacity', + }, + opacityFiveMinutes: { + type: 'number', + perm: ['invasions'], + category: 'dynamic_opacity', + }, + opacityOneMinute: { + type: 'number', + perm: ['invasions'], + category: 'dynamic_opacity', }, - hasQuestIndicator: { type: 'bool', perm: ['quests'] }, - showArBadge: { type: 'bool', perm: ['pokestops'] }, - invasionOpacity: { type: 'bool', perm: ['invasions'] }, - opacityTenMinutes: { type: 'number', perm: ['invasions'] }, - opacityFiveMinutes: { type: 'number', perm: ['invasions'] }, - opacityOneMinute: { type: 'number', perm: ['invasions'] }, enablePokestopPopupCoords: map.misc.enablePokestopPopupCoordsSelector - ? { type: 'bool', perm: ['pokestops'] } + ? { type: 'bool', perm: ['pokestops'], category: 'popups' } + : undefined, + }, + stations: { + clustering: { + type: 'bool', + perm: ['stations', 'dynamax'], + category: 'markers', + }, + stationTimers: { + type: 'bool', + perm: ['stations', 'dynamax'], + category: 'tooltips', + }, + stationsOpacity: { + type: 'bool', + perm: ['stations', 'dynamax'], + category: 'dynamic_opacity', + }, + opacityTenMinutes: { + type: 'number', + perm: ['stations', 'dynamax'], + category: 'dynamic_opacity', + }, + opacityFiveMinutes: { + type: 'number', + perm: ['stations', 'dynamax'], + category: 'dynamic_opacity', + }, + opacityOneMinute: { + type: 'number', + perm: ['stations', 'dynamax'], + category: 'dynamic_opacity', + }, + enableStationPopupCoords: map.misc.enableStationPopupCoordsSelector + ? { type: 'bool', perm: ['stations', 'dynamax'], category: 'popups' } : undefined, }, pokemon: { - clustering: { type: 'bool', perm: ['pokemon'] }, - linkGlobalAndAdvanced: { type: 'bool', perm: ['pokemon'] }, - pokemonTimers: { type: 'bool', perm: ['pokemon'] }, - ivCircles: { type: 'bool', perm: ['iv'] }, - minIvCircle: { type: 'number', perm: ['iv'], label: '%' }, - levelCircles: { type: 'bool', perm: ['iv'] }, - minLevelCircle: { type: 'number', perm: ['iv'] }, - interactionRanges: { type: 'bool', perm: ['pokemon'] }, - spacialRendRange: { type: 'bool', perm: ['pokemon'] }, - showDexNumInPopup: { type: 'bool', perm: ['pokemon'] }, - weatherIndicator: { type: 'bool', perm: ['pokemon'] }, - pvpMega: { type: 'bool', perm: ['pvp'] }, - showAllPvpRanks: { type: 'bool', perm: ['pvp'] }, - showSizeIndicator: { type: 'bool', perm: ['pokemon'] }, - pokemonOpacity: { type: 'bool', perm: ['pokemon'] }, - opacityTenMinutes: { type: 'number', perm: ['pokemon'] }, - opacityFiveMinutes: { type: 'number', perm: ['pokemon'] }, - opacityOneMinute: { type: 'number', perm: ['pokemon'] }, + clustering: { type: 'bool', perm: ['pokemon'], category: 'markers' }, + linkGlobalAndAdvanced: { + type: 'bool', + perm: ['pokemon'], + category: 'filters', + }, + pokemonTimers: { type: 'bool', perm: ['pokemon'], category: 'tooltips' }, + ivCircles: { type: 'bool', perm: ['iv'], category: 'tooltips' }, + minIvCircle: { + type: 'number', + perm: ['iv'], + label: '%', + category: 'tooltips', + }, + levelCircles: { type: 'bool', perm: ['iv'], category: 'tooltips' }, + minLevelCircle: { type: 'number', perm: ['iv'], category: 'tooltips' }, + interactionRanges: { + type: 'bool', + perm: ['pokemon'], + category: 'markers', + }, + spacialRendRange: { + type: 'bool', + perm: ['pokemon'], + category: 'markers', + }, + showDexNumInPopup: { + type: 'bool', + perm: ['pokemon'], + category: 'popups', + }, + weatherIndicator: { + type: 'bool', + perm: ['pokemon'], + category: 'tooltips', + }, + pvpMega: { type: 'bool', perm: ['pvp'], category: 'filters' }, + showAllPvpRanks: { type: 'bool', perm: ['pvp'], category: 'filters' }, + showSizeIndicator: { + type: 'bool', + perm: ['pokemon'], + category: 'tooltips', + }, + pokemonOpacity: { + type: 'bool', + perm: ['pokemon'], + category: 'dynamic_opacity', + }, + opacityTenMinutes: { + type: 'number', + perm: ['pokemon'], + category: 'dynamic_opacity', + }, + opacityFiveMinutes: { + type: 'number', + perm: ['pokemon'], + category: 'dynamic_opacity', + }, + opacityOneMinute: { + type: 'number', + perm: ['pokemon'], + category: 'dynamic_opacity', + }, ...Object.fromEntries( - levels.map((level) => [`pvp${level}`, { type: 'bool', perm: ['pvp'] }]), + levels.map((level) => [ + `pvp${level}`, + { type: 'bool', perm: ['pvp'], category: 'filters' }, + ]), ), legacyFilter: map.misc.enableMapJsFilter - ? { type: 'bool', perm: ['iv'] } + ? { type: 'bool', perm: ['iv'], category: 'filters' } : undefined, glow: clientSideOptions.pokemon.glow.length ? { type: 'bool', perm: ['pokemon'], + category: 'markers', sub: Object.fromEntries( clientSideOptions.pokemon.glow.map(({ name, ...glow }) => [ name, @@ -96,24 +246,60 @@ function clientOptions(perms) { } : undefined, enablePokemonPopupCoords: map.misc.enablePokemonPopupCoordsSelector - ? { type: 'bool', perm: ['pokemon'] } + ? { type: 'bool', perm: ['pokemon'], category: 'popups' } : undefined, }, wayfarer: { - clustering: { type: 'bool', perm: ['portals'] }, - oldPortals: { type: 'color', perm: ['portals'] }, - newPortals: { type: 'color', perm: ['portals'] }, - oneStopTillNext: { type: 'color', perm: ['submissionCells'] }, - twoStopsTillNext: { type: 'color', perm: ['submissionCells'] }, - noMoreGyms: { type: 'color', perm: ['submissionCells'] }, - lightMapBorder: { type: 'color', perm: ['submissionCells'] }, - darkMapBorder: { type: 'color', perm: ['submissionCells'] }, - cellBlocked: { type: 'color', perm: ['submissionCells'] }, - poiColor: { type: 'color', perm: ['submissionCells'] }, - partnerColor: { type: 'color', perm: ['submissionCells'] }, - showcaseColor: { type: 'color', perm: ['submissionCells'] }, + clustering: { type: 'bool', perm: ['portals'], category: 'markers' }, + oldPortals: { type: 'color', perm: ['portals'], category: 'filters' }, + newPortals: { type: 'color', perm: ['portals'], category: 'filters' }, + oneStopTillNext: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + twoStopsTillNext: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + noMoreGyms: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + lightMapBorder: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + darkMapBorder: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + cellBlocked: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + poiColor: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + partnerColor: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, + showcaseColor: { + type: 'color', + perm: ['submissionCells'], + category: 'markers', + }, enablePortalPopupCoords: map.misc.enablePortalPopupCoordsSelector - ? { type: 'bool', perm: ['portals'] } + ? { type: 'bool', perm: ['portals'], category: 'popups' } : undefined, }, s2cells: { @@ -212,4 +398,4 @@ function clientOptions(perms) { return { clientValues, clientMenus } } -module.exports = clientOptions +module.exports = { clientOptions } diff --git a/server/src/ui/primary.js b/server/src/ui/drawer.js similarity index 93% rename from server/src/ui/primary.js rename to server/src/ui/drawer.js index e437ec5ae..473decc8f 100644 --- a/server/src/ui/primary.js +++ b/server/src/ui/drawer.js @@ -1,6 +1,6 @@ // @ts-check const config = require('@rm/config') -const state = require('../services/state') +const { state } = require('../services/state') /** @typedef {import('@rm/types').RMSlider} Slider */ @@ -13,7 +13,7 @@ const BLOCKED = /** @type {undefined} */ (undefined) * @param {import("@rm/types").Permissions} perms * @returns */ -function generateUi(req, perms) { +function drawer(req, perms) { const mapConfig = config.getMapConfig(req) const nestFilters = config.getSafe('defaultFilters.nests') const leagues = config.getSafe('api.pvp.leagues') @@ -63,6 +63,13 @@ function generateUi(req, perms) { arEligible: perms.pokestops || BLOCKED, } : BLOCKED, + stations: + (perms.stations || perms.dynamax) && state.db.models.Station + ? { + allStations: perms.stations || BLOCKED, + maxBattles: perms.dynamax || BLOCKED, + } + : BLOCKED, pokemon: (perms.pokemon || perms.iv || perms.pvp) && state.db.models.Pokemon ? { @@ -159,7 +166,7 @@ function generateUi(req, perms) { : undefined, s2cells: perms.s2cells ? { enabled: true } : BLOCKED, scanAreas: perms.scanAreas - ? { filterByAreas: true, enabled: true } + ? { enabled: true, filterByAreas: true } : undefined, weather: perms.weather && state.db.models.Weather ? { enabled: true } : BLOCKED, @@ -194,4 +201,4 @@ function generateUi(req, perms) { return { ...sortedUi, ...ui } } -module.exports = generateUi +module.exports = { drawer } diff --git a/server/src/utils/areaPerms.js b/server/src/utils/areaPerms.js index 9b7b16d92..596f239fe 100644 --- a/server/src/utils/areaPerms.js +++ b/server/src/utils/areaPerms.js @@ -30,4 +30,4 @@ function areaPerms(roles) { return [...new Set(perms)] } -module.exports = areaPerms +module.exports = { areaPerms } diff --git a/server/src/utils/evalWebhookId.js b/server/src/utils/evalWebhookId.js index 0d682b2cf..959f57473 100644 --- a/server/src/utils/evalWebhookId.js +++ b/server/src/utils/evalWebhookId.js @@ -16,4 +16,4 @@ function evalWebhookId(user) { } } -module.exports = evalWebhookId +module.exports = { evalWebhookId } diff --git a/server/src/utils/fetchJson.js b/server/src/utils/fetchJson.js index 03e6ed5c9..64097b045 100644 --- a/server/src/utils/fetchJson.js +++ b/server/src/utils/fetchJson.js @@ -55,4 +55,4 @@ async function fetchJson(url, options = undefined) { } } -module.exports = fetchJson +module.exports = { fetchJson } diff --git a/server/src/utils/filterComponents.js b/server/src/utils/filterComponents.js index 47b413dee..bdc62044c 100644 --- a/server/src/utils/filterComponents.js +++ b/server/src/utils/filterComponents.js @@ -28,4 +28,4 @@ function filterComponents(components, loggedIn, donor) { }) } -module.exports = filterComponents +module.exports = { filterComponents } diff --git a/server/src/utils/getAreaSql.js b/server/src/utils/getAreaSql.js index f7292ee86..7174cff15 100644 --- a/server/src/utils/getAreaSql.js +++ b/server/src/utils/getAreaSql.js @@ -11,7 +11,7 @@ const { consolidateAreas } = require('./consolidateAreas') * @param {string} [category] * @returns */ -function getAreaRestrictionSql( +function getAreaSql( query, areaRestrictions, onlyAreas, @@ -71,4 +71,4 @@ function getAreaRestrictionSql( return true } -module.exports = getAreaRestrictionSql +module.exports = { getAreaSql } diff --git a/server/src/utils/getClientTime.js b/server/src/utils/getClientTime.js index 36f18d205..a35271d61 100644 --- a/server/src/utils/getClientTime.js +++ b/server/src/utils/getClientTime.js @@ -11,7 +11,7 @@ const { getCenter } = require('./getCenter') /** * - * @param {Date} date + * @param {Date} [date] * @returns */ function getEpoch(date = new Date()) { diff --git a/server/src/utils/getPlacementCells.js b/server/src/utils/getPlacementCells.js index 2b512cf6f..86a0d1eb6 100644 --- a/server/src/utils/getPlacementCells.js +++ b/server/src/utils/getPlacementCells.js @@ -5,8 +5,8 @@ const { S2CellId, S2LatLngRect, } = require('nodes2ts') -const getPolyVector = require('./getPolyVector') -const PoI = require('../models/PoI') +const { getPolyVector } = require('./getPolyVector') +const { PoI } = require('../models/PoI') /** * @@ -73,4 +73,4 @@ function getPlacementCells(filters, pokestops, gyms) { } } -module.exports = getPlacementCells +module.exports = { getPlacementCells } diff --git a/server/src/utils/getPolyVector.js b/server/src/utils/getPolyVector.js index 589d69e67..9a66f4d47 100644 --- a/server/src/utils/getPolyVector.js +++ b/server/src/utils/getPolyVector.js @@ -36,4 +36,4 @@ function getPolyVector(s2cellId, polyline = false) { return result } -module.exports = getPolyVector +module.exports = { getPolyVector } diff --git a/server/src/utils/getServerSettings.js b/server/src/utils/getServerSettings.js index 0761721e8..790845db0 100644 --- a/server/src/utils/getServerSettings.js +++ b/server/src/utils/getServerSettings.js @@ -1,20 +1,21 @@ // @ts-check const config = require('@rm/config') -const clientOptions = require('../ui/clientOptions') -const advMenus = require('../ui/advMenus') -const generateUi = require('../ui/primary') +const { clientOptions } = require('../ui/clientOptions') +const { advMenus } = require('../ui/advMenus') +const { drawer } = require('../ui/drawer') /** * * @param {import("express").Request} req */ function getServerSettings(req) { - const user = { - ...(req.user ? req.user : req.session), - loggedIn: !!req.user, - cooldown: req.session?.cooldown || 0, - } + const user = + /** @type {import('@rm/types').ExpressUser & { loggedIn: boolean; cooldown: number }} */ ({ + ...(req.user ? req.user : req.session), + loggedIn: !!req.user, + cooldown: req.session?.cooldown || 0, + }) const { clientValues, clientMenus } = clientOptions(user.perms) @@ -60,13 +61,13 @@ function getServerSettings(req) { }, tileServers: config.getSafe('tileServers'), navigation: config.getSafe('navigation'), - menus: advMenus(), + menus: advMenus(user.perms), userSettings: clientValues, clientMenus, - ui: generateUi(req, user.perms), + ui: drawer(req, user.perms), } return serverSettings } -module.exports = getServerSettings +module.exports = { getServerSettings } diff --git a/server/src/utils/getTypeCells.js b/server/src/utils/getTypeCells.js index 37fd59c3a..7f06cacc0 100644 --- a/server/src/utils/getTypeCells.js +++ b/server/src/utils/getTypeCells.js @@ -5,7 +5,7 @@ const { S2CellId, S2LatLngRect, } = require('nodes2ts') -const getPolyVector = require('./getPolyVector') +const { getPolyVector } = require('./getPolyVector') /** * @@ -68,4 +68,4 @@ function getTypeCells(filters, pokestops, gyms) { return Object.values(indexedCells) } -module.exports = getTypeCells +module.exports = { getTypeCells } diff --git a/server/src/utils/getValidCoords.js b/server/src/utils/getValidCoords.js index 4c3cbcf53..795bbb324 100644 --- a/server/src/utils/getValidCoords.js +++ b/server/src/utils/getValidCoords.js @@ -29,6 +29,4 @@ function getValidCoords(mode, points, perms) { return [] } -module.exports = { - getValidCoords, -} +module.exports = { getValidCoords } diff --git a/server/src/utils/mergePerms.js b/server/src/utils/mergePerms.js index c63981117..c578593be 100644 --- a/server/src/utils/mergePerms.js +++ b/server/src/utils/mergePerms.js @@ -18,4 +18,4 @@ function mergePerms(existingPerms, incomingPerms) { ) } -module.exports = mergePerms +module.exports = { mergePerms } diff --git a/server/src/utils/reloadConfig.js b/server/src/utils/reloadConfig.js index 5596bab7d..d82adaff1 100644 --- a/server/src/utils/reloadConfig.js +++ b/server/src/utils/reloadConfig.js @@ -3,7 +3,7 @@ const dlv = require('dlv') const { log, TAGS } = require('@rm/logger') -const state = require('../services/state') +const { state } = require('../services/state') const { bindConnections } = require('../models') const { loadLatestAreas } = require('../services/areas') const { loadAuthStrategies } = require('../routes/authRouter') @@ -94,11 +94,17 @@ async function reloadConfig() { } catch { // do nothing } - log.info(TAGS.config, `'${key}' -`, 'old:', oldValue, 'new:', newValue) + log.info( + TAGS.config, + `updated '${key}' -`, + 'old:', + oldValue, + 'new:', + newValue, + ) } if (valid.length) { - log.info(TAGS.config, 'updating the following config values:') valid.forEach(print) } if (invalid.length) { diff --git a/server/src/utils/scannerPerms.js b/server/src/utils/scannerPerms.js index ec72cf319..bcfbdbaa0 100644 --- a/server/src/utils/scannerPerms.js +++ b/server/src/utils/scannerPerms.js @@ -4,7 +4,7 @@ const config = require('@rm/config') /** * * @param {string[]} roles - * @param {'discordRoles' | 'telegramGroups'} provider + * @param {'discordRoles' | 'telegramGroups' | 'local'} provider * @param {boolean} [trialActive] * @returns {string[]} */ @@ -28,4 +28,4 @@ function scannerPerms(roles, provider, trialActive = false) { return [...new Set(perms)] } -module.exports = scannerPerms +module.exports = { scannerPerms } diff --git a/server/src/utils/validateSelectedWebhook.js b/server/src/utils/validateSelectedWebhook.js index ac39ee4e5..d01c5db58 100644 --- a/server/src/utils/validateSelectedWebhook.js +++ b/server/src/utils/validateSelectedWebhook.js @@ -28,4 +28,4 @@ async function validateSelectedWebhook(user, Db, Event) { return null } -module.exports = validateSelectedWebhook +module.exports = { validateSelectedWebhook } diff --git a/server/src/utils/webhookPerms.js b/server/src/utils/webhookPerms.js index 260f93817..e02c4d32b 100644 --- a/server/src/utils/webhookPerms.js +++ b/server/src/utils/webhookPerms.js @@ -24,4 +24,4 @@ function webhookPerms(roles, provider, trialActive = false) { return [...new Set(perms)] } -module.exports = webhookPerms +module.exports = { webhookPerms } diff --git a/src/assets/constants.js b/src/assets/constants.js index 2842a3bee..47bd16d8f 100644 --- a/src/assets/constants.js +++ b/src/assets/constants.js @@ -1,3 +1,5 @@ +// @ts-check + export const ICON_SIZES = /** @type {const} */ (['sm', 'md', 'lg', 'xl']) export const XXS_XXL = /** @type {const} */ (['xxs', 'xxl']) @@ -9,7 +11,20 @@ export const ENUM_GENDER = /** @type {const} */ ([0, 1, 2, 3]) export const ENUM_BADGES = /** @type {const} */ ([0, 1, 2, 3, 4]) export const S2_LEVELS = /** @type {const} */ ([ - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ...(process.env.NODE_ENV === 'development' + ? [1, 2, 3, 4, 5, 6, 7, 8, 9] + : []), + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, ]) export const FORT_LEVELS = /** @type {const} */ (['all', '1', '2', '3']) @@ -49,6 +64,6 @@ export const FILTER_SKIP_LIST = ['filter', 'enabled', 'legacy'] export const ALWAYS_EXCLUDED = new Set(['donor', 'blockedGuildNames', 'admin']) -export const SCAN_MODES = /** @type */ (['confirmed', 'loading', 'error']) +export const SCAN_MODES = ['confirmed', 'loading', 'error'] export const SCAN_SIZES = /** @type {const} */ (['S', 'M', 'XL']) diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 85d3d74a2..e231a06eb 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -564,6 +564,13 @@ input[type='time']::-webkit-calendar-picker-indicator { width: 100%; } +.popup-card-media { + display: grid; + grid-template-columns: 1fr 1fr; + object-fit: contain; + column-gap: 8px; +} + .badge-diamond { clip-path: polygon(50% 0%, 85% 55%, 50% 100%, 15% 55%); position: absolute; @@ -585,3 +592,14 @@ input[type='time']::-webkit-calendar-picker-indicator { grid-template-rows: auto 1fr auto; /* Header, table, footer */ min-height: 100svh; } + +.expand-collapse-layout { + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 1fr auto; + padding: 12px 0; +} + +.expand-collapse-layout > :nth-child(3) { + grid-column: 1 / span 2; +} diff --git a/src/assets/fallbackMarker.js b/src/assets/fallbackMarker.js index 2ef3d56a3..885ec0d20 100644 --- a/src/assets/fallbackMarker.js +++ b/src/assets/fallbackMarker.js @@ -1,8 +1,9 @@ -import markerIconPng from 'leaflet/dist/images/marker-icon.png' +// @ts-check + import { Icon } from 'leaflet' export const fallbackMarker = new Icon({ - iconUrl: markerIconPng, + iconUrl: '/images/fallback-marker.png', iconSize: [25, 41], iconAnchor: [12, 35], popupAnchor: [1, -30], diff --git a/src/assets/theme.js b/src/assets/theme.js index 280cfc9b5..22c9f21c1 100644 --- a/src/assets/theme.js +++ b/src/assets/theme.js @@ -18,22 +18,10 @@ const components = { }, }, }, - MuiStack: { - defaultProps: { - direction: 'column', - justifyContent: 'flex-start', - alignItems: 'flex-start', - sx: (t) => ({ - width: { xs: 50, sm: 65 }, - zIndex: 5000, - '& > *': { - margin: `${t.spacing(1)} !important`, - position: 'sticky', - top: 0, - left: 5, - zIndex: 1000, - width: 10, - }, + MuiRating: { + styleOverrides: { + iconFilled: ({ theme }) => ({ + color: theme.palette.secondary.main, }), }, }, @@ -74,6 +62,13 @@ const components = { }, }, }, + MuiCardActions: { + styleOverrides: { + root: { + padding: '0px 8px', + }, + }, + }, MuiSelect: { defaultProps: { size: 'small', @@ -181,6 +176,7 @@ export function useCustomTheme() { main: '#2AB5F6', contrastText: '#fff', }, + // TODO: Augment Mui Types discord: { main: '#5865F2', green: '#57F287', diff --git a/src/components/Config.jsx b/src/components/Config.jsx index 2fce99826..9b7b5c804 100644 --- a/src/components/Config.jsx +++ b/src/components/Config.jsx @@ -1,3 +1,5 @@ +// @ts-check + import * as React from 'react' import { useTranslation } from 'react-i18next' import { setUser } from '@sentry/react' @@ -28,9 +30,8 @@ export function Config({ children }) { const getServerSettings = async () => { const data = await getSettings() - if (data) { + if (data && !('error' in data)) { document.title = data?.map?.general.headerTitle || document.title - analytics( 'User', data.user ? `${data.user.username} (${data.user.id})` : 'Not Logged In', @@ -66,10 +67,12 @@ export function Config({ children }) { data.map.general.startLat, data.map.general.startLon, ]) - const location = updatePositionState(defaultLocation, 'location').map( - (x, i) => - x || - (i === 0 ? data.map.general.startLat : data.map.general.startLon), + const location = /** @type {[number, number]} */ ( + updatePositionState(defaultLocation, 'location').map( + (x, i) => + x || + (i === 0 ? data.map.general.startLat : data.map.general.startLon), + ) ) const zoom = updatePositionState(data.map.general.startZoom, 'zoom') @@ -113,7 +116,7 @@ export function Config({ children }) { ? JSON.parse(data.user?.data) : data.user?.data : {}, - counts: data.authReferences || {}, + counts: data.authReferences, userBackupLimits: data.database.settings.userBackupLimits || 0, excludeList: data.authentication.excludeList || [], }, @@ -132,7 +135,6 @@ export function Config({ children }) { polling: data.api.polling, settings, gymValidDataLimit: data.api.gymValidDataLimit, - tutorialExcludeList: data.authentication.excludeFromTutorial || [], }) useStorage.setState((prev) => ({ @@ -143,13 +145,15 @@ export function Config({ children }) { menus: deepMerge({}, data.menus, prev.menus), userSettings: deepMerge({}, data.userSettings, prev.userSettings), settings: { + ...prev.settings, ...Object.fromEntries( Object.entries(settings).map(([k, v]) => [ k, - data.map.misc[k] || Object.keys(v)[0], + prev.settings[k] in v + ? prev.settings[k] + : data.map.misc[k] || Object.keys(v)[0], ]), ), - ...prev.settings, }, zoom: safeZoom, location, diff --git a/src/components/I.jsx b/src/components/I.jsx index da04124e1..7987605ff 100644 --- a/src/components/I.jsx +++ b/src/components/I.jsx @@ -1,5 +1,9 @@ +// @ts-check + import { styled } from '@mui/material/styles' +// TODO: Figure out how to type this + /** @type {import('react').FC} */ export const I = styled('i', { shouldForwardProp: (prop) => prop !== 'color' && prop !== 'size', diff --git a/src/components/Img.jsx b/src/components/Img.jsx index 0cc43aaad..3f4429882 100644 --- a/src/components/Img.jsx +++ b/src/components/Img.jsx @@ -2,22 +2,35 @@ import * as React from 'react' import { styled } from '@mui/material/styles' import Typography from '@mui/material/Typography' +import { useMemory } from '@store/useMemory' +import { useTranslateById } from '@hooks/useTranslateById' +import { NameTT } from './popups/NameTT' /** * @typedef {React.ImgHTMLAttributes} ImgProps - * @typedef {{ maxHeight?: React.CSSProperties['maxHeight'], maxWidth?: React.CSSProperties['maxWidth'], sx?: import('@mui/material').SxProps, zIndex?: React.CSSProperties['zIndex'] }} ExtraProps + * @typedef {Pick & { sx?: import('@mui/material').SxProps }} ExtraProps * @typedef {ImgProps & Partial} Props */ /** @type {React.FC} */ export const Img = styled('img', { shouldForwardProp: (prop) => - prop !== 'maxWidth' && prop !== 'maxHeight' && prop !== 'zIndex', -})((/** @type {Props} */ { maxWidth, maxHeight, zIndex }) => ({ - maxWidth, - maxHeight, - zIndex, -})) + prop !== 'maxWidth' && + prop !== 'maxHeight' && + prop !== 'zIndex' && + prop !== 'minHeight' && + prop !== 'minWidth', +})( + ( + /** @type {Props} */ { maxWidth, maxHeight, minHeight, minWidth, zIndex }, + ) => ({ + maxWidth, + maxHeight, + minHeight, + minWidth, + zIndex, + }), +) /** * A small wrapper around the Img component to display an icon next to text @@ -45,3 +58,49 @@ export const TextWithIcon = ({ {alt} ) + +/** + * + * @param {{ + * id: number, + * form?: number, + * evolution?: number, + * gender?: number, + * costume?: number, + * alignment?: number, + * shiny?: boolean, + * bread?: number, + * } & Omit} props + * @returns + */ +export const PokemonImg = ({ + id, + form = 0, + evolution = 0, + gender = 0, + costume = 0, + alignment = 0, + shiny = false, + bread = 0, + ...props +}) => { + const src = useMemory((s) => + s.Icons.getPokemon( + id, + form, + evolution, + gender, + costume, + alignment, + shiny, + bread, + ), + ) + const alt = useTranslateById().t(`${id}-${form}`) + + return ( + + {alt} + + ) +} diff --git a/src/components/Menu.jsx b/src/components/Menu.jsx index f4216f506..ccf139208 100644 --- a/src/components/Menu.jsx +++ b/src/components/Menu.jsx @@ -1,3 +1,5 @@ +// @ts-check + import * as React from 'react' import Box from '@mui/material/Box' import DialogContent from '@mui/material/DialogContent' @@ -26,7 +28,7 @@ import { GenericSearch } from './inputs/GenericSearch' * category: T * webhookCategory?: string * children: (index: number, key: string) => React.ReactNode - * categories?: import('@rm/types').Available[] + * categories?: (keyof import('@rm/types').Available)[] * extraButtons?: import('@components/dialogs/Footer').FooterButton[] * }} props */ diff --git a/src/components/StatusIcon.jsx b/src/components/StatusIcon.jsx index 648e244d1..f20a20503 100644 --- a/src/components/StatusIcon.jsx +++ b/src/components/StatusIcon.jsx @@ -13,10 +13,9 @@ import RuleIcon from '@mui/icons-material/Rule' * } & import('@mui/material').SvgIconProps} StatusIconProps */ -/** @type {React.ForwardRefExoticComponent} */ export const StatusIcon = React.forwardRef( ( - { + /** @type {StatusIconProps} */ { status, color, partialColor = color || 'info', diff --git a/src/components/auth/Discord.jsx b/src/components/auth/Discord.jsx index 954c4071d..af4168db6 100644 --- a/src/components/auth/Discord.jsx +++ b/src/components/auth/Discord.jsx @@ -1,3 +1,5 @@ +// @ts-check + import * as React from 'react' import Button from '@mui/material/Button' import { useTranslation } from 'react-i18next' @@ -19,6 +21,7 @@ export function DiscordButton({ const { t } = useTranslation() return ( + // TODO: Augment Mui Types