diff --git a/.env.example b/.env.example index 86f7088..f3ce17d 100644 --- a/.env.example +++ b/.env.example @@ -13,9 +13,17 @@ USER_AGENT= # Sentry SENTRY_DSN= +# Browserless (for screenshots) +BROWSERLESS_ENDPOINT=ws://localhost:3000 +SCREENSHOT_HOST=host.docker.internal +BROWSERLESS_CONCURRENT=2 + # S3 parameters AWS_S3_ENDPOINT= +AWS_REGION= AWS_S3_BUCKET= +AWS_S3_ARCHIVE_BUCKET= +AWS_S3_PRIVATE_BUCKET= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..0bd04a7 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,87 @@ +name: Deploy + +on: + push: + branches: + - main + - develop + +jobs: + deploy-frontend: + runs-on: ubuntu-latest + + environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Deploy + uses: jakejarvis/s3-sync-action@master + with: + args: --acl public-read + env: + AWS_S3_ENDPOINT: ${{ secrets.AWS_S3_ENDPOINT }} + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SOURCE_DIR: 'dist' + + deploy-backend: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/app/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/app/data/DataArchiver.mjs b/app/data/DataArchiver.mjs index d5a2040..2e3e328 100644 --- a/app/data/DataArchiver.mjs +++ b/app/data/DataArchiver.mjs @@ -37,8 +37,8 @@ export default class DataArchiver get canRun() { return process.env.AWS_S3_ENDPOINT - && process.env.AWS_S3_REGION - && process.env.AWS_S3_BUCKET + && process.env.AWS_REGION + && process.env.AWS_S3_ARCHIVE_BUCKET && process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY; } @@ -46,7 +46,7 @@ export default class DataArchiver get s3Client() { return this._client ??= new S3Client({ endpoint: process.env.AWS_S3_ENDPOINT, - region: process.env.AWS_S3_REGION, + region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, @@ -88,7 +88,7 @@ export default class DataArchiver async uploadViaS3(file, destination) { return this.s3Client.send(new PutObjectCommand({ - Bucket: process.env.AWS_S3_BUCKET, + Bucket: process.env.AWS_S3_ARCHIVE_BUCKET, Key: destination, Body: await fs.readFile(file), ACL: 'public-read', diff --git a/app/data/ImageProcessor.mjs b/app/data/ImageProcessor.mjs index dd84a9f..ffa28a0 100644 --- a/app/data/ImageProcessor.mjs +++ b/app/data/ImageProcessor.mjs @@ -1,10 +1,14 @@ import fs from 'fs/promises'; import path from 'path'; import mkdirp from 'mkdirp'; +// eslint-disable-next-line import/no-unresolved +import PQueue from 'p-queue'; import prefixedConsole from '../common/prefixedConsole.mjs'; import { normalizeSplatnetResourcePath } from '../common/util.mjs'; import { exists } from '../common/fs.mjs'; +const queue = new PQueue({ concurrency: 4 }); + export default class ImageProcessor { destinationDirectory = 'dist'; @@ -15,17 +19,27 @@ export default class ImageProcessor this.siteUrl = process.env.SITE_URL; } - async process(url) { + async process(url, defer = true) { // Normalize the path let destination = this.normalize(url); // Download the image if necessary - await this.maybeDownload(url, destination); + let job = () => this.maybeDownload(url, destination); + // defer ? queue.add(job) : await job(); + if (defer) { + queue.add(job); + } else { + await job(); + } // Return the new public URL return [destination, this.publicUrl(destination)]; } + static onIdle() { + return queue.onIdle(); + } + normalize(url) { return normalizeSplatnetResourcePath(url); } @@ -53,6 +67,10 @@ export default class ImageProcessor try { let result = await fetch(url); + if (!result.ok) { + throw new Error(`Invalid image response code: ${result.status}`); + } + await mkdirp(path.dirname(this.localPath(destination))); await fs.writeFile(this.localPath(destination), result.body); } catch (e) { diff --git a/app/data/LocalizationProcessor.mjs b/app/data/LocalizationProcessor.mjs index 8941019..1b4c108 100644 --- a/app/data/LocalizationProcessor.mjs +++ b/app/data/LocalizationProcessor.mjs @@ -4,6 +4,9 @@ import mkdirp from 'mkdirp'; import jsonpath from 'jsonpath'; import get from 'lodash/get.js'; import set from 'lodash/set.js'; +import pLimit from 'p-limit'; + +const limit = pLimit(1); function makeArray(value) { return Array.isArray(value) ? value : [value]; @@ -55,6 +58,12 @@ export class LocalizationProcessor { } async updateLocalizations(data) { + // We're reading, modifying, and writing back to the same file, + // so we have to make sure the whole operation is atomic. + return limit(() => this._updateLocalizations(data)); + } + + async _updateLocalizations(data) { let localizations = await this.readData(); for (let { path, value } of this.dataIterations(data)) { diff --git a/app/data/index.mjs b/app/data/index.mjs index 8f7eae7..a371098 100644 --- a/app/data/index.mjs +++ b/app/data/index.mjs @@ -1,10 +1,13 @@ import * as Sentry from '@sentry/node'; +import S3Syncer from '../sync/S3Syncer.mjs'; +import { canSync } from '../sync/index.mjs'; import GearUpdater from './updaters/GearUpdater.mjs'; import StageScheduleUpdater from './updaters/StageScheduleUpdater.mjs'; import CoopUpdater from './updaters/CoopUpdater.mjs'; import FestivalUpdater from './updaters/FestivalUpdater.mjs'; import XRankUpdater from './updaters/XRankUpdater.mjs'; import StagesUpdater from './updaters/StagesUpdater.mjs'; +import ImageProcessor from './ImageProcessor.mjs'; function updaters() { return [ @@ -40,7 +43,7 @@ export async function update(config = 'default') { let settings = configs[config]; - for (let updater of updaters()) { + await Promise.all(updaters().map(async updater => { updater.settings = settings; try { await updater.updateIfNeeded(); @@ -48,6 +51,11 @@ export async function update(config = 'default') { console.error(e); Sentry.captureException(e); } + })); + + if (canSync()) { + await ImageProcessor.onIdle(); + await (new S3Syncer).upload(); } console.info(`Done running ${config} updaters`); diff --git a/app/data/updaters/DataUpdater.mjs b/app/data/updaters/DataUpdater.mjs index 72a1cf8..dadcead 100644 --- a/app/data/updaters/DataUpdater.mjs +++ b/app/data/updaters/DataUpdater.mjs @@ -4,6 +4,7 @@ import { Console } from 'node:console'; import mkdirp from 'mkdirp'; import jsonpath from 'jsonpath'; import ical from 'ical-generator'; +import pFilter from 'p-filter'; import prefixedConsole from '../../common/prefixedConsole.mjs'; import SplatNet3Client from '../../splatnet/SplatNet3Client.mjs'; import ImageProcessor from '../ImageProcessor.mjs'; @@ -11,7 +12,6 @@ import NsoClient from '../../splatnet/NsoClient.mjs'; import { locales, regionalLocales, defaultLocale } from '../../../src/common/i18n.mjs'; import { LocalizationProcessor } from '../LocalizationProcessor.mjs'; import { deriveId, getDateParts, getTopOfCurrentHour } from '../../common/util.mjs'; - export default class DataUpdater { name = null; @@ -70,7 +70,6 @@ export default class DataUpdater async updateIfNeeded() { if (!(await this.shouldUpdate())) { - this.console.info('No need to update data'); return; } @@ -135,16 +134,18 @@ export default class DataUpdater } // Retrieve data for missing languages - for (let locale of this.locales.filter(l => l !== initialLocale)) { - processor = new LocalizationProcessor(locale, this.localizations); - - if (await processor.hasMissingLocalizations(data)) { - this.console.info(`Retrieving localized data for ${locale.code}`); + let processors = this.locales.filter(l => l !== initialLocale) + .map(l => new LocalizationProcessor(l, this.localizations)); + let missing = await pFilter(processors, p => p.hasMissingLocalizations(data)); - let regionalData = await this.getData(locale); + if (missing.length > 0) { + await Promise.all(missing.map(async (processor) => { + let regionalData = await this.getData(processor.locale); this.deriveIds(regionalData); await processor.updateLocalizations(regionalData); - } + + this.console.info(`Retrieved localized data for: ${processor.locale.code}`); + })); } } @@ -166,6 +167,8 @@ export default class DataUpdater jsonpath.apply(data, expression, url => mapping[url]); } + await ImageProcessor.onIdle(); + return images; } diff --git a/app/data/updaters/FestivalRankingUpdater.mjs b/app/data/updaters/FestivalRankingUpdater.mjs index 9df6f3f..1880a87 100644 --- a/app/data/updaters/FestivalRankingUpdater.mjs +++ b/app/data/updaters/FestivalRankingUpdater.mjs @@ -43,7 +43,7 @@ export default class FestivalRankingUpdater extends DataUpdater async getData(locale) { const data = await this.splatnet(locale).getFestRankingData(this.festID); - for (const team of data.data.fest.teams) { + await Promise.all(data.data.fest.teams.map(async (team) => { let pageInfo = team.result?.rankingHolders?.pageInfo; while (pageInfo?.hasNextPage) { @@ -62,7 +62,7 @@ export default class FestivalRankingUpdater extends DataUpdater pageInfo = page.data.node.result.rankingHolders.pageInfo; } - } + })); return data; } diff --git a/app/data/updaters/FestivalUpdater.mjs b/app/data/updaters/FestivalUpdater.mjs index d84d92e..919476e 100644 --- a/app/data/updaters/FestivalUpdater.mjs +++ b/app/data/updaters/FestivalUpdater.mjs @@ -1,11 +1,14 @@ import fs from 'fs/promises'; import jsonpath from 'jsonpath'; +import pLimit from 'p-limit'; import { getFestId } from '../../common/util.mjs'; import ValueCache from '../../common/ValueCache.mjs'; import { regionTokens } from '../../splatnet/NsoClient.mjs'; import FestivalRankingUpdater from './FestivalRankingUpdater.mjs'; import DataUpdater from './DataUpdater.mjs'; +const limit = pLimit(1); + function generateFestUrl(id) { return process.env.DEBUG ? `https://s.nintendo.com/av5ja-lp1/znca/game/4834290508791808?p=/fest_record/${id}` : @@ -79,7 +82,7 @@ export default class FestivalUpdater extends DataUpdater // Get the detailed data for each Splatfest // (unless we're getting localization-specific data) if (locale === this.defaultLocale) { - for (let node of result.data.festRecords.nodes) { + await Promise.all(result.data.festRecords.nodes.map(async node => { let detailResult = await this.getFestivalDetails(node); Object.assign(node, detailResult.data.fest); @@ -93,7 +96,7 @@ export default class FestivalUpdater extends DataUpdater this.console.error(e); } } - } + })); } return result; @@ -128,7 +131,11 @@ export default class FestivalUpdater extends DataUpdater return data; } - async formatDataForWrite(data) { + formatDataForWrite(data) { + return limit(() => this._formatDataForWrite(data)); + } + + async _formatDataForWrite(data) { // Combine this region's data with the other regions' data. let result = null; try { diff --git a/app/data/updaters/XRankUpdater.mjs b/app/data/updaters/XRankUpdater.mjs index 0e4b275..12c8615 100644 --- a/app/data/updaters/XRankUpdater.mjs +++ b/app/data/updaters/XRankUpdater.mjs @@ -46,10 +46,9 @@ export default class XRankUpdater extends DataUpdater let result = await this.splatnet(locale).getXRankingData(this.divisionKey); let seasons = this.getSeasons(result.data); - for (let season of seasons) { - this.deriveSeasonId(season); - await this.updateSeasonDetail(season); - } + seasons.forEach(s => this.deriveSeasonId(s)); + + await Promise.all(seasons.map(season => this.updateSeasonDetail(season))); return result; } @@ -66,9 +65,9 @@ export default class XRankUpdater extends DataUpdater } async updateSeasonDetail(season) { - for (let type of this.splatnet().getXRankingDetailQueryTypes()) { + await Promise.all(this.splatnet().getXRankingDetailQueryTypes().map(type => { let updater = new XRankDetailUpdater(season.id, season.endTime, type); - await updater.updateIfNeeded(); - } + return updater.updateIfNeeded(); + })); } } diff --git a/app/index.mjs b/app/index.mjs index 2128c32..85c23fa 100644 --- a/app/index.mjs +++ b/app/index.mjs @@ -10,6 +10,7 @@ import BlueskyClient from './social/clients/BlueskyClient.mjs'; import ThreadsClient from './social/clients/ThreadsClient.mjs'; import { archiveData } from './data/DataArchiver.mjs'; import { sentryInit } from './common/sentry.mjs'; +import { sync, syncUpload, syncDownload } from './sync/index.mjs'; consoleStamp(console); dotenv.config(); @@ -26,6 +27,9 @@ const actions = { splatnet: update, warmCaches, dataArchive: archiveData, + sync, + syncUpload, + syncDownload, }; const command = process.argv[2]; diff --git a/app/screenshots/ScreenshotHelper.mjs b/app/screenshots/ScreenshotHelper.mjs index 39b9510..ba7e195 100644 --- a/app/screenshots/ScreenshotHelper.mjs +++ b/app/screenshots/ScreenshotHelper.mjs @@ -1,5 +1,5 @@ import { URL } from 'url'; -import puppeteer from 'puppeteer'; +import puppeteer from 'puppeteer-core'; import HttpServer from './HttpServer.mjs'; const defaultViewport = { @@ -36,12 +36,9 @@ export default class ScreenshotHelper this.#httpServer = new HttpServer; await this.#httpServer.open(); - // Launch a new Chrome instance - this.#browser = await puppeteer.launch({ - args: [ - '--no-sandbox', // Allow running as root inside the Docker container - ], - // headless: false, // For testing + // Connect to Browserless + this.#browser = await puppeteer.connect({ + browserWSEndpoint: process.env.BROWSERLESS_ENDPOINT, }); // Create a new page and set the viewport @@ -66,7 +63,8 @@ export default class ScreenshotHelper await this.applyViewport(options.viewport); // Navigate to the URL - let url = new URL(`http://localhost:${this.#httpServer.port}/screenshots/`); + let host = process.env.SCREENSHOT_HOST || 'localhost'; + let url = new URL(`http://${host}:${this.#httpServer.port}/screenshots/`); url.hash = path; let params = { diff --git a/app/social/StatusGeneratorManager.mjs b/app/social/StatusGeneratorManager.mjs index c442285..591f6a2 100644 --- a/app/social/StatusGeneratorManager.mjs +++ b/app/social/StatusGeneratorManager.mjs @@ -12,9 +12,6 @@ export default class StatusGeneratorManager /** @type {Client[]} */ clients; - /** @type {ScreenshotHelper} */ - screenshotHelper; - console(generator = null, client = null) { let prefixes = ['Social', generator?.name, client?.name].filter(s => s); return prefixedConsole(...prefixes); @@ -23,45 +20,82 @@ export default class StatusGeneratorManager constructor(generators = [], clients = []) { this.generators = generators; this.clients = clients; - this.screenshotHelper = new ScreenshotHelper; } async sendStatuses(force = false) { - for (let generator of this.generators) { - try { - await this.#generateAndSend(generator, force); - } catch (e) { - this.console(generator).error(`Error generating status: ${e}`); - Sentry.captureException(e); - } - } + let availableClients = await this.#getAvailableClients(); + + // Create screenshots in parallel (via Browserless) + let statusPromises = this.#getStatuses(availableClients, force); - await this.screenshotHelper.close(); + // Process each client in parallel (while maintaining post order) + await this.#sendStatusesToClients(statusPromises, availableClients); } - async #generateAndSend(generator, force) { - let clientsToPost = []; + async #getAvailableClients() { + let clients = []; for (let client of this.clients) { if (!(await client.canSend())) { - this.console(generator, client).warn('Client cannot send (missing credentials)'); + this.console(client).warn('Client cannot send (missing credentials)'); continue; } - if (force || await generator.shouldPost(client)) { - clientsToPost.push(client); - } + clients.push(client); } - if (clientsToPost.length === 0) { - this.console(generator).info('No status to post, skipping'); + return clients; + } + + #getStatuses(availableClients, force) { + return this.generators.map(generator => this.#getStatus(availableClients, generator, force)); + } + + async #getStatus(availableClients, generator, force) { + let screenshotHelper = new ScreenshotHelper; + try { + let clients = []; + + for (let client of availableClients) { + if (force || await generator.shouldPost(client)) { + clients.push(client); + } + } + + if (clients.length === 0) { + this.console(generator).info('No status to post, skipping'); - return; + return null; + } + + await screenshotHelper.open(); + let status = await generator.getStatus(screenshotHelper); + + return { generator, status, clients }; + } catch (e) { + this.console(generator).error(`Error generating status: ${e}`); + Sentry.captureException(e); + } finally { + await screenshotHelper.close(); } - let status = await generator.getStatus(this.screenshotHelper); + return null; + } + + #sendStatusesToClients(statusPromises, availableClients) { + return Promise.allSettled(availableClients.map(client => this.#sendStatusesToClient(statusPromises, client))); + } + + async #sendStatusesToClient(statusPromises, client) { + for (let promise of statusPromises) { + let statusDetails = await promise; - await Promise.all(clientsToPost.map(client => this.#sendToClient(generator, status, client))); + if (statusDetails && statusDetails.clients.includes(client)) { + let { generator, status } = statusDetails; + + await this.#sendToClient(generator, status, client); + } + } } async #sendToClient(generator, status, client) { diff --git a/app/social/clients/TwitterClient.mjs b/app/social/clients/TwitterClient.mjs index e1b456d..2ca54e9 100644 --- a/app/social/clients/TwitterClient.mjs +++ b/app/social/clients/TwitterClient.mjs @@ -33,7 +33,7 @@ export default class TwitterClient extends Client // Upload images let mediaIds = await Promise.all( status.media.map(async m => { - let id = await this.api().v1.uploadMedia(m.file, { mimeType: m.type }); + let id = await this.api().v1.uploadMedia(Buffer.from(m.file), { mimeType: m.type }); if (m.altText) { await this.api().v1.createMediaMetadata(id, { alt_text: { text: m.altText } }); diff --git a/app/social/generators/ChallengeStatus.mjs b/app/social/generators/ChallengeStatus.mjs index 286bdeb..2fe1981 100644 --- a/app/social/generators/ChallengeStatus.mjs +++ b/app/social/generators/ChallengeStatus.mjs @@ -13,7 +13,7 @@ export default class ChallengeStatus extends StatusGenerator let schedule = useEventSchedulesStore().activeSchedule; - if (schedule.activeTimePeriod) { + if (schedule?.activeTimePeriod) { return schedule; } } diff --git a/app/social/generators/EggstraWorkStatus.mjs b/app/social/generators/EggstraWorkStatus.mjs index 5f14590..bebc031 100644 --- a/app/social/generators/EggstraWorkStatus.mjs +++ b/app/social/generators/EggstraWorkStatus.mjs @@ -17,7 +17,7 @@ export default class EggstraWorkStatus extends StatusGenerator async getDataTime() { let schedule = await this.getActiveSchedule(); - return Date.parse(schedule.startTime); + return Date.parse(schedule?.startTime); } async _getStatus() { diff --git a/app/social/index.mjs b/app/social/index.mjs index 9634eea..cdbf538 100644 --- a/app/social/index.mjs +++ b/app/social/index.mjs @@ -1,3 +1,5 @@ +import S3Syncer from '../sync/S3Syncer.mjs'; +import { canSync } from '../sync/index.mjs'; import FileWriter from './clients/FileWriter.mjs'; import ImageWriter from './clients/ImageWriter.mjs'; import MastodonClient from './clients/MastodonClient.mjs'; @@ -63,8 +65,12 @@ export function testStatusGeneratorManager(additionalClients) { ); } -export function sendStatuses() { - return defaultStatusGeneratorManager().sendStatuses(); +export async function sendStatuses() { + await defaultStatusGeneratorManager().sendStatuses(); + + if (canSync()) { + await (new S3Syncer).upload(); + } } export function testStatuses(additionalClients = []) { diff --git a/app/splatnet/NsoClient.mjs b/app/splatnet/NsoClient.mjs index 0dd11e9..1e0e31b 100644 --- a/app/splatnet/NsoClient.mjs +++ b/app/splatnet/NsoClient.mjs @@ -1,9 +1,13 @@ // eslint-disable-next-line import/no-unresolved import CoralApi from 'nxapi/coral'; import { addUserAgent } from 'nxapi'; +import pLimit from 'p-limit'; import ValueCache from '../common/ValueCache.mjs'; import prefixedConsole from '../common/prefixedConsole.mjs'; +const coralLimit = pLimit(1); +const webServiceLimit = pLimit(1); + let _nxapiInitialized = false; function initializeNxapi() { @@ -72,15 +76,17 @@ export default class NsoClient } async getCoralApi(useCache = true) { - let data = useCache - ? await this._getCoralCache().getData() - : null; + return coralLimit(async () => { + let data = useCache + ? await this._getCoralCache().getData() + : null; - if (!data) { - data = await this._createCoralSession(); - } + if (!data) { + data = await this._createCoralSession(); + } - return CoralApi.createWithSavedToken(data); + return CoralApi.createWithSavedToken(data); + }); } async _createCoralSession() { @@ -101,16 +107,18 @@ export default class NsoClient } async getWebServiceToken(id, useCache = true) { - let tokenCache = this._getWebServiceTokenCache(id); - let token = useCache - ? await tokenCache.getData() - : null; - - if (!token) { - token = await this._createWebServiceToken(id, tokenCache); - } - - return token.accessToken; + return webServiceLimit(async () => { + let tokenCache = this._getWebServiceTokenCache(id); + let token = useCache + ? await tokenCache.getData() + : null; + + if (!token) { + token = await this._createWebServiceToken(id, tokenCache); + } + + return token.accessToken; + }); } async _createWebServiceToken(id, tokenCache) { diff --git a/app/splatnet/SplatNet3Client.mjs b/app/splatnet/SplatNet3Client.mjs index f27fcdf..0de9bb7 100644 --- a/app/splatnet/SplatNet3Client.mjs +++ b/app/splatnet/SplatNet3Client.mjs @@ -1,9 +1,13 @@ import fs from 'fs/promises'; +import pLimit from 'p-limit'; import ValueCache from '../common/ValueCache.mjs'; import prefixedConsole from '../common/prefixedConsole.mjs'; export const SPLATNET3_WEB_SERVICE_ID = '4834290508791808'; +// Concurrent request limit +const limit = pLimit(5); + export default class SplatNet3Client { baseUrl = 'https://api.lp1.av5ja.srv.nintendo.net'; @@ -108,17 +112,18 @@ export default class SplatNet3Client async getGraphQL(body = {}) { await this._maybeStartSession(); + let webViewVersion = await this._webViewVersion(); - let response = await fetch(this.baseUrl + '/api/graphql', { + let response = await limit(() => fetch(this.baseUrl + '/api/graphql', { method: 'POST', headers: { 'Authorization': `Bearer ${this.bulletToken.bulletToken}`, - 'X-Web-View-Ver': await this._webViewVersion(), + 'X-Web-View-Ver': webViewVersion, 'Content-Type': 'application/json', 'Accept-Language': this.acceptLanguage, }, body: JSON.stringify(body), - }); + })); if (!response.ok) { throw new Error(`Invalid GraphQL response code: ${response.status}`); diff --git a/app/sync/S3Syncer.mjs b/app/sync/S3Syncer.mjs new file mode 100644 index 0000000..0240660 --- /dev/null +++ b/app/sync/S3Syncer.mjs @@ -0,0 +1,88 @@ +import path from 'path'; +import { S3Client } from '@aws-sdk/client-s3'; +import { S3SyncClient } from 's3-sync-client'; +import mime from 'mime-types'; + +export default class S3Syncer +{ + download() { + this.log('Downloading files...'); + + return Promise.all([ + this.syncClient.sync(this.publicBucket, `${this.localPath}/dist`, { + filters: this.filters, + }), + this.syncClient.sync(this.privateBucket, `${this.localPath}/storage`, { + filters: this.privateFilters, + }), + ]); + } + + upload() { + this.log('Uploading files...'); + + return Promise.all([ + this.syncClient.sync(`${this.localPath}/dist`, this.publicBucket, { + filters: this.filters, + commandInput: input => ({ + ACL: 'public-read', + ContentType: mime.lookup(input.Key), + CacheControl: input.Key.startsWith('data/') + ? 'no-cache, stale-while-revalidate=5, stale-if-error=86400' + : undefined, + }), + }), + this.syncClient.sync(`${this.localPath}/storage`, this.privateBucket, { + filters: this.privateFilters, + }), + ]); + } + + get s3Client() { + return this._s3Client ??= new S3Client({ + endpoint: process.env.AWS_S3_ENDPOINT, + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, + }); + } + + /** @member {S3SyncClient} */ + get syncClient() { + return this._syncClient ??= new S3SyncClient({ client: this.s3Client }); + } + + get publicBucket() { + return `s3://${process.env.AWS_S3_BUCKET}`; + } + + get privateBucket() { + return `s3://${process.env.AWS_S3_PRIVATE_BUCKET}`; + } + + get localPath() { + return path.resolve('.'); + } + + get filters() { + return [ + { exclude: () => true }, // Exclude everything by default + { include: (key) => key.startsWith('assets/splatnet/') }, + { include: (key) => key.startsWith('data/') }, + { exclude: (key) => key.startsWith('data/archive/') }, + { include: (key) => key.startsWith('status-screenshots/') }, + ]; + } + + get privateFilters() { + return [ + { exclude: (key) => key.startsWith('archive/') }, + ]; + } + + log(message) { + console.log(`[S3] ${message}`); + } +} diff --git a/app/sync/index.mjs b/app/sync/index.mjs new file mode 100644 index 0000000..ddb7dea --- /dev/null +++ b/app/sync/index.mjs @@ -0,0 +1,41 @@ +import S3Syncer from './S3Syncer.mjs'; + +export function canSync() { + return !!( + process.env.AWS_ACCESS_KEY_ID && + process.env.AWS_SECRET_ACCESS_KEY && + process.env.AWS_S3_BUCKET && + process.env.AWS_S3_PRIVATE_BUCKET + ); +} + +async function doSync(download, upload) { + if (!canSync()) { + console.warn('Missing S3 connection parameters'); + return; + } + + const syncer = new S3Syncer(); + + if (download) { + console.info('Downloading files...'); + await syncer.download(); + } + + if (upload) { + console.info('Uploading files...'); + await syncer.upload(); + } +} + +export function sync() { + return doSync(true, true); +} + +export function syncUpload() { + return doSync(false, true); +} + +export function syncDownload() { + return doSync(true, false); +} diff --git a/docker-compose.override.yml.dev.example b/docker-compose.override.yml.dev.example new file mode 100644 index 0000000..a663fc2 --- /dev/null +++ b/docker-compose.override.yml.dev.example @@ -0,0 +1,21 @@ +# Example Docker Compose override file for local development + +services: + app: + platform: linux/amd64 # Needed for Apple Silicon + build: + dockerfile: docker/app/Dockerfile + init: true + restart: unless-stopped + environment: + BROWSERLESS_ENDPOINT: ws://browserless:3000 + SCREENSHOT_HOST: app + depends_on: + - browserless + volumes: + - .:/app + + browserless: + platform: linux/arm64 # Needed for Apple Silicon + ports: + - 3000:3000 diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example deleted file mode 100644 index 0e087ba..0000000 --- a/docker-compose.override.yml.example +++ /dev/null @@ -1,22 +0,0 @@ -version: "3.8" - -services: - app: - # This may be necessary on M1 Macs: - #platform: linux/amd64 - - nginx: - #ports: - # - "127.0.0.1:8888:80" - - environment: - VIRTUAL_HOST: splatoon3.ink,www.splatoon3.ink - - networks: - - default - - nginx-proxy - -networks: - nginx-proxy: - external: - name: nginxproxy_default diff --git a/docker-compose.override.yml.prod.example b/docker-compose.override.yml.prod.example new file mode 100644 index 0000000..97697de --- /dev/null +++ b/docker-compose.override.yml.prod.example @@ -0,0 +1,25 @@ +# Example Docker Compose override file for production + +services: + app: + image: ghcr.io/misenhower/splatoon3.ink:main + init: true + restart: unless-stopped + environment: + BROWSERLESS_ENDPOINT: ws://browserless:3000 + SCREENSHOT_HOST: app + depends_on: + - browserless + env_file: + - .env + labels: [ "com.centurylinklabs.watchtower.scope=splatoon3ink" ] + + browserless: + labels: [ "com.centurylinklabs.watchtower.scope=splatoon3ink" ] + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 30 --scope splatoon3ink + labels: [ "com.centurylinklabs.watchtower.scope=splatoon3ink" ] diff --git a/docker-compose.yml b/docker-compose.yml index c9c74d2..5091e6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,9 @@ -version: "3.8" +# See docker-compose.override.yml.* example files for dev/prod environments services: - app: - build: docker/app - init: true + browserless: + image: ghcr.io/browserless/chromium restart: unless-stopped - working_dir: /app - volumes: - - ./:/app - command: npm run cron - - nginx: - image: nginx - restart: unless-stopped - volumes: - - ./docker/nginx/conf.d:/etc/nginx/conf.d:ro - - ./dist:/usr/share/nginx/html:ro + environment: + CONCURRENT: ${BROWSERLESS_CONCURRENT:-1} + QUEUED: ${BROWSERLESS_QUEUED:-100} diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index e771e31..af26e7b 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -1,12 +1,15 @@ FROM node:20 -# Puppeteer support -# Adapted from: https://github.com/puppeteer/puppeteer/blob/2d50ec5b384f2ae8eb02a534843caceca9f58ffe/docker/Dockerfile -RUN apt-get update \ - && apt-get install -y wget gnupg \ - && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update \ - && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* +# App setup +WORKDIR /app +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install NPM dependencies +COPY package*.json ./ +RUN npm ci + +# Copy app files and build +COPY . . +RUN npm run build + +CMD ["npm", "run", "start"] diff --git a/package-lock.json b/package-lock.json index 0e08069..2ba5e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,12 @@ "masto": "^6.7.0", "mkdirp": "^1.0.4", "nxapi": "^1.4.0", + "p-filter": "^4.1.0", + "p-limit": "^6.1.0", + "p-queue": "^8.0.1", "pinia": "^2.0.22", - "puppeteer": "^18.0.3", + "puppeteer-core": "^23.8.0", + "s3-sync-client": "^4.3.1", "sharp": "^0.32.0", "threads-api": "^1.4.0", "twitter-api-v2": "^1.12.7", @@ -261,6 +265,47 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@aws-sdk/abort-controller": { + "version": "3.370.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.370.0.tgz", + "integrity": "sha512-/W4arzC/+yVW/cvEXbuwvG0uly4yFSZnnIA+gkqgAm+0HVfacwcPpNf4BjyxjnvIdh03l7w2DriF6MlKUfiQ3A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.370.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/abort-controller/node_modules/@aws-sdk/types": { + "version": "3.370.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.370.0.tgz", + "integrity": "sha512-8PGMKklSkRKjunFhzM2y5Jm0H2TBu7YRNISdYzXLUHKSP9zlMEYagseKVdmox0zKHf1LXVNuSlUV2b6SRrieCQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/abort-controller/node_modules/@smithy/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.535.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.535.0.tgz", @@ -1925,6 +1970,53 @@ "node": ">=14" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.1.tgz", + "integrity": "sha512-0kdAbmic3J09I6dT8e9vE2JOCSt13wHCW5x/ly8TSt2bDtuIWe2TgLZZDHdcziw9AVCzflMAXCrVyRIhIs44Ng==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.7", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -2657,6 +2749,12 @@ "vue": "^2.7.0 || ^3.0.0" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2681,6 +2779,7 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -2852,14 +2951,15 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -3129,6 +3229,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3280,7 +3392,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/bare-events": { "version": "2.2.1", @@ -3334,6 +3447,15 @@ } ] }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3416,6 +3538,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3491,6 +3614,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", "engines": { "node": "*" } @@ -3673,6 +3797,20 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/chromium-bidi": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.8.0.tgz", + "integrity": "sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/cli-table": { "version": "0.3.11", "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", @@ -3774,7 +3912,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -3851,33 +3990,6 @@ "luxon": "~3.3.0" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dependencies": { - "node-fetch": "2.6.7" - } - }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3983,11 +4095,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4067,6 +4180,54 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/degenerator/node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/degenerator/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/delay": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-6.0.0.tgz", @@ -4122,9 +4283,10 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1045489", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1045489.tgz", - "integrity": "sha512-D+PTmWulkuQW4D1NTiCRCFxF7pQPn0hgp4YyX4wAQ6xYXKOadSWPR3ENGDQ47MW/Ewc9v2rpC/UEEGahgBYpSQ==" + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause" }, "node_modules/didyoumean": { "version": "1.2.2", @@ -5421,7 +5583,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -5447,6 +5608,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/events-to-async": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.1.tgz", @@ -5526,6 +5693,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -5628,6 +5796,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -5876,10 +6045,25 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -5969,6 +6153,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -5996,6 +6181,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -6005,6 +6214,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6073,6 +6283,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -6191,16 +6407,30 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/ical-generator": { @@ -6322,6 +6552,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -6351,6 +6582,19 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6849,6 +7093,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -6955,6 +7205,18 @@ "semver": "bin/semver.js" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonpath": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", @@ -7082,14 +7344,12 @@ } }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/luxon": { @@ -7209,6 +7469,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7233,6 +7494,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7258,9 +7525,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/multiformats": { "version": "9.9.0", @@ -7319,6 +7587,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -7649,16 +7926,31 @@ "node": ">= 0.8.0" } }, + "node_modules/p-filter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", + "license": "MIT", + "dependencies": { + "p-map": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz", + "integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==", + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7679,6 +7971,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.2.tgz", + "integrity": "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.0.1.tgz", + "integrity": "sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.3.tgz", + "integrity": "sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -7688,6 +8049,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -7748,6 +8141,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -7800,7 +8194,8 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" }, "node_modules/picocolors": { "version": "1.0.0", @@ -8154,6 +8549,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -8181,6 +8577,25 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -8204,60 +8619,21 @@ "node": ">=6" } }, - "node_modules/puppeteer": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-18.2.1.tgz", - "integrity": "sha512-7+UhmYa7wxPh2oMRwA++k8UGVDxh3YdWFB52r9C3tM81T6BU7cuusUSxImz0GEYSOYUKk/YzIhkQ6+vc0gHbxQ==", - "deprecated": "< 21.5.0 is no longer supported", - "hasInstallScript": true, - "dependencies": { - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "puppeteer-core": "18.2.1" - }, - "engines": { - "node": ">=14.1.0" - } - }, "node_modules/puppeteer-core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-18.2.1.tgz", - "integrity": "sha512-MRtTAZfQTluz3U2oU/X2VqVWPcR1+94nbA2V6ZrSZRVEwLqZ8eclZ551qGFQD/vD2PYqHJwWOW/fpC721uznVw==", + "version": "23.8.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.8.0.tgz", + "integrity": "sha512-c2ymGN2M//We7pC+JhP2dE/g4+qnT89BO+EMSZyJmecN3DN6RNqErA7eH7DrWoNIcU75r2nP4VHa4pswAL6NVg==", + "license": "Apache-2.0", "dependencies": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.1045489", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.9.0" + "@puppeteer/browsers": "2.4.1", + "chromium-bidi": "0.8.0", + "debug": "^4.3.7", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" }, "engines": { - "node": ">=14.1.0" - } - }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">=18" } }, "node_modules/qs": { @@ -8502,6 +8878,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8558,6 +8935,19 @@ "tslib": "^2.1.0" } }, + "node_modules/s3-sync-client": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/s3-sync-client/-/s3-sync-client-4.3.1.tgz", + "integrity": "sha512-nWbbKCNnXmWvD8XwdWhX25VNxIhgQEm6vXqSYjwyBNZI07OuMOr/LNOYmEPcLfqFFjy55ZNcFSBI18W29ybuUw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/abort-controller": "^3.x.x", + "@aws-sdk/client-s3": "^3.x.x" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -8618,12 +9008,10 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8678,11 +9066,6 @@ "node": ">=4" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/sentence-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", @@ -8899,6 +9282,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -8908,6 +9301,34 @@ "tslib": "^2.0.3" } }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8951,6 +9372,12 @@ "resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.2.20231119210145.tgz", "integrity": "sha512-ySnftvxfn7zuGUgBxcjGPYhVHs8QaUCxHdbUNw4u46bXwtxQ82eyvXc+sDQI5TcHHPHz821PyrhtWW3pt9QdAg==" }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", @@ -9353,7 +9780,8 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" }, "node_modules/tinycolor2": { "version": "1.6.0", @@ -9567,6 +9995,12 @@ "rxjs": "*" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/uint8arrays": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", @@ -9594,6 +10028,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -9610,6 +10045,15 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "optional": true }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -9678,6 +10122,12 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10001,9 +10451,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -10037,11 +10488,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yaml": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", @@ -10109,27 +10555,29 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 7b0ce4e..ad529c7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore", "lint-fix": "npm run lint -- --fix", "cron": "node app/index.mjs cron", + "start": "npm run sync:download && npm run splatnet:quick && npm run social && npm run cron", "social": "node app/index.mjs social", "social:test": "node app/index.mjs socialTest", "social:test:image": "node app/index.mjs socialTestImage", @@ -18,7 +19,10 @@ "splatnet": "node app/index.mjs splatnet default", "splatnet:all": "node app/index.mjs splatnet all", "warmCaches": "node app/index.mjs warmCaches", - "data:archive": "node app/index.mjs dataArchive" + "data:archive": "node app/index.mjs dataArchive", + "sync": "node app/index.mjs sync", + "sync:upload": "node app/index.mjs syncUpload", + "sync:download": "node app/index.mjs syncDownload" }, "dependencies": { "@atproto/api": "^0.11.2", @@ -37,8 +41,12 @@ "masto": "^6.7.0", "mkdirp": "^1.0.4", "nxapi": "^1.4.0", + "p-filter": "^4.1.0", + "p-limit": "^6.1.0", + "p-queue": "^8.0.1", "pinia": "^2.0.22", - "puppeteer": "^18.0.3", + "puppeteer-core": "^23.8.0", + "s3-sync-client": "^4.3.1", "sharp": "^0.32.0", "threads-api": "^1.4.0", "twitter-api-v2": "^1.12.7", diff --git a/src/common/time.js b/src/common/time.js index 1f3f779..6ae63be 100644 --- a/src/common/time.js +++ b/src/common/time.js @@ -51,13 +51,18 @@ export function formatShortDuration(value) { const { t } = useI18n(); let { negative, days, hours, minutes, seconds } = getDurationParts(value); + days = days && t('time.days', days); + hours = hours && t('time.hours', hours); + minutes = minutes && t('time.minutes', { n: minutes }, minutes); + seconds = t('time.seconds', { n: seconds }, seconds); + if (days) - return t('time.days', { n: `${negative}${days}` }, days); + return hours ? `${negative}${days} ${hours}` : `${negative}${days}`; if (hours) - return t('time.hours', { n: `${negative}${hours}` }, hours); + return `${negative}${hours}`; if (minutes) - return t('time.minutes', { n: `${negative}${minutes}` }, minutes); - return t('time.seconds', { n: `${negative}${seconds}` }, seconds); + return `${negative}${minutes}`; + return `${negative}${seconds}`; } export function formatShortDurationFromNow(value) { diff --git a/src/components/gear/GearCard.vue b/src/components/gear/GearCard.vue index 13fdbef..a67d594 100644 --- a/src/components/gear/GearCard.vue +++ b/src/components/gear/GearCard.vue @@ -72,7 +72,7 @@