mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-03-21 17:24:37 -05:00
commit
6029efd782
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
storage
|
||||
15
.env.example
15
.env.example
|
|
@ -12,15 +12,16 @@ VUE_APP_GOOGLE_ANALYTICS_ID=
|
|||
# (Optional) Sentry error reporting (https://sentry.io)
|
||||
SENTRY_DSN=
|
||||
|
||||
# (Optional) S3 parameters
|
||||
AWS_S3_ENDPOINT=
|
||||
AWS_REGION=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_PRIVATE_BUCKET=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
# (Optional) Twitter API parameters
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ACCESS_TOKEN_KEY=
|
||||
TWITTER_ACCESS_TOKEN_SECRET=
|
||||
|
||||
# (Optional) Deployment server info (user@host)
|
||||
SHIPIT_PRODUCTION_SERVER=user@host
|
||||
SHIPIT_PRODUCTION_DIR=/absolute/path
|
||||
|
||||
SHIPIT_STAGING_SERVER=user@host
|
||||
SHIPIT_STAGING_DIR=/absolute/path
|
||||
|
|
|
|||
46
.eslintrc.cjs
Normal file
46
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-env node */
|
||||
// const createAliasSetting = require('@vue/eslint-config-airbnb/createAliasSetting');
|
||||
|
||||
module.exports = {
|
||||
'root': true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'eslint:recommended',
|
||||
],
|
||||
'rules': {
|
||||
// ESLint
|
||||
// 'indent': ['warn', 2, { 'SwitchCase': 1 }],
|
||||
'comma-dangle': ['warn', 'always-multiline'],
|
||||
'no-unused-vars': ['warn', { 'args': 'none' }],
|
||||
'semi': 'warn',
|
||||
'quotes': ['warn' , 'single'],
|
||||
'object-curly-spacing': ['warn', 'always'],
|
||||
|
||||
// Vue
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/max-attributes-per-line': ['warn', { singleline: { max: 4 } }],
|
||||
'vue/html-self-closing': ['warn', { html: { void: 'always' } }],
|
||||
'vue/no-deprecated-filter': 'off',
|
||||
'vue/require-toggle-inside-transition': 'off',
|
||||
'vue/no-deprecated-destroyed-lifecycle': 'off',
|
||||
},
|
||||
'globals': {
|
||||
'__dirname': 'readonly',
|
||||
'process': 'readonly',
|
||||
'require': 'readonly',
|
||||
'module': 'readonly',
|
||||
'Buffer': 'readonly',
|
||||
},
|
||||
'env': {
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
'ignorePatterns': [
|
||||
'src/assets/i18n/index.mjs', // "assert" syntax is currently unrecognized
|
||||
],
|
||||
settings: {
|
||||
// ...createAliasSetting({
|
||||
// '@': './src',
|
||||
// }),
|
||||
},
|
||||
};
|
||||
87
.github/workflows/deploy.yml
vendored
Normal file
87
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -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
|
||||
14
docker-compose.override.yml.dev.example
Normal file
14
docker-compose.override.yml.dev.example
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Example Docker Compose override file for local development
|
||||
|
||||
services:
|
||||
app:
|
||||
platform: linux/amd64 # Needed for Apple Silicon
|
||||
build:
|
||||
dockerfile: docker/app/Dockerfile
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
browserless:
|
||||
platform: linux/arm64 # Needed for Apple Silicon
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
version: '2'
|
||||
|
||||
# This adds config options for use with nginx-proxy:
|
||||
# https://github.com/jwilder/nginx-proxy
|
||||
|
||||
# Alternatively, a public port for nginx could be exposed here.
|
||||
|
||||
services:
|
||||
|
||||
nginx:
|
||||
environment:
|
||||
VIRTUAL_HOST: splatoon2.ink,www.splatoon2.ink
|
||||
networks:
|
||||
- default
|
||||
- nginx-proxy
|
||||
|
||||
networks:
|
||||
nginx-proxy:
|
||||
external:
|
||||
name: nginxproxy_default
|
||||
17
docker-compose.override.yml.prod.example
Normal file
17
docker-compose.override.yml.prod.example
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Example Docker Compose override file for production
|
||||
|
||||
services:
|
||||
app:
|
||||
# Use a different tag if needed
|
||||
# image: ghcr.io/misenhower/splatoon2.ink:develop
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=splatoon2ink" ]
|
||||
|
||||
browserless:
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=splatoon2ink" ]
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command: --interval 30 --scope splatoon2ink
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=splatoon2ink" ]
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
build: docker/app
|
||||
image: ghcr.io/misenhower/splatoon2.ink
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./:/app
|
||||
environment:
|
||||
USE_BROWSERLESS: true
|
||||
BROWSERLESS_ENDPOINT: ws://browserless:3000
|
||||
SCREENSHOT_HOST: app
|
||||
depends_on:
|
||||
- browserless
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./logs:/logs
|
||||
- ./dist:/usr/share/nginx/html:ro
|
||||
environment:
|
||||
CONCURRENT: ${BROWSERLESS_CONCURRENT:-1}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,15 @@
|
|||
FROM node:12
|
||||
|
||||
# Chrome/Puppeteer support
|
||||
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-kacst fonts-freefont-ttf libxss1 \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dumb-init
|
||||
# https://github.com/Yelp/dumb-init
|
||||
# This fixes issues with zombie Chrome processes:
|
||||
# https://github.com/GoogleChrome/puppeteer/issues/615
|
||||
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64.deb &&\
|
||||
dpkg -i dumb-init_*.deb
|
||||
FROM node:20
|
||||
|
||||
# App setup
|
||||
WORKDIR /app
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
CMD ["yarn", "cron"]
|
||||
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"]
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
# Rate limiting zone (Note: 10m is the size of the address pool, not time)
|
||||
limit_req_zone $binary_remote_addr zone=main:10m rate=10r/s;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
charset utf-8;
|
||||
charset_types application/json;
|
||||
|
||||
# Docker reverse proxy IP passthrough
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
real_ip_header X-Real-IP;
|
||||
|
||||
# Log to the main (Docker stdout) log
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
# Rate limiting: allow bursts with no delay
|
||||
limit_req zone=main burst=500 nodelay;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Force browsers to check for updated data
|
||||
location /data/ {
|
||||
add_header Cache-Control no-cache;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location /twitter-images/ {
|
||||
add_header Cache-Control no-cache;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
|
||||
location ~ \.ics$ {
|
||||
add_header Content-Type text/calendar;
|
||||
}
|
||||
|
||||
# CORS (for modern ES module support and third-party integrations)
|
||||
location /assets/ {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
|
||||
# Block access to the file used for screenshots
|
||||
location = /screenshots.html {
|
||||
internal;
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect www to non-www
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name www.splatoon2.ink;
|
||||
return 301 $scheme://splatoon2.ink$request_uri;
|
||||
}
|
||||
23033
package-lock.json
generated
Normal file
23033
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
|
|
@ -6,15 +6,20 @@
|
|||
"build": "vue-cli-service build --modern --no-clean",
|
||||
"lint": "vue-cli-service lint",
|
||||
"cron": "node src/app/cron",
|
||||
"start": "npm run sync:download && npm run splatnet && npm run twitter && npm run cron",
|
||||
"locale-man": "node node_modules/locale-man/ -l en,es,es-MX,fr,fr-CA,de,nl,it,ru,ja -o src/locale",
|
||||
"splatnet": "node src/app splatnet",
|
||||
"twitter": "node src/app twitter",
|
||||
"twitter:test": "node src/app twitterTest",
|
||||
"sync": "node src/app sync",
|
||||
"sync:upload": "node src/app syncUpload",
|
||||
"sync:download": "node src/app syncDownload",
|
||||
"utility:copyTranslation": "node src/utility copyTranslation",
|
||||
"utility:getSplatNetLanguageFiles": "node src/utility getSplatNetLanguageFiles",
|
||||
"utility:updateGear": "node src/utility updateGear"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.685.0",
|
||||
"axios": "^0.19.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bulma": "~0.6.1",
|
||||
|
|
@ -25,18 +30,20 @@
|
|||
"delay": "^4.4.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"ecstatic": "^4.1.4",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"he": "^1.1.1",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"jsonpath": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"make-runnable": "^1.3.6",
|
||||
"mime-types": "^2.1.35",
|
||||
"mkdirp": "^1.0.4",
|
||||
"module-alias": "^2.1.0",
|
||||
"moment-timezone": "^0.5.13",
|
||||
"puppeteer": "^13.6.0",
|
||||
"puppeteer": "^23.7.1",
|
||||
"raven": "^2.1.1",
|
||||
"s3-sync-client": "^4.3.1",
|
||||
"twitter-api-v2": "^1.15.0",
|
||||
"v-click-outside": "^3.0.1",
|
||||
"vue": "^2.6.11",
|
||||
|
|
@ -54,25 +61,8 @@
|
|||
"locale-man": "^0.0.5",
|
||||
"sass": "^1.50.1",
|
||||
"sass-loader": "^10",
|
||||
"shipit-cli": "^5.3.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-prototype-builtins": 0
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
|
|
|
|||
30
readme.md
30
readme.md
|
|
@ -1,5 +1,8 @@
|
|||
# [Splatoon2.ink](https://splatoon2.ink)
|
||||
|
||||
> [!NOTE]
|
||||
> Check out [Splatoon3.ink](https://github.com/misenhower/splatoon3.ink) as well!
|
||||
|
||||
Splatoon2.ink shows the current and upcoming map schedules for Splatoon 2.
|
||||
|
||||
This site was built with [Vue.js](https://vuejs.org/) and [Bulma](http://bulma.io/).
|
||||
|
|
@ -7,8 +10,6 @@ All data comes from the SplatNet 2 API.
|
|||
|
||||
More information about Nintendo's API can be found [here](https://github.com/ZekeSnider/NintendoSwitchRESTAPI).
|
||||
|
||||
**Note:** The Splatnet updater script requires Node.js v8 or later.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Clone this repo:
|
||||
|
|
@ -26,38 +27,27 @@ cp .env.example .env
|
|||
You can retrieve your `iksm_session` ID value (`NINTENDO_SESSION_ID` inside `.env`) using [Fiddler](http://www.telerik.com/fiddler) or a similar tool.
|
||||
See [here](https://github.com/frozenpandaman/splatnet2statink#setup-instructions) for further instructions.
|
||||
|
||||
I recommend using [Yarn](https://yarnpkg.com/en/) to manage JS dependencies.
|
||||
|
||||
```shell
|
||||
yarn install # Install dependencies
|
||||
yarn splatnet # Retrieve updates from Splatnet and the Salmon Run calendar
|
||||
yarn serve # Start the webpack dev server
|
||||
npm install # Install dependencies
|
||||
npm run splatnet # Retrieve updates from Splatnet and the Salmon Run calendar
|
||||
npm run serve # Start the webpack dev server
|
||||
```
|
||||
|
||||
Data retrieved from Splatnet is stored in the `public/data` directory.
|
||||
Data retrieved from Splatnet is stored in the `dist/data` directory.
|
||||
|
||||
By default, the dev server will run on port 8080.
|
||||
When running `yarn serve` you can access the site by going to http://localhost:8080.
|
||||
When running `npm run serve` you can access the site by going to http://localhost:8080.
|
||||
|
||||
## Production
|
||||
|
||||
Build minified assets for production:
|
||||
|
||||
```shell
|
||||
yarn build
|
||||
npm run build
|
||||
```
|
||||
|
||||
Retrieve updates from Splatnet every hour via [node-cron](https://github.com/kelektiv/node-cron):
|
||||
|
||||
```shell
|
||||
yarn cron
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
I use a Docker container on my production server to build production assets and retrieve data from Splatnet.
|
||||
|
||||
```shell
|
||||
sudo docker-compose run --rm app yarn build # Build production assets
|
||||
sudo docker-compose up -d app # Start periodic updates
|
||||
npm run cron
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
require('dotenv').config();
|
||||
|
||||
module.exports = shipit => {
|
||||
shipit.initConfig({
|
||||
production: {
|
||||
servers: process.env.SHIPIT_PRODUCTION_SERVER,
|
||||
dir: process.env.SHIPIT_PRODUCTION_DIR,
|
||||
},
|
||||
staging: {
|
||||
servers: process.env.SHIPIT_STAGING_SERVER,
|
||||
dir: process.env.SHIPIT_STAGING_DIR,
|
||||
},
|
||||
});
|
||||
|
||||
const dockerApp = 'sudo docker-compose exec -T app ';
|
||||
|
||||
function options() {
|
||||
return { cwd: shipit.config.dir };
|
||||
}
|
||||
|
||||
// Git
|
||||
|
||||
shipit.blTask('pull', () => {
|
||||
return shipit.remote('git pull', options());
|
||||
});
|
||||
|
||||
// JS
|
||||
|
||||
shipit.blTask('install-js-deps', () => {
|
||||
return shipit.remote(dockerApp + 'yarn install', options());
|
||||
});
|
||||
|
||||
shipit.blTask('build-js', ['install-js-deps'], () => {
|
||||
return shipit.remote(dockerApp + 'yarn run build', options());
|
||||
});
|
||||
|
||||
shipit.blTask('deploy-js', [
|
||||
'pull',
|
||||
'build-js',
|
||||
]);
|
||||
|
||||
// App
|
||||
|
||||
shipit.blTask('restart-app', () => {
|
||||
return shipit.remote('sudo docker-compose restart app', options());
|
||||
});
|
||||
|
||||
shipit.blTask('splatnet', () => {
|
||||
return shipit.remote(dockerApp + 'yarn splatnet', options());
|
||||
});
|
||||
|
||||
// Combination Tasks
|
||||
|
||||
shipit.blTask('deploy', [
|
||||
'pull',
|
||||
'deploy-js',
|
||||
'restart-app',
|
||||
]);
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ module.exports = {
|
|||
splatnet: require('./updater').updateAll,
|
||||
twitter: require('./twitter').maybePostTweets,
|
||||
twitterTest: require('./twitter').testScreenshots,
|
||||
sync: require('./sync').sync,
|
||||
syncUpload: require('./sync').syncUpload,
|
||||
syncDownload: require('./sync').syncDownload,
|
||||
};
|
||||
|
||||
require('make-runnable/custom')({ printOutputFrame: false });
|
||||
|
|
|
|||
|
|
@ -21,18 +21,30 @@ function startHttpServer() {
|
|||
});
|
||||
}
|
||||
|
||||
function getBrowser() {
|
||||
// Use Browserless when configured
|
||||
if (process.env.USE_BROWSERLESS) {
|
||||
return puppeteer.connect({
|
||||
browserWSEndpoint: process.env.BROWSERLESS_ENDPOINT,
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise just launch normally
|
||||
return puppeteer.launch({
|
||||
args: [
|
||||
'--no-sandbox', // Allow running as root inside the Docker container
|
||||
],
|
||||
// headless: false, // For testing
|
||||
});
|
||||
}
|
||||
|
||||
async function captureScreenshot(options) {
|
||||
// Create an HTTP server
|
||||
const server = await startHttpServer();
|
||||
const { port } = server.address();
|
||||
|
||||
// Launch a new Chrome instance
|
||||
const browser = await puppeteer.launch({
|
||||
args: [
|
||||
'--no-sandbox', // Allow running as root inside the Docker container
|
||||
],
|
||||
// headless: false, // For testing
|
||||
});
|
||||
const browser = await getBrowser();
|
||||
|
||||
// Create a new page and set the viewport
|
||||
const page = await browser.newPage();
|
||||
|
|
@ -40,7 +52,8 @@ async function captureScreenshot(options) {
|
|||
page.setViewport(thisViewport);
|
||||
|
||||
// Navigate to the URL
|
||||
let url = new URL(`http://localhost:${port}/screenshots.html`);
|
||||
let host = process.env.SCREENSHOT_HOST || 'localhost';
|
||||
let url = new URL(`http://${host}:${port}/screenshots.html`);
|
||||
url.hash = options.hash;
|
||||
await page.goto(url, {
|
||||
waitUntil: 'networkidle0', // Wait until the network is idle
|
||||
|
|
|
|||
79
src/app/sync/S3Syncer.js
Normal file
79
src/app/sync/S3Syncer.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
const path = require('path');
|
||||
const { S3Client } = require('@aws-sdk/client-s3');
|
||||
const { S3SyncClient } = require('s3-sync-client');
|
||||
const mime = require('mime-types');
|
||||
|
||||
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`),
|
||||
]);
|
||||
}
|
||||
|
||||
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),
|
||||
]);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {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/') },
|
||||
{ include: (key) => key.startsWith('twitter-images/') },
|
||||
];
|
||||
}
|
||||
|
||||
log(message) {
|
||||
console.log(`[S3] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = S3Syncer;
|
||||
43
src/app/sync/index.js
Normal file
43
src/app/sync/index.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const S3Syncer = require('./S3Syncer');
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function sync() {
|
||||
return doSync(true, true);
|
||||
}
|
||||
|
||||
function syncUpload() {
|
||||
return doSync(false, true);
|
||||
}
|
||||
|
||||
function syncDownload() {
|
||||
return doSync(true, false);
|
||||
}
|
||||
|
||||
module.exports = { canSync, sync, syncUpload, syncDownload };
|
||||
|
|
@ -31,7 +31,7 @@ class GearTweet extends TwitterPostBase {
|
|||
return captureGearScreenshot(now);
|
||||
}
|
||||
|
||||
getPublicImageFilename(data) {
|
||||
getPublicImageFilename() {
|
||||
return 'gear.png';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class ScheduleTweet extends TwitterPostBase {
|
|||
return captureScheduleScreenshot(data.regular.start_time, this.globalSplatfestOpenInAllRegions());
|
||||
}
|
||||
|
||||
getPublicImageFilename(data) {
|
||||
getPublicImageFilename() {
|
||||
return 'schedule.png';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,14 +124,15 @@ class SplatfestTweet extends TwitterPostBase {
|
|||
return `Reminder: The ${isGlobal ? 'global' : regionDemonyms} Splatfest starts in ${data.text}! #splatfest #splatoon2`;
|
||||
return `Reminder: The Splatfest starts in ${this.regionInfo.name} in ${data.text}! #splatfest #splatoon2`;
|
||||
|
||||
case 'end':
|
||||
case 'end': {
|
||||
let hours = (data.festival.times.result - this.getDataTime()) / 60 / 60;
|
||||
let duration = (hours == 1) ? '1 hour' : `${hours} hours`;
|
||||
if (isSimultaneous)
|
||||
return `The ${isGlobal ? 'global' : regionDemonyms} Splatfest is now closed. Results will be posted in ${duration}! #splatfest #splatoon2`;
|
||||
return `The Splatfest is now closed in ${this.regionInfo.name}. Results will be posted in ${duration}! #splatfest #splatoon2`;
|
||||
}
|
||||
|
||||
case 'result':
|
||||
case 'result': {
|
||||
let winner = data.results.summary.total ? 'bravo' : 'alpha';
|
||||
|
||||
// Just hardcoding this in here for now to avoid dealing with loading the Vuex store separately
|
||||
|
|
@ -141,6 +142,7 @@ class SplatfestTweet extends TwitterPostBase {
|
|||
let results = resultsFormat.replace('{team}', teamName);
|
||||
|
||||
return `${isGlobal ? 'Global' : regionDemonyms} Splatfest results: ${results} #splatfest #splatoon2`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const mkdirp = require('mkdirp').sync;
|
||||
const { postMediaTweet } = require('../client');
|
||||
const { canTweet, postMediaTweet } = require('../client');
|
||||
const { getTopOfCurrentHour, readJson, writeJson } = require('@/common/utilities');
|
||||
|
||||
const lastTweetTimesPath = path.resolve('storage/twitter-lastTweetTimes.json');
|
||||
|
|
@ -19,6 +19,12 @@ class TwitterPostBase {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Make sure we can post or save to a file
|
||||
if (!canTweet() && !this.getPublicImageFilename()) {
|
||||
this.error('Twitter API parameters not specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.postTweet();
|
||||
}
|
||||
|
||||
|
|
@ -32,13 +38,15 @@ class TwitterPostBase {
|
|||
// Maybe save the image
|
||||
this.maybeSavePublicImage(data, image);
|
||||
|
||||
// Post to Twitter
|
||||
let tweet = await postMediaTweet(text, image);
|
||||
if (canTweet()) {
|
||||
// Post to Twitter
|
||||
let tweet = await postMediaTweet(text, image);
|
||||
|
||||
// Update the last post time
|
||||
this.updateLastTweetTime();
|
||||
// Update the last post time
|
||||
this.updateLastTweetTime();
|
||||
|
||||
this.info('Posted Tweet');
|
||||
this.info('Posted Tweet');
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.error('Couldn\'t post Tweet');
|
||||
|
|
@ -47,11 +55,12 @@ class TwitterPostBase {
|
|||
}
|
||||
|
||||
maybeSavePublicImage(data, image) {
|
||||
let filename = this.getPublicImageFilename(data);
|
||||
let filename = this.getPublicImageFilename();
|
||||
if (filename) {
|
||||
let outputFilename = path.resolve(`dist/twitter-images/${filename}`);
|
||||
mkdirp(path.dirname(outputFilename));
|
||||
fs.writeFileSync(outputFilename, image);
|
||||
this.info(`Saved public image as ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +164,7 @@ class TwitterPostBase {
|
|||
getImage(data) { }
|
||||
|
||||
// The filename to store the image as (optional)
|
||||
getPublicImageFilename(data) { }
|
||||
getPublicImageFilename() { }
|
||||
|
||||
// The text body of the Tweet
|
||||
getText(data) { }
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
const { canTweet } = require('./client');
|
||||
const S3Syncer = require('../sync/S3Syncer');
|
||||
const { canSync } = require('../sync');
|
||||
const tweets = require('./tweets');
|
||||
|
||||
async function maybePostTweets() {
|
||||
if (!canTweet()) {
|
||||
console.warn('Twitter API parameters not specified');
|
||||
return;
|
||||
}
|
||||
const syncer = canSync() ? new S3Syncer() : null;
|
||||
|
||||
for (let tweet of tweets)
|
||||
await tweet.maybePostTweet();
|
||||
|
||||
if (syncer) {
|
||||
await syncer.upload();
|
||||
}
|
||||
}
|
||||
|
||||
async function testScreenshots() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ const TimelineUpdater = require('./updaters/TimelineUpdater');
|
|||
const OriginalGearImageUpdater = require('./updaters/OriginalGearImageUpdater');
|
||||
const FestivalsUpdater = require('./updaters/FestivalsUpdater');
|
||||
const MerchandisesUpdater = require('./updaters/MerchandisesUpdater');
|
||||
const S3Syncer = require('../sync/S3Syncer');
|
||||
const { canSync } = require('../sync');
|
||||
|
||||
const updaters = [
|
||||
new OriginalGearImageUpdater,
|
||||
|
|
@ -17,6 +19,8 @@ const updaters = [
|
|||
];
|
||||
|
||||
async function updateAll() {
|
||||
const syncer = canSync() ? new S3Syncer() : null;
|
||||
|
||||
for (let updater of updaters) {
|
||||
try {
|
||||
await updater.update();
|
||||
|
|
@ -25,6 +29,10 @@ async function updateAll() {
|
|||
}
|
||||
}
|
||||
|
||||
if (syncer) {
|
||||
await syncer.upload();
|
||||
}
|
||||
|
||||
return 'Done';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const LocalizationProcessor = require('../LocalizationProcessor');
|
|||
|
||||
const dataPath = path.resolve('dist/data');
|
||||
const splatnetAssetPath = path.resolve('dist/assets/splatnet');
|
||||
const cdnAltPath = path.resolve('src/common/cdn');
|
||||
|
||||
class Updater {
|
||||
constructor(options = {}) {
|
||||
|
|
@ -153,7 +154,7 @@ class Updater {
|
|||
return false;
|
||||
|
||||
// Remove timeline items with an importance of -1
|
||||
if (value.hasOwnProperty('importance'))
|
||||
if ('importance' in value)
|
||||
return value.importance > -1;
|
||||
|
||||
return true;
|
||||
|
|
@ -184,6 +185,15 @@ class Updater {
|
|||
if (fs.existsSync(localPath))
|
||||
return;
|
||||
|
||||
// Certain images are not available on the CDN anymore
|
||||
if (fs.existsSync(cdnAltPath + imagePath)) {
|
||||
this.info(`Using CDN backup: ${imagePath}`);
|
||||
let image = fs.readFileSync(cdnAltPath + imagePath);
|
||||
this.writeFile(localPath, image);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, download the image
|
||||
this.info(`Downloading image: ${imagePath}`);
|
||||
let splatnet = new SplatNet;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
|
|
@ -22,7 +22,7 @@ module.exports = async function retrieveGearData() {
|
|||
|
||||
// Find all gear and skills
|
||||
let players = [battleData.player_result].concat(battleData.my_team_members, battleData.other_team_members);
|
||||
for (player of players) {
|
||||
for (let player of players) {
|
||||
// Get the main and sub skills for each type of gear
|
||||
let gearSkills = [
|
||||
player.player.head_skills.main,
|
||||
|
|
@ -30,13 +30,13 @@ module.exports = async function retrieveGearData() {
|
|||
player.player.shoes_skills.main,
|
||||
].concat(player.player.head_skills.subs, player.player.clothes_skills.subs, player.player.shoes_skills.subs);
|
||||
|
||||
for (skill of gearSkills) {
|
||||
for (let skill of gearSkills) {
|
||||
if (skill)
|
||||
skills[skill.id] = skill;
|
||||
}
|
||||
|
||||
// Get brands
|
||||
for (brand of [player.player.head.brand, player.player.clothes.brand, player.player.shoes.brand])
|
||||
for (let brand of [player.player.head.brand, player.player.clothes.brand, player.player.shoes.brand])
|
||||
brands[brand.id] = brand;
|
||||
|
||||
// Get gear
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ module.exports = async () => {
|
|||
|
||||
// Update gear/brand/skill data from SplatNet
|
||||
// (This takes a while and doesn't need to be updated frequently, so just disabling this here for now)
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false) {
|
||||
let gearData = await retrieveGearData();
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ module.exports = async () => {
|
|||
|
||||
let regex = /\{\{GearList\/Item.*?filter_brand/g;
|
||||
let row;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (row = regex.exec(response.data)) {
|
||||
// Format: name=value|brand=value|...
|
||||
let details = row[0].split('|')
|
||||
|
|
|
|||
8
src/utility/website.json
Normal file
8
src/utility/website.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"IndexDocument": {
|
||||
"Suffix": "index.html"
|
||||
},
|
||||
"ErrorDocument": {
|
||||
"Key": "index.html"
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,7 @@
|
|||
</h3>
|
||||
<div class="inner-content">
|
||||
<p>
|
||||
Splatoon2.ink shows the current and upcoming map schedules for
|
||||
<a href="https://www.amazon.com/gp/product/B01N9QVIRV/ref=as_li_tl?ie=UTF8&tag=matisesblo-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B01N9QVIRV&linkId=01d5f076af5b6008436d9843c40a28db" target="_blank">Splatoon 2</a>.
|
||||
Splatoon2.ink shows the current and upcoming map schedules for Splatoon 2.
|
||||
</p>
|
||||
<p>
|
||||
This site was built with <a href="https://vuejs.org/" target="_blank">Vue.js</a>
|
||||
|
|
@ -36,14 +35,6 @@
|
|||
If you'd like to use this site's data in your own site/bot/app, please review the data access policy
|
||||
<a href="https://github.com/misenhower/splatoon2.ink/wiki/Data-access-policy" target="_blank">here</a>.
|
||||
</p>
|
||||
<h5 class="font-splatoon2 title is-5">
|
||||
Donations
|
||||
</h5>
|
||||
<p>
|
||||
If you'd like to support ongoing development and maintenance of this site, please consider
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7KETJ7PSE5PQC" target="_blank">making a donation</a>.
|
||||
Thanks :)
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://twitter.com/mattisenhower" target="_blank">Follow me on Twitter</a>
|
||||
or <a href="mailto:matt@isenhower.com" target="_blank">email me</a> with any questions!
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ function generateModule(region) {
|
|||
allSplatfests(state, getters, rootState, rootGetters) {
|
||||
return rootGetters['splatoon/splatfests/allSplatfests']
|
||||
&& rootGetters['splatoon/splatfests/allSplatfests']
|
||||
.filter(s => s.regions.hasOwnProperty(region)) // Only show Splatfests for this region
|
||||
.filter(s => region in s.regions) // Only show Splatfests for this region
|
||||
.map(s => ({ ...s, ...s.regions[region] })); // Apply this region's data
|
||||
},
|
||||
currentSplatfest(state, getters, { splatoon }) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user