From 02f98e33a4354b8ab72c8ae8f2c9f4577a6e0762 Mon Sep 17 00:00:00 2001 From: Danyl Mishyn <35381314+lendihop@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:20:03 +0200 Subject: [PATCH 1/3] TW-1406: [EVM] balances loading (#162) * added endpoints * rm commented imports * refactoring according to comments * separate router for evm endpoints --- package.json | 3 + src/config.ts | 3 +- src/index.ts | 3 + src/routers/evm/covalent.ts | 68 ++++++++ src/routers/evm/index.ts | 44 +++++ src/utils/errors.ts | 2 +- src/utils/express-helpers.ts | 48 ++++++ src/utils/schemas.ts | 5 + yarn.lock | 324 ++++++++++++++++++++++++++++++++++- 9 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 src/routers/evm/covalent.ts create mode 100644 src/routers/evm/index.ts diff --git a/package.json b/package.json index fd947d1..e7a064d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "author": "Inokentii Mazhara ", "license": "MIT", "dependencies": { + "@covalenthq/client-sdk": "^1.0.2", "@ethersproject/address": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/strings": "^5.7.0", @@ -16,6 +17,7 @@ "@taquito/tzip12": "14.0.0", "@taquito/tzip16": "14.0.0", "@taquito/utils": "14.0.0", + "async-retry": "^1.3.3", "axios": "^0.27.2", "bignumber.js": "^9.1.0", "body-parser": "^1.20.2", @@ -49,6 +51,7 @@ "db-migration": "cd migrations/notifications && npx ts-node index.ts" }, "devDependencies": { + "@types/async-retry": "^1.4.8", "@types/body-parser": "^1.19.2", "@types/express": "^4.17.17", "@types/express-jwt": "^7.4.2", diff --git a/src/config.ts b/src/config.ts index 7ed45ca..f5f3a20 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,7 +12,8 @@ export const EnvVars = { THREE_ROUTE_API_AUTH_TOKEN: getEnv('THREE_ROUTE_API_AUTH_TOKEN'), REDIS_URL: getEnv('REDIS_URL'), ADMIN_USERNAME: getEnv('ADMIN_USERNAME'), - ADMIN_PASSWORD: getEnv('ADMIN_PASSWORD') + ADMIN_PASSWORD: getEnv('ADMIN_PASSWORD'), + COVALENT_API_KEY: getEnv('COVALENT_API_KEY') }; for (const name in EnvVars) { diff --git a/src/index.ts b/src/index.ts index 80d4f01..8b1d9ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { getNotifications } from './notifications/utils/get-notifications.util'; import { getParsedContent } from './notifications/utils/get-parsed-content.util'; import { getPlatforms } from './notifications/utils/get-platforms.util'; import { redisClient } from './redis'; +import { evmRouter } from './routers/evm'; import { adRulesRouter } from './routers/slise-ad-rules'; import { getABData } from './utils/ab-test'; import { cancelAliceBobOrder } from './utils/alice-bob/cancel-alice-bob-order'; @@ -334,6 +335,8 @@ app.get('/api/advertising-info', (_req, res) => { app.use('/api/slise-ad-rules', adRulesRouter); +app.use('/api/evm', evmRouter); + app.post('/api/magic-square-quest/start', async (req, res) => { try { await startMagicSquareQuest(req.body); diff --git a/src/routers/evm/covalent.ts b/src/routers/evm/covalent.ts new file mode 100644 index 0000000..e71fc3b --- /dev/null +++ b/src/routers/evm/covalent.ts @@ -0,0 +1,68 @@ +import { ChainID, CovalentClient } from '@covalenthq/client-sdk'; +import retry from 'async-retry'; + +import { EnvVars } from '../../config'; +import { CodedError } from '../../utils/errors'; + +const client = new CovalentClient(EnvVars.COVALENT_API_KEY, { enableRetry: false, threadCount: 10 }); + +const RETRY_OPTIONS = { maxRetryTime: 30_000 }; + +export const getEvmBalances = async (walletAddress: string, chainId: string) => + await retry( + async () => + client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, { + nft: true, + noNftAssetMetadata: true, + quoteCurrency: 'USD', + noSpam: false + }).then(({ data, error, error_message, error_code }) => { + if (error) { + throw new CodedError(Number(error_code) || 500, error_message); + } + + return data; + }), + RETRY_OPTIONS + ); + +export const getEvmTokensMetadata = async (walletAddress: string, chainId: string) => + await retry( + async () => + client.BalanceService.getTokenBalancesForWalletAddress(Number(chainId) as ChainID, walletAddress, { + nft: false, + quoteCurrency: 'USD', + noSpam: false + }).then(({ data, error, error_message, error_code }) => { + if (error) { + throw new CodedError(Number(error_code) || 500, error_message); + } + + return data; + }), + RETRY_OPTIONS + ); + +const CHAIN_IDS_WITHOUT_CACHE_SUPPORT = [10, 11155420, 43114, 43113]; + +export const getEvmCollectiblesMetadata = async (walletAddress: string, chainId: string) => { + const withUncached = CHAIN_IDS_WITHOUT_CACHE_SUPPORT.includes(Number(chainId)); + + return await retry( + async () => + client.NftService.getNftsForAddress(Number(chainId) as ChainID, walletAddress, { + withUncached, + noSpam: false + }).then(({ data, error, error_message, error_code }) => { + if (error) { + throw new CodedError(Number(error_code) || 500, error_message); + } + + return data; + }), + RETRY_OPTIONS + ); +}; + +export const getStringifiedResponse = (response: any) => + JSON.stringify(response, (_, value) => (typeof value === 'bigint' ? value.toString() : value)); diff --git a/src/routers/evm/index.ts b/src/routers/evm/index.ts new file mode 100644 index 0000000..5eb4ac5 --- /dev/null +++ b/src/routers/evm/index.ts @@ -0,0 +1,44 @@ +import { Router } from 'express'; + +import { withCodedExceptionHandler, withEvmQueryValidation } from '../../utils/express-helpers'; +import { getEvmBalances, getEvmCollectiblesMetadata, getEvmTokensMetadata, getStringifiedResponse } from './covalent'; + +export const evmRouter = Router(); + +evmRouter + .get( + '/balances', + withCodedExceptionHandler( + withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => { + const { walletAddress, chainId } = evmQueryParams; + + const data = await getEvmBalances(walletAddress, chainId); + + res.status(200).send(getStringifiedResponse(data)); + }) + ) + ) + .get( + '/tokens-metadata', + withCodedExceptionHandler( + withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => { + const { walletAddress, chainId } = evmQueryParams; + + const data = await getEvmTokensMetadata(walletAddress, chainId); + + res.status(200).send(getStringifiedResponse(data)); + }) + ) + ) + .get( + '/collectibles-metadata', + withCodedExceptionHandler( + withEvmQueryValidation(async (_1, res, _2, evmQueryParams) => { + const { walletAddress, chainId } = evmQueryParams; + + const data = await getEvmCollectiblesMetadata(walletAddress, chainId); + + res.status(200).send(getStringifiedResponse(data)); + }) + ) + ); diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 1877d31..1acaa32 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -8,7 +8,7 @@ interface CodedErrorForResponse { type StatusCodeNumber = (typeof StatusCodes)[keyof typeof StatusCodes]; export class CodedError extends Error { - constructor(public code: StatusCodeNumber, message: string, public errorCode?: string) { + constructor(public code: StatusCodeNumber | number, message: string, public errorCode?: string) { super(message); } diff --git a/src/utils/express-helpers.ts b/src/utils/express-helpers.ts index a1e58f9..9ad300f 100644 --- a/src/utils/express-helpers.ts +++ b/src/utils/express-helpers.ts @@ -2,7 +2,9 @@ import { NextFunction, Request, RequestHandler, Response, Router } from 'express import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, ValidationError } from 'yup'; import { basicAuth } from '../middlewares/basic-auth.middleware'; +import { CodedError } from './errors'; import logger from './logger'; +import { evmQueryParamsSchema } from './schemas'; interface ObjectStorageMethods { getByKey: (key: string) => Promise; @@ -33,6 +35,36 @@ export const withBodyValidation = return handler(req, res, next); }; +interface EvmQueryParams { + walletAddress: string; + chainId: string; +} + +type TypedEvmQueryRequestHandler = ( + req: Request, + res: Response, + next: NextFunction, + evmQueryParams: EvmQueryParams +) => void; + +export const withEvmQueryValidation = + (handler: TypedEvmQueryRequestHandler): RequestHandler => + async (req, res, next) => { + let evmQueryParams: EvmQueryParams; + + try { + evmQueryParams = await evmQueryParamsSchema.validate(req.query); + } catch (error) { + if (error instanceof ValidationError) { + return res.status(400).send({ error: error.message }); + } + + throw error; + } + + return handler(req, res, next, evmQueryParams); + }; + export const withExceptionHandler = (handler: RequestHandler): RequestHandler => async (req, res, next) => { @@ -44,6 +76,22 @@ export const withExceptionHandler = } }; +export const withCodedExceptionHandler = + (handler: RequestHandler): RequestHandler => + async (req, res, next) => { + try { + await handler(req, res, next); + } catch (error: any) { + logger.error(error); + + if (error instanceof CodedError) { + res.status(error.code).send(error.buildResponse()); + } else { + res.status(500).send({ message: error?.message }); + } + } + }; + interface ObjectStorageMethodsEntrypointsConfig { path: string; methods: ObjectStorageMethods; diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index d624f45..21e0253 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -108,6 +108,11 @@ const adStylesOverridesSchema = objectSchema().shape({ style: styleSchema.clone().required() }); +export const evmQueryParamsSchema = objectSchema().shape({ + walletAddress: nonEmptyStringSchema.clone().required('walletAddress is undefined'), + chainId: nonEmptyStringSchema.clone().required('chainId is undefined') +}); + const adPlacesRulesSchema = arraySchema() .of( objectSchema() diff --git a/yarn.lock b/yarn.lock index 028fa57..ac2c2e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,6 +55,26 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/runtime@^7.21.0": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57" + integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ== + dependencies: + regenerator-runtime "^0.14.0" + +"@covalenthq/client-sdk@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@covalenthq/client-sdk/-/client-sdk-1.0.2.tgz#58937f405f65e0e5eb1c7a6264da0da9b3371aac" + integrity sha512-jOFUW83qiONMm3jDeEEtQotJXPkQ9qJW9VRqxpLPifi+Nm6HPFKdRWwSRrpE5SZMDpZuK9VZQFk3t5tiJzys7w== + dependencies: + "@rollup/plugin-commonjs" "^25.0.4" + "@rollup/plugin-node-resolve" "^15.2.1" + big.js "^6.2.1" + date-fns "^2.30.0" + rollup "^3.29.1" + rollup-plugin-typescript2 "^0.35.0" + typescript "^5.1.6" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -427,6 +447,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -519,6 +544,47 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@rollup/plugin-commonjs@^25.0.4": + version "25.0.7" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" + integrity sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.30.3" + +"@rollup/plugin-node-resolve@^15.2.1": + version "15.2.3" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" + integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-builtin-module "^3.2.1" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^4.1.2": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^5.0.1": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" + integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + "@stablelib/binary@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" @@ -691,6 +757,13 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/async-retry@^1.4.8": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.8.tgz#eb32df13aceb9ba1a8a80e7fe518ff4e3fe46bb3" + integrity sha512-Qup/B5PWLe86yI5I3av6ePGaeQrIHNKCwbsQotD6aHQ6YkHsMUxVZkZsmx/Ry3VZQ6uysHwTjQ7666+k6UjVJA== + dependencies: + "@types/retry" "*" + "@types/body-parser@*": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -721,6 +794,11 @@ dependencies: "@types/node" "*" +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/express-jwt@0.0.42": version "0.0.42" resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz#4f04e1fadf9d18725950dc041808a4a4adf7f5ae" @@ -886,6 +964,16 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@types/retry@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e" + integrity sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw== + "@types/semaphore@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/semaphore/-/semaphore-1.1.1.tgz#035baeec703fe8cf9f56a2b7af22bdda4f22d6ea" @@ -1220,6 +1308,11 @@ base64-js@^1.3.0, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +big.js@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f" + integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ== + bignumber.js@^9.0.0, bignumber.js@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" @@ -1294,6 +1387,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1340,6 +1440,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -1463,6 +1568,11 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + compressible@^2.0.12: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -1588,6 +1698,13 @@ date-and-time@^2.0.0: resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-2.3.0.tgz#1b509be4c938dbbf5fc9c14d66e1daf9fe3cef13" integrity sha512-DY53oj742mykXjZzDxT7NxH5cxwBRb7FsVG5+8pcV96qU9JQd0UhA21pQB18fwwsXOXeSM0RJV4OzgVxu8eatg== +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + dateformat@^4.5.1: version "4.5.1" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c" @@ -1619,6 +1736,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -2096,6 +2218,11 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^2.0.1, estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2276,6 +2403,23 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-cache-dir@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + firebase-admin@^10.0.2: version "10.0.2" resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-10.0.2.tgz#d1142fb40738fa9b62f6625c4e3fc8cbc0ba61c6" @@ -2341,6 +2485,15 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2351,11 +2504,21 @@ fsevents@~2.3.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -2474,6 +2637,17 @@ glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^13.6.0, globals@^13.9.0: version "13.19.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" @@ -2553,6 +2727,11 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + gtoken@^5.0.4: version "5.3.2" resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" @@ -2640,6 +2819,13 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -2802,6 +2988,13 @@ is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.2" +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -2812,6 +3005,13 @@ is-callable@^1.1.4, is-callable@^1.2.3: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-core-module@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" @@ -2855,6 +3055,11 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -2885,6 +3090,13 @@ is-promise@^2.2.2: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-regex@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" @@ -3041,6 +3253,15 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -3140,6 +3361,13 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -3260,7 +3488,14 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -make-dir@^3.0.0: +magic-string@^0.30.3: + version "0.30.8" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -3379,6 +3614,13 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -3561,6 +3803,13 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + p-limit@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -3568,6 +3817,18 @@ p-limit@^3.0.1: dependencies: yocto-queue "^0.1.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -3588,6 +3849,11 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -3630,7 +3896,7 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== -picomatch@^2.3.1: +picomatch@^2.2.2, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -3694,6 +3960,13 @@ pino@^6.0.0, pino@^6.11.2: quick-format-unescaped "^4.0.3" sonic-boom "^1.0.2" +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -3873,6 +4146,11 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -3919,6 +4197,15 @@ resolve@^1.20.0, resolve@^1.22.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + retry-request@^4.0.0, retry-request@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.2.2.tgz#b7d82210b6d2651ed249ba3497f07ea602f1a903" @@ -3964,6 +4251,24 @@ ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rollup-plugin-typescript2@^0.35.0: + version "0.35.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.35.0.tgz#a84fb4e802b919613f31552c69c3415101b547c1" + integrity sha512-szcIO9hPUx3PhQl91u4pfNAH2EKbtrXaES+m163xQVE5O1CC0ea6YZV/5woiDDW3CR9jF2CszPrKN+AFiND0bg== + dependencies: + "@rollup/pluginutils" "^4.1.2" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + semver "^7.3.7" + tslib "^2.4.0" + +rollup@^3.29.1: + version "3.29.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + optionalDependencies: + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -4484,6 +4789,11 @@ tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -4552,6 +4862,11 @@ typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.1.6: + version "5.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" + integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== + unbox-primitive@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -4579,6 +4894,11 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 9afaa86f8467fbb7c1f4bc9d74a2a15a6f0ef498 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Thu, 11 Jul 2024 11:20:44 +0300 Subject: [PATCH 2/3] TW-1464 Prepare backend for new native ads (#165) * TW-1464 Prepare backend for new native ads * TW-1464 Update API documentation * TW-1464 Enable adding native ads in replace-only mode --- src/advertising/external-ads.ts | 8 +++++- src/routers/slise-ad-rules/ad-places.ts | 36 ++++++++++++++++++++++++- src/utils/schemas.ts | 8 +++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/advertising/external-ads.ts b/src/advertising/external-ads.ts index a735339..16328b9 100644 --- a/src/advertising/external-ads.ts +++ b/src/advertising/external-ads.ts @@ -99,6 +99,7 @@ export interface AdPlacesRule extends ExtVersionConstraints { }; stylesOverrides?: AdStylesOverrides[]; shouldHideOriginal?: boolean; + isNative?: boolean; } export interface PermanentAdPlacesRule extends ExtVersionConstraints { @@ -117,9 +118,14 @@ export interface PermanentAdPlacesRule extends ExtVersionConstraints { insertBeforeSelector?: string; insertAfterSelector?: string; insertionsCount?: number; - shouldUseDivWrapper: boolean; + shouldUseDivWrapper?: boolean; + wrapperType?: string; + colsBefore?: number; + colspan?: number; + colsAfter?: number; elementStyle?: Record; divWrapperStyle?: Record; + wrapperStyle?: Record; elementToMeasureSelector?: string; stylesOverrides?: AdStylesOverrides[]; shouldHideOriginal?: boolean; diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index 169fd29..40058d1 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -108,6 +108,10 @@ const transformAdPlacesDictionary = (rules: Rec * type: boolean * description: Whether original ads banners should be hidden but not removed * default: false + * isNative: + * type: boolean + * description: Whether the ad is native + * default: false * example: * urlRegexes: * - '^https://goerli\.etherscan\.io/?$' @@ -158,7 +162,6 @@ const transformAdPlacesDictionary = (rules: Rec * - urlRegexes * - adSelector * - parentSelector - * - shouldUseDivWrapper * properties: * urlRegexes: * type: array @@ -233,6 +236,32 @@ const transformAdPlacesDictionary = (rules: Rec * shouldUseDivWrapper: * type: boolean * description: Whether the ads banner should be wrapped in a div + * wrapperType: + * type: string + * enum: + * - div + * - tbody + * colsBefore: + * type: number + * integer: true + * min: 0 + * description: > + * If `wrapperType` is `tbody`, this property describes how many table columns should be inserted before + * the new ads banner. + * colspan: + * type: number + * integer: true + * min: 1 + * description: > + * If `wrapperType` is `tbody`, this property describes how many table columns should be spanned by the + * new ads banner. + * colsAfter: + * type: number + * integer: true + * min: 0 + * description: > + * If `wrapperType` is `tbody`, this property describes how many table columns should be inserted after + * the new ads banner. * elementStyle: * type: object * description: Style of the new ad banner @@ -243,6 +272,11 @@ const transformAdPlacesDictionary = (rules: Rec * description: Style of the div wrapper * additionalProperties: * type: string + * wrapperStyle: + * type: object + * description: Style of the new ad banner's wrapper + * additionalProperties: + * type: string * elementToMeasureSelector: * type: string * description: A selector of the element which should be measured to define banner size diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 21e0253..0128d4e 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -129,6 +129,7 @@ const adPlacesRulesSchema = arraySchema() .required(), stylesOverrides: arraySchema().of(adStylesOverridesSchema.clone().required()), shouldHideOriginal: booleanSchema(), + isNative: booleanSchema(), extVersion: versionRangeSchema.clone().required() }) .required() @@ -163,9 +164,14 @@ const permanentAdPlacesRulesSchema = arraySchema() insertBeforeSelector: cssSelectorSchema, insertAfterSelector: cssSelectorSchema, insertionsCount: numberSchema().integer().min(1), - shouldUseDivWrapper: booleanSchema().required(), + shouldUseDivWrapper: booleanSchema(), + wrapperType: stringSchema().oneOf(['div', 'tbody']), + colsBefore: numberSchema().integer().min(0), + colspan: numberSchema().integer().min(1), + colsAfter: numberSchema().integer().min(0), elementStyle: styleSchema, divWrapperStyle: styleSchema, + wrapperStyle: styleSchema, elementToMeasureSelector: cssSelectorSchema, stylesOverrides: arraySchema().of(adStylesOverridesSchema.clone().required()), shouldHideOriginal: booleanSchema(), From 3a36381cc3ec6deae7dc11aaa6a710dc5f369f95 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 29 Jul 2024 13:41:43 +0300 Subject: [PATCH 3/3] TW-1492: Ads replacement rework for mobile browsing (#167) * TW-1464 Prepare backend for new native ads * TW-1464 Update API documentation * TW-1464 Enable adding native ads in replace-only mode * TW-1492 Add some properties for ads definitions * TW-1492 Prepare the backend to additional fixtures for Mises * TW-1492 Add an entrypoint for elements to hide or remove --- src/advertising/external-ads.ts | 42 +++- src/routers/slise-ad-rules/ad-places.ts | 22 +- .../elements-to-hide-or-remove.ts | 210 ++++++++++++++++++ src/routers/slise-ad-rules/index.ts | 2 + src/routers/slise-ad-rules/providers.ts | 62 +++++- .../slise-ad-rules/replace-urls-blacklist.ts | 6 +- src/utils/schemas.ts | 71 ++++-- 7 files changed, 391 insertions(+), 24 deletions(-) create mode 100644 src/routers/slise-ad-rules/elements-to-hide-or-remove.ts diff --git a/src/advertising/external-ads.ts b/src/advertising/external-ads.ts index 16328b9..597ec44 100644 --- a/src/advertising/external-ads.ts +++ b/src/advertising/external-ads.ts @@ -11,6 +11,11 @@ export const stylePropsNames = [ 'aspect-ratio', 'background', 'border', + 'border-top', + 'border-bottom', + 'border-left', + 'border-right', + 'border-color', 'border-radius', 'bottom', 'box-shadow', @@ -49,6 +54,7 @@ export const stylePropsNames = [ 'min-inline-size', 'min-width', 'opacity', + 'order', 'overflow', 'overflow-anchor', 'overflow-wrap', @@ -127,8 +133,10 @@ export interface PermanentAdPlacesRule extends ExtVersionConstraints { divWrapperStyle?: Record; wrapperStyle?: Record; elementToMeasureSelector?: string; + elementsToMeasureSelectors?: Record<'width' | 'height', string>; stylesOverrides?: AdStylesOverrides[]; shouldHideOriginal?: boolean; + displayWidth?: string; } export interface AdProvidersByDomainRule extends ExtVersionConstraints { @@ -138,7 +146,9 @@ export interface AdProvidersByDomainRule extends ExtVersionConstraints { export interface AdProviderSelectorsRule extends ExtVersionConstraints { selectors: string[]; + negativeSelectors?: string[]; parentDepth?: number; + enableForMises?: boolean; } export interface AdProviderForAllSitesRule extends ExtVersionConstraints { @@ -149,6 +159,14 @@ export interface ReplaceAdsUrlsBlacklistEntry extends ExtVersionConstraints { regexes: string[]; } +export interface ElementsToHideOrRemoveEntry extends ExtVersionConstraints { + cssString: string; + parentDepth: number; + isMultiple: boolean; + urlRegexes: string[]; + shouldHide: boolean; +} + const AD_PLACES_RULES_KEY = 'ad_places_rules'; const AD_PROVIDERS_BY_SITES_KEY = 'ad_providers_by_sites'; const AD_PROVIDERS_ALL_SITES_KEY = 'ad_providers_all_sites'; @@ -156,6 +174,7 @@ const AD_PROVIDERS_LIST_KEY = 'ad_providers_list'; const PERMANENT_AD_PLACES_RULES_KEY = 'permanent_ad_places_rules'; const PERMANENT_NATIVE_AD_PLACES_RULES_KEY = 'permanent_native_ad_places_rules'; const REPLACE_ADS_URLS_BLACKLIST_KEY = 'replace_ads_urls_blacklist'; +const ELEMENTS_TO_HIDE_OR_REMOVE_KEY = 'elements_to_hide_or_remove'; export const adPlacesRulesMethods = objectStorageMethodsFactory(AD_PLACES_RULES_KEY, []); @@ -181,6 +200,11 @@ export const replaceAdsUrlsBlacklistMethods = objectStorageMethodsFactory( + ELEMENTS_TO_HIDE_OR_REMOVE_KEY, + [] +); + export const getAdProvidersForAllSites = async () => redisClient.smembers(AD_PROVIDERS_ALL_SITES_KEY); export const addAdProvidersForAllSites = async (providers: string[]) => @@ -191,5 +215,19 @@ export const removeAdProvidersForAllSites = async (providers: string[]) => const FALLBACK_VERSION = '0.0.0'; -export const filterByVersion = (rules: T[], version?: string) => - rules.filter(({ extVersion }) => versionSatisfiesRange(version ?? FALLBACK_VERSION, extVersion)); +export function filterRules(rules: T[], version: string | undefined): T[]; +export function filterRules( + rules: T[], + version: string | undefined, + isMisesBrowser: boolean +): T[]; +export function filterRules( + rules: T[], + version: string | undefined, + isMisesBrowser = false +) { + return rules.filter( + ({ extVersion, enableForMises = true }) => + versionSatisfiesRange(version ?? FALLBACK_VERSION, extVersion) && (!isMisesBrowser || enableForMises) + ); +} diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index 40058d1..4fbd172 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -1,7 +1,7 @@ import { Request, Router } from 'express'; import { - filterByVersion, + filterRules, permanentNativeAdPlacesMethods, permanentAdPlacesMethods, adPlacesRulesMethods, @@ -18,7 +18,7 @@ import { } from '../../utils/schemas'; const transformAdPlaces = (value: T[], req: Request) => - filterByVersion(value, req.query.extVersion as string | undefined); + filterRules(value, req.query.extVersion as string | undefined); const transformAdPlacesDictionary = (rules: Record, req: Request) => transformValues(rules, value => transformAdPlaces(value, req)); @@ -280,6 +280,18 @@ const transformAdPlacesDictionary = (rules: Rec * elementToMeasureSelector: * type: string * description: A selector of the element which should be measured to define banner size + * elementsToMeasureSelectors: + * type: object + * required: + * - width + * - height + * properties: + * width: + * type: string + * description: A selector of the element which should be measured to define banner width + * height: + * type: string + * description: A selector of the element which should be measured to define banner height * stylesOverrides: * type: array * items: @@ -288,6 +300,12 @@ const transformAdPlacesDictionary = (rules: Rec * type: boolean * description: Whether original ads banners should be hidden but not removed * default: false + * displayWidth: + * type: string + * description: > + * A range of display widths in a semver-like format where the rule is applicable. Numbers can be only + * integers. If not specified, the rule is applicable for all display widths. + * example: '>=1024 <1280' * example: * urlRegexes: * - '^https://etherscan\.io/tx/' diff --git a/src/routers/slise-ad-rules/elements-to-hide-or-remove.ts b/src/routers/slise-ad-rules/elements-to-hide-or-remove.ts new file mode 100644 index 0000000..1e479e0 --- /dev/null +++ b/src/routers/slise-ad-rules/elements-to-hide-or-remove.ts @@ -0,0 +1,210 @@ +import { Router, Request } from 'express'; + +import { + ElementsToHideOrRemoveEntry, + elementsToHideOrRemoveMethods, + filterRules +} from '../../advertising/external-ads'; +import { addObjectStorageMethodsToRouter } from '../../utils/express-helpers'; +import { transformValues } from '../../utils/helpers'; +import { elementsToHideOrRemoveDictionarySchema, hostnamesListSchema } from '../../utils/schemas'; + +export const elementsToHideOrRemoveRouter = Router(); + +const transformElementsToHideOrRemoveRules = (value: ElementsToHideOrRemoveEntry[], req: Request) => + filterRules(value, req.query.extVersion as string | undefined); +const transformRulesDictionary = (value: Record, req: Request) => + transformValues(value, rules => filterRules(rules, req.query.extVersion as string | undefined)); + +/** + * @swagger + * tags: + * name: Elements to hide or remove + * components: + * schemas: + * ElementsToHideOrRemoveEntry: + * allOf: + * - $ref: '#/components/schemas/ExtVersionConstraints' + * - type: object + * required: + * - cssString + * - parentDepth + * - isMultiple + * - urlRegexes + * - shouldHide + * properties: + * cssString: + * type: string + * parentDepth: + * type: number + * min: 0 + * integer: true + * description: > + * Indicates the depth of the parent element of the selected element + * isMultiple: + * type: boolean + * description: Whether the selector should select multiple elements + * urlRegexes: + * type: array + * items: + * type: string + * format: regex + * shouldHide: + * type: boolean + * ElementsToHideOrRemoveDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * $ref: '#/components/schemas/ElementsToHideOrRemoveEntry' + * example: + * 'm.economictimes.com': + * - extVersion: '>=1.21.1' + * cssString: '#iframeDisplay, #closeDisplay' + * parentDepth: 0 + * isMultiple: true + * urlRegexes: + * - "^https://m\\.economictimes\\.com" + * shouldHide: true + * /api/slise-ad-rules/elements-to-hide-or-remove/raw/all: + * get: + * summary: Get all rules for hiding or removing elements + * tags: + * - Elements to hide or remove + * responses: + * '200': + * description: A dictionary of all rules + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ElementsToHideOrRemoveDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/elements-to-hide-or-remove/{domain}/raw: + * get: + * summary: Get rules for hiding or removing elements by domain + * tags: + * - Elements to hide or remove + * parameters: + * - name: domain + * in: path + * required: true + * schema: + * type: string + * description: Domain name + * responses: + * '200': + * description: An array of rules + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ElementsToHideOrRemoveEntry' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/elements-to-hide-or-remove/{domain}: + * get: + * summary: Get rules for hiding or removing elements by domain and extension version + * tags: + * - Elements to hide or remove + * parameters: + * - name: domain + * in: path + * required: true + * schema: + * type: string + * description: Domain name + * - name: extVersion + * in: query + * schema: + * type: string + * default: '0.0.0' + * description: Extension version + * responses: + * '200': + * description: An array of rules + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/ElementsToHideOrRemoveEntry' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/elements-to-hide-or-remove: + * get: + * summary: Get all rules for hiding or removing elements filtered by extension version + * tags: + * - Elements to hide or remove + * parameters: + * - name: extVersion + * in: query + * schema: + * type: string + * default: '0.0.0' + * description: Extension version + * responses: + * '200': + * description: A dictionary of rules + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ElementsToHideOrRemoveDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Add rules for hiding or removing elements. If a rule already exists, it will be updated + * tags: + * - Elements to hide or remove + * security: + * - basicAuth: [] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ElementsToHideOrRemoveDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Delete rules for hiding or removing elements + * tags: + * - Elements to hide or remove + * security: + * - basicAuth: [] + * requestBody: + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - 'm.economictimes.com' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +addObjectStorageMethodsToRouter(elementsToHideOrRemoveRouter, { + path: '/', + methods: elementsToHideOrRemoveMethods, + keyName: 'domain', + objectValidationSchema: elementsToHideOrRemoveDictionarySchema, + keysArrayValidationSchema: hostnamesListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} entries have been removed`, + objectTransformFn: transformRulesDictionary, + valueTransformFn: transformElementsToHideOrRemoveRules +}); diff --git a/src/routers/slise-ad-rules/index.ts b/src/routers/slise-ad-rules/index.ts index ea84c98..a136617 100644 --- a/src/routers/slise-ad-rules/index.ts +++ b/src/routers/slise-ad-rules/index.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import { adPlacesRulesRouter } from './ad-places'; +import { elementsToHideOrRemoveRouter } from './elements-to-hide-or-remove'; import { adProvidersRouter } from './providers'; import { replaceUrlsBlacklistRouter } from './replace-urls-blacklist'; @@ -43,3 +44,4 @@ export const adRulesRouter = Router(); adRulesRouter.use('/ad-places', adPlacesRulesRouter); adRulesRouter.use('/providers', adProvidersRouter); adRulesRouter.use('/replace-urls-blacklist', replaceUrlsBlacklistRouter); +adRulesRouter.use('/elements-to-hide-or-remove', elementsToHideOrRemoveRouter); diff --git a/src/routers/slise-ad-rules/providers.ts b/src/routers/slise-ad-rules/providers.ts index e4f443a..253df9d 100644 --- a/src/routers/slise-ad-rules/providers.ts +++ b/src/routers/slise-ad-rules/providers.ts @@ -8,7 +8,7 @@ import { adProvidersMethods, adProvidersByDomainRulesMethods, AdProviderSelectorsRule, - filterByVersion, + filterRules, AdProvidersByDomainRule } from '../../advertising/external-ads'; import { basicAuth } from '../../middlewares/basic-auth.middleware'; @@ -72,10 +72,18 @@ import { * type: array * items: * type: string + * negativeSelectors: + * descriptions: Selectors for the elements that should not be touched even if they match `selectors` + * type: array + * items: + * type: string * parentDepth: * type: integer * minimum: 0 * default: 0 + * enableForMises: + * type: boolean + * default: true * AdByProviderSelector: * oneOf: * - type: string @@ -320,6 +328,46 @@ addObjectStorageMethodsToRouter(adProvidersRouter, { objectTransformFn: identity }); +/** + * @swagger + * /api/slise-ad-rules/providers/negative-selectors: + * get: + * summary: Get negative selectors for all providers filtered by extension version + * tags: + * - Known ads providers + * parameters: + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned + * responses: + * '200': + * description: Provider - selectors dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AdProvidersDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +adProvidersRouter.get( + '/negative-selectors', + withExceptionHandler(async (req, res) => { + const allRules = await adProvidersMethods.getAllValues(); + + const entries = Object.entries(allRules).map(([providerId, providerRules]): [string, string[]] => [ + providerId, + filterRules(providerRules, req.query.extVersion as string | undefined) + .map(({ negativeSelectors }) => negativeSelectors ?? []) + .flat() + ]); + + res.status(200).send(Object.fromEntries(entries)); + }) +); + /** * @swagger * /api/slise-ad-rules/providers/raw/all: @@ -377,6 +425,11 @@ addObjectStorageMethodsToRouter(adProvidersRouter, { * type: string * default: '0.0.0' * description: The extension version for which the rules should be returned + * - in: query + * name: isMisesBrowser + * schema: + * type: boolean + * default: false * responses: * '200': * description: List of CSS selectors @@ -406,6 +459,11 @@ addObjectStorageMethodsToRouter(adProvidersRouter, { * type: string * default: '0.0.0' * description: The extension version for which the rules should be returned + * - in: query + * name: isMisesBrowser + * schema: + * type: boolean + * default: false * responses: * '200': * description: Provider - selectors dictionary @@ -466,7 +524,7 @@ addObjectStorageMethodsToRouter(adProvidersRouter, { const transformAdProviderSelectorsRules = (rules: AdProviderSelectorsRule[], req: Request) => Array.from( new Set( - filterByVersion(rules, req.query.extVersion as string | undefined) + filterRules(rules, req.query.extVersion as string | undefined, req.query.isMisesBrowser === 'true') .map(({ selectors, parentDepth }) => isDefined(parentDepth) && parentDepth > 0 ? { selector: selectors.join(', '), parentDepth } : selectors ) diff --git a/src/routers/slise-ad-rules/replace-urls-blacklist.ts b/src/routers/slise-ad-rules/replace-urls-blacklist.ts index 35482d7..3d7b7de 100644 --- a/src/routers/slise-ad-rules/replace-urls-blacklist.ts +++ b/src/routers/slise-ad-rules/replace-urls-blacklist.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import { - filterByVersion, + filterRules, ReplaceAdsUrlsBlacklistEntry, replaceAdsUrlsBlacklistMethods } from '../../advertising/external-ads'; @@ -182,11 +182,11 @@ addObjectStorageMethodsToRouter `${entriesCount} blacklist entries have been removed`, objectTransformFn: (value, req) => - filterByVersion(Object.values(value).flat(), req.query.extVersion as string | undefined) + filterRules(Object.values(value).flat(), req.query.extVersion as string | undefined) .map(({ regexes }) => regexes) .flat(), valueTransformFn: (value, req) => - filterByVersion(value, req.query.extVersion as string | undefined) + filterRules(value, req.query.extVersion as string | undefined) .map(({ regexes }) => regexes) .flat() }); diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 0128d4e..be62bcc 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -18,7 +18,8 @@ import { StylePropName, stylePropsNames, AdProviderSelectorsRule, - ReplaceAdsUrlsBlacklistEntry + ReplaceAdsUrlsBlacklistEntry, + ElementsToHideOrRemoveEntry } from '../advertising/external-ads'; import { isValidSelectorsGroup } from '../utils/selectors.min.js'; import { isDefined } from './helpers'; @@ -173,9 +174,35 @@ const permanentAdPlacesRulesSchema = arraySchema() divWrapperStyle: styleSchema, wrapperStyle: styleSchema, elementToMeasureSelector: cssSelectorSchema, + elementsToMeasureSelectors: objectSchema() + .shape({ width: cssSelectorSchema.clone(), height: cssSelectorSchema.clone() }) + .test('all-fields-present', function (value: unknown) { + if (!value || typeof value !== 'object') { + return true; + } + + if (typeof (value as any).width === 'string' && typeof (value as any).height === 'string') { + return true; + } + + throw this.createError({ path: this.path, message: 'Both `width` and `height` fields must be specified' }); + }) + .default(undefined) as unknown as IObjectSchema<{ width: string; height: string } | undefined>, stylesOverrides: arraySchema().of(adStylesOverridesSchema.clone().required()), shouldHideOriginal: booleanSchema(), - extVersion: versionRangeSchema.clone().required() + extVersion: versionRangeSchema.clone().required(), + displayWidth: versionRangeSchema.clone().test('valid-boundary-values', (value: string | undefined) => { + if (!isDefined(value) || value.length === 0) { + return true; + } + + const nonIntegerNumberMatches = value.match(/\d+\.\d+/g); + if (isDefined(nonIntegerNumberMatches)) { + throw new Error('Display width must be an integer'); + } + + return true; + }) }) .test('insertion-place-specified', (value: PermanentAdPlacesRule | undefined) => { if (!value) { @@ -215,25 +242,39 @@ const adProvidersByDomainRulesSchema = arraySchema() export const adProvidersByDomainsRulesDictionarySchema: IObjectSchema> = makeDictionarySchema(hostnameSchema, adProvidersByDomainRulesSchema).required(); -const adProvidersSelectorsRuleSchema = objectSchema().shape({ +const adProvidersSelectorsRuleSchema: IObjectSchema = objectSchema().shape({ selectors: cssSelectorsListSchema.clone().required(), + negativeSelectors: cssSelectorsListSchema.clone(), extVersion: versionRangeSchema.clone().required(), - parentDepth: numberSchema().integer().min(0).default(0) + parentDepth: numberSchema().integer().min(0).default(0), + enableForMises: booleanSchema().default(true) }); -export const adProvidersDictionarySchema: IObjectSchema> = - makeDictionarySchema( - nonEmptyStringSchema.clone().required(), - arraySchema().of(adProvidersSelectorsRuleSchema.clone().required()).required() - ).required(); +export const adProvidersDictionarySchema = makeDictionarySchema( + nonEmptyStringSchema.clone().required(), + arraySchema().of(adProvidersSelectorsRuleSchema.clone().required()).required() +).required(); -const replaceUrlsBlacklistEntrySchema = objectSchema().shape({ +const replaceUrlsBlacklistEntrySchema: IObjectSchema = objectSchema().shape({ extVersion: versionRangeSchema.clone().required(), regexes: regexStringListSchema.clone().required() }); -export const replaceUrlsBlacklistDictionarySchema: IObjectSchema> = - makeDictionarySchema( - nonEmptyStringSchema.clone().required(), - arraySchema().of(replaceUrlsBlacklistEntrySchema.clone().required()).required() - ).required(); +export const replaceUrlsBlacklistDictionarySchema = makeDictionarySchema( + nonEmptyStringSchema.clone().required(), + arraySchema().of(replaceUrlsBlacklistEntrySchema.clone().required()).required() +).required(); + +const elementsToHideOrRemoveEntrySchema = objectSchema().shape({ + extVersion: versionRangeSchema.clone().required(), + cssString: cssSelectorSchema.clone().required(), + parentDepth: numberSchema().integer().min(0).required(), + isMultiple: booleanSchema().required(), + urlRegexes: regexStringListSchema.clone().required(), + shouldHide: booleanSchema().required() +}); + +export const elementsToHideOrRemoveDictionarySchema = makeDictionarySchema( + hostnameSchema.clone().required(), + arraySchema().of(elementsToHideOrRemoveEntrySchema.clone().required()).required() +).required();