├── .env.example ├── .eslintrc.js ├── .github └── workflows │ └── main.yaml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── github-master-hook ├── hooks.js ├── index.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── shims └── index.d.ts ├── src ├── app.ts ├── config.ts ├── controllers │ ├── api │ │ ├── others.ts │ │ └── report │ │ │ ├── v2.ts │ │ │ └── v3.ts │ └── index.ts ├── models │ ├── index.ts │ └── report │ │ ├── aaci.ts │ │ ├── battle-api.ts │ │ ├── create-item.ts │ │ ├── create-ship.ts │ │ ├── drop-ship.ts │ │ ├── enemy-info.ts │ │ ├── night-battle-ci.ts │ │ ├── night-contact.ts │ │ ├── pass-event.ts │ │ ├── quest-reward.ts │ │ ├── quest.ts │ │ ├── recipe.ts │ │ ├── remodel-item.ts │ │ ├── select-rank.ts │ │ └── ship-stat.ts └── sentry.ts ├── supervisior.conf └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | POI_SERVER_DISABLE_LOGGER=0 2 | POI_SERVER_DB=mongodb://localhost:27017/poi-development 3 | POI_SERVER_PORT=17027 4 | NODE_ENV=development 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:import/errors', 9 | 'plugin:import/warnings', 10 | 'plugin:import/typescript', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'prettier', 13 | 'prettier/@typescript-eslint', 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | plugins: ['import', 'prettier', '@typescript-eslint'], 17 | rules: { 18 | 'no-console': 'off', 19 | 'prettier/prettier': 'error', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 14 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14.x 22 | - run: npm ci 23 | - run: npm run lint 24 | - run: npm run type-check 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | public 30 | 31 | .env 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "files.insertFinalNewline": true, 7 | "files.trimTrailingWhitespace": true, 8 | "typescript.tsdk": "node_modules/typescript/lib" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yudachi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poi-server 2 | 3 | ![service status](https://api.poi.moe/api/service-status-badge) 4 | ![service version](https://api.poi.moe/api/service-version-badge) 5 | 6 | poi server. 7 | 8 | ## Usage 9 | 10 | See the [wiki](https://github.com/poooi/poi-server/wiki). 11 | 12 | ## Development 13 | 14 | ### Prerequists: 15 | 16 | - Node.js 14.x 17 | - MongoDB v4.2 18 | 19 | Other versions are not tested 20 | 21 | ### Setup 22 | 23 | - Install dependencies with npm install 24 | - copy `.env.example` to create `.env`, this contains config file for the server 25 | - start mongodb, if the db path or port is different, specify them in the `.env` file 26 | - start the server by `node index.js` 27 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: '14' } }], '@babel/preset-typescript'], 3 | plugins: [require.resolve('babel-plugin-add-module-exports')], 4 | } 5 | -------------------------------------------------------------------------------- /github-master-hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /srv/poi 5 | git fetch origin 6 | supervisorctl stop poi 7 | git checkout master 8 | git reset --hard origin/master 9 | npm install 10 | supervisorctl start poi 11 | supervisorctl restart poi-hook 12 | -------------------------------------------------------------------------------- /hooks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const Koa = require('koa') 4 | const Router = require('@koa/router') 5 | const ChildProcess = require('child_process') 6 | require('dotenv').config() 7 | 8 | const app = new Koa() 9 | const router = new Router() 10 | 11 | router.post('/api/github-master-hook', async (ctx, next) => { 12 | console.log(`====================Master hook ${new Date()} ====================`) 13 | const cp = ChildProcess.spawn('./github-master-hook', { stdio: 'inherit' }) 14 | cp.on('close', (code) => console.log('* Master hook exit code:' + code)) 15 | ctx.status = 200 16 | await next() 17 | }) 18 | 19 | // Start server 20 | const Port = 11280 21 | app.use(router.routes()) 22 | app.listen(Port, '127.0.0.1', () => { 23 | console.log(`Server is listening at port ${Port}`) 24 | }) 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const { register } = require('esbuild-register/dist/node') 4 | 5 | require('dotenv').config() 6 | register() 7 | 8 | require('./src/app') 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poi-server", 3 | "version": "1.0.0", 4 | "description": "Server side of poi.", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node --harmony index.js", 9 | "dev": "webpack-dev-server --content-base public/ --config webpack.config.dev.js --colors --watch --inline --progress --hot", 10 | "build": "NODE_ENV=production webpack -p --config webpack.config.prod.js", 11 | "type-check": "tsc --noEmit", 12 | "lint": "eslint . --ext .ts --ext .js", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/poooi/poi-server.git" 18 | }, 19 | "keywords": [ 20 | "poi", 21 | "KanColle" 22 | ], 23 | "author": "Magica", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/yudachi/poi-server/issues" 27 | }, 28 | "homepage": "https://github.com/yudachi/poi-server#readme", 29 | "dependencies": { 30 | "@koa/router": "^10.0.0", 31 | "@sentry/node": "^6.19.7", 32 | "@sentry/tracing": "^6.19.7", 33 | "@sindresorhus/df": "^3.1.1", 34 | "babel-plugin-add-module-exports": "^1.0.4", 35 | "badge-maker": "^3.3.0", 36 | "bluebird": "^3.7.2", 37 | "dotenv": "^8.2.0", 38 | "esbuild": "^0.14.38", 39 | "esbuild-register": "^3.3.2", 40 | "koa": "^2.13.1", 41 | "koa-bodyparser": "^4.3.0", 42 | "koa-cash": "^4.0.5", 43 | "koa-pino-logger": "^3.0.0", 44 | "lodash": "^4.17.20", 45 | "mongoose": "^5.11.13", 46 | "node-cache": "^5.1.2", 47 | "node-fetch": "^2.6.1", 48 | "semver": "^7.3.4" 49 | }, 50 | "devDependencies": { 51 | "@babel/preset-typescript": "^7.12.7", 52 | "@types/babel__core": "^7.1.12", 53 | "@types/babel__preset-env": "^7.9.1", 54 | "@types/bluebird": "^3.5.33", 55 | "@types/bytes": "^3.1.0", 56 | "@types/eslint": "^7.2.6", 57 | "@types/eslint-plugin-prettier": "^3.1.0", 58 | "@types/koa": "^2.11.6", 59 | "@types/koa__router": "^8.0.4", 60 | "@types/koa-bodyparser": "^4.3.0", 61 | "@types/koa-cash": "^4.0.0", 62 | "@types/koa-pino-logger": "^3.0.0", 63 | "@types/lodash": "^4.14.168", 64 | "@types/mongoose": "^5.10.3", 65 | "@types/node": "^14.14.22", 66 | "@types/node-fetch": "^2.5.8", 67 | "@types/prettier": "^2.1.6", 68 | "@types/semver": "^7.3.4", 69 | "@typescript-eslint/eslint-plugin": "^4.14.0", 70 | "@typescript-eslint/parser": "^4.14.0", 71 | "babel-eslint": "^10.1.0", 72 | "eslint": "^7.18.0", 73 | "eslint-config-prettier": "^7.2.0", 74 | "eslint-plugin-import": "^2.22.1", 75 | "eslint-plugin-prettier": "^3.3.1", 76 | "prettier": "^2.2.1", 77 | "typescript": "^4.1.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | printWidth: 100, 6 | } 7 | -------------------------------------------------------------------------------- /shims/index.d.ts: -------------------------------------------------------------------------------- 1 | declare let latestCommit: string 2 | 3 | declare namespace NodeJS { 4 | interface Global { 5 | latestCommit: typeof latestCommit 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import bodyparser from 'koa-bodyparser' 3 | import cache from 'koa-cash' 4 | import logger from 'koa-pino-logger' 5 | import Cache from 'node-cache' 6 | import mongoose from 'mongoose' 7 | import childProcess from 'child_process' 8 | import { trim } from 'lodash' 9 | import bytes from 'bytes' 10 | 11 | import { config } from './config' 12 | import { captureException, sentryTracingMiddileaware } from './sentry' 13 | 14 | import './models' 15 | import { router } from './controllers' 16 | 17 | const app = new Koa() 18 | 19 | // Database 20 | mongoose.connect(config.db, { 21 | useNewUrlParser: true, 22 | useUnifiedTopology: true, 23 | useCreateIndex: true, 24 | }) 25 | mongoose.connection.on('error', () => { 26 | throw new Error('Unable to connect to database at ' + config.db) 27 | }) 28 | 29 | app.use(sentryTracingMiddileaware) 30 | 31 | // Logger 32 | if (!config.disableLogger) { 33 | app.use(logger()) 34 | } 35 | 36 | // Cache 37 | const _cache = new Cache({ 38 | stdTTL: 10 * 60, 39 | checkperiod: 0, 40 | }) 41 | app.use( 42 | cache({ 43 | threshold: bytes('1GB'), // Compression is handled by nginx. 44 | get: async (key) => _cache.get(key), 45 | set: async (key, value, maxAge) => { 46 | _cache.set(key, value, maxAge > 0 ? maxAge : 0) 47 | }, 48 | }), 49 | ) 50 | 51 | // Body Parser 52 | app.use( 53 | bodyparser({ 54 | strict: true, 55 | onerror: (err, ctx) => { 56 | captureException(err, ctx) 57 | console.error(`bodyparser error`) 58 | }, 59 | }), 60 | ) 61 | 62 | // Controllers 63 | app.use(router.routes()) 64 | 65 | app.listen(config.port, '127.0.0.1', () => { 66 | console.log(`Koa is listening on port ${config.port}`) 67 | }) 68 | 69 | app.on('error', captureException) 70 | 71 | childProcess.exec('git rev-parse HEAD', (err, stdout) => { 72 | if (!err) { 73 | global.latestCommit = trim(stdout) 74 | } else { 75 | console.error(err) 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | const rootPath = path.normalize(__dirname) 3 | import { defaults } from 'lodash' 4 | 5 | interface EnvConfig { 6 | root: string 7 | port: number 8 | db: string 9 | disableLogger?: number 10 | env: string 11 | } 12 | 13 | const defaultConfig: EnvConfig = { 14 | root: rootPath, 15 | port: 17027, 16 | db: 'mongodb://localhost:27017/poi-development', 17 | env: 'development', 18 | disableLogger: 0, 19 | } 20 | 21 | const parseEnvInt = (value: string | undefined) => { 22 | const res = parseInt(value as string, 10) 23 | return Number.isFinite(res) ? res : undefined 24 | } 25 | 26 | export const config: Readonly = defaults, EnvConfig>( 27 | { 28 | port: parseEnvInt(process.env.POI_SERVER_PORT), 29 | db: process.env.POI_SERVER_DB, 30 | env: process.env.NODE_ENV, 31 | disableLogger: parseEnvInt(process.env.POI_SERVER_DISABLE_LOGGER), 32 | }, 33 | defaultConfig, 34 | ) 35 | -------------------------------------------------------------------------------- /src/controllers/api/others.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | import df from '@sindresorhus/df' 3 | import childProcess from 'child_process' 4 | import mongoose from 'mongoose' 5 | import { makeBadge } from 'badge-maker' 6 | import path from 'path' 7 | 8 | import { config } from '../../config' 9 | 10 | export const router = new Router() 11 | 12 | const CreateShipRecord = mongoose.model('CreateShipRecord') 13 | const CreateItemRecord = mongoose.model('CreateItemRecord') 14 | const RemodelItemRecord = mongoose.model('RemodelItemRecord') 15 | const DropShipRecord = mongoose.model('DropShipRecord') 16 | const SelectRankRecord = mongoose.model('SelectRankRecord') 17 | const PassEventRecord = mongoose.model('PassEventRecord') 18 | const Quest = mongoose.model('Quest') 19 | const BattleAPI = mongoose.model('BattleAPI') 20 | const AACIRecord = mongoose.model('AACIRecord') 21 | const NightContactRecord = mongoose.model('NightContactRecord') 22 | 23 | router.get('/status', async (ctx, next) => { 24 | const dsk = await df() 25 | const ret = { 26 | env: process.env.NODE_ENV, 27 | disk: dsk.filter((e) => e.mountpoint == '/'), 28 | mongo: { 29 | CreateShipRecord: await CreateShipRecord.count().exec(), 30 | CreateItemRecord: await CreateItemRecord.count().exec(), 31 | RemodelItemRecord: await RemodelItemRecord.count().exec(), 32 | DropShipRecord: await DropShipRecord.count().exec(), 33 | SelectRankRecord: await SelectRankRecord.count().exec(), 34 | PassEventRecord: await PassEventRecord.count().exec(), 35 | Quest: await Quest.count().exec(), 36 | BattleAPI: await BattleAPI.count().exec(), 37 | AACIRecord: await AACIRecord.count().exec(), 38 | NightContactRecord: await NightContactRecord.count().exec(), 39 | }, 40 | } 41 | ctx.status = 200 42 | ctx.body = ret 43 | await next() 44 | }) 45 | 46 | router.post('/github-master-hook', async (ctx, next) => { 47 | const update = childProcess.spawn(path.resolve(config.root, '../github-master-hook'), []) 48 | update.stdout.on('data', (data) => console.log('GitHub hook out: ' + data)) 49 | update.stderr.on('data', (data) => console.log('GitHub hook err: ' + data)) 50 | update.on('close', (code) => console.log('GitHub hook exit: ' + code)) 51 | ctx.status = 200 52 | ctx.body = { 53 | code: 0, 54 | } 55 | await next() 56 | }) 57 | 58 | router.get('/latest-commit', async (ctx, next) => { 59 | ctx.status = 200 60 | ctx.body = global.latestCommit 61 | await next() 62 | }) 63 | 64 | let serviceUpBadge: string 65 | 66 | router.get('/service-status-badge', async (ctx, next) => { 67 | if (!serviceUpBadge) { 68 | serviceUpBadge = makeBadge({ 69 | label: 'service', 70 | message: 'up', 71 | color: 'success', 72 | style: 'flat-square', 73 | }) 74 | } 75 | ctx.status = 200 76 | ctx.set('Content-Type', 'image/svg+xml') 77 | ctx.body = serviceUpBadge 78 | await next() 79 | }) 80 | 81 | let serviceVersionBadge: string 82 | 83 | router.get('/service-version-badge', async (ctx, next) => { 84 | if (!serviceVersionBadge) { 85 | serviceVersionBadge = makeBadge({ 86 | label: 'version', 87 | message: global.latestCommit?.slice(0, 8) || '', 88 | color: 'informational', 89 | style: 'flat-square', 90 | }) 91 | } 92 | ctx.status = 200 93 | ctx.set('Content-Type', 'image/svg+xml') 94 | ctx.body = serviceVersionBadge 95 | await next() 96 | }) 97 | -------------------------------------------------------------------------------- /src/controllers/api/report/v2.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | import mongoose from 'mongoose' 3 | import semver from 'semver' 4 | import { ParameterizedContext } from 'koa' 5 | import { isString, flatMap, drop } from 'lodash' 6 | 7 | import { captureException } from '../../../sentry' 8 | import { DropShipRecord, SelectRankRecord } from '../../../models' 9 | 10 | export const router = new Router() 11 | 12 | const CreateShipRecord = mongoose.model('CreateShipRecord') 13 | const CreateItemRecord = mongoose.model('CreateItemRecord') 14 | const RemodelItemRecord = mongoose.model('RemodelItemRecord') 15 | const PassEventRecord = mongoose.model('PassEventRecord') 16 | const Quest = mongoose.model('Quest') 17 | const BattleAPI = mongoose.model('BattleAPI') 18 | const NightContactRecord = mongoose.model('NightContactRecord') 19 | const AACIRecord = mongoose.model('AACIRecord') 20 | const RecipeRecord = mongoose.model('RecipeRecord') 21 | const NightBattleCI = mongoose.model('NightBattleCI') 22 | const ShipStat = mongoose.model('ShipStat') 23 | const EnemyInfo = mongoose.model('EnemyInfo') 24 | 25 | function parseInfo(ctx: ParameterizedContext) { 26 | const info = isString(ctx.request.body.data) 27 | ? JSON.parse(ctx.request.body.data) 28 | : ctx.request.body.data 29 | if (info.origin == null) { 30 | info.origin = ctx.headers['x-reporter'] || ctx.headers['user-agent'] 31 | } 32 | return info 33 | } 34 | 35 | router.post('/create_ship', async (ctx, next) => { 36 | try { 37 | const info = parseInfo(ctx) 38 | const record = new CreateShipRecord(info) 39 | await record.save() 40 | ctx.status = 200 41 | await next() 42 | } catch (err) { 43 | captureException(err, ctx) 44 | ctx.status = 500 45 | await next() 46 | } 47 | }) 48 | 49 | router.post('/create_item', async (ctx, next) => { 50 | try { 51 | const info = parseInfo(ctx) 52 | const record = new CreateItemRecord(info) 53 | await record.save() 54 | ctx.status = 200 55 | await next() 56 | } catch (err) { 57 | captureException(err, ctx) 58 | ctx.status = 500 59 | await next() 60 | } 61 | }) 62 | 63 | router.post('/remodel_item', async (ctx, next) => { 64 | try { 65 | const info = parseInfo(ctx) 66 | const record = new RemodelItemRecord(info) 67 | await record.save() 68 | ctx.status = 200 69 | await next() 70 | } catch (err) { 71 | captureException(err, ctx) 72 | ctx.status = 500 73 | await next() 74 | } 75 | }) 76 | 77 | router.post('/drop_ship', async (ctx, next) => { 78 | try { 79 | const info = parseInfo(ctx) 80 | const record = new DropShipRecord(info) 81 | // drop own ship snapshot for non-event maps and normal maps except 7-3/7-4 82 | if (record.mapId < 73) { 83 | record.ownedShipSnapshot = {} 84 | } 85 | await record.save() 86 | ctx.status = 200 87 | await next() 88 | } catch (err) { 89 | captureException(err, ctx) 90 | ctx.status = 500 91 | await next() 92 | } 93 | }) 94 | 95 | router.post('/select_rank', async (ctx, next) => { 96 | try { 97 | const info = parseInfo(ctx) 98 | let record = await SelectRankRecord.findOne({ 99 | teitokuId: info.teitokuId, 100 | mapareaId: info.mapareaId, 101 | }).exec() 102 | if (record != null) { 103 | record.teitokuLv = info.teitokuLv 104 | record.rank = info.rank 105 | record.origin = info.origin 106 | } else { 107 | record = new SelectRankRecord(info) 108 | } 109 | await record.save() 110 | ctx.status = 200 111 | await next() 112 | } catch (err) { 113 | captureException(err, ctx) 114 | ctx.status = 500 115 | await next() 116 | } 117 | }) 118 | 119 | router.post('/pass_event', async (ctx, next) => { 120 | try { 121 | const info = parseInfo(ctx) 122 | const record = new PassEventRecord(info) 123 | await record.save() 124 | ctx.status = 200 125 | await next() 126 | } catch (err) { 127 | captureException(err, ctx) 128 | ctx.status = 500 129 | await next() 130 | } 131 | }) 132 | 133 | // Use knownQuests to cache current known quests state. 134 | router.get('/known_quests', async (ctx, next) => { 135 | try { 136 | if (await ctx.cashed()) return // Cache control 137 | const knownQuests = await Quest.find().distinct('questId').exec() 138 | knownQuests.sort() 139 | ctx.status = 200 140 | ctx.body = { 141 | quests: knownQuests, 142 | } 143 | await next() 144 | } catch (err) { 145 | captureException(err, ctx) 146 | ctx.status = 500 147 | await next() 148 | } 149 | }) 150 | 151 | router.post('/quest/:id', async (ctx, next) => { 152 | ctx.status = 200 153 | await next() 154 | }) 155 | 156 | router.post('/battle_api', async (ctx, next) => { 157 | try { 158 | const info = parseInfo(ctx) 159 | const record = new BattleAPI(info) 160 | await record.save() 161 | ctx.status = 200 162 | await next() 163 | } catch (err) { 164 | captureException(err, ctx) 165 | ctx.status = 500 166 | await next() 167 | } 168 | }) 169 | 170 | router.post('/night_contcat', async (ctx, next) => { 171 | try { 172 | const info = parseInfo(ctx) 173 | const record = new NightContactRecord(info) 174 | await record.save() 175 | ctx.status = 200 176 | await next() 177 | } catch (err) { 178 | captureException(err, ctx) 179 | ctx.status = 500 180 | await next() 181 | } 182 | }) 183 | 184 | router.post('/aaci', async (ctx, next) => { 185 | try { 186 | const info = parseInfo(ctx) 187 | // aaci type 7 in poi <= 7.9.0 is not correctly detected 188 | // reporter < 3.6.0 cannot send untriggered aaci report 189 | // so we add a semver check 190 | if ( 191 | semver.gt(info.poiVersion, '7.9.1') && 192 | info.origin.startsWith('Reporter ') && 193 | semver.gte(info.origin.replace('Reporter ', ''), '3.6.0') 194 | ) { 195 | const record = new AACIRecord(info) 196 | await record.save() 197 | } 198 | ctx.status = 200 199 | await next() 200 | } catch (err) { 201 | captureException(err, ctx) 202 | ctx.status = 500 203 | await next() 204 | } 205 | }) 206 | 207 | // FIXME: this action is no longer in use, keeping it until changes made in reporter 208 | router.get('/known_recipes', async (ctx, next) => { 209 | try { 210 | ctx.status = 200 211 | ctx.body = { 212 | recipes: [], 213 | } 214 | await next() 215 | } catch (err) { 216 | captureException(err, ctx) 217 | ctx.status = 500 218 | await next() 219 | } 220 | }) 221 | 222 | router.post('/remodel_recipe', async (ctx, next) => { 223 | try { 224 | const info = parseInfo(ctx) 225 | if (info.stage != -1) { 226 | const lastReported = +new Date() 227 | const { recipeId, itemId, stage, day, secretary } = info 228 | 229 | await RecipeRecord.updateOne( 230 | { recipeId, itemId, stage, day, secretary }, 231 | { ...info, lastReported, $inc: { count: 1 } }, 232 | { upsert: true }, 233 | ) 234 | } 235 | ctx.status = 200 236 | await next() 237 | } catch (err) { 238 | captureException(err, ctx) 239 | ctx.status = 500 240 | await next() 241 | } 242 | }) 243 | 244 | router.post('/remodel_recipe_deduplicate', async (ctx, next) => { 245 | try { 246 | const duplicates = await RecipeRecord.aggregate([ 247 | { $group: { _id: '$key', count: { $sum: 1 }, records: { $addToSet: '$_id' } } }, 248 | { $match: { _id: { $ne: null }, count: { $gt: 1 } } }, 249 | ]).exec() 250 | 251 | const recordsToDelete = flatMap(duplicates, (item) => drop(item.records, 1)) 252 | 253 | await RecipeRecord.deleteMany({ _id: { $in: recordsToDelete } }) 254 | 255 | ctx.status = 200 256 | ctx.body = { 257 | recipes: recordsToDelete, 258 | } 259 | await next() 260 | } catch (err) { 261 | captureException(err, ctx) 262 | ctx.status = 500 263 | await next() 264 | } 265 | }) 266 | 267 | router.post('/night_battle_ci', async (ctx, next) => { 268 | try { 269 | const info = parseInfo(ctx) 270 | const record = new NightBattleCI(info) 271 | await record.save() 272 | ctx.status = 200 273 | await next() 274 | } catch (err) { 275 | captureException(err, ctx) 276 | ctx.status = 500 277 | await next() 278 | } 279 | }) 280 | 281 | // Compat for legacy plugin's night battle ss ci reporter 282 | // which is now night battle ci reporter and has changed url to above 283 | router.post('/night_battle_ss_ci', async (ctx, next) => { 284 | ctx.status = 200 285 | await next() 286 | }) 287 | 288 | router.post('/ship_stat', async (ctx, next) => { 289 | try { 290 | const { id, lv, los, los_max, asw, asw_max, evasion, evasion_max } = parseInfo(ctx) 291 | const last_timestamp = +new Date() 292 | await ShipStat.updateOne( 293 | { 294 | id, 295 | lv, 296 | los, 297 | los_max, 298 | asw, 299 | asw_max, 300 | evasion, 301 | evasion_max, 302 | }, 303 | { 304 | id, 305 | lv, 306 | los, 307 | los_max, 308 | asw, 309 | asw_max, 310 | evasion, 311 | evasion_max, 312 | last_timestamp, 313 | $inc: { count: 1 }, 314 | }, 315 | { 316 | upsert: true, 317 | }, 318 | ) 319 | ctx.status = 200 320 | await next() 321 | } catch (err) { 322 | captureException(err, ctx) 323 | ctx.status = 500 324 | await next() 325 | } 326 | }) 327 | 328 | router.post('/enemy_info', async (ctx, next) => { 329 | try { 330 | const info = parseInfo(ctx) 331 | const { 332 | ships1, 333 | levels1, 334 | hp1, 335 | stats1, 336 | equips1, 337 | ships2, 338 | levels2, 339 | hp2, 340 | stats2, 341 | equips2, 342 | planes, 343 | bombersMin, 344 | bombersMax, 345 | } = info 346 | await EnemyInfo.updateOne( 347 | { 348 | ships1, 349 | levels1, 350 | hp1, 351 | stats1, 352 | equips1, 353 | ships2, 354 | levels2, 355 | hp2, 356 | stats2, 357 | equips2, 358 | planes, 359 | }, 360 | { 361 | ships1, 362 | levels1, 363 | hp1, 364 | stats1, 365 | equips1, 366 | ships2, 367 | levels2, 368 | hp2, 369 | stats2, 370 | equips2, 371 | planes, 372 | $min: { bombersMax }, 373 | $max: { bombersMin }, 374 | $inc: { count: 1 }, 375 | }, 376 | { 377 | upsert: true, 378 | }, 379 | ) 380 | ctx.status = 200 381 | await next() 382 | } catch (err) { 383 | captureException(err, ctx) 384 | ctx.status = 500 385 | await next() 386 | } 387 | }) 388 | -------------------------------------------------------------------------------- /src/controllers/api/report/v3.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | import mongoose from 'mongoose' 3 | import crypto from 'crypto' 4 | import _ from 'lodash' 5 | import bluebird from 'bluebird' 6 | import { ParameterizedContext } from 'koa' 7 | 8 | import { captureException } from '../../../sentry' 9 | import { 10 | QuestPayload, 11 | QuestRewardPayload, 12 | Quest, 13 | QuestReward, 14 | QuestDocument, 15 | } from '../../../models' 16 | 17 | export const router = new Router() 18 | 19 | const parseInfo = (ctx: ParameterizedContext) => { 20 | const info = ctx.request.body.data 21 | if (info.origin == null) { 22 | info.origin = ctx.headers['x-reporter'] || ctx.headers['user-agent'] 23 | } 24 | return info 25 | } 26 | 27 | const createHash = _.memoize((text) => crypto.createHash('md5').update(text).digest('hex')) 28 | 29 | const createQuestHash = ({ title, detail }: QuestPayload | QuestRewardPayload) => 30 | createHash(`${title}${detail}`) 31 | 32 | router.get('/known_quests', async (ctx, next) => { 33 | try { 34 | if (await ctx.cashed()) return // Cache control 35 | const knownQuests: QuestDocument['key'][] = await Quest.distinct('key').exec() 36 | const quests = knownQuests.map((key) => key.slice(0, 8)) 37 | ctx.status = 200 38 | ctx.body = { 39 | quests, 40 | } 41 | await next() 42 | } catch (err) { 43 | captureException(err, ctx) 44 | ctx.status = 500 45 | await next() 46 | } 47 | }) 48 | 49 | router.post('/quest', async (ctx, next) => { 50 | try { 51 | const info = parseInfo(ctx) 52 | const records = _.map(info.quests, (quest) => ({ 53 | ...quest, 54 | key: createQuestHash(quest), 55 | origin: info.origin, 56 | })) 57 | 58 | await bluebird.map(records, (quest) => { 59 | return Quest.updateOne( 60 | { 61 | key: quest.key, 62 | questId: quest.questId, 63 | category: quest.category, 64 | }, 65 | { $setOnInsert: quest }, 66 | { upsert: true }, 67 | ) 68 | }) 69 | 70 | ctx.status = 200 71 | await next() 72 | } catch (err) { 73 | captureException(err, ctx) 74 | ctx.status = 500 75 | await next() 76 | } 77 | }) 78 | 79 | router.post('/quest_reward', async (ctx, next) => { 80 | try { 81 | const info = parseInfo(ctx) 82 | 83 | const key = createQuestHash(info) 84 | 85 | await QuestReward.updateOne( 86 | { 87 | key, 88 | questId: info.questId, 89 | selections: info.selections, 90 | bounsCount: info.bounsCount, 91 | }, 92 | { $setOnInsert: info }, 93 | { upsert: true }, 94 | ) 95 | 96 | ctx.status = 200 97 | await next() 98 | } catch (err) { 99 | captureException(err, ctx) 100 | ctx.status = 500 101 | await next() 102 | } 103 | }) 104 | -------------------------------------------------------------------------------- /src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { router as othersRouter } from './api/others' 4 | import { router as reportV2Router } from './api/report/v2' 5 | import { router as reportV3Router } from './api/report/v3' 6 | 7 | export const router = new Router() 8 | 9 | router.use('/api', othersRouter.routes(), othersRouter.allowedMethods()) 10 | 11 | router.use('/api/report/v2', reportV2Router.routes(), reportV2Router.allowedMethods()) 12 | 13 | router.use('/api/report/v3', reportV3Router.routes(), reportV3Router.allowedMethods()) 14 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './report/aaci' 2 | export * from './report/battle-api' 3 | export * from './report/create-item' 4 | export * from './report/create-ship' 5 | export * from './report/drop-ship' 6 | export * from './report/enemy-info' 7 | export * from './report/night-battle-ci' 8 | export * from './report/night-contact' 9 | export * from './report/pass-event' 10 | export * from './report/quest' 11 | export * from './report/recipe' 12 | export * from './report/remodel-item' 13 | export * from './report/select-rank' 14 | export * from './report/ship-stat' 15 | export * from './report/quest-reward' 16 | -------------------------------------------------------------------------------- /src/models/report/aaci.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | interface AACIRecordDocument extends Document { 4 | poiVersion: string 5 | available: number[] 6 | triggered: number 7 | items: number[] 8 | improvement: number[] 9 | rawLuck: number 10 | rawTaiku: number 11 | lv: number 12 | hpPercent: number 13 | pos: number 14 | origin: string 15 | } 16 | 17 | export interface AACIRecordPayload { 18 | poiVersion: AACIRecordDocument['poiVersion'] 19 | available: AACIRecordDocument['available'] 20 | triggered: AACIRecordDocument['triggered'] 21 | items: AACIRecordDocument['items'] 22 | improvement: AACIRecordDocument['improvement'] 23 | rawLuck: AACIRecordDocument['rawLuck'] 24 | rawTaiku: AACIRecordDocument['rawTaiku'] 25 | lv: AACIRecordDocument['lv'] 26 | hpPercent: AACIRecordDocument['hpPercent'] 27 | pos: AACIRecordDocument['pos'] 28 | origin: AACIRecordDocument['origin'] 29 | } 30 | 31 | const AACIRecordSchema = new mongoose.Schema({ 32 | poiVersion: String, 33 | available: [Number], 34 | triggered: Number, 35 | items: [Number], 36 | improvement: [Number], 37 | rawLuck: Number, 38 | rawTaiku: Number, 39 | lv: Number, 40 | hpPercent: Number, 41 | pos: Number, 42 | origin: String, 43 | }) 44 | 45 | export const AACIRecord = mongoose.model('AACIRecord', AACIRecordSchema) 46 | -------------------------------------------------------------------------------- /src/models/report/battle-api.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | interface BattleAPIDocument extends Document { 4 | origin: string 5 | path: string 6 | data: any 7 | } 8 | 9 | export interface BattleAPIPayload { 10 | path: BattleAPIDocument['path'] 11 | data: BattleAPIDocument['data'] 12 | origin: BattleAPIDocument['origin'] 13 | } 14 | 15 | const BattleAPISchema = new mongoose.Schema({ 16 | origin: String, 17 | path: String, 18 | data: Object, 19 | }) 20 | 21 | export const BattleAPI = mongoose.model('BattleAPI', BattleAPISchema) 22 | -------------------------------------------------------------------------------- /src/models/report/create-item.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface CreateItemRecordPayload { 4 | items: number[] 5 | secretary: number 6 | itemId: number 7 | teitokuLv: number 8 | successful: boolean 9 | origin: string 10 | } 11 | 12 | interface CreateItemRecordDocument extends Document, CreateItemRecordPayload {} 13 | 14 | const CreateItemRecordSchema = new mongoose.Schema({ 15 | items: [Number], 16 | secretary: Number, 17 | itemId: Number, 18 | teitokuLv: Number, 19 | successful: Boolean, 20 | origin: String, 21 | }) 22 | 23 | export const CreateItemRecord = mongoose.model( 24 | 'CreateItemRecord', 25 | CreateItemRecordSchema, 26 | ) 27 | -------------------------------------------------------------------------------- /src/models/report/create-ship.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface CreateShipRecordPayload { 4 | items: number[] 5 | kdockId: number 6 | secretary: number 7 | shipId: number 8 | highspeed: number 9 | teitokuLv: number 10 | largeFlag: boolean 11 | origin: string 12 | } 13 | 14 | interface CreateShipRecordDocument extends Document, CreateShipRecordPayload {} 15 | 16 | const CreateShipRecordSchema = new mongoose.Schema({ 17 | items: [Number], 18 | kdockId: Number, 19 | secretary: Number, 20 | shipId: Number, 21 | highspeed: Number, 22 | teitokuLv: Number, 23 | largeFlag: Boolean, 24 | origin: String, 25 | }) 26 | 27 | export const CreateShipRecord = mongoose.model( 28 | 'CreateShipRecord', 29 | CreateShipRecordSchema, 30 | ) 31 | -------------------------------------------------------------------------------- /src/models/report/drop-ship.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface DropShipRecordPayload { 4 | shipId: number 5 | itemId: number 6 | mapId: number 7 | quest: string 8 | cellId: number 9 | enemy: string 10 | rank: string 11 | isBoss: boolean 12 | teitokuLv: number 13 | mapLv: number 14 | enemyShips1: number[] 15 | enemyShips2: number[] 16 | enemyFormation: number 17 | baseExp: number 18 | teitokuId: string 19 | shipCounts: number[] 20 | ownedShipSnapshot: Record 21 | origin: string 22 | } 23 | 24 | interface DropShipRecordDocument extends Document, DropShipRecordPayload {} 25 | 26 | const DropShipRecordSchema = new mongoose.Schema({ 27 | shipId: Number, 28 | itemId: Number, 29 | mapId: Number, 30 | quest: String, 31 | cellId: Number, 32 | enemy: String, 33 | rank: String, 34 | isBoss: Boolean, 35 | teitokuLv: Number, 36 | mapLv: Number, 37 | enemyShips1: [Number], 38 | enemyShips2: [Number], 39 | enemyFormation: Number, 40 | baseExp: Number, 41 | teitokuId: String, 42 | ownedShipSnapshot: mongoose.SchemaTypes.Mixed, 43 | origin: String, 44 | }) 45 | 46 | export const DropShipRecord = mongoose.model( 47 | 'DropShipRecord', 48 | DropShipRecordSchema, 49 | ) 50 | -------------------------------------------------------------------------------- /src/models/report/enemy-info.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface EnemyInfoPayload { 4 | ships1: number[] 5 | levels1: number[] 6 | hp1: number[] 7 | stats1: number[][] 8 | equips1: number[][] 9 | ships2: number[] 10 | levels2: number[] 11 | hp2: number[] 12 | stats2: number[][] 13 | equips2: number[][] 14 | planes: number 15 | bombersMin: number 16 | bombersMax: number 17 | count: number 18 | } 19 | 20 | interface EnemyInfoDocument extends EnemyInfoPayload, Document {} 21 | 22 | const EnemyInfoSchema = new mongoose.Schema({ 23 | ships1: [Number], 24 | levels1: [Number], 25 | hp1: [Number], 26 | stats1: [[Number]], 27 | equips1: [[Number]], 28 | ships2: [Number], 29 | levels2: [Number], 30 | hp2: [Number], 31 | stats2: [[Number]], 32 | equips2: [[Number]], 33 | planes: Number, 34 | bombersMin: Number, 35 | bombersMax: Number, 36 | count: Number, 37 | }) 38 | 39 | export const EnemyInfo = mongoose.model('EnemyInfo', EnemyInfoSchema) 40 | -------------------------------------------------------------------------------- /src/models/report/night-battle-ci.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface NightBattleCIPayload { 4 | shipId: number 5 | CI: string 6 | type: string 7 | lv: number 8 | rawLuck: number 9 | pos: number 10 | status: string 11 | items: number[] 12 | improvement: number[] 13 | searchLight: boolean 14 | flare: number 15 | defenseId: number 16 | defenseTypeId: number 17 | ciType: number 18 | display: number[] 19 | hitType: number[] 20 | damage: number[] 21 | damageTotal: number 22 | time: number 23 | origin: string 24 | } 25 | 26 | interface NightBattleCIDocument extends NightBattleCIPayload, Document {} 27 | 28 | const NightBattleCISchema = new mongoose.Schema({ 29 | shipId: Number, 30 | CI: String, 31 | type: String, 32 | lv: Number, 33 | rawLuck: Number, 34 | pos: Number, 35 | status: String, 36 | items: [Number], 37 | improvement: [Number], 38 | searchLight: Boolean, 39 | flare: Number, 40 | defenseId: Number, 41 | defenseTypeId: Number, 42 | ciType: Number, 43 | display: [Number], 44 | hitType: [Number], 45 | damage: [Number], 46 | damageTotal: Number, 47 | time: Number, 48 | origin: String, 49 | }) 50 | 51 | export const NightBattleCI = mongoose.model( 52 | 'NightBattleCI', 53 | NightBattleCISchema, 54 | ) 55 | -------------------------------------------------------------------------------- /src/models/report/night-contact.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface NightContactRecordPayload { 4 | fleetType: number 5 | shipId: number 6 | shipLv: number 7 | itemId: number 8 | itemLv: number 9 | contact: boolean 10 | } 11 | 12 | interface NightContactRecordDocument extends NightContactRecordPayload, Document {} 13 | 14 | const NightContactRecordSchema = new mongoose.Schema({ 15 | fleetType: Number, 16 | shipId: Number, 17 | shipLv: Number, 18 | itemId: Number, 19 | itemLv: Number, 20 | contact: Boolean, 21 | }) 22 | 23 | export const NightContactRecord = mongoose.model( 24 | 'NightContactRecord', 25 | NightContactRecordSchema, 26 | ) 27 | -------------------------------------------------------------------------------- /src/models/report/pass-event.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface PassEventRecordPayload { 4 | teitokuId: string 5 | teitokuLv: number 6 | mapId: number 7 | mapLv: number 8 | rewards: { 9 | rewardType: number 10 | rewardId: number 11 | rewardCount: number 12 | rewardLevel: number 13 | }[] 14 | origin: string 15 | } 16 | 17 | interface PassEventRecordDocument extends PassEventRecordPayload, Document {} 18 | 19 | const PassEventRecordSchema = new mongoose.Schema({ 20 | teitokuId: String, 21 | teitokuLv: Number, 22 | mapId: Number, 23 | mapLv: Number, 24 | rewards: [ 25 | { 26 | rewardType: Number, 27 | rewardId: Number, 28 | rewardCount: Number, 29 | rewardLevel: Number, 30 | }, 31 | ], 32 | origin: String, 33 | }) 34 | 35 | export const PassEventRecord = mongoose.model( 36 | 'PassEventRecord', 37 | PassEventRecordSchema, 38 | ) 39 | -------------------------------------------------------------------------------- /src/models/report/quest-reward.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface QuestRewardPayload { 4 | questId: number 5 | title: string 6 | detail: string 7 | category: number 8 | type: number 9 | origin: string 10 | selections: [number] 11 | material: [number] 12 | bonus: any[] 13 | bounsCount: number 14 | } 15 | 16 | interface QuestReardDocument extends Document, QuestRewardPayload { 17 | key: string 18 | } 19 | 20 | const QuestRewardSchema = new mongoose.Schema({ 21 | questId: Number, 22 | title: String, 23 | detail: String, 24 | category: Number, 25 | type: Number, 26 | origin: String, 27 | key: String, 28 | selections: [Number], 29 | material: [Number], 30 | bonus: [{}], 31 | bounsCount: Number, 32 | }) 33 | 34 | export const QuestReward = mongoose.model('QuestReward', QuestRewardSchema) 35 | -------------------------------------------------------------------------------- /src/models/report/quest.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface QuestPayload { 4 | questId: number 5 | title: string 6 | detail: string 7 | category: number 8 | type: number 9 | origin: string 10 | } 11 | 12 | export interface QuestDocument extends Document, QuestPayload { 13 | key: string 14 | } 15 | 16 | const QuestSchema = new mongoose.Schema({ 17 | questId: Number, 18 | title: String, 19 | detail: String, 20 | category: Number, 21 | type: Number, 22 | origin: String, 23 | key: String, 24 | }) 25 | 26 | export const Quest = mongoose.model('Quest', QuestSchema) 27 | -------------------------------------------------------------------------------- /src/models/report/recipe.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface RecipeRecordPayload { 4 | recipeId: number 5 | itemId: number 6 | stage: number 7 | day: number 8 | secretary: number 9 | fuel: number 10 | ammo: number 11 | steel: number 12 | bauxite: number 13 | reqItemId: number 14 | reqItemCount: number 15 | buildkit: number 16 | remodelkit: number 17 | certainBuildkit: number 18 | certainRemodelkit: number 19 | upgradeToItemId: number 20 | upgradeToItemLevel: number 21 | lastReported: number 22 | count: number 23 | key: string 24 | origin: string 25 | } 26 | 27 | interface RecipeRecordDocument extends Document, RecipeRecordPayload {} 28 | 29 | const RecipeRecordSchema = new mongoose.Schema({ 30 | recipeId: Number, 31 | itemId: Number, 32 | stage: Number, 33 | day: Number, 34 | secretary: Number, 35 | fuel: Number, 36 | ammo: Number, 37 | steel: Number, 38 | bauxite: Number, 39 | reqItemId: Number, 40 | reqItemCount: Number, 41 | buildkit: Number, 42 | remodelkit: Number, 43 | certainBuildkit: Number, 44 | certainRemodelkit: Number, 45 | upgradeToItemId: Number, 46 | upgradeToItemLevel: Number, 47 | lastReported: Number, 48 | count: Number, 49 | key: String, 50 | origin: String, 51 | }) 52 | 53 | export const RecipeRecord = mongoose.model('RecipeRecord', RecipeRecordSchema) 54 | -------------------------------------------------------------------------------- /src/models/report/remodel-item.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface RemodelItemRecordPayload { 4 | successful: boolean 5 | itemId: number 6 | itemLevel: number 7 | flagshipId: number 8 | flagshipLevel: number 9 | flagshipCond: number 10 | consortId: number 11 | consortLevel: number 12 | consortCond: number 13 | teitokuLv: number 14 | certain: boolean 15 | } 16 | 17 | interface RemodelItemRecordDocument extends RemodelItemRecordPayload, Document {} 18 | 19 | const RemodelItemRecordSchema = new mongoose.Schema({ 20 | successful: Boolean, 21 | itemId: Number, 22 | itemLevel: Number, 23 | flagshipId: Number, 24 | flagshipLevel: Number, 25 | flagshipCond: Number, 26 | consortId: Number, 27 | consortLevel: Number, 28 | consortCond: Number, 29 | teitokuLv: Number, 30 | certain: Boolean, 31 | }) 32 | 33 | export const RemodelItemRecord = mongoose.model( 34 | 'RemodelItemRecord', 35 | RemodelItemRecordSchema, 36 | ) 37 | -------------------------------------------------------------------------------- /src/models/report/select-rank.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface SelectRankRecordPayload { 4 | teitokuId: string 5 | teitokuLv: number 6 | mapareaId: number 7 | rank: number 8 | origin: string 9 | } 10 | 11 | interface SelectRankRecordDocument extends SelectRankRecordPayload, Document {} 12 | 13 | const SelectRankRecordSchema = new mongoose.Schema({ 14 | teitokuId: String, 15 | teitokuLv: Number, 16 | mapareaId: Number, 17 | rank: Number, 18 | origin: String, 19 | }) 20 | 21 | export const SelectRankRecord = mongoose.model( 22 | 'SelectRankRecord', 23 | SelectRankRecordSchema, 24 | ) 25 | -------------------------------------------------------------------------------- /src/models/report/ship-stat.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document } from 'mongoose' 2 | 3 | export interface ShipStatPayload { 4 | id: number 5 | lv: number 6 | los: number 7 | los_max: number 8 | asw: number 9 | asw_max: number 10 | evasion: number 11 | evasion_max: number 12 | last_timestamp: number 13 | count: number 14 | } 15 | 16 | // FIXME: ship stat id type overrides document's 17 | type ShipStatDocument = Document & ShipStatPayload 18 | 19 | const ShipStatSchema = new mongoose.Schema({ 20 | id: Number, 21 | lv: Number, 22 | los: Number, 23 | los_max: Number, 24 | asw: Number, 25 | asw_max: Number, 26 | evasion: Number, 27 | evasion_max: Number, 28 | last_timestamp: Number, 29 | count: Number, 30 | }) 31 | 32 | export const ShipStat = mongoose.model('ShipStat', ShipStatSchema) 33 | -------------------------------------------------------------------------------- /src/sentry.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { ExpressRequest } from '@sentry/node/dist/handlers' 3 | import { extractTraceparentData, stripUrlQueryAndFragment, Integrations } from '@sentry/tracing' 4 | import { DefaultState, DefaultContext, Middleware, ParameterizedContext } from 'koa' 5 | 6 | import { config } from './config' 7 | 8 | Sentry.init({ 9 | dsn: 'https://99bc543aa0984d51917e02a873bb244f@o171991.ingest.sentry.io/5594215', 10 | environment: config.env, 11 | tracesSampleRate: 0.001, 12 | integrations: [new Integrations.Mongo()], 13 | }) 14 | 15 | export const captureException = ( 16 | err: Error, 17 | ctx: ParameterizedContext, 18 | ): void => { 19 | Sentry.withScope(function (scope) { 20 | scope.setUser({ ip_address: ctx.headers['x-real-ip'] || ctx.headers['x-forwarded-for'] }) 21 | scope.setTags({ 22 | reporter: ctx.headers['x-reporter'] || ctx.headers['user-agent'], 23 | version: global.latestCommit?.slice(0, 8), 24 | }) 25 | scope.addEventProcessor(function (event) { 26 | return Sentry.Handlers.parseRequest(event, (ctx.request as any) as ExpressRequest) 27 | }) 28 | Sentry.captureException(err) 29 | }) 30 | } 31 | 32 | export const sentryTracingMiddileaware: Middleware = async (ctx, next) => { 33 | const reqMethod = (ctx.method || '').toUpperCase() 34 | const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url) 35 | 36 | // connect to trace of upstream app 37 | let traceparentData 38 | if (ctx.request.get('sentry-trace')) { 39 | traceparentData = extractTraceparentData(ctx.request.get('sentry-trace')) 40 | } 41 | 42 | const transaction = Sentry.startTransaction({ 43 | name: `${reqMethod} ${reqUrl}`, 44 | op: 'http.server', 45 | ...traceparentData, 46 | }) 47 | 48 | ctx.__sentry_transaction = transaction 49 | await next() 50 | 51 | const mountPath = ctx.mountPath || '' 52 | transaction.setName(`${reqMethod} ${mountPath}${ctx.path}`) 53 | 54 | transaction.setHttpStatus(ctx.status) 55 | Sentry.withScope((scope) => { 56 | scope.setUser({ ip_address: ctx.headers['x-real-ip'] || ctx.headers['x-forwarded-for'] }) 57 | scope.setTags({ 58 | reporter: ctx.headers['x-reporter'] || ctx.headers['user-agent'], 59 | url: ctx.request.url, 60 | version: global.latestCommit?.slice(0, 8), 61 | }) 62 | scope.setContext('data', ctx.request.body.data) 63 | transaction.finish() 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /supervisior.conf: -------------------------------------------------------------------------------- 1 | [program:poi] 2 | command=/usr/bin/node index.js 3 | directory=/srv/poi 4 | environment= 5 | NODE_ENV=production, 6 | BABEL_DISABLE_CACHE=1, 7 | POI_SERVER_DISABLE_LOGGER=1, 8 | POI_SERVER_DB=mongodb://localhost:27017/poi-production, 9 | POI_SERVER_PORT=17027 10 | user=www-data 11 | 12 | [program:poi-hook] 13 | command=/usr/bin/node hooks.js 14 | directory=/srv/poi 15 | environment= 16 | NODE_ENV=production, 17 | BABEL_DISABLE_CACHE=1 18 | user=poi 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | --------------------------------------------------------------------------------