├── .github └── workflows │ ├── docker_compose.yml │ ├── node_version_10x.yml │ ├── node_version_12x.yml │ └── node_version_14x.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── image ├── arcaeabots_16x16.png ├── arcaeabots_32x32.png ├── arcaeabots_64x64.png └── favicon.ico ├── package-lock.json ├── package.json ├── source ├── @declares │ ├── archash4all.d.ts │ ├── global.d.ts │ ├── safe-eval.d.ts │ └── sqlite-async.d.ts ├── __loader__.ts ├── corefunc │ ├── config.ts │ ├── database.ts │ └── utils.ts ├── index.ts ├── modules │ ├── account │ │ ├── alloc.managed.ts │ │ ├── alloc.ts │ │ ├── feed.managed.ts │ │ ├── fromtoken.ts │ │ ├── recycle.managed.ts │ │ └── recycle.ts │ ├── apierror │ │ └── apierror.ts │ ├── arcfetch │ │ ├── arcapi.aggregate.ts │ │ ├── arcapi.any.ts │ │ ├── arcapi.error.ts │ │ ├── arcapi.friend.add.ts │ │ ├── arcapi.friend.clear.ts │ │ ├── arcapi.friend.delete.ts │ │ ├── arcapi.login.ts │ │ ├── arcapi.rank.friend.ts │ │ ├── arcapi.rank.me.ts │ │ ├── arcapi.rank.world.ts │ │ ├── arcapi.userme.ts │ │ ├── arcfetch.ts │ │ └── interfaces │ │ │ ├── IArcAccount.ts │ │ │ ├── IArcPlayer.ts │ │ │ ├── IArcScore.ts │ │ │ └── IArcUserMe.ts │ ├── database │ │ ├── database.arcaccount.all.ts │ │ ├── database.arcaccount.init.ts │ │ ├── database.arcaccount.update.ts │ │ ├── database.arcbest30.byuid.ts │ │ ├── database.arcbest30.init.ts │ │ ├── database.arcbest30.update.ts │ │ ├── database.arcplayer.byany.ts │ │ ├── database.arcplayer.byusercode.ts │ │ ├── database.arcplayer.byuserid.ts │ │ ├── database.arcplayer.init.ts │ │ ├── database.arcplayer.update.ts │ │ ├── database.arcrecord.byuserid.ts │ │ ├── database.arcrecord.history.ts │ │ ├── database.arcrecord.init.ts │ │ ├── database.arcrecord.update.ts │ │ ├── database.arcsong.alias.byid.ts │ │ ├── database.arcsong.allcharts.ts │ │ ├── database.arcsong.byrand.ts │ │ ├── database.arcsong.byrating.ts │ │ ├── database.arcsong.bysongid.ts │ │ ├── database.arcsong.init.ts │ │ ├── database.arcsong.sid.byany.ts │ │ ├── database.arcsong.update.songlist.ts │ │ └── interfaces │ │ │ ├── IArcBest30Result.ts │ │ │ ├── IArcSong.ts │ │ │ ├── IArcSongList.ts │ │ │ ├── IDatabaseArcBest30.ts │ │ │ ├── IDatabaseArcPlayer.ts │ │ │ ├── IDatabaseArcRecord.ts │ │ │ ├── IDatabaseArcSong.ts │ │ │ ├── IDatabaseArcSongAlias.ts │ │ │ └── IDatabaseArcSongChart.ts │ └── syslog │ │ └── syslog.ts └── publicapi │ ├── v1 │ ├── arc │ │ ├── alloc.ts │ │ ├── forward.ts │ │ └── recycle.ts │ ├── random.ts │ ├── songalias.ts │ ├── songinfo.ts │ ├── userbest.ts │ ├── userbest30.ts │ └── userinfo.ts │ ├── v2 │ ├── arc │ │ ├── alloc.ts │ │ ├── forward.ts │ │ └── recycle.ts │ ├── connect.ts │ ├── random.ts │ ├── songalias.ts │ ├── songinfo.ts │ ├── userbest.ts │ ├── userbest30.ts │ └── userinfo.ts │ ├── v3 │ ├── arc │ │ ├── alloc.ts │ │ ├── forward.ts │ │ └── recycle.ts │ ├── connect.ts │ ├── random.ts │ ├── songalias.ts │ ├── songinfo.ts │ ├── update.ts │ ├── userbest.ts │ ├── userbest30.ts │ └── userinfo.ts │ └── v4 │ ├── batch.ts │ ├── connect.ts │ ├── forward │ ├── alloc.ts │ ├── feed.ts │ ├── forward.ts │ └── recycle.ts │ ├── song │ ├── alias.ts │ ├── id.ts │ ├── info.ts │ ├── random.ts │ └── rating.ts │ ├── update.ts │ └── user │ ├── best.ts │ ├── best30.ts │ ├── history.ts │ └── info.ts ├── tests └── index.ts └── tsconfig.json /.github/workflows/docker_compose.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Docker Compose 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | compose-linux: 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | # Checkout the repo 20 | - uses: actions/checkout@v2 21 | 22 | # Docker compose 23 | - name: Test Docker Compose 24 | run: docker-compose up --no-start 25 | -------------------------------------------------------------------------------- /.github/workflows/node_version_10x.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: NodeJS 10.x 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | node-10x: 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | # Checkout the repo 20 | - uses: actions/checkout@v2 21 | 22 | # NodeJS 10.x 23 | - name: Use Node.js 10.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 10.x 27 | 28 | # Initialize node modules 29 | - name: Initialize 30 | run: npm ci 31 | 32 | # Compile the project 33 | - name: Compile 34 | run: npm i -g typescript && tsc --build tsconfig.json 35 | 36 | # Test run 37 | - name: Test Run 38 | run: npm test 39 | -------------------------------------------------------------------------------- /.github/workflows/node_version_12x.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: NodeJS 12.x 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | node-12x: 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | # Checkout the repo 20 | - uses: actions/checkout@v2 21 | 22 | # NodeJS 12.x 23 | - name: Use Node.js 12.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 12.x 27 | 28 | # Initialize node modules 29 | - name: Initialize 30 | run: npm ci 31 | 32 | # Compile the project 33 | - name: Compile 34 | run: npm i -g typescript && tsc --build tsconfig.json 35 | 36 | # Test run 37 | - name: Test Run 38 | run: npm test 39 | -------------------------------------------------------------------------------- /.github/workflows/node_version_14x.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: NodeJS 14.x 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | node-14x: 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | # Checkout the repo 20 | - uses: actions/checkout@v2 21 | 22 | # NodeJS 14.x 23 | - name: Use Node.js 14.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 14.x 27 | 28 | # Initialize node modules 29 | - name: Initialize 30 | run: npm ci 31 | 32 | # Compile the project 33 | - name: Compile 34 | run: npm i -g typescript && tsc --build tsconfig.json 35 | 36 | # Test run 37 | - name: Test Run 38 | run: npm test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Glitch 2 | .glitchdotcom.json 3 | 4 | # node modules 5 | [Nn]ode_modules/ 6 | 7 | # database files 8 | [Ss]avedata/ 9 | *.db 10 | 11 | # logs 12 | [Ss]avelogs/ 13 | 14 | # ignore test apis 15 | [Ss]ource/publicapi/debug/ 16 | 17 | # ignore build 18 | [Bb]uild/ 19 | 20 | # windows file 21 | desktop.ini 22 | 23 | # vscode file 24 | .vscode -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.11 2 | 3 | # copy source files to container 4 | COPY . botarcapi 5 | 6 | # install nodejs and npm 7 | RUN apk --update add nodejs npm 8 | 9 | # initialize the node modules 10 | # and compile the project 11 | RUN cd /botarcapi \ 12 | && npm i \ 13 | && npm i -g typescript \ 14 | && tsc --build tsconfig.json 15 | 16 | # start service 17 | WORKDIR /botarcapi 18 | CMD ["npm", "start"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TheSnowfield 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BotArcAPI [![image](image/arcaeabots_32x32.png)](#) 2 | 3 | [![version](https://img.shields.io/static/v1?label=api-ver&message=v4&color=green)](#) 4 | [![status](https://img.shields.io/static/v1?label=status&message=develop&color=red)](#) 5 | [![chat](https://img.shields.io/static/v1?label=qq-group&message=866231846&color=blue)](#) 6 | [![Docker Image](https://github.com/TheSnowfield/BotArcAPI/workflows/Docker%20Compose/badge.svg)](#) 7 | 8 | A fast and easy Arcaea API for your bot. 9 | 10 | ## 🤔 Dependences 11 | - abab `^2.0.5` 12 | - archash4all `^1.2.0` 13 | - node-fetch `^2.6.1` 14 | - sqlite-async `^1.1.0` 15 | - safe-eval `^0.4.1` 16 | 17 | ## 🌈 Guide for using 18 | - [Get Started](../../wiki/Get-Started) 19 | - [Get Started With Docker](../../wiki/Get-Started-With-Docker) 20 | - [API Reference v4](../../wiki/API-Reference-v4) 21 | 22 | ## 🧰 NPM Package 23 | There's an NPM package now available! It offers you an interface to use BotArcAPI quickly! 24 | Best thanks for [@ReiKohaku](https://github.com/ReiKohaku)! 25 | See more information on [botarcapi_lib](https://www.npmjs.com/package/botarcapi_lib). 26 | 27 | [![NPM](https://nodei.co/npm/botarcapi_lib.png)](https://www.npmjs.com/package/botarcapi_lib) 28 | 29 | ## 🙌 Contribute with song database 30 | We're looking for new song alias now. 31 | If you know the new song alias which is not recorded in the database yet, Please [draft a discussion](../../discussions) to talk about this. Your name will be recorded in this file while we accepted your offer. 32 | 33 | Special thanks for arcsong.db contributors: 34 | [@AzusaYukina](https://github.com/AzusaYukina) [@littlenine12](https://github.com/littlenine12) [@iyume](https://github.com/iyume), 35 | And [@All Arcaea Players](#) ! 36 | 37 | ## 💕 License 38 | Licensed under `MIT License`. 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker compose file for BotArcAPI 2 | 3 | version: '3' 4 | 5 | services: 6 | botarcapi: 7 | build: . 8 | restart: always 9 | container_name: 'botarcapi' 10 | environment: 11 | 12 | # modify this will 13 | # overriding default configs 14 | BOTARCAPI_CONFIG: | 15 | { 16 | "SERVER_PORT": 80 17 | "BOTARCAPI_VERSTR": "v0.3.7", 18 | "ARCAPI_VERSION": 15, 19 | "ARCAPI_APPVERSION": "3.6.4c", 20 | "ARCAPI_USERAGENT": "Grievous Lady (Linux; U; Android 2.3.3; BotArcAPI)", 21 | "LOG_LEVEL": 0 22 | } 23 | 24 | # expose service port 25 | # to localhost at 8000 26 | ports: 27 | - '8000:80' 28 | -------------------------------------------------------------------------------- /image/arcaeabots_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcaea-Infinity/BotArcAPIs-Memories/35f2bb563c0db0d02589028ecf4194971af69f58/image/arcaeabots_16x16.png -------------------------------------------------------------------------------- /image/arcaeabots_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcaea-Infinity/BotArcAPIs-Memories/35f2bb563c0db0d02589028ecf4194971af69f58/image/arcaeabots_32x32.png -------------------------------------------------------------------------------- /image/arcaeabots_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcaea-Infinity/BotArcAPIs-Memories/35f2bb563c0db0d02589028ecf4194971af69f58/image/arcaeabots_64x64.png -------------------------------------------------------------------------------- /image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arcaea-Infinity/BotArcAPIs-Memories/35f2bb563c0db0d02589028ecf4194971af69f58/image/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botarcapi", 3 | "version": "0.3.7", 4 | "description": "A fast and easy Arcaea API for your bot.", 5 | "dependencies": { 6 | "abab": "^2.0.5", 7 | "archash4all": "^1.2.0", 8 | "node-fetch": "^2.6.1", 9 | "safe-eval": "^0.4.1", 10 | "sqlite-async": "^1.1.0" 11 | }, 12 | "devDependencies": { 13 | "@types/node-fetch": "^2.5.7", 14 | "@types/sqlite3": "^3.1.6", 15 | "@mapbox/node-pre-gyp": "^1.0.5", 16 | "node-pre-gyp": "^0.17.0", 17 | "typescript": "^4.3.3", 18 | "sqlite3": "^5.0.2" 19 | }, 20 | "bin": "build/source/index.js", 21 | "pkg": { 22 | "scripts": "build/source/publicapi/**/*.js", 23 | "assets": [ 24 | "image/**", 25 | "node_modules/sqlite3/lib/binding/**/node_sqlite3.node", 26 | "node_modules/archash4all/lib/binding/**/archash4all.node" 27 | ], 28 | "targets": [ 29 | "node14-win-x64", 30 | "node14-linux-x64" 31 | ], 32 | "outputPath": "build/bin" 33 | }, 34 | "scripts": { 35 | "start": "tsc --build --clean && tsc --build && node build/source/index.js", 36 | "test": "node build/tests/index.js" 37 | }, 38 | "engines": { 39 | "node": "14.x" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/TheSnowfield/BotArcAPI" 44 | }, 45 | "author": "TheSnowfield", 46 | "license": "MIT" 47 | } 48 | -------------------------------------------------------------------------------- /source/@declares/archash4all.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'archash4all' { 2 | export function archash(input: string): string 3 | } 4 | -------------------------------------------------------------------------------- /source/@declares/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var BOTARCAPI_MAJOR: number; 2 | declare var BOTARCAPI_MINOR: number; 3 | declare var BOTARCAPI_VERSION: number; 4 | declare var BOTARCAPI_VERSTR: string; 5 | declare var BOTARCAPI_WHITELIST: Array; 6 | declare var BOTARCAPI_BATCH_ENDPOINTS_MAX: number; 7 | declare var BOTARCAPI_FORWARD_WHITELIST: Array; 8 | declare var BOTARCAPI_FORWARD_TIMESEC_MAX: number; 9 | declare var BOTARCAPI_FORWARD_TIMESEC_DEFAULT: number; 10 | declare var BOTARCAPI_FORWARD_FEED_MAX: number; 11 | declare var BOTARCAPI_USERBEST_HISTORY_MAX: number; 12 | declare var BOTARCAPI_USERBEST_HISTORY_DEFAULT: number; 13 | declare var BOTARCAPI_AGGREGATE_LIMITATION: number; 14 | declare var BOTARCAPI_AGGREGATE_ENABLED: boolean; 15 | declare var BOTARCAPI_AGGREGATE_CONCURRENT: boolean; 16 | declare var BOTARCAPI_FRONTPROXY_NODES: Array; 17 | declare var BOTARCAPI_FRONTPROXY_CHANGE_NODE: boolean; 18 | 19 | interface FontProxyNode { 20 | url: string, 21 | weight: number 22 | enabled: boolean, 23 | } 24 | 25 | declare var ARCAPI_RETRY: number; 26 | declare var ARCAPI_VERSION: number; 27 | declare var ARCAPI_APPVERSION: string; 28 | declare var ARCAPI_USERAGENT: string; 29 | declare var ARCAPI_URL: string; 30 | declare var ARCAPI_URL_CODENAME: string; 31 | 32 | declare var DATABASE_PATH: string; 33 | declare var SERVER_PORT: number; 34 | 35 | declare var LOG_LEVEL: number; 36 | declare var LOG_PATH: string; 37 | 38 | declare var ARCACCOUNT: any; 39 | declare var ARCPERSISTENT: { 40 | [key: string]: { 41 | account: any, 42 | feed: number, 43 | feeded: number, 44 | validtime: number, 45 | proc?: any 46 | } 47 | }; 48 | declare var DATABASE_ARCACCOUNT: any; 49 | declare var DATABASE_ARCBEST30: any; 50 | declare var DATABASE_ARCPLAYER: any; 51 | declare var DATABASE_ARCRECORD: any; 52 | declare var DATABASE_ARCSONG: any; 53 | -------------------------------------------------------------------------------- /source/@declares/safe-eval.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'safe-eval' { 2 | export default function safeEval(code: string, context: any, opts?: any): any 3 | } 4 | -------------------------------------------------------------------------------- /source/@declares/sqlite-async.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sqlite-async' { 2 | export const OPEN_CREATE: number 3 | export const OPEN_READWRITE: number 4 | export const open: any; 5 | export const close: any; 6 | } 7 | -------------------------------------------------------------------------------- /source/__loader__.ts: -------------------------------------------------------------------------------- 1 | // filename : __loader__.js 2 | // author : TheSnowfield, t404owo 3 | // date : 02/09/2020 4 | // comment : loader handles api requests and map to require files 5 | // it's also a global access point 6 | 7 | const TAG: string = 'source/__loader__.ts'; 8 | 9 | import Utils from './corefunc/utils'; 10 | import APIError from './modules/apierror/apierror'; 11 | import syslog from './modules/syslog/syslog'; 12 | import { IncomingMessage, ServerResponse, IncomingHttpHeaders } from 'http'; 13 | 14 | const handler_request_notfound = async (response: ServerResponse, message = '') => { 15 | response.statusCode = 404; 16 | response.setHeader('Content-Type', 'text/html; charset=utf-8'); 17 | response.setHeader('Server', `${BOTARCAPI_VERSTR}`); 18 | response.end(message); 19 | syslog.v(TAG, 'Send response back'); 20 | } 21 | 22 | const handler_request_error = async (response: ServerResponse, message = '') => { 23 | response.statusCode = 500; 24 | response.setHeader('Content-Type', 'text/html; charset=utf-8'); 25 | response.setHeader('Server', `${BOTARCAPI_VERSTR}`); 26 | response.end(message); 27 | syslog.v(TAG, 'Send response back'); 28 | } 29 | 30 | const handler_request_favicon = async (response: ServerResponse) => { 31 | 32 | let _http_body: string = ''; 33 | let _http_status: number = 0; 34 | let _http_content_type: string = ''; 35 | 36 | const file = require('fs'); 37 | try { 38 | await file.promises.readFile('./image/favicon.ico') 39 | .then((data: any) => { 40 | _http_status = 200; 41 | _http_body = data; 42 | _http_content_type = 'image/x-icon'; 43 | }); 44 | } catch (e) { 45 | syslog.e(e.stack); 46 | return handler_request_notfound(response); 47 | } 48 | 49 | // send result to client 50 | response.statusCode = _http_status; 51 | response.setHeader('Content-Type', _http_content_type); 52 | response.setHeader('Server', `${BOTARCAPI_VERSTR}`); 53 | response.end(_http_body); 54 | syslog.v(TAG, 'Send response back'); 55 | } 56 | 57 | const forward_route: { [key: string]: RegExp } = { 58 | '/v1/arc/forward': /^\/v1\/arc\/forward\//, 59 | '/v2/arc/forward': /^\/v2\/arc\/forward\//, 60 | '/v3/arc/forward': /^\/v3\/arc\/forward\//, 61 | '/v4/forward/forward': /^\/v4\/forward\/forward\// 62 | }; 63 | 64 | const handler_request_publicapi = 65 | async (response: ServerResponse, argument: string, 66 | method: string, path: string, header: IncomingHttpHeaders, databody: string | null) => { 67 | 68 | let _http_body: string = ''; 69 | let _http_status: number = 0; 70 | let _http_content_type: string = ''; 71 | let _api_entry: any; 72 | 73 | try { 74 | 75 | // try to match the route to forward 76 | for (const v in forward_route) { 77 | if (new RegExp(forward_route[v]).test(path)) { 78 | 79 | path = path.replace(forward_route[v], ''); 80 | _api_entry = await import(`./publicapi/${v}`); 81 | 82 | break; 83 | } 84 | } 85 | 86 | // require directly if no match 87 | if (!_api_entry) 88 | _api_entry = await import(`./publicapi/${path}.js`); 89 | } 90 | catch (e) { 91 | syslog.w(TAG, `Invalid request path => ${path}`); 92 | return handler_request_notfound(response, 'request path notfound =(:3) z)_'); 93 | } 94 | 95 | try { 96 | 97 | const _api_result: any = {}; 98 | 99 | // try to invoke method 100 | _api_entry = _api_entry.default; 101 | await _api_entry(argument, method, path, header, databody) 102 | .then((result: any) => { 103 | _api_result.status = 0; 104 | _api_result.content = result; 105 | }) 106 | .catch((error: APIError) => { 107 | _api_result.status = error.status; 108 | _api_result.message = error.notify; 109 | }); 110 | 111 | _http_status = 200; 112 | _http_body = JSON.stringify(_api_result); 113 | _http_content_type = 'application/json; charset=utf-8'; 114 | } 115 | catch (e) { 116 | syslog.e(TAG, e.stack); 117 | return handler_request_error(response, 'some error occurred! (>_<)|||'); 118 | } 119 | 120 | // send result to client 121 | response.statusCode = _http_status; 122 | response.setHeader('Content-Type', _http_content_type); 123 | response.setHeader('Access-Control-Allow-Origin', '*'); 124 | response.setHeader('Server', `${BOTARCAPI_VERSTR}`); 125 | response.end(_http_body); 126 | 127 | syslog.v(TAG, 'Send response back'); 128 | } 129 | 130 | const handler = async (request: IncomingMessage, response: ServerResponse) => { 131 | 132 | // user-agent and client-agent 133 | const _sign_agent: string = 134 | request.headers['client-agent'] ?? 135 | request.headers['user-agent'] ?? ''; 136 | 137 | // match useragent 138 | if (!Utils.httpMatchUserAgent(_sign_agent)) { 139 | syslog.w(TAG, `Invalid user agent => ${_sign_agent}`); 140 | return handler_request_notfound(response); 141 | } 142 | 143 | // process request url 144 | const _api_url = new URL(`http://0.0.0.0${request.url}`); 145 | const _api_path = _api_url.pathname; 146 | const _api_headers = request.headers; 147 | const _api_arguments = Utils.httpGetAllParams(_api_url.searchParams); 148 | 149 | syslog.i(TAG, 150 | `Accept ${_sign_agent} ` + 151 | `request => ${request.method} ` + 152 | `${_api_path} ${JSON.stringify(_api_arguments)}` 153 | ); 154 | 155 | let _rawdata = ''; 156 | request.on('data', (data) => { _rawdata += data; }); 157 | request.on('end', () => { 158 | 159 | // handle favicon request 160 | if (request.method == 'GET') { 161 | if (_api_path == '/favicon.ico') 162 | return handler_request_favicon(response); 163 | } 164 | 165 | // receive the body data for post requests 166 | let _api_bodydata: string | null = null; 167 | if (request.method == 'POST') 168 | _api_bodydata = _rawdata; 169 | 170 | // handle public api request 171 | return handler_request_publicapi( 172 | response, _api_arguments, request.method ?? '', _api_path, 173 | _api_headers, _api_bodydata 174 | ); 175 | 176 | }); 177 | 178 | } 179 | 180 | export default handler; 181 | -------------------------------------------------------------------------------- /source/corefunc/config.ts: -------------------------------------------------------------------------------- 1 | const TAG = 'corefunc/config.ts'; 2 | 3 | import syslog from '../modules/syslog/syslog'; 4 | 5 | const _default_config: any = { 6 | // botarcapi version 7 | 'BOTARCAPI_MAJOR': 0, 8 | 'BOTARCAPI_MINOR': 3, 9 | 'BOTARCAPI_VERSION': 7, 10 | 'BOTARCAPI_VERSTR': 'BotArcAPI v0.3.7', 11 | 12 | // useragent whitelist 13 | // if set '[]' will accept all requests 14 | // supported regex 15 | 'BOTARCAPI_WHITELIST': [], 16 | 17 | // the max of endpoint limitation for batch api 18 | 'BOTARCAPI_BATCH_ENDPOINTS_MAX': 10, 19 | 20 | // forwarding whitelist 21 | // BotArcAPI can forward apis in whitelist only 22 | 'BOTARCAPI_FORWARD_WHITELIST': [ 23 | /^user\/me/g, 24 | /^friend\/me\/add/g, 25 | /^friend\/me\/delete/g, 26 | /^score\/song/g, 27 | /^score\/song\/me/g, 28 | /^score\/song\/friend/g 29 | ], 30 | 31 | // the valid time for the token 32 | // BotArcAPI will recycle the token while time exceed 33 | 'BOTARCAPI_FORWARD_TIMESEC_MAX': 120, 34 | 35 | // the default valid time for the token 36 | 'BOTARCAPI_FORWARD_TIMESEC_DEFAULT': 10, 37 | 38 | // the number of times the callers can extend the valid time of the token 39 | // BotArcAPI will decline the feeds while exceeding the limitation 40 | 'BOTARCAPI_FORWARD_FEED_MAX': 2, 41 | 42 | 'BOTARCAPI_USERBEST_HISTORY_MAX': 20, 43 | 'BOTARCAPI_USERBEST_HISTORY_DEFAULT': 7, 44 | 45 | // aggregate limit 46 | 'BOTARCAPI_AGGREGATE_LIMITATION': 6, 47 | 48 | // sending only one 'compose/aggregate' request instead 49 | // of send a huge of requests to arcapi. 50 | // this feature is for 3.6.0 arcapi 51 | 'BOTARCAPI_AGGREGATE_ENABLED': false, 52 | 53 | // sending requests concurrently 54 | // only valid on 'BOTARCAPI_AGGREGATE_ENABLED' set to false 55 | 'BOTARCAPI_AGGREGATE_CONCURRENT': false, 56 | 57 | // frontend http proxy 58 | // used to load balance for per ip 59 | // change this will ignore the 'ARCAPI_URL' config 60 | // if set to '[]' will disable the frontend proxy 61 | 'BOTARCAPI_FRONTPROXY_NODES': [ 62 | // { url: "https://arcapi.lowiro.com", weight: 1.0 }, 63 | // { url: "https://example.com", weight: 0.8 }, 64 | // { url: "https://your.proxy.node.com", weight: 0.3 }, 65 | ], 66 | 67 | // change proxy node when request fail 68 | // this feature is for 3.6.0 arcapi 69 | 'BOTARCAPI_FRONTPROXY_CHANGE_NODE': false, 70 | 71 | // arcaea api config 72 | 'ARCAPI_RETRY': 3, 73 | 'ARCAPI_VERSION': 15, 74 | 'ARCAPI_APPVERSION': '3.6.4c', 75 | 'ARCAPI_USERAGENT': 'Grievous Lady (Linux; U; Android 2.3.3; BotArcAPI)', 76 | 'ARCAPI_URL': 'https://arcapi.lowiro.com', 77 | 'ARCAPI_URL_CODENAME': 'kusoatui', 78 | 79 | // path to database folder 80 | 'DATABASE_PATH': './savedata/', 81 | 82 | // http server listening post 83 | 'SERVER_PORT': 80, 84 | 85 | // log level 86 | // 3: Error, Fatal 87 | // 2: Warning, Error, Fatal 88 | // 1: Information, Warning, Error, Fatal 89 | // 0: Verbose, Information, Warning, Error, Fatal 90 | 'LOG_LEVEL': 0, 91 | 92 | // path to log folder 93 | 'LOG_PATH': './savelogs/' 94 | } 95 | 96 | /** 97 | * load config to global space 98 | */ 99 | const loadConfigs = (): void => { 100 | 101 | // load environment from docker containers 102 | // and overriding default configs =(:3) z)_ 103 | const _external_config: string | undefined = process.env.BOTARCAPI_CONFIG; 104 | if (_external_config) { 105 | 106 | try { 107 | 108 | const _root = JSON.parse(_external_config); 109 | for (const v in _root) 110 | _default_config[v] = _root[v]; 111 | 112 | } catch (e) {/* do nothing */ } 113 | 114 | } 115 | 116 | for (const [k, v] of Object.entries(_default_config)) { 117 | Object.defineProperty(global, k, { value: v, writable: false, configurable: false }); 118 | } 119 | 120 | } 121 | 122 | const printConfigs = (): void => { 123 | 124 | syslog.v(TAG, 'Global Config'); 125 | 126 | for (let [k, v] of Object.entries(_default_config)) { 127 | syslog.v(TAG, ` ${k} => ${v}`); 128 | } 129 | } 130 | 131 | export default { loadConfigs, printConfigs }; 132 | -------------------------------------------------------------------------------- /source/corefunc/utils.ts: -------------------------------------------------------------------------------- 1 | class Utils { 2 | 3 | // calc song rating for arcaea 4 | static arcCalcSongRating(score: number, ptt: number): number { 5 | 6 | if (score >= 10_000_000) 7 | return ptt + 2; 8 | 9 | else if (score > 9_800_000) 10 | return ptt + 1 + (score - 9_800_000) / 200_000; 11 | 12 | let _value = ptt + (score - 9_500_000) / 300_000; 13 | return _value < 0 ? 0 : _value; 14 | 15 | } 16 | 17 | // map song difficulty to specific format 18 | static arcMapDiffFormat(input: string | number, format: number): string | null { 19 | 20 | const _table_format: Array = [ 21 | '0', '1', '2', '3', 22 | 'pst', 'prs', 'ftr', 'byn', 23 | 'PST', 'PRS', 'FTR', 'BYN', 24 | 'past', 'present', 'future', 'beyond', 25 | 'PAST', 'PRESENT', 'FUTURE', 'BEYOND' 26 | ]; 27 | 28 | if (typeof input == 'string') { 29 | input = input.toLowerCase(); 30 | // byd to byn 31 | if (input == 'byd') input = 'byn'; 32 | } 33 | 34 | // try parse input as an integer 35 | let _to_format: string | null = null; 36 | _table_format.every((element: string, index: number) => { 37 | if (input != element) 38 | return true; 39 | 40 | _to_format = _table_format[format * 4 + index % 4]; 41 | return false; 42 | }); 43 | 44 | if (!_to_format) return ''; 45 | 46 | return _to_format; 47 | 48 | } 49 | 50 | // instead of Object.fromEntries 51 | static httpGetAllParams(searchParams: URLSearchParams): any { 52 | if (!(searchParams instanceof URLSearchParams)) 53 | return null; 54 | 55 | const _return: any = {}; 56 | 57 | searchParams.forEach((v, k) => { _return[k] = v; }); 58 | 59 | return _return; 60 | } 61 | 62 | // match client user agent 63 | static httpMatchUserAgent(ua: string): boolean { 64 | 65 | if (typeof BOTARCAPI_WHITELIST != 'object') return true; 66 | if (!BOTARCAPI_WHITELIST.length) return true; 67 | 68 | for (const v of BOTARCAPI_WHITELIST) { 69 | if (v.test(ua)) return true; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | // check illegal bind variables 76 | static checkBindStatement(bind: any): boolean { 77 | const beCheck: any = Object.values(bind); 78 | 79 | for (let i = 0; i < beCheck.length; ++i) { 80 | if (beCheck[i].match(/;|\(|\)|var|let|const|delete|undefined|null|=>|\(\)|{|}|{}|=|#|==|&|\||\^|!|\*|\/|-|\+|>|<|\?/g)) 81 | return false; 82 | } 83 | 84 | return true; 85 | } 86 | 87 | } 88 | 89 | export default Utils; 90 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'source/index.ts'; 2 | 3 | import http from 'http'; 4 | import config from './corefunc/config'; 5 | import syslog from './modules/syslog/syslog'; 6 | import database from './corefunc/database'; 7 | import __loader__ from './__loader__'; 8 | 9 | (function main(): void { 10 | 11 | // initialize config first 12 | config.loadConfigs(); 13 | process.title = `${BOTARCAPI_VERSTR}`; 14 | 15 | // initialize system log 16 | syslog.startLogging(); 17 | 18 | // print configs 19 | config.printConfigs(); 20 | 21 | // initialize database 22 | database.initDataBases(); 23 | 24 | // goto main entry ヾ(^▽^*))) 25 | const service = http.createServer(__loader__).listen(SERVER_PORT); 26 | syslog.i(`Http server started at 0.0.0.0:${SERVER_PORT}`); 27 | 28 | // nodejs event handlers 29 | process.on('exit', (code) => { 30 | syslog.i(TAG, '** Stop Service **'); 31 | // stop http server 32 | service.close(); 33 | syslog.i(TAG, 'Stop http server'); 34 | // close databases 35 | database.close(); 36 | syslog.i(TAG, 'Stop all database access'); 37 | // stop syslog 38 | syslog.i(TAG, 'Stop logging'); 39 | syslog.stop(); 40 | }); 41 | process.on('SIGINT', () => { 42 | syslog.w(`You pressed ctrl + c`); 43 | process.exit(0); 44 | }); 45 | process.on('warning', (w) => { 46 | syslog.w(`warning => ${w.message}`); 47 | }); 48 | process.on('uncaughtException', (reason) => { 49 | syslog.f(`unhandledRejection => ${(reason)?.stack ?? 'unknown'}`); 50 | }); 51 | process.on('unhandledRejection', (reason, promise) => { 52 | syslog.f(`unhandledRejection => ${(reason)?.stack ?? 'unknown'}`); 53 | }); 54 | 55 | })(); 56 | -------------------------------------------------------------------------------- /source/modules/account/alloc.managed.ts: -------------------------------------------------------------------------------- 1 | const TAG = 'account/alloc.managed.ts'; 2 | 3 | import crypto from 'crypto'; 4 | import syslog from '../syslog/syslog'; 5 | import account_alloc from './alloc'; 6 | import account_recycle_managed from './recycle.managed'; 7 | import arcapi_friend_clear from '../arcfetch/arcapi.friend.clear'; 8 | import IArcAccount from '../arcfetch/interfaces/IArcAccount'; 9 | 10 | export default (valid_time: number, clear: boolean = false): Promise => { 11 | 12 | return new Promise(async (resolve, reject) => { 13 | 14 | // validate data 15 | if (valid_time < BOTARCAPI_FORWARD_TIMESEC_DEFAULT 16 | || valid_time > BOTARCAPI_FORWARD_TIMESEC_MAX) 17 | return reject(new Error('Invalid time')); 18 | 19 | // try to grab an account 20 | let _account: IArcAccount; 21 | try { 22 | _account = await account_alloc(); 23 | } catch (e) { return reject(new Error('Allocate account failed')); } 24 | 25 | // clear friends 26 | if (clear) { 27 | try { 28 | await arcapi_friend_clear(_account); 29 | } catch (e) { return reject(new Error('Clear friend failed')); } 30 | } 31 | 32 | // save account to persistent list 33 | const _token: string = crypto.randomBytes(16).toString('hex'); 34 | ARCPERSISTENT[_token] = { 35 | feed: 1, // must be 1 36 | feeded: 0, 37 | account: _account, 38 | validtime: valid_time * 1000 39 | }; 40 | 41 | // start check 42 | check_recycle(_token); 43 | 44 | resolve(_token); 45 | 46 | }); 47 | 48 | } 49 | 50 | const check_recycle = (token: string) => { 51 | 52 | // Validate the token 53 | if (!ARCPERSISTENT[token]) { 54 | syslog.w(TAG, 'Token not exists while recycling.'); 55 | return; 56 | } 57 | 58 | // Recycle the token 59 | if (--ARCPERSISTENT[token].feed < 0) { 60 | account_recycle_managed(token) 61 | .catch((e) => { /* do nothing */ }); 62 | return; 63 | } 64 | 65 | // reset the timeout 66 | ARCPERSISTENT[token].proc = 67 | setTimeout(() => check_recycle(token), ARCPERSISTENT[token].validtime); 68 | }; 69 | -------------------------------------------------------------------------------- /source/modules/account/alloc.ts: -------------------------------------------------------------------------------- 1 | const TAG = 'account/alloc.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcapi_login from '../arcfetch/arcapi.login'; 5 | import arcapi_userme from '../arcfetch/arcapi.userme'; 6 | import arcaccount_update from '../database/database.arcaccount.update'; 7 | import IArcAccount from '../arcfetch/interfaces/IArcAccount'; 8 | 9 | export default (): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | let _account: IArcAccount | undefined; 14 | 15 | while (true) { 16 | 17 | if (!ARCACCOUNT.length) 18 | return reject(new Error('ARCACCOUNT length is zero')); 19 | 20 | // grab an account from queue 21 | _account = ARCACCOUNT.shift(); 22 | if (typeof _account == 'undefined') 23 | return reject(new Error('Element is undefined?')); 24 | 25 | // if the account has no token 26 | if (_account.token == '') { 27 | 28 | // try login and request a new token 29 | try { 30 | _account.token = await arcapi_login(_account.name, _account.passwd, _account.device); 31 | } catch (e) { 32 | 33 | // this account has been banned 34 | if (e == 106) { 35 | _account.banned = true; 36 | syslog.w(TAG, `This account has been banned. remove from pool => ${_account.name}`); 37 | } 38 | 39 | syslog.e(TAG, e.stack); 40 | 41 | } finally { 42 | 43 | // fetch information of account if needed 44 | if (!_account.banned && (_account.uid == 0 && _account.ucode == '')) { 45 | const _info = await arcapi_userme(_account); 46 | _account.uid = _info.user_id; 47 | _account.ucode = _info.user_code; 48 | } 49 | 50 | // update the database 51 | arcaccount_update(_account); 52 | 53 | // available account 54 | if (!_account.banned && _account.token != '') 55 | break; 56 | 57 | } 58 | 59 | } else break; 60 | 61 | } 62 | 63 | resolve(_account); 64 | 65 | syslog.i(TAG, `Allocated account => ${_account.name} ${_account.token}`); 66 | 67 | }); 68 | 69 | } 70 | -------------------------------------------------------------------------------- /source/modules/account/feed.managed.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'account/feed.managed.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | 5 | export default (token: string): Promise => { 6 | 7 | return new Promise(async (resolve, reject) => { 8 | 9 | // validate data 10 | if (!ARCPERSISTENT[token]) 11 | return reject(new Error('Invalid token')); 12 | 13 | // Check feed 14 | if (++ARCPERSISTENT[token].feeded >= BOTARCAPI_FORWARD_FEED_MAX) 15 | return reject(new Error('Feed token failed')); 16 | 17 | // Feed for the valid time 18 | ++ARCPERSISTENT[token].feed; 19 | 20 | resolve(ARCPERSISTENT[token].feed * ARCPERSISTENT[token].validtime); 21 | 22 | }); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /source/modules/account/fromtoken.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'account/fromtoken.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import IArcAccount from '../arcfetch/interfaces/IArcAccount'; 5 | 6 | export default (token: string): Promise => { 7 | 8 | return new Promise((resolve, reject) => { 9 | 10 | // validate token 11 | if (!ARCPERSISTENT[token]) 12 | return reject(new Error('Invalid token')); 13 | 14 | resolve(ARCPERSISTENT[token].account); 15 | 16 | }); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /source/modules/account/recycle.managed.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'account/recycle.managed.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import account_recycle from './recycle'; 5 | 6 | export default (token: string): Promise => { 7 | 8 | return new Promise((resolve, reject) => { 9 | 10 | // validate data 11 | if (!ARCPERSISTENT[token]) { 12 | return reject(new Error('Invalid token or token has beed recycled')); 13 | } 14 | 15 | // recycle the account 16 | account_recycle(ARCPERSISTENT[token].account); 17 | 18 | // Clear timeout 19 | clearTimeout(ARCPERSISTENT[token].proc); 20 | 21 | // Remove it from persistent 22 | delete ARCPERSISTENT[token]; 23 | 24 | resolve(); 25 | 26 | }); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /source/modules/account/recycle.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'account/recycle.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import IArcAccount from '../arcfetch/interfaces/IArcAccount'; 5 | 6 | export default (account: IArcAccount) => { 7 | 8 | ARCACCOUNT.push(account); 9 | syslog.i(TAG, `Recycled account => ${account.name} ${account.token}`); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /source/modules/apierror/apierror.ts: -------------------------------------------------------------------------------- 1 | class APIError extends Error { 2 | 3 | public status: number; 4 | public notify: string; 5 | 6 | /** 7 | * make an error with status code 8 | * @param {number} status 9 | * @param {string} notify 10 | */ 11 | constructor(status: number, notify: string) { 12 | 13 | super(`status: ${status}, notify: ${notify}`); 14 | 15 | this.name = 'APIError'; 16 | this.status = status; 17 | this.notify = notify; 18 | 19 | } 20 | 21 | } 22 | 23 | export default APIError; 24 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.aggregate.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.aggregate.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | import arcapi_any from './arcapi.any'; 7 | 8 | export default async (account: IArcAccount, 9 | endpoints: Array): Promise => { 10 | 11 | // account will be BANNED from server if exceed 12 | if (endpoints.length > BOTARCAPI_AGGREGATE_LIMITATION) 13 | throw new Error('Endpoints limit exceeded'); 14 | 15 | if (BOTARCAPI_AGGREGATE_ENABLED) { 16 | 17 | // construct endpoint object 18 | const _endpoints: Array = []; 19 | endpoints.forEach((element, index) => { 20 | _endpoints.push({ endpoint: element, id: index }); 21 | }); 22 | 23 | // construct remote request 24 | const _remote_request = 25 | new ArcFetchRequest(ArcFetchMethod.GET, 'compose/aggregate', { 26 | deviceId: account.device, 27 | userToken: account.token, 28 | submitData: new URLSearchParams({ 'calls': JSON.stringify(_endpoints) }) 29 | }); 30 | 31 | // send request 32 | return arcfetch(_remote_request) 33 | .then((root: any) => { 34 | 35 | // teardown the object and pack data into array 36 | const _data: Array = []; 37 | root.value.forEach((element: any) => { 38 | _data[element.id] = element.value[0]; 39 | }) 40 | 41 | return _data; 42 | }) 43 | .catch((e: string) => { 44 | 45 | // if token is invalid 46 | // just erase the token and wait for 47 | // auto login in next time allocating 48 | if (e == 'UnauthorizedError') { 49 | account.token = ''; 50 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 51 | } 52 | 53 | throw e; 54 | }); 55 | 56 | } else { 57 | if (BOTARCAPI_AGGREGATE_CONCURRENT) { 58 | 59 | // construct tasks 60 | const _tasks: Array> = []; 61 | endpoints.forEach((element, index) => { 62 | _tasks.push(arcapi_any(account, ArcFetchMethod.GET, element, "")); 63 | }); 64 | 65 | // wait for data coming in 66 | return Promise.all(_tasks) 67 | .then(data => { 68 | let _results: any[] = []; 69 | 70 | data.forEach((element, index) => 71 | _results.push(element.value[0])); 72 | 73 | return _results; 74 | 75 | }).catch((e: string) => { 76 | 77 | // if token is invalid 78 | // just erase the token and wait for 79 | // auto login in next time allocating 80 | if (e == 'UnauthorizedError') { 81 | account.token = ''; 82 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 83 | } 84 | throw e; 85 | }); 86 | 87 | } else { 88 | 89 | let _results: any[] = []; 90 | 91 | try { 92 | 93 | // request one by one 94 | for (let i = 0; i < endpoints.length; ++i) { 95 | const _data: any = await arcapi_any(account, ArcFetchMethod.GET, endpoints[i], ""); 96 | _results.push(_data.value[0]); 97 | } 98 | return _results; 99 | 100 | } catch (e) { 101 | 102 | // if token is invalid 103 | // just erase the token and wait for 104 | // auto login in next time allocating 105 | if (e == 'UnauthorizedError') { 106 | account.token = ''; 107 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 108 | } 109 | throw e; 110 | } 111 | 112 | } 113 | 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.any.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.any.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | 7 | export default (account: IArcAccount, method: ArcFetchMethod, 8 | path: string, databody: any): Promise => { 9 | 10 | // construct remote request 11 | const _remote_request = 12 | new ArcFetchRequest(method, path, { 13 | userToken: account.token, 14 | deviceId: account.device, 15 | submitData: databody 16 | }); 17 | 18 | // send request 19 | return arcfetch(_remote_request) 20 | .catch((e) => { 21 | 22 | // if token is invalid 23 | // just erase the token and wait for 24 | // auto login in next time allocating 25 | if (e == 'UnauthorizedError') { 26 | account.token = ''; 27 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 28 | } 29 | 30 | throw e; 31 | 32 | }); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.error.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.error.ts'; 2 | 3 | const _error_table: { [key: string]: string } = { 4 | 0: 'An error occurred completing purchases. Please try restarting your device or Arcaea and ensuring that you\'re logged in to.', 5 | 1: 'This item is currently unavailable to purchase.', 6 | 2: 'All songs are already downloaded!', 7 | 3: 'You have been logged out by another device. Please restart Arcaea.', 8 | 4: 'Could not connect to online server.', 9 | 5: 'Incorrect app version.', 10 | 6: 'An unknown error has occurred.', 11 | 7: 'An unknown error has occurred.', 12 | 8: 'An unknown error has occurred.', 13 | 9: 'The Arcaea network is currently under maintenance.', 14 | 10: 'An unknown error has occured.', 15 | 11: 'An unknown error has occured.', 16 | 12: 'Please update Arcaea to the latest version.', 17 | 100: 'Registrations from this IP address are restricted.\nTry again later or contact support@lowiro.com.', 18 | 101: 'This username is already in use.', 19 | 102: 'This email address is already in use.', 20 | 103: 'An account has already been made from this device.', 21 | 104: 'Username or password incorrect.', 22 | 105: 'You\'ve logged into over 2 devices in 24 hours. Please wait before using this new device.', 23 | 106: 'This account is locked.', 24 | 107: 'You do not have enough stamina.', 25 | 108: 'An unknown error has occurred.', 26 | 109: 'An unknown error has occurred.', 27 | 110: 'An unknown error has occurred.', 28 | 111: 'An unknown error has occurred.', 29 | 112: 'World map not unlocked.', 30 | 113: 'This event map has ended and is no longer available.', 31 | 114: 'An unknown error has occurred.', 32 | 115: 'An unknown error has occurred.', 33 | 116: 'An unknown error has occurred.', 34 | 117: 'An unknown error has occurred.', 35 | 118: 'An unknown error has occurred.', 36 | 119: 'An unknown error has occurred.', 37 | 120: 'WARNING! You are using a modified version of Arcaea.\nContinued use will result in the banning of your account.\nThis is a final warning.', 38 | 121: 'This account is locked.', 39 | 122: 'A temporary hold has been placed on your account.\nPlease visit the official website to resolve the issue.', 40 | 150: 'This feature has been restricted for your account.\nIf you are unsure why, please contact support@lowiro.com', 41 | 401: 'This user does not exist.', 42 | 403: 'Could not connect to online server.', 43 | 501: 'This item is currently unavailable to purchase.', 44 | 502: 'This item is currently unavailable to purchase.', 45 | 503: 'An unknown error has occured.', 46 | 504: 'Invalid Code', 47 | 505: 'This code has already been claimed.', 48 | 506: 'You already own this item.', 49 | 604: 'You can\'t be friends with yourself ;-;', 50 | 601: 'Your friends list is full.', 51 | 602: 'This user is already your friend.', 52 | 801: 'There was a problem receiving the server response. Please check your progress after re-entering World Mode.', 53 | 802: 'This score could not be submitted online. Please restart or update Arcaea.', 54 | 803: 'There was a problem submitting this score online. WARNING!Stamina has already been consumed. Exiting will lose World Mode progress.', 55 | 804: 'Password reset expired. Please request a new reset link.', 56 | 805: '', 57 | 903: 'Max downloads exceeded. Please wait 24 hours and try again.', 58 | 905: 'Please wait 24 hours before using this feature again.', 59 | 9701: 'Game data is out of sync due to another device. Please check your progress after re-entering World Mode.', 60 | 9801: 'An error occured downloading the song.Please try again.', 61 | 9802: 'There was a problem saving the song.Please check storage.', 62 | 9905: 'No data found to sync.', 63 | 9906: 'Sync failed due to conflicting data from another device. Please perform sync from Main Menu > Network.', 64 | 9907: 'A problem occured updating data...', 65 | 9908: 'There is a new version of Arcaea available.Please update.', 66 | 'InvalidHeader': 'BasicAuth content is invalid.', 67 | 'UnauthorizedError': 'Bearer token invalid. Follow the oauth2-token link to get a valid one!', 68 | 'BadRequestError': 'Bearer token required. Follow the oauth2-token link to get a valid one!' 69 | }; 70 | 71 | const action = (code: string | number): string => { 72 | if (typeof _error_table[code] == 'undefined') 73 | return `An unknown error has occured. ${code}`; 74 | return _error_table[code]; 75 | } 76 | 77 | export default action; 78 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.friend.add.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.friend.add.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | import IArcPlayer from './interfaces/IArcPlayer'; 7 | 8 | export default (account: IArcAccount, usercode: string): Promise> => { 9 | 10 | return new Promise((resolve, reject) => { 11 | 12 | // construct remote request 13 | const _remote_request = 14 | new ArcFetchRequest(ArcFetchMethod.POST, 'friend/me/add', { 15 | userToken: account.token, 16 | deviceId: account.device, 17 | submitData: new URLSearchParams({ 'friend_code': usercode }) 18 | }); 19 | 20 | // send request 21 | arcfetch(_remote_request) 22 | .then((root) => { resolve(root.value.friends); }) 23 | .catch((e) => { 24 | 25 | // if token is invalid 26 | // just erase the token and wait for 27 | // auto login in next time allocating 28 | if (e == 'UnauthorizedError') { 29 | account.token = ''; 30 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 31 | } 32 | 33 | reject(e); 34 | 35 | }); 36 | 37 | }); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.friend.clear.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.friend.clear.ts'; 2 | 3 | import arcapi_userme from './arcapi.userme'; 4 | import arcapi_friend_delete from './arcapi.friend.delete'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | import IArcPlayer from './interfaces/IArcPlayer'; 7 | 8 | export default async (account: IArcAccount, 9 | friends?: Array): Promise => { 10 | 11 | let _friends: Array = []; 12 | 13 | // we must request the origin arcapi 14 | // if no friend list passed in 15 | if (!friends) _friends = (await arcapi_userme(account)).friends; 16 | 17 | // clear friend list 18 | for (const v of _friends) { 19 | await arcapi_friend_delete(account, v.user_id); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.friend.delete.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.friend.delete.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | import IArcPlayer from './interfaces/IArcPlayer'; 7 | 8 | export default (account: IArcAccount, userid: number): Promise> => { 9 | 10 | return new Promise((resolve, reject) => { 11 | 12 | // construct remote request 13 | const _remote_request = 14 | new ArcFetchRequest(ArcFetchMethod.POST, 'friend/me/delete', { 15 | userToken: account.token, 16 | deviceId: account.device, 17 | submitData: new URLSearchParams({ 'friend_id': userid } as any) 18 | }); 19 | 20 | // send request 21 | arcfetch(_remote_request) 22 | .then((root) => { resolve(root.value.friends); }) 23 | .catch((e) => { 24 | 25 | if (e == 'UnauthorizedError') { 26 | account.token = ''; 27 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 28 | } 29 | 30 | reject(e); 31 | 32 | }); 33 | 34 | }); 35 | 36 | } -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.login.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.login.ts'; 2 | 3 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 4 | 5 | export default (name: string, password: string, deviceid: string): Promise => { 6 | 7 | return new Promise((resolve, reject) => { 8 | 9 | // construct remote request 10 | const _remote_request = 11 | new ArcFetchRequest(ArcFetchMethod.POST, `auth/login`, { 12 | userName: name, 13 | userPasswd: password, 14 | deviceId: deviceid, 15 | submitData: new URLSearchParams({ 'grant_type': 'client_credentials' }) 16 | }); 17 | 18 | // send request 19 | arcfetch(_remote_request) 20 | .then((root) => { resolve(root.access_token); }) 21 | .catch((e) => { reject(e); }) 22 | 23 | }); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.rank.friend.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.rank.friend.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | 7 | export default (account: IArcAccount, songid: string, 8 | difficulty: number, start: number = 0, limit: number = 11) => { 9 | 10 | return new Promise((resolve, reject) => { 11 | // construct remote request 12 | const _remote_request = 13 | new ArcFetchRequest(ArcFetchMethod.GET, 'score/song/friend', { 14 | userToken: account.token, 15 | deviceId: account.device, 16 | submitData: new URLSearchParams({ 17 | 'song_id': songid, 18 | 'difficulty': difficulty, 19 | 'start': start, 20 | 'limit': limit 21 | } as any), 22 | }); 23 | 24 | // send request 25 | arcfetch(_remote_request) 26 | .then((root) => { resolve(root.value); }) 27 | .catch((e) => { 28 | 29 | if (e == 'UnauthorizedError') { 30 | account.token = ''; 31 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 32 | } 33 | 34 | reject(e); 35 | 36 | }); 37 | 38 | }); 39 | 40 | } -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.rank.me.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.rank.me.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | 7 | export default (account: IArcAccount, songid: string, 8 | difficulty: number, start: number = 4, limit: number = 18) => { 9 | 10 | return new Promise((resolve, reject) => { 11 | 12 | // construct remote request 13 | const _remote_request = 14 | new ArcFetchRequest(ArcFetchMethod.GET, 'score/song/me', { 15 | userToken: account.token, 16 | deviceId: account.device, 17 | submitData: new URLSearchParams({ 18 | 'song_id': songid, 19 | 'difficulty': difficulty, 20 | 'start': start, 21 | 'limit': limit 22 | } as any) 23 | }); 24 | 25 | // send request 26 | arcfetch(_remote_request) 27 | .then((root) => { resolve(root.value); }) 28 | .catch((e) => { 29 | 30 | if (e == 'UnauthorizedError') { 31 | account.token = ''; 32 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 33 | } 34 | 35 | reject(e); 36 | 37 | }); 38 | 39 | }); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.rank.world.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.rank.world.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | 7 | export default (account: IArcAccount, songid: string, 8 | difficulty: number, start: number = 0, limit: number = 20) => { 9 | 10 | return new Promise((resolve, reject) => { 11 | 12 | // construct remote request 13 | const _remote_request = 14 | new ArcFetchRequest(ArcFetchMethod.GET, 'score/song', { 15 | userToken: account.token, 16 | deviceId: account.device, 17 | submitData: new URLSearchParams({ 18 | 'song_id': songid, 19 | 'difficulty': difficulty, 20 | 'start': start, 21 | 'limit': limit 22 | } as any) 23 | }); 24 | 25 | // send request 26 | arcfetch(_remote_request) 27 | .then((root) => { resolve(root.value); }) 28 | .catch((e) => { 29 | 30 | if (e == 'UnauthorizedError') { 31 | account.token = ''; 32 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 33 | } 34 | 35 | reject(e); 36 | 37 | }); 38 | 39 | }); 40 | 41 | } -------------------------------------------------------------------------------- /source/modules/arcfetch/arcapi.userme.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'arcapi.userme.ts'; 2 | 3 | import syslog from '../syslog/syslog'; 4 | import arcfetch, { ArcFetchRequest, ArcFetchMethod } from './arcfetch'; 5 | import IArcAccount from './interfaces/IArcAccount'; 6 | import { IArcUserMe } from './interfaces/IArcUserMe'; 7 | 8 | export default (account: IArcAccount): Promise => { 9 | 10 | return new Promise((resolve, reject) => { 11 | 12 | // construct remote request 13 | const _remote_request = 14 | new ArcFetchRequest(ArcFetchMethod.GET, 'user/me', { 15 | deviceId: account.device, 16 | userToken: account.token 17 | }); 18 | 19 | // send request 20 | arcfetch(_remote_request) 21 | .then((root) => { resolve(root.value); }) 22 | .catch((e) => { 23 | 24 | if (e == 'UnauthorizedError') { 25 | account.token = ''; 26 | syslog.w(TAG, `Invalid token => ${account.name} ${account.token}`); 27 | } 28 | 29 | reject(e); 30 | 31 | }); 32 | 33 | }); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /source/modules/arcfetch/interfaces/IArcAccount.ts: -------------------------------------------------------------------------------- 1 | export default interface IArcAccount { 2 | uid: number, 3 | ucode: string, 4 | token: string, 5 | banned: 'true' | 'false' | true | false, 6 | name: string, 7 | passwd: string, 8 | device: string 9 | } 10 | -------------------------------------------------------------------------------- /source/modules/arcfetch/interfaces/IArcPlayer.ts: -------------------------------------------------------------------------------- 1 | import IArcScore from "./IArcScore"; 2 | 3 | export default interface IArcPlayer { 4 | rating: number, 5 | join_date: number, 6 | character: number, 7 | recent_score: Array, 8 | name: string, 9 | user_id: number, 10 | code: string 11 | } 12 | -------------------------------------------------------------------------------- /source/modules/arcfetch/interfaces/IArcScore.ts: -------------------------------------------------------------------------------- 1 | export default interface IArcScore { 2 | rating: number, 3 | modifier: number, 4 | time_played: number, 5 | health: number, 6 | best_clear_type: number, 7 | clear_type: number, 8 | miss_count: number, 9 | near_count: number, 10 | perfect_count: number, 11 | shiny_perfect_count: number, 12 | score: number, 13 | difficulty: number, 14 | song_id: string 15 | } 16 | -------------------------------------------------------------------------------- /source/modules/arcfetch/interfaces/IArcUserMe.ts: -------------------------------------------------------------------------------- 1 | import IArcScore from "./IArcScore"; 2 | import IArcPlayer from "./IArcPlayer"; 3 | 4 | export interface IArcUserMe { 5 | friends: Array, 6 | user_id: number, 7 | name: string, 8 | user_code: string, 9 | display_name: string, 10 | character: number, 11 | is_skill_sealed: boolean, 12 | recent_score: Array, 13 | max_friend: number, 14 | rating: number, 15 | id: number, 16 | join_date: number 17 | } 18 | -------------------------------------------------------------------------------- /source/modules/database/database.arcaccount.all.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcaccount.all.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IArcAccount from "../arcfetch/interfaces/IArcAccount"; 5 | 6 | export default (): Promise | null> => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `accounts` WHERE `banned` == "false"'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCACCOUNT.all(_sql) 14 | .then((data: Array | null) => { 15 | 16 | if (!data) return null; 17 | 18 | return data.map((element) => { 19 | element.banned = element.banned == 'true' ? true : false; 20 | return element; 21 | }); 22 | 23 | }); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /source/modules/database/database.arcaccount.init.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcaccount.init.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | 5 | export default (): Promise => { 6 | 7 | const _sql: string = 8 | 'CREATE TABLE IF NOT EXISTS `accounts` (' + 9 | '`name` TEXT NOT NULL,' + 10 | '`passwd` TEXT NOT NULL,' + 11 | '`device` TEXT NOT NULL DEFAULT (LOWER(HEX(RANDOMBLOB(8)))),' + 12 | '`uid` INTEGER NOT NULL DEFAULT 0,' + 13 | '`ucode` TEXT NOT NULL DEFAULT "", ' + 14 | '`token` TEXT NOT NULL DEFAULT "",' + 15 | '`banned` TEXT NOT NULL DEFAULT "false" CHECK(`banned` IN("true", "false")),' + 16 | 'PRIMARY KEY (`name` ASC));'; 17 | syslog.v(TAG, _sql); 18 | 19 | return DATABASE_ARCACCOUNT.exec(_sql); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /source/modules/database/database.arcaccount.update.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcaccount.update.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IArcAccount from "../arcfetch/interfaces/IArcAccount"; 5 | 6 | export default (account: IArcAccount): Promise => { 7 | 8 | const _sqlbinding: IArcAccount = { 9 | passwd: account.passwd, 10 | device: account.device, 11 | uid: account.uid, 12 | ucode: account.ucode, 13 | token: account.token, 14 | banned: account.banned ? 'true' : 'false', 15 | name: account.name // the last argument 16 | }; 17 | 18 | const _binding_updates: string = (() => { 19 | let _array: Array = []; 20 | Object.keys(_sqlbinding).forEach((v) => { 21 | if (v != 'name') _array.push(`${v} = ?`); 22 | }); 23 | return _array.join(', '); 24 | })(); 25 | 26 | const _sql: string = 27 | 'UPDATE `accounts` ' + 28 | `SET ${_binding_updates} WHERE \`name\` == ?`; 29 | syslog.v(TAG, _sql); 30 | 31 | // execute sql 32 | return DATABASE_ARCACCOUNT.run(_sql, Object.values(_sqlbinding)); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /source/modules/database/database.arcbest30.byuid.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcbest30.byuid.ts'; 2 | 3 | import { atob } from 'abab'; 4 | import syslog from "../syslog/syslog"; 5 | import IArcBest30Result from "./interfaces/IArcBest30Result"; 6 | import IDatabaseArcBest30 from "./interfaces/IDatabaseArcBest30"; 7 | 8 | export default (userid: number): Promise => { 9 | 10 | const _sql: string = 11 | 'SELECT * FROM `cache` WHERE `uid` == ?'; 12 | syslog.v(TAG, _sql); 13 | 14 | // execute sql 15 | return DATABASE_ARCBEST30.get(_sql, [userid]) 16 | .then((data: IDatabaseArcBest30 | null) => { 17 | 18 | return data ? { 19 | last_played: data.last_played, 20 | best30_avg: data.best30_avg / 10000, 21 | recent10_avg: data.recent10_avg / 10000, 22 | best30_list: JSON.parse(atob(data.best30_list) ?? '[]'), 23 | best30_overflow: JSON.parse(atob(data.best30_overflow) ?? '[]') 24 | } as IArcBest30Result : null; 25 | 26 | }); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /source/modules/database/database.arcbest30.init.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcbest30.init.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | 5 | export default (): Promise => { 6 | 7 | const _sql = 8 | 'CREATE TABLE IF NOT EXISTS `cache` (' + 9 | '`uid` INTEGER NOT NULL,' + 10 | '`last_played` INTEGER NOT NULL DEFAULT 0,' + 11 | '`best30_avg` INTEGER NOT NULL DEFAULT 0,' + 12 | '`recent10_avg` INTEGER NOT NULL DEFAULT 0,' + 13 | '`best30_list` TEXT DEFAULT "",' + 14 | '`best30_overflow` TEXT DEFAULT "",' + 15 | 'PRIMARY KEY (`uid` ASC));'; 16 | syslog.v(TAG, _sql); 17 | 18 | // execute sql 19 | return DATABASE_ARCBEST30.exec(_sql); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /source/modules/database/database.arcbest30.update.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcbest30.update.ts'; 2 | 3 | import { btoa } from 'abab'; 4 | import syslog from "../syslog/syslog"; 5 | import IArcBest30Result from './interfaces/IArcBest30Result'; 6 | import IDatabaseArcBest30 from './interfaces/IDatabaseArcBest30'; 7 | 8 | export default (userid: number, best30: IArcBest30Result) => { 9 | 10 | const _sqlbinding: IDatabaseArcBest30 = { 11 | uid: userid, 12 | last_played: best30.last_played, 13 | best30_avg: Math.floor(best30.best30_avg * 10000), 14 | recent10_avg: Math.floor(best30.recent10_avg * 10000), 15 | best30_list: best30.best30_list ? (btoa(JSON.stringify(best30.best30_list)) ?? 'W10=') : 'W10=', 16 | best30_overflow: best30.best30_overflow ? (btoa(JSON.stringify(best30.best30_overflow)) ?? 'W10=') : 'W10=', 17 | }; 18 | 19 | const _sql: string = 20 | 'INSERT OR REPLACE INTO ' + 21 | `\`cache\`(${Object.keys(_sqlbinding).join()}) ` + 22 | `VALUES(${new Array(Object.keys(_sqlbinding).length).fill('?').join(',')});`; 23 | syslog.v(TAG, _sql); 24 | 25 | // execute sql 26 | return DATABASE_ARCBEST30.run(_sql, Object.values(_sqlbinding)); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /source/modules/database/database.arcplayer.byany.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcplayer.byany.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcPlayer from "./interfaces/IDatabaseArcPlayer"; 5 | 6 | export default (user: string): Promise => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `players` WHERE `code` == ? OR `name`== ?'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCPLAYER.all(_sql, [user, user]); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /source/modules/database/database.arcplayer.byusercode.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcplayer.byusercode.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcPlayer from "./interfaces/IDatabaseArcPlayer"; 5 | 6 | export default (usercode: string): Promise => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `players` WHERE `code` == ?'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCPLAYER.get(_sql, [usercode]); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /source/modules/database/database.arcplayer.byuserid.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcplayer.byuserid.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcPlayer from "./interfaces/IDatabaseArcPlayer"; 5 | 6 | export default (userid: number): Promise => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `players` WHERE `uid` == ?'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCPLAYER.get(_sql, [userid]); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /source/modules/database/database.arcplayer.init.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcplayer.init.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | 5 | export default (): Promise => { 6 | 7 | const _sql: string = 8 | 'CREATE TABLE IF NOT EXISTS `players` (' + 9 | '`uid` INTEGER NOT NULL,' + // user id 10 | '`code` TEXT NOT NULL,' + // user code 11 | '`name` TEXT NOT NULL,' + // user name 12 | '`ptt` INTEGER DEFAULT -1,' + // user ptt 13 | '`join_date` INTEGER NOT NULL,' + // join date 14 | 'PRIMARY KEY (`uid` ASC));'; 15 | syslog.v(TAG, _sql); 16 | 17 | // execute sql 18 | return DATABASE_ARCPLAYER.exec(_sql); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /source/modules/database/database.arcplayer.update.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcplayer.update.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IArcPlayer from "../arcfetch/interfaces/IArcPlayer"; 5 | import IDatabaseArcPlayer from "./interfaces/IDatabaseArcPlayer"; 6 | 7 | export default (userinfos: IArcPlayer): Promise> => { 8 | 9 | // always pack object to array 10 | let _wrapper: Array; 11 | 12 | if (userinfos instanceof Array) 13 | _wrapper = userinfos; 14 | else 15 | _wrapper = [userinfos]; 16 | 17 | // enum data and insert them 18 | const _promises: Array> = 19 | _wrapper.map((element) => { 20 | 21 | const _sqlbinding: IDatabaseArcPlayer = { 22 | uid: element.user_id, 23 | code: element.code, 24 | name: element.name, 25 | ptt: element.rating, 26 | join_date: element.join_date, 27 | }; 28 | 29 | // this user ptt is hidden 30 | if (element.rating == -1) 31 | delete _sqlbinding.ptt; 32 | 33 | const _sql: string = 34 | `INSERT OR REPLACE INTO ` + 35 | `\`players\`(${Object.keys(_sqlbinding).join()}) ` + 36 | `VALUES(${new Array(Object.keys(_sqlbinding).length).fill('?').join(',')});`; 37 | syslog.v(TAG, _sql); 38 | 39 | // execute sql 40 | return DATABASE_ARCPLAYER.run(_sql, Object.values(_sqlbinding)); 41 | 42 | }); 43 | 44 | return Promise.all(_promises); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /source/modules/database/database.arcrecord.byuserid.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcrecord.byuserid.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcRecord from './interfaces/IDatabaseArcRecord'; 5 | 6 | export default (userid: number, count: number): Promise> => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `records` WHERE `uid` == ? ' + 10 | 'ORDER BY `time_played` DESC LIMIT ?'; 11 | syslog.v(TAG, _sql); 12 | 13 | // execute sql 14 | return DATABASE_ARCRECORD.all(_sql, [userid, count]) 15 | .then((data: Array | null) => { 16 | if (!data) return null; 17 | 18 | return data.map((element) => { 19 | element.rating /= 10000; 20 | return element; 21 | }); 22 | 23 | });; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /source/modules/database/database.arcrecord.history.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcrecord.history.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcRecord from './interfaces/IDatabaseArcRecord'; 5 | 6 | export default (userid: number, songid: string, 7 | difficulty: number, count: number): Promise => { 8 | 9 | const _sql: string = 10 | 'SELECT * FROM `records` WHERE `uid` == ? ' + 11 | 'AND `song_id` == ? AND `difficulty` == ? ' + 12 | 'ORDER BY `time_played` DESC LIMIT ?'; 13 | syslog.v(TAG, _sql); 14 | 15 | // execute sql 16 | return DATABASE_ARCRECORD.all(_sql, [userid, songid, difficulty, count]) 17 | .then((data: Array | null) => { 18 | if (!data) return null; 19 | 20 | return data.map((element) => { 21 | element.rating /= 10000; 22 | return element; 23 | }); 24 | 25 | });; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /source/modules/database/database.arcrecord.init.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcrecord.init.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | 5 | export default (): Promise => { 6 | 7 | const _sql: string = 8 | 'CREATE TABLE IF NOT EXISTS `records` (' + 9 | '`uid` INTEGER NOT NULL,' + 10 | '`score` INTEGER NOT NULL,' + 11 | '`health` INTEGER NOT NULL,' + 12 | '`rating` INTEGER NOT NULL,' + 13 | '`song_id` TEXT NOT NULL,' + 14 | '`modifier` INTEGER NOT NULL,' + 15 | '`difficulty` INTEGER NOT NULL,' + 16 | '`clear_type` INTEGER NOT NULL,' + 17 | '`best_clear_type` INTEGER NOT NULL,' + 18 | '`time_played` INTEGER NOT NULL,' + 19 | '`near_count` INTEGER NOT NULL,' + 20 | '`miss_count` INTEGER NOT NULL,' + 21 | '`perfect_count` INTEGER NOT NULL,' + 22 | '`shiny_perfect_count` INTEGER NOT NULL, ' + 23 | 'PRIMARY KEY ("uid" ASC, "song_id" ASC, "time_played" ASC));'; 24 | syslog.v(TAG, _sql); 25 | 26 | // execute sql 27 | return DATABASE_ARCRECORD.exec(_sql); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /source/modules/database/database.arcrecord.update.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcrecord.update.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IArcScore from "../arcfetch/interfaces/IArcScore"; 5 | 6 | export default (userid: number, 7 | records: Array | IArcScore): Promise> => { 8 | 9 | // always pack object to array 10 | let _wrapper: Array; 11 | 12 | if (records instanceof Array) 13 | _wrapper = records; 14 | else 15 | _wrapper = [records]; 16 | 17 | // enum data and insert them 18 | const _promises: Array> = 19 | _wrapper.map((element) => { 20 | 21 | const _sqlbinding: any = { 22 | uid: userid, 23 | score: element.score, 24 | health: element.health, 25 | rating: Math.floor(element.rating * 10000), 26 | song_id: element.song_id, 27 | modifier: element.modifier, 28 | difficulty: element.difficulty, 29 | clear_type: element.clear_type, 30 | best_clear_type: element.best_clear_type, 31 | time_played: element.time_played, 32 | near_count: element.near_count, 33 | miss_count: element.miss_count, 34 | perfect_count: element.perfect_count, 35 | shiny_perfect_count: element.shiny_perfect_count, 36 | }; 37 | 38 | const _sql: string = 39 | 'INSERT OR IGNORE INTO ' + 40 | `\`records\`(${Object.keys(_sqlbinding).join()}) ` + 41 | `VALUES(${new Array(Object.keys(_sqlbinding).length).fill('?').join(',')});`; 42 | syslog.v(TAG, _sql); 43 | 44 | // execute sql 45 | return DATABASE_ARCRECORD.run(_sql, Object.values(_sqlbinding)); 46 | 47 | }); 48 | 49 | return Promise.all(_promises); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.alias.byid.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.alias.byid.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcSongAlias from '../../modules/database/interfaces/IDatabaseArcSongAlias'; 5 | 6 | export default (songid: string): Promise => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `alias` WHERE `sid` == ?'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCSONG.all(_sql, [songid]); 14 | } 15 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.allcharts.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.allcharts.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcSongChart from "./interfaces/IDatabaseArcSongChart"; 5 | 6 | export default (): Promise | null> => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `charts` ORDER BY `rating` DESC'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCSONG.all(_sql) 14 | .then((data: Array | null) => { 15 | if (!data) return null; 16 | 17 | return data.map((element) => { 18 | element.rating /= 10; 19 | return element; 20 | }); 21 | 22 | }); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.byrand.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.byrand.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | 5 | export default (start: number, end: number): Promise => { 6 | 7 | const _sqlbinding: any = { 8 | start: start, 9 | end: end 10 | }; 11 | 12 | if (end == 0) 13 | delete _sqlbinding.end; 14 | 15 | const _sql: string = 16 | 'SELECT `sid`, `rating_class` FROM `charts` AS c WHERE ' + 17 | `${end == 0 ? `c.difficultly==?` : `c.difficultly>=? AND c.difficultly<=?`} ` + 18 | 'ORDER BY RANDOM() LIMIT 1'; 19 | syslog.v(TAG, _sql); 20 | 21 | // execute sql 22 | return DATABASE_ARCSONG.get(_sql, Object.values(_sqlbinding)); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.byrating.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.byrating.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcSongChart from "./interfaces/IDatabaseArcSongChart"; 5 | 6 | export default (rating_start: number, rating_end: number | null) 7 | : Promise => { 8 | 9 | // convert rating data like database stored 10 | rating_start *= 10; 11 | rating_end = !rating_end ? rating_start : rating_end * 10; 12 | 13 | const _sql: string = 14 | 'SELECT * FROM `charts` WHERE `rating` >= ? AND `rating` <= ? ORDER BY `rating` ASC'; 15 | syslog.v(TAG, _sql); 16 | 17 | // execute sql 18 | return DATABASE_ARCSONG.all(_sql, [rating_start, rating_end]); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.bysongid.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.bysongid.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcSong from "./interfaces/IDatabaseArcSong"; 5 | 6 | export default (songid: string): Promise => { 7 | 8 | const _sql: string = 9 | 'SELECT * FROM `songs` WHERE `sid` == ?'; 10 | syslog.v(TAG, _sql); 11 | 12 | // execute sql 13 | return DATABASE_ARCSONG.get(_sql, [songid]) 14 | .then((data: any | null) => { 15 | 16 | if (!data) return null; 17 | 18 | data.rating_pst /= 10; 19 | data.rating_prs /= 10; 20 | data.rating_ftr /= 10; 21 | 22 | if(data.rating_byn != -1) 23 | data.rating_byn/= 10; 24 | 25 | return data; 26 | 27 | }); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.init.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.init.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | 5 | export default (): Promise => { 6 | 7 | return Promise.resolve() 8 | 9 | // the 'songs' table 10 | .then(() => { 11 | 12 | const _sql: string = 13 | 'CREATE TABLE IF NOT EXISTS `songs` (' + 14 | '`sid` TEXT NOT NULL,' + // song id 15 | 16 | '`name_en` TEXT NOT NULL,' + // english name 17 | '`name_jp` TEXT NOT NULL DEFAULT "",' + // japanese name 18 | 19 | '`bpm` TEXT NOT NULL DEFAULT "",' + // bpm 20 | '`bpm_base` INTEGER NOT NULL DEFAULT 0,' + // bpm base 21 | 22 | '`pakset` TEXT NOT NULL DEFAULT "",' + // songpack set 23 | '`artist` TEXT NOT NULL DEFAULT "",' + // artist name 24 | '`time` INTEGER NOT NULL DEFAULT 0,' + // total time sec 25 | '`side` INTEGER NOT NULL CHECK(`side` IN(0, 1)) DEFAULT 0,' + // side 0 hikari 1 conflict 26 | '`date` INTEGER NOT NULL DEFAULT 0,' + // release date 27 | '`version` TEXT NOT NULL DEFAULT "",' + // release version 28 | 29 | '`world_unlock` TEXT NOT NULL CHECK(`world_unlock` IN("true", "false")) DEFAULT "false",' + // is world unlock 30 | '`remote_download` TEXT NOT NULL CHECK(`remote_download` IN("true", "false")) DEFAULT "false",' + // is remote download 31 | 32 | '`rating_pst` INTEGER NOT NULL DEFAULT 0,' + // rating past 33 | '`rating_prs` INTEGER NOT NULL DEFAULT 0,' + // rating present 34 | '`rating_ftr` INTEGER NOT NULL DEFAULT 0,' + // rating future 35 | '`rating_byn` INTEGER NOT NULL DEFAULT 0,' + // rating beyond 36 | 37 | '`difficultly_pst` INTEGER NOT NULL DEFAULT 0,' + // difficultly past 38 | '`difficultly_prs` INTEGER NOT NULL DEFAULT 0,' + // difficultly present 39 | '`difficultly_ftr` INTEGER NOT NULL DEFAULT 0,' + // difficultly future 40 | '`difficultly_byn` INTEGER NOT NULL DEFAULT 0,' + // difficultly beyond 41 | 42 | '`notes_pst` INTEGER NOT NULL DEFAULT 0, ' + // notes past 43 | '`notes_prs` INTEGER NOT NULL DEFAULT 0, ' + // notes present 44 | '`notes_ftr` INTEGER NOT NULL DEFAULT 0, ' + // notes future 45 | '`notes_byn` INTEGER NOT NULL DEFAULT 0, ' + // notes beyond 46 | 47 | '`chart_designer_pst` TEXT NOT NULL DEFAULT "",' + // chart designer past 48 | '`chart_designer_prs` TEXT NOT NULL DEFAULT "",' + // chart designer present 49 | '`chart_designer_ftr` TEXT NOT NULL DEFAULT "",' + // chart designer future 50 | '`chart_designer_byn` TEXT NOT NULL DEFAULT "",' + // chart designer beyond 51 | 52 | '`jacket_designer_pst` TEXT NOT NULL DEFAULT "",' + // jacket designer past 53 | '`jacket_designer_prs` TEXT NOT NULL DEFAULT "",' + // jacket designer present 54 | '`jacket_designer_ftr` TEXT NOT NULL DEFAULT "",' + // jacket designer future 55 | '`jacket_designer_byn` TEXT NOT NULL DEFAULT "",' + // jacket designer beyond 56 | 57 | '`jacket_override_pst` TEXT NOT NULL CHECK(`jacket_override_pst` IN("true", "false")) DEFAULT "false",' + // jacket override past 58 | '`jacket_override_prs` TEXT NOT NULL CHECK(`jacket_override_prs` IN("true", "false")) DEFAULT "false",' + // jacket override present 59 | '`jacket_override_ftr` TEXT NOT NULL CHECK(`jacket_override_ftr` IN("true", "false")) DEFAULT "false",' + // jacket override future 60 | '`jacket_override_byn` TEXT NOT NULL CHECK(`jacket_override_byn` IN("true", "false")) DEFAULT "false",' + // jacket override beyond 61 | 62 | 'PRIMARY KEY ("sid" ASC))'; 63 | syslog.v(TAG, _sql); 64 | 65 | return DATABASE_ARCSONG.exec(_sql); 66 | 67 | }) 68 | 69 | // the 'alias' table 70 | .then(() => { 71 | 72 | const _sql: string = 73 | 'CREATE TABLE IF NOT EXISTS `alias` (' + 74 | '`sid` TEXT NOT NULL,' + 75 | '`alias` TEXT NOT NULL,' + 76 | 'PRIMARY KEY(`sid` ASC, `alias` ASC), ' + 77 | 'FOREIGN KEY(`sid`) REFERENCES `songs`(`sid`))'; 78 | syslog.v(TAG, _sql); 79 | 80 | return DATABASE_ARCSONG.exec(_sql); 81 | }) 82 | 83 | // the 'charts' table 84 | .then(() => { 85 | 86 | const _sql: string = 87 | 'CREATE TABLE IF NOT EXISTS `charts` (' + 88 | '`sid` TEXT NOT NULL,' + 89 | '`rating_class` INTEGER NOT NULL CHECK(`rating_class` IN (0, 1, 2, 3)),' + 90 | '`difficultly` INTEGER NOT NULL CHECK(`difficultly` != -1),' + 91 | '`rating` INTEGER NOT NULL CHECK(`rating` != -1),' + 92 | 'FOREIGN KEY(`sid`) REFERENCES `songs`(`sid`))'; 93 | syslog.v(TAG, _sql); 94 | 95 | return DATABASE_ARCSONG.exec(_sql); 96 | 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.sid.byany.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.sid.byany.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IDatabaseArcSongAlias from "./interfaces/IDatabaseArcSongAlias"; 5 | import IDatabaseArcSong from "./interfaces/IDatabaseArcSong"; 6 | 7 | export default (anystr: string): Promise> => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | // accurate search an alias 12 | // such as 'AB' (Aterlbus) 'GL' (GrievousLady) 13 | try { 14 | 15 | const _sql: string = 16 | 'SELECT * FROM `alias` WHERE REPLACE(`alias`,\' \',\'\') LIKE REPLACE(?,\' \',\'\')'; 17 | syslog.v(TAG, _sql); 18 | 19 | const _result: Array | null = 20 | await DATABASE_ARCSONG.all(_sql, [anystr]); 21 | 22 | if (_result && _result.length == 1) 23 | return resolve([_result[0].sid]); 24 | 25 | } catch (e) { return reject(e); } 26 | 27 | 28 | // not found? then fuzzy search 29 | // whole the database in 'name_en' 'name_jp' 'alias' 'songid' 30 | try { 31 | 32 | const _sql: string = 33 | 'SELECT DISTINCT `sid` ' + 34 | 'FROM (SELECT `sid`,`name_en`,`name_jp`,`alias` FROM `songs` LEFT JOIN `alias` USING (`sid`))' + 35 | 'WHERE' + 36 | '`sid` LIKE replace(?,\' \',\'\') OR ' + 37 | 'replace(`name_en`,\' \',\'\') LIKE replace(?,\' \',\'\') OR ' + 38 | 'replace(`name_jp`,\' \',\'\') LIKE replace(?,\' \',\'\') OR ' + 39 | 'replace(`alias`,\' \',\'\') LIKE replace(?,\' \',\'\')' 40 | syslog.v(TAG, _sql); 41 | 42 | const _result: Array | null = 43 | await DATABASE_ARCSONG.all(_sql, Array(4).fill(`%${anystr}%`)); 44 | 45 | if (!_result || !_result.length) 46 | return reject(new Error('no such record')); 47 | 48 | // return all results 49 | resolve(_result.map((element) => { 50 | return element.sid; 51 | })); 52 | 53 | } catch (e) { return reject(e); } 54 | 55 | }); 56 | 57 | } 58 | -------------------------------------------------------------------------------- /source/modules/database/database.arcsong.update.songlist.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'database.arcsong.update.songlist.ts'; 2 | 3 | import syslog from "../syslog/syslog"; 4 | import IArcSongList from "./interfaces/IArcSongList"; 5 | import IDatabaseArcSong from "./interfaces/IDatabaseArcSong"; 6 | 7 | export default (songlist: IArcSongList): Promise => { 8 | 9 | return Promise.resolve() 10 | 11 | // do update 'songs' table 12 | .then(() => { 13 | 14 | const _promises: Array> = 15 | songlist.songs.map((element) => { 16 | 17 | const _sqlbinding: IDatabaseArcSong = { 18 | sid: element.id, 19 | name_en: element.title_localized.en, 20 | name_jp: element.title_localized.ja ?? '', 21 | bpm: element.bpm, 22 | bpm_base: element.bpm_base, 23 | pakset: element.set, 24 | artist: element.artist, 25 | side: element.side, 26 | date: element.date, 27 | version: element.version ?? '', 28 | world_unlock: element.world_unlock == true ? 'true' : 'false', 29 | remote_download: element.remote_dl == true ? 'true' : 'false', 30 | difficultly_pst: element.difficulties[0].rating * 2, 31 | difficultly_prs: element.difficulties[1].rating * 2, 32 | difficultly_ftr: element.difficulties[2].rating * 2, 33 | difficultly_byn: element.difficulties[3] ? element.difficulties[3].rating * 2 : -1, 34 | chart_designer_pst: element.difficulties[0].chartDesigner, 35 | chart_designer_prs: element.difficulties[1].chartDesigner, 36 | chart_designer_ftr: element.difficulties[2].chartDesigner, 37 | chart_designer_byn: element.difficulties[3] ? element.difficulties[3].chartDesigner : '', 38 | jacket_designer_pst: element.difficulties[0].jacketDesigner, 39 | jacket_designer_prs: element.difficulties[1].jacketDesigner, 40 | jacket_designer_ftr: element.difficulties[2].jacketDesigner, 41 | jacket_designer_byn: element.difficulties[3] ? element.difficulties[3].jacketDesigner : '', 42 | jacket_override_pst: element.difficulties[0].jacketOverride == true ? 'true' : 'false', 43 | jacket_override_prs: element.difficulties[1].jacketOverride == true ? 'true' : 'false', 44 | jacket_override_ftr: element.difficulties[2].jacketOverride == true ? 'true' : 'false', 45 | jacket_override_byn: (element.difficulties[3] && element.difficulties[3].jacketOverride == true) ? 'true' : 'false', 46 | }; 47 | 48 | // processing "ratingPlus" 49 | if (element.difficulties[0]?.ratingPlus == true) 50 | ++_sqlbinding.difficultly_pst; 51 | if (element.difficulties[1]?.ratingPlus == true) 52 | ++_sqlbinding.difficultly_prs; 53 | if (element.difficulties[2]?.ratingPlus == true) 54 | ++_sqlbinding.difficultly_ftr; 55 | if (element.difficulties[3]?.ratingPlus == true) 56 | ++_sqlbinding.difficultly_byn; 57 | 58 | const _binding_keys: string = 59 | Object.keys(_sqlbinding).join(); 60 | 61 | const _binding_vals: string = 62 | new Array(Object.keys(_sqlbinding).length).fill('?').join(','); 63 | 64 | const _binding_conflicts: string = (() => { 65 | let _array: Array = []; 66 | Object.keys(_sqlbinding).forEach((v, i) => { 67 | if (v != 'sid') 68 | _array.push(`${v} = excluded.${v}`); 69 | }); 70 | return _array.join(', '); 71 | })(); 72 | 73 | const _sql: string = 74 | 'INSERT INTO ' + 75 | `\`songs\`(${_binding_keys}) VALUES(${_binding_vals})` + 76 | `ON CONFLICT(\`sid\`) DO UPDATE SET ${_binding_conflicts}` 77 | syslog.v(TAG, _sql); 78 | 79 | // execute sql 80 | return DATABASE_ARCSONG.run(_sql, Object.values(_sqlbinding)); 81 | 82 | }); 83 | 84 | return Promise.all(_promises); 85 | 86 | }) 87 | 88 | // then do update 'chart' table 89 | .then(() => { 90 | 91 | const _sql = 92 | 'DELETE FROM `charts`; ' + 93 | 'INSERT INTO `charts` (`sid`, `rating_class`, `difficultly`, `rating`) ' + 94 | ' SELECT `sid`, 0, `difficultly_pst`, `rating_pst` FROM `songs`; ' + 95 | 'INSERT INTO `charts` (`sid`, `rating_class`, `difficultly`, `rating`) ' + 96 | ' SELECT `sid`, 1, `difficultly_prs`, `rating_prs` FROM `songs`; ' + 97 | 'INSERT INTO `charts` (`sid`, `rating_class`, `difficultly`, `rating`) ' + 98 | ' SELECT `sid`, 2, `difficultly_ftr`, `rating_ftr` FROM `songs`; ' + 99 | 'INSERT OR IGNORE INTO `charts` (`sid`, `rating_class`, `difficultly`, `rating`) ' + 100 | ' SELECT `sid`, 3, `difficultly_byn`, `rating_byn` FROM `songs`;'; 101 | syslog.v(TAG, _sql); 102 | 103 | // execute sql 104 | return DATABASE_ARCSONG.exec(_sql); 105 | 106 | }); 107 | 108 | } 109 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IArcBest30Result.ts: -------------------------------------------------------------------------------- 1 | import IArcScore from "../../arcfetch/interfaces/IArcScore"; 2 | 3 | export default interface IArcBest30Result { 4 | last_played: number, 5 | best30_avg: number, 6 | recent10_avg: number, 7 | best30_list: Array, 8 | best30_overflow: Array 9 | } 10 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IArcSong.ts: -------------------------------------------------------------------------------- 1 | export default interface IArcSong { 2 | id: string, 3 | title_localized: { 4 | en: string, 5 | ja?: string 6 | }, 7 | artist: string, 8 | bpm: string, 9 | bpm_base: number, 10 | set: string, 11 | purchase?: string, 12 | audioPreview?: number, 13 | audioPreviewEnd?: number, 14 | side: number, 15 | world_unlock?: boolean, 16 | bg?: string, 17 | remote_dl?: boolean, 18 | source_localized?: { 19 | en: string 20 | }, 21 | date: number, 22 | version?: string, 23 | difficulties: [ 24 | { 25 | ratingClass: number, 26 | chartDesigner: string, 27 | jacketDesigner: string, 28 | jacketOverride?: boolean, 29 | rating: number, 30 | plusFingers?: boolean, 31 | ratingPlus?: boolean, 32 | totalNotes?: number 33 | }, 34 | { 35 | ratingClass: number, 36 | chartDesigner: string, 37 | jacketDesigner: string, 38 | jacketOverride?: boolean, 39 | rating: number, 40 | plusFingers?: boolean, 41 | ratingPlus?: boolean 42 | totalNotes?: number 43 | }, 44 | { 45 | ratingClass: number, 46 | chartDesigner: string, 47 | jacketDesigner: string, 48 | jacketOverride?: boolean, 49 | rating: number, 50 | plusFingers?: boolean, 51 | ratingPlus?: boolean 52 | totalNotes?: number 53 | }, 54 | { 55 | ratingClass: number, 56 | chartDesigner: string, 57 | jacketDesigner: string, 58 | jacketOverride?: boolean, 59 | rating: number, 60 | plusFingers?: boolean, 61 | ratingPlus?: boolean 62 | totalNotes?: number 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IArcSongList.ts: -------------------------------------------------------------------------------- 1 | import IArcSong from "./IArcSong"; 2 | 3 | export default interface IArcSongList { 4 | songs: Array 5 | } 6 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IDatabaseArcBest30.ts: -------------------------------------------------------------------------------- 1 | import IDatabaseArcPlayer from "./IDatabaseArcPlayer"; 2 | 3 | export default interface IDatabaseArcBest30 { 4 | uid: number, 5 | last_played: number, 6 | best30_avg: number, 7 | recent10_avg: number, 8 | best30_list: string, 9 | best30_overflow: string, 10 | } 11 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IDatabaseArcPlayer.ts: -------------------------------------------------------------------------------- 1 | export default interface IDatabaseArcPlayer { 2 | uid: number, 3 | code: string, 4 | name: string, 5 | ptt?: number, 6 | join_date: number 7 | } 8 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IDatabaseArcRecord.ts: -------------------------------------------------------------------------------- 1 | export default interface IDatabaseArcRecord { 2 | uid: number, 3 | score: number, 4 | health: number, 5 | rating: number, 6 | song_id: string, 7 | modifier: number, 8 | difficulty: number, 9 | clear_type: number, 10 | best_clear_type: number, 11 | time_played: number, 12 | near_count: number, 13 | miss_count: number, 14 | perfect_count: number, 15 | shiny_perfect_count: number, 16 | } 17 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IDatabaseArcSong.ts: -------------------------------------------------------------------------------- 1 | export default interface IDatabaseArcSong { 2 | sid: string, 3 | name_en: string, 4 | name_jp: string, 5 | bpm: string, 6 | bpm_base: number, 7 | pakset: string, 8 | artist: string, 9 | time?: number, 10 | side: number, 11 | date: number, 12 | version: string, 13 | world_unlock: 'true' | 'false', 14 | remote_download: 'true' | 'false', 15 | rating_pst?: number, 16 | rating_prs?: number, 17 | rating_ftr?: number, 18 | rating_byn?: number, 19 | difficultly_pst: number, 20 | difficultly_prs: number, 21 | difficultly_ftr: number, 22 | difficultly_byn: number, 23 | notes_pst?: number, 24 | notes_prs?: number, 25 | notes_ftr?: number, 26 | notes_byn?: number, 27 | chart_designer_pst: string, 28 | chart_designer_prs: string, 29 | chart_designer_ftr: string, 30 | chart_designer_byn: string, 31 | jacket_designer_pst: string, 32 | jacket_designer_prs: string, 33 | jacket_designer_ftr: string, 34 | jacket_designer_byn: string, 35 | jacket_override_pst: 'true' | 'false', 36 | jacket_override_prs: 'true' | 'false', 37 | jacket_override_ftr: 'true' | 'false', 38 | jacket_override_byn: 'true' | 'false' 39 | } 40 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IDatabaseArcSongAlias.ts: -------------------------------------------------------------------------------- 1 | export default interface IDatabaseArcSongAlias { 2 | sid: string, 3 | alias: string 4 | } 5 | -------------------------------------------------------------------------------- /source/modules/database/interfaces/IDatabaseArcSongChart.ts: -------------------------------------------------------------------------------- 1 | export default interface IDatabaseArcSongChart { 2 | sid: string, 3 | rating_class: number, 4 | difficultly: number, 5 | rating: number 6 | } 7 | -------------------------------------------------------------------------------- /source/modules/syslog/syslog.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'syslog.ts\t'; 2 | 3 | class SystemLog { 4 | 5 | private static _internal_log: Function = console.log; 6 | private static _internal_log_debug: Function = console.debug; 7 | private static _internal_log_info: Function = console.info; 8 | private static _internal_log_warn: Function = console.warn; 9 | private static _internal_log_error: Function = console.error; 10 | private static _internal_log_assert: Function = console.assert; 11 | 12 | private static _file = require('fs'); 13 | private static _level_table = ['V', 'I', 'W', 'E', 'F']; 14 | private static _color_table = [ 15 | '\x1b[39m', // V FgGrey 16 | '\x1b[32m\x1b[1m', // I FgGreen 17 | '\x1b[33m\x1b[1m', // W FgYellow 18 | '\x1b[35m\x1b[1m', // E FgMagenta 19 | '\x1b[31m\x1b[1m' // F FgRed 20 | ]; 21 | 22 | private constructor() { } 23 | 24 | public static startLogging() { 25 | 26 | // create folder first 27 | if (!this._file.existsSync(LOG_PATH)) 28 | this._file.mkdirSync(LOG_PATH); 29 | 30 | // set object to global space 31 | Object.defineProperty(global, 'syslog', 32 | { value: SystemLog, writable: false, configurable: false }); 33 | 34 | // hack the console output function 35 | console.log = (...args: Array) => { this._console(0, args) }; 36 | console.debug = (...args: Array) => { this._console(0, args) }; 37 | console.info = (...args: Array) => { this._console(1, args) }; 38 | console.warn = (...args: Array) => { this._console(2, args) }; 39 | console.error = (...args: Array) => { this._console(3, args) }; 40 | console.assert = (...args: Array) => { this._console(4, args) }; 41 | 42 | this.i(TAG, `System log started.`); 43 | this.i(TAG, `Welcome to BotArcAPI (。・∀・)ノ゙hi~`); 44 | this.i(TAG, `Current version is ${BOTARCAPI_VERSTR}`); 45 | this.i(TAG, '** Start Service **'); 46 | } 47 | 48 | public static stop(): void { 49 | // do nothing 50 | } 51 | 52 | /** 53 | * base function for logx, pls do not using directly. 54 | * @param {number} level log level 55 | * @param {string} tag tag for code 56 | * @param {Array} message print somthing 57 | */ 58 | private static log(level: number, tag: string, ...message: Array): void { 59 | if (level < 0 || level > 5) 60 | throw new RangeError('invalid loglevel'); 61 | if (level < LOG_LEVEL) return; 62 | 63 | // format date time 64 | const _time = new Date(); 65 | const _time_string = 66 | `[${_time.getFullYear()}-` + 67 | `${String(_time.getMonth() + 1).padStart(2, '0')}-` + 68 | `${String(_time.getDate()).padStart(2, '0')} ` + 69 | `${String(_time.getHours()).padStart(2, '0')}:` + 70 | `${String(_time.getMinutes()).padStart(2, '0')}:` + 71 | `${String(_time.getSeconds()).padStart(2, '0')}.` + 72 | `${String(_time.getMilliseconds()).padStart(3, '0')}]`; 73 | 74 | // print log string to screen 75 | const _log_content = `${_time_string} ` + 76 | `${this._level_table[level]} ${tag}\t${message}`; 77 | this._internal_log(`${this._color_table[level]}${_log_content}`); 78 | 79 | // write to log file 80 | const _log_file = LOG_PATH + '/' + 81 | `${_time.getFullYear()}_` + 82 | `${String(_time.getMonth() + 1).padStart(2, '0')}_` + 83 | `${String(_time.getDate()).padStart(2, '0')}.log`; 84 | this._file.promises.appendFile(_log_file, `${_log_content}\n`, { flag: 'a' }); 85 | } 86 | 87 | /** 88 | * Log Verbose 89 | * @param {string} tag tag for code 90 | * @param {Array} message print somthing 91 | */ 92 | public static v(tag: string, ...message: Array): void { 93 | this.log(0, tag, message); 94 | } 95 | 96 | /** 97 | * Log Information 98 | * @param {string} tag tag for code 99 | * @param {Array} message print somthing 100 | */ 101 | public static i(tag: string, ...message: Array): void { 102 | this.log(1, tag, message); 103 | } 104 | 105 | /** 106 | * Log Warning 107 | * @param {string} tag tag for code 108 | * @param {Array} message print somthing 109 | */ 110 | public static w(tag: string, ...message: Array): void { 111 | this.log(2, tag, message); 112 | } 113 | 114 | /** 115 | * Log Error 116 | * @param {string} tag tag for code 117 | * @param {Array} message print somthing 118 | */ 119 | public static e(tag: string, ...message: Array): void { 120 | this.log(3, tag, message); 121 | } 122 | 123 | /** 124 | * Log Fatal 125 | * @param {string} tag tag for code 126 | * @param {Array} message print somthing 127 | */ 128 | public static f(tag: string, ...message: Array): void { 129 | this.log(4, tag, message); 130 | } 131 | 132 | /** 133 | * log debug (debug only) 134 | * @param {Array} args 135 | */ 136 | public static d(...args: Array): void { 137 | this._internal_log(args); 138 | } 139 | 140 | /** 141 | * overriding console object 142 | * @param {number} level 143 | * @param {...any} args 144 | */ 145 | private static _console(level: number, ...args: Array): void { 146 | this.log(level, 'unknown\t\t', args); 147 | } 148 | } 149 | 150 | export default SystemLog; -------------------------------------------------------------------------------- /source/publicapi/v1/arc/alloc.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/arc/alloc.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_alloc_managed from '../../../modules/account/alloc.managed'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /arc/alloc[?time=xxx][&clear=true] 14 | // validate the request arguments 15 | argument.time = parseInt(argument.time); 16 | argument.clear = argument.clear == 'true' ? true : false; 17 | 18 | // default time is 30 sec 19 | if (isNaN(argument.time) || argument.time == 0) 20 | argument.time = 30; 21 | 22 | // clamp the range 23 | if (argument.time < 30 || argument.time > 240) 24 | throw new APIError(-1, 'invalid time'); 25 | 26 | let _token = null; 27 | try { _token = await account_alloc_managed(argument.time, argument.clear); } 28 | catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 29 | 30 | const _return = { 31 | access_token: _token, 32 | valid_time: argument.time 33 | }; 34 | 35 | resolve(_return); 36 | 37 | } catch (e) { 38 | 39 | if (e instanceof APIError) 40 | return reject(e); 41 | 42 | syslog.e(TAG, e.stack); 43 | return reject(new APIError(-233, 'unknown error occurred')); 44 | 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v1/arc/forward.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/arc/forward.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcapi_any from '../../../modules/arcfetch/arcapi.any'; 6 | import account_fromtoken from '../../../modules/account/fromtoken'; 7 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 8 | 9 | export default (argument: any, method: ArcFetchMethod, 10 | path: string, header: any, databody: any): Promise => { 11 | 12 | return new Promise(async (resolve, reject) => { 13 | 14 | try { 15 | 16 | // /arc/forward[/url/to/arcapi?foo=xx&bar=xx] 17 | // get token from GET parameters 18 | let _access_token: string | null = null; 19 | if (argument.token) { 20 | _access_token = argument.token; 21 | 22 | // delete access token from parameters 23 | delete argument.token; 24 | } 25 | 26 | // compatible with arcapi request format 27 | else if (header.authorization) { 28 | const _array = header.authorization.split(' '); 29 | if (_array.length == 2 && _array[0] == 'Bearer') 30 | _access_token = _array[1]; 31 | 32 | // delete access token from header 33 | delete header.authorization; 34 | } 35 | 36 | // validate the token 37 | if (!_access_token) 38 | throw new APIError(-1, 'invalid token'); 39 | 40 | // get account from token 41 | let _account = null; 42 | try { _account = await account_fromtoken(_access_token); } 43 | catch (e) { throw new APIError(-2, 'invalid token'); } 44 | 45 | // request arcapi 46 | let _return: any = {}; 47 | try { 48 | _return = await arcapi_any(_account, method, 49 | path + '?' + new URLSearchParams(argument), databody); 50 | } 51 | catch (e) { 52 | _return.error_code = e; 53 | _return.success = 'false'; 54 | } 55 | 56 | resolve(_return); 57 | 58 | } catch (e) { 59 | if (e instanceof APIError) 60 | return reject(e); 61 | 62 | syslog.e(TAG, e.stack); 63 | return reject(new APIError(-233, 'unknown error occurred')); 64 | } 65 | 66 | }); 67 | 68 | } 69 | -------------------------------------------------------------------------------- /source/publicapi/v1/arc/recycle.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/arc/recycle.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_recycle_managed from '../../../modules/account/recycle.managed'; 6 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 7 | 8 | export default (argument: any, method: ArcFetchMethod, 9 | path: string, header: any, databody: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /arc/recycle[token=xxx] 16 | // get token from GET parameters 17 | let _access_token = null; 18 | if (argument.token) { 19 | _access_token = argument.token; 20 | } 21 | 22 | // compatible with arcapi request format 23 | else if (header.authorization) { 24 | const _array = header.authorization.split(' '); 25 | if (_array.length == 2 && _array[0] == 'Bearer') 26 | _access_token = _array[1]; 27 | } 28 | 29 | // validate the token 30 | if (!_access_token) 31 | throw new APIError(-1, 'invalid token'); 32 | 33 | // recycle the account 34 | try { await account_recycle_managed(_access_token); } 35 | catch (e) { throw new APIError(-2, 'invalid token'); } 36 | 37 | resolve(null); 38 | 39 | } catch (e) { 40 | if (e instanceof APIError) 41 | return reject(e); 42 | 43 | syslog.e(TAG, e.stack); 44 | return reject(new APIError(-233, 'unknown error occurred')); 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v1/random.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/random.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_random from '../../modules/database/database.arcsong.byrand'; 6 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 7 | import IDatabaseArcSong from '../../modules/database/interfaces/IDatabaseArcSong'; 8 | 9 | export default (argument: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /random?start=xx[&end=xx][&info=true] 16 | // check for request arguments 17 | argument.start = parseInt(argument.start); 18 | if (isNaN(argument.start)) argument.start = 0; 19 | argument.end = parseInt(argument.end); 20 | if (isNaN(argument.end)) argument.end = 0; 21 | 22 | // default range 23 | if (argument.start == 0 && argument.end == 0) { 24 | argument.start = 1; 25 | argument.end = 11; 26 | } 27 | 28 | if (argument.start < 1 || argument.start > 11) 29 | throw new APIError(-1, 'invalid range of start'); 30 | if (argument.end != 0 && (argument.end < argument.start || argument.end > 11)) 31 | throw new APIError(-2, 'invalid range of end'); 32 | 33 | let _arc_song: any = null; 34 | let _arc_songinfo: IDatabaseArcSong = {}; 35 | let _return: any = {}; 36 | 37 | // select song 38 | try { 39 | _arc_song = await arcsong_random(argument.start, argument.end); 40 | _return.id = _arc_song.sid; 41 | _return.rating_class = _arc_song.rating_class; 42 | } catch (e) { throw new APIError(-3, 'internal error'); } 43 | 44 | // return song info if needed 45 | if (argument.info == 'true') { 46 | 47 | try { 48 | _arc_songinfo = await arcsong_bysongid(_arc_song.sid); 49 | _return.song_info = { 50 | id: _arc_songinfo.sid, 51 | title_localized: { 52 | en: _arc_songinfo.name_en, 53 | ja: _arc_songinfo.name_jp 54 | }, 55 | artist: _arc_songinfo.artist, 56 | bpm: _arc_songinfo.bpm, 57 | bpm_base: _arc_songinfo.bpm_base, 58 | set: _arc_songinfo.pakset, 59 | audioTimeSec: _arc_songinfo.time, 60 | side: _arc_songinfo.side, 61 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 62 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 63 | date: _arc_songinfo.date, 64 | difficulties: [ 65 | { 66 | ratingClass: 0, 67 | chartDesigner: _arc_songinfo.chart_designer_pst, 68 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 69 | rating: _arc_songinfo.rating_pst 70 | }, 71 | { 72 | ratingClass: 1, 73 | chartDesigner: _arc_songinfo.chart_designer_prs, 74 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 75 | rating: _arc_songinfo.rating_prs 76 | }, 77 | { 78 | ratingClass: 2, 79 | chartDesigner: _arc_songinfo.chart_designer_ftr, 80 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 81 | rating: _arc_songinfo.rating_ftr 82 | } 83 | ] 84 | }; 85 | 86 | // remove empty field 87 | if (_return.song_info.title_localized.ja == "") 88 | delete _return.song_info.title_localized.ja; 89 | 90 | } catch (e) { throw new APIError(-4, 'internal error'); } 91 | 92 | } 93 | 94 | resolve(_return); 95 | 96 | } catch (e) { 97 | 98 | if (e instanceof APIError) 99 | return reject(e); 100 | 101 | syslog.e(TAG, e.stack); 102 | return reject(new APIError(-233, 'unknown error occurred')); 103 | 104 | } 105 | 106 | }); 107 | 108 | } 109 | -------------------------------------------------------------------------------- /source/publicapi/v1/songalias.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/songalias.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /songalias?songname=xxx 14 | // validate request arguments 15 | if (typeof argument.songname == 'undefined' || argument.songname == '') 16 | throw new APIError(-1, 'invalid songname'); 17 | 18 | let _arc_songid: Array; 19 | 20 | // query songid by any string 21 | try { 22 | _arc_songid = await arcsong_sid_byany(argument.songname); 23 | } catch (e) { throw new APIError(-2, 'no result'); } 24 | 25 | if (_arc_songid.length > 1) 26 | throw new APIError(-3, 'too many records'); 27 | 28 | resolve({ id: _arc_songid[0] }); 29 | 30 | } catch (e) { 31 | 32 | if (e instanceof APIError) 33 | return reject(e); 34 | 35 | syslog.e(TAG, e.stack); 36 | return reject(new APIError(-233, 'unknown error occurred')); 37 | 38 | } 39 | 40 | }); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /source/publicapi/v1/songinfo.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/songinfo.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 6 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 7 | 8 | export default (argument: any): Promise => { 9 | 10 | return new Promise(async (resolve, reject) => { 11 | 12 | try { 13 | 14 | // /songinfo?songname=xxx 15 | // check for request arguments 16 | if (typeof argument.songname == 'undefined' || argument.songname == '') 17 | throw new APIError(-1, 'invalid songname'); 18 | 19 | let _arc_songid = null; 20 | let _arc_songinfo = null; 21 | 22 | // query songid by any string 23 | try { 24 | _arc_songid = await arcsong_sid_byany(argument.songname); 25 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-2, 'this song is not recorded in the database'); } 26 | 27 | if (_arc_songid.length > 1) 28 | throw new APIError(-3, 'too many records'); 29 | _arc_songid = _arc_songid[0]; 30 | 31 | // get song information by songid 32 | try { 33 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 34 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-4, 'internal error'); } 35 | 36 | const _return: any = { 37 | id: _arc_songinfo.sid, 38 | title_localized: { 39 | en: _arc_songinfo.name_en, 40 | ja: _arc_songinfo.name_jp 41 | }, 42 | artist: _arc_songinfo.artist, 43 | bpm: _arc_songinfo.bpm, 44 | bpm_base: _arc_songinfo.bpm_base, 45 | set: _arc_songinfo.pakset, 46 | audioTimeSec: _arc_songinfo.time, 47 | side: _arc_songinfo.side, 48 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 49 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 50 | date: _arc_songinfo.date, 51 | difficulties: [ 52 | { 53 | ratingClass: 0, 54 | chartDesigner: _arc_songinfo.chart_designer_pst, 55 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 56 | rating: _arc_songinfo.rating_pst 57 | }, 58 | { 59 | ratingClass: 1, 60 | chartDesigner: _arc_songinfo.chart_designer_prs, 61 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 62 | rating: _arc_songinfo.rating_prs 63 | }, 64 | { 65 | ratingClass: 2, 66 | chartDesigner: _arc_songinfo.chart_designer_ftr, 67 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 68 | rating: _arc_songinfo.rating_ftr 69 | } 70 | ] 71 | }; 72 | 73 | // remove empty field 74 | if (_return.title_localized.ja == '') 75 | delete _return.title_localized.ja; 76 | 77 | resolve(_return); 78 | 79 | } catch (e) { 80 | 81 | if (e instanceof APIError) 82 | return reject(e); 83 | 84 | syslog.e(TAG, e.stack); 85 | return reject(new APIError(-233, 'unknown error occurred')); 86 | 87 | } 88 | 89 | }); 90 | 91 | } 92 | -------------------------------------------------------------------------------- /source/publicapi/v1/userbest.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/userbest.ts\t'; 2 | 3 | import Utils from '../../corefunc/utils'; 4 | import syslog from '../../modules/syslog/syslog'; 5 | import APIError from '../../modules/apierror/apierror'; 6 | 7 | import arcapi_friend_add from '../../modules/arcfetch/arcapi.friend.add'; 8 | import arcapi_friend_clear from '../../modules/arcfetch/arcapi.friend.clear'; 9 | import arcapi_rank_friend from '../../modules/arcfetch/arcapi.rank.friend'; 10 | import account_alloc from '../../modules/account/alloc'; 11 | import account_recycle from '../../modules/account/recycle'; 12 | 13 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 14 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 15 | import arcrecord_update from '../../modules/database/database.arcrecord.update'; 16 | import arcplayer_update from '../../modules/database/database.arcplayer.update'; 17 | 18 | export default (argument: any): Promise => { 19 | 20 | return new Promise(async (resolve, reject): Promise => { 21 | 22 | try { 23 | 24 | // /userbest?usercode=xxx&songname=xxx&difficulty=x 25 | // validate request arguments 26 | if (typeof argument.usercode == 'undefined' || argument.usercode == '') 27 | throw new APIError(-1, 'invalid usercode'); 28 | if (typeof argument.songname == 'undefined' || argument.songname == '') 29 | throw new APIError(-2, 'invalid songname'); 30 | if (typeof argument.difficulty == 'undefined' || argument.difficulty == '') 31 | throw new APIError(-3, 'invalid difficulty'); 32 | 33 | let _arc_difficulty: any = Utils.arcMapDiffFormat(argument.difficulty, 0); 34 | if (!_arc_difficulty) 35 | throw new APIError(-4, 'invalid difficulty'); 36 | 37 | let _arc_account: any = null; 38 | let _arc_songid: any = null; 39 | let _arc_songinfo: any = null; 40 | let _arc_friendlist: any = null; 41 | let _arc_friend: any = null; 42 | let _arc_ranklist: any = null; 43 | let _arc_rank: any = null; 44 | 45 | // check songid valid 46 | try { 47 | _arc_songid = await arcsong_sid_byany(argument.songname); 48 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-5, 'this song is not recorded in the database'); } 49 | 50 | if (_arc_songid.length > 1) 51 | throw new APIError(-6, 'too many records'); 52 | _arc_songid = _arc_songid[0]; 53 | 54 | try { 55 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 56 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-7, 'internal error'); } 57 | 58 | // request an arc account 59 | try { 60 | _arc_account = await account_alloc(); 61 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-8, 'allocate an arc account failed'); } 62 | 63 | try { 64 | 65 | // clear friend list 66 | try { 67 | await arcapi_friend_clear(_arc_account); 68 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-9, 'clear friend list failed'); } 69 | 70 | // add friend 71 | try { 72 | _arc_friendlist = await arcapi_friend_add(_arc_account, argument.usercode); 73 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-10, 'add friend failed'); } 74 | 75 | // length must be 1 76 | if (_arc_friendlist.length != 1) 77 | throw new APIError(-11, 'internal error occurred'); 78 | 79 | // result of arcapi not include 80 | // user code anymore since v6 81 | _arc_friend = _arc_friendlist[0]; 82 | _arc_friend.code = argument.usercode; 83 | 84 | // get rank result 85 | try { 86 | _arc_ranklist = await arcapi_rank_friend(_arc_account, _arc_songid, _arc_difficulty, 0, 1); 87 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-12, 'internal error occurred'); } 88 | 89 | if (!_arc_ranklist.length) 90 | throw new APIError(-13, 'not played yet'); 91 | 92 | // calculate song rating 93 | _arc_rank = _arc_ranklist[0]; 94 | _arc_rank.rating = Utils.arcCalcSongRating( 95 | _arc_rank.score, 96 | _arc_songinfo[`rating_${Utils.arcMapDiffFormat(argument.difficulty, 1)}`] 97 | ); 98 | 99 | const _return = _arc_rank; 100 | delete _return.name; 101 | delete _return.user_id; 102 | 103 | // result of arcapi not include 104 | // user code anymore since v6 105 | delete _return.user_code; 106 | 107 | resolve(_return); 108 | 109 | } catch (e) { 110 | // recycle account when any error occurred 111 | if (_arc_account) 112 | account_recycle(_arc_account); 113 | // re-throw the error 114 | throw e; 115 | } 116 | 117 | // release account 118 | account_recycle(_arc_account); 119 | 120 | // update user info and recently played 121 | arcplayer_update(_arc_friend) 122 | .catch((error) => { syslog.e(error.stack); }); 123 | 124 | // insert new record into database 125 | if (_arc_friend.recent_score.length) 126 | arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 127 | .catch((error) => { syslog.e(error.stack); }); 128 | 129 | // insert new record into database 130 | arcrecord_update(_arc_friend.user_id, _arc_rank) 131 | .catch((error) => { syslog.e(error.stack); }); 132 | 133 | } catch (e) { 134 | 135 | if (e instanceof APIError) 136 | return reject(e); 137 | 138 | syslog.e(TAG, e.stack); 139 | return reject(new APIError(-233, 'unknown error occurred')); 140 | 141 | } 142 | 143 | }); 144 | 145 | } 146 | -------------------------------------------------------------------------------- /source/publicapi/v1/userinfo.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v1/userinfo.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | 6 | import arcapi_friend_add from '../../modules/arcfetch/arcapi.friend.add'; 7 | import arcapi_friend_clear from '../../modules/arcfetch/arcapi.friend.clear'; 8 | import account_alloc from '../../modules/account/alloc'; 9 | import account_recycle from '../../modules/account/recycle'; 10 | 11 | import arcrecord_update from '../../modules/database/database.arcrecord.update'; 12 | import arcplayer_update from '../../modules/database/database.arcplayer.update'; 13 | 14 | import IArcAccount from '../../modules/arcfetch/interfaces/IArcAccount'; 15 | import IArcPlayer from '../../modules/arcfetch/interfaces/IArcPlayer'; 16 | 17 | export default (argument: any): Promise => { 18 | 19 | return new Promise(async (resolve, reject) => { 20 | 21 | try { 22 | 23 | // /userinfo?usercode=xxx[&recent=true] 24 | // check for request arguments 25 | if (typeof argument.usercode == 'undefined' || argument.usercode == '') 26 | throw new APIError(-1, 'invalid usercode'); 27 | 28 | let _arc_account: IArcAccount | null = null; 29 | let _arc_friendlist: Array | null = null; 30 | let _arc_friend: IArcPlayer | null = null; 31 | 32 | // request an arc account 33 | try { 34 | _arc_account = await account_alloc(); 35 | } catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 36 | 37 | try { 38 | 39 | // clear friend list 40 | try { 41 | await arcapi_friend_clear(_arc_account); 42 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-3, 'clear friend list failed'); } 43 | 44 | // add friend 45 | try { 46 | _arc_friendlist = await arcapi_friend_add(_arc_account, argument.usercode); 47 | } catch (e) { throw new APIError(-4, 'add friend failed'); } 48 | 49 | // length must be 1 50 | if (_arc_friendlist.length != 1) 51 | throw new APIError(-5, 'internal error occurred'); 52 | 53 | // result of arcapi not include 54 | // user code anymore since v6 55 | _arc_friend = _arc_friendlist[0]; 56 | _arc_friend.code = argument.usercode; 57 | 58 | // must do deep copy 59 | const _return = JSON.parse(JSON.stringify(_arc_friend)); 60 | _return.recent_score = _arc_friend.recent_score[0]; 61 | 62 | // delete field if needn't recent data or not played yet 63 | if (argument.recent != 'true' || !_arc_friend.recent_score.length) 64 | delete _return.recent_score; 65 | 66 | // delete usercode field 67 | delete _return.code; 68 | 69 | resolve(_return); 70 | 71 | } catch (e) { 72 | // recycle account when any error occurred 73 | if (_arc_account) 74 | account_recycle(_arc_account); 75 | // re-throw the error 76 | throw e; 77 | } 78 | 79 | // release account 80 | account_recycle(_arc_account); 81 | 82 | // update user info and recently played 83 | arcplayer_update(_arc_friend) 84 | .catch((error) => { syslog.e(error.stack); }); 85 | 86 | // insert new record into database 87 | if (_arc_friend.recent_score.length) 88 | arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 89 | .catch((error) => { syslog.e(error.stack); }); 90 | 91 | } catch (e) { 92 | 93 | if (e instanceof APIError) 94 | return reject(e); 95 | 96 | syslog.e(TAG, e.stack); 97 | return reject(new APIError(-233, 'unknown error occurred')); 98 | 99 | } 100 | 101 | }); 102 | 103 | } 104 | -------------------------------------------------------------------------------- /source/publicapi/v2/arc/alloc.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/arc/alloc.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_alloc_managed from '../../../modules/account/alloc.managed'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /arc/alloc[?time=xxx][&clear=true] 14 | // validate the request arguments 15 | argument.time = parseInt(argument.time); 16 | argument.clear = argument.clear == 'true' ? true : false; 17 | 18 | // default time is 30 sec 19 | if (isNaN(argument.time) || argument.time == 0) 20 | argument.time = 30; 21 | 22 | // clamp the range 23 | if (argument.time < 30 || argument.time > 240) 24 | throw new APIError(-1, 'invalid time'); 25 | 26 | let _token = null; 27 | try { _token = await account_alloc_managed(argument.time, argument.clear); } 28 | catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 29 | 30 | const _return = { 31 | access_token: _token, 32 | valid_time: argument.time 33 | }; 34 | 35 | resolve(_return); 36 | 37 | } catch (e) { 38 | 39 | if (e instanceof APIError) 40 | return reject(e); 41 | 42 | syslog.e(TAG, e.stack); 43 | return reject(new APIError(-233, 'unknown error occurred')); 44 | 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v2/arc/forward.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/arc/forward.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcapi_any from '../../../modules/arcfetch/arcapi.any'; 6 | import account_fromtoken from '../../../modules/account/fromtoken'; 7 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 8 | 9 | export default (argument: any, method: ArcFetchMethod, 10 | path: string, header: any, databody: any): Promise => { 11 | 12 | return new Promise(async (resolve, reject) => { 13 | 14 | try { 15 | 16 | // /arc/forward[/url/to/arcapi?foo=xx&bar=xx] 17 | // get token from GET parameters 18 | let _access_token: string | null = null; 19 | if (argument.token) { 20 | _access_token = argument.token; 21 | 22 | // delete access token from parameters 23 | delete argument.token; 24 | } 25 | 26 | // compatible with arcapi request format 27 | else if (header.authorization) { 28 | const _array = header.authorization.split(' '); 29 | if (_array.length == 2 && _array[0] == 'Bearer') 30 | _access_token = _array[1]; 31 | 32 | // delete access token from header 33 | delete header.authorization; 34 | } 35 | 36 | // validate the token 37 | if (!_access_token) 38 | throw new APIError(-1, 'invalid token'); 39 | 40 | // get account from token 41 | let _account = null; 42 | try { _account = await account_fromtoken(_access_token); } 43 | catch (e) { throw new APIError(-2, 'invalid token'); } 44 | 45 | // request arcapi 46 | let _return: any = {}; 47 | try { 48 | _return = await arcapi_any(_account, method, 49 | path + '?' + new URLSearchParams(argument), databody); 50 | } 51 | catch (e) { 52 | _return.error_code = e; 53 | _return.success = 'false'; 54 | } 55 | 56 | resolve(_return); 57 | 58 | } catch (e) { 59 | if (e instanceof APIError) 60 | return reject(e); 61 | 62 | syslog.e(TAG, e.stack); 63 | return reject(new APIError(-233, 'unknown error occurred')); 64 | } 65 | 66 | }); 67 | 68 | } 69 | -------------------------------------------------------------------------------- /source/publicapi/v2/arc/recycle.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/arc/recycle.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_recycle_managed from '../../../modules/account/recycle.managed'; 6 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 7 | 8 | export default (argument: any, method: ArcFetchMethod, 9 | path: string, header: any, databody: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /arc/recycle[token=xxx] 16 | // get token from GET parameters 17 | let _access_token = null; 18 | if (argument.token) { 19 | _access_token = argument.token; 20 | } 21 | 22 | // compatible with arcapi request format 23 | else if (header.authorization) { 24 | const _array = header.authorization.split(' '); 25 | if (_array.length == 2 && _array[0] == 'Bearer') 26 | _access_token = _array[1]; 27 | } 28 | 29 | // validate the token 30 | if (!_access_token) 31 | throw new APIError(-1, 'invalid token'); 32 | 33 | // recycle the account 34 | try { await account_recycle_managed(_access_token); } 35 | catch (e) { throw new APIError(-2, 'invalid token'); } 36 | 37 | resolve(null); 38 | 39 | } catch (e) { 40 | if (e instanceof APIError) 41 | return reject(e); 42 | 43 | syslog.e(TAG, e.stack); 44 | return reject(new APIError(-233, 'unknown error occurred')); 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v2/connect.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/connect.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import crypto from 'crypto'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | const _date: Date = new Date(); 12 | const _table = 'qwertyuiopasdfghjklzxcvbnm1234567890'; 13 | 14 | const _secret = 15 | _date.getUTCFullYear() + 'ori' + 16 | _date.getUTCMonth() + 'wol' + 17 | _date.getUTCDate() + 'oihs' + 18 | _date.getUTCDate() + 'otas'; 19 | 20 | // calculate md5 hash 21 | const _hash = crypto.createHash('md5').update(_secret).digest('hex'); 22 | 23 | let _result = ''; 24 | for (let i = 0; i < _hash.length; ++i) { 25 | _result += _table[_hash[i].charCodeAt(0) % 36]; 26 | } 27 | 28 | const _return = `${_result[1]}${_result[20]}${_result[4]}${_result[30]}${_result[2]}${_result[11]}${_result[23]}`; 29 | 30 | resolve({ key: _return }); 31 | }); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /source/publicapi/v2/random.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/random.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_random from '../../modules/database/database.arcsong.byrand'; 6 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 7 | import IDatabaseArcSong from '../../modules/database/interfaces/IDatabaseArcSong'; 8 | 9 | export default (argument: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /random?start=xx[&end=xx][&info=true] 16 | // check for request arguments 17 | argument.start = parseInt(argument.start); 18 | if (isNaN(argument.start)) argument.start = 0; 19 | argument.end = parseInt(argument.end); 20 | if (isNaN(argument.end)) argument.end = 0; 21 | 22 | // default range 23 | // 1 1+ 2 2+ ... 10 10+ 11 24 | if (argument.start == 0 && argument.end == 0) { 25 | argument.start = 2; 26 | argument.end = 23; 27 | } 28 | 29 | if (argument.start < 2 || argument.start > 23) 30 | throw new APIError(-1, 'invalid range of start'); 31 | if (argument.end != 0 && (argument.end < argument.start || argument.end > 23)) 32 | throw new APIError(-2, 'invalid range of end'); 33 | 34 | let _arc_song: any = null; 35 | let _arc_songinfo: IDatabaseArcSong = {}; 36 | let _return: any = {}; 37 | 38 | // select song 39 | try { 40 | _arc_song = await arcsong_random(argument.start, argument.end); 41 | _return.id = _arc_song.sid; 42 | _return.rating_class = _arc_song.rating_class; 43 | } catch (e) { throw new APIError(-3, 'internal error'); } 44 | 45 | // return song info if needed 46 | if (argument.info == 'true') { 47 | 48 | try { 49 | _arc_songinfo = await arcsong_bysongid(_arc_song.sid); 50 | _return.song_info = { 51 | id: _arc_songinfo.sid, 52 | title_localized: { 53 | en: _arc_songinfo.name_en, 54 | ja: _arc_songinfo.name_jp 55 | }, 56 | artist: _arc_songinfo.artist, 57 | bpm: _arc_songinfo.bpm, 58 | bpm_base: _arc_songinfo.bpm_base, 59 | set: _arc_songinfo.pakset, 60 | audioTimeSec: _arc_songinfo.time, 61 | side: _arc_songinfo.side, 62 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 63 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 64 | date: _arc_songinfo.date, 65 | difficulties: [ 66 | { 67 | ratingClass: 0, 68 | chartDesigner: _arc_songinfo.chart_designer_pst, 69 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 70 | rating: _arc_songinfo.difficultly_pst, 71 | ratingReal: _arc_songinfo.rating_pst, 72 | ratingPlus: (_arc_songinfo.difficultly_pst % 2 != 0) 73 | }, 74 | { 75 | ratingClass: 1, 76 | chartDesigner: _arc_songinfo.chart_designer_prs, 77 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 78 | rating: _arc_songinfo.difficultly_prs, 79 | ratingReal: _arc_songinfo.rating_prs, 80 | ratingPlus: (_arc_songinfo.difficultly_prs % 2 != 0) 81 | }, 82 | { 83 | ratingClass: 2, 84 | chartDesigner: _arc_songinfo.chart_designer_ftr, 85 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 86 | rating: _arc_songinfo.difficultly_ftr, 87 | ratingReal: _arc_songinfo.rating_ftr, 88 | ratingPlus: (_arc_songinfo.difficultly_ftr % 2 != 0) 89 | } 90 | ] 91 | }; 92 | 93 | // append beyond rating 94 | if (_arc_songinfo.difficultly_byn != -1) { 95 | _return.song_info.difficulties[3] = { 96 | ratingClass: 3, 97 | chartDesigner: _arc_songinfo.chart_designer_byn, 98 | jacketDesigner: _arc_songinfo.jacket_designer_byn, 99 | rating: _arc_songinfo.difficultly_byn, 100 | ratingReal: _arc_songinfo.rating_byn, 101 | ratingPlus: (_arc_songinfo.difficultly_byn % 2 != 0) 102 | }; 103 | } 104 | 105 | // append rating 106 | _return.song_info.difficulties.map((element: any) => { 107 | element.rating = Math.floor(element.rating / 2); 108 | if (!element.ratingPlus) 109 | delete element.ratingPlus; 110 | return element; 111 | }); 112 | 113 | // remove empty field 114 | if (_return.song_info.title_localized.ja == '') 115 | delete _return.song_info.title_localized.ja; 116 | 117 | } catch (e) { throw new APIError(-4, 'internal error'); } 118 | 119 | } 120 | 121 | resolve(_return); 122 | 123 | } catch (e) { 124 | 125 | if (e instanceof APIError) 126 | return reject(e); 127 | 128 | syslog.e(TAG, e.stack); 129 | return reject(new APIError(-233, 'unknown error occurred')); 130 | 131 | } 132 | 133 | }); 134 | 135 | } 136 | -------------------------------------------------------------------------------- /source/publicapi/v2/songalias.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/songalias.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /songalias?songname=xxx 14 | // validate request arguments 15 | if (typeof argument.songname == 'undefined' || argument.songname == '') 16 | throw new APIError(-1, 'invalid songname'); 17 | 18 | let _arc_songid: Array; 19 | 20 | // query songid by any string 21 | try { 22 | _arc_songid = await arcsong_sid_byany(argument.songname); 23 | } catch (e) { throw new APIError(-2, 'no result'); } 24 | 25 | if (_arc_songid.length > 1) 26 | throw new APIError(-3, 'too many records'); 27 | 28 | resolve({ id: _arc_songid[0] }); 29 | 30 | } catch (e) { 31 | 32 | if (e instanceof APIError) 33 | return reject(e); 34 | 35 | syslog.e(TAG, e.stack); 36 | return reject(new APIError(-233, 'unknown error occurred')); 37 | 38 | } 39 | 40 | }); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /source/publicapi/v2/songinfo.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/songinfo.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 6 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 7 | 8 | export default (argument: any): Promise => { 9 | 10 | return new Promise(async (resolve, reject) => { 11 | 12 | try { 13 | 14 | // /songinfo?songname=xxx 15 | // check for request arguments 16 | if (typeof argument.songname == 'undefined' || argument.songname == '') 17 | throw new APIError(-1, 'invalid songname'); 18 | 19 | let _arc_songid = null; 20 | let _arc_songinfo = null; 21 | 22 | // query songid by any string 23 | try { 24 | _arc_songid = await arcsong_sid_byany(argument.songname); 25 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-2, 'this song is not recorded in the database'); } 26 | 27 | if (_arc_songid.length > 1) 28 | throw new APIError(-3, 'too many records'); 29 | _arc_songid = _arc_songid[0]; 30 | 31 | // get song information by songid 32 | try { 33 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 34 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-4, 'internal error'); } 35 | 36 | const _return: any = { 37 | id: _arc_songinfo.sid, 38 | title_localized: { 39 | en: _arc_songinfo.name_en, 40 | ja: _arc_songinfo.name_jp 41 | }, 42 | artist: _arc_songinfo.artist, 43 | bpm: _arc_songinfo.bpm, 44 | bpm_base: _arc_songinfo.bpm_base, 45 | set: _arc_songinfo.pakset, 46 | audioTimeSec: _arc_songinfo.time, 47 | side: _arc_songinfo.side, 48 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 49 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 50 | date: _arc_songinfo.date, 51 | difficulties: [ 52 | { 53 | ratingClass: 0, 54 | chartDesigner: _arc_songinfo.chart_designer_pst, 55 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 56 | rating: _arc_songinfo.difficultly_pst, 57 | ratingReal: _arc_songinfo.rating_pst, 58 | ratingPlus: (_arc_songinfo.difficultly_pst % 2 != 0) 59 | }, 60 | { 61 | ratingClass: 1, 62 | chartDesigner: _arc_songinfo.chart_designer_prs, 63 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 64 | rating: _arc_songinfo.difficultly_prs, 65 | ratingReal: _arc_songinfo.rating_prs, 66 | ratingPlus: (_arc_songinfo.difficultly_prs % 2 != 0) 67 | }, 68 | { 69 | ratingClass: 2, 70 | chartDesigner: _arc_songinfo.chart_designer_ftr, 71 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 72 | rating: _arc_songinfo.difficultly_ftr, 73 | ratingReal: _arc_songinfo.rating_ftr, 74 | ratingPlus: (_arc_songinfo.difficultly_ftr % 2 != 0) 75 | } 76 | ] 77 | }; 78 | 79 | // append beyond rating 80 | if (_arc_songinfo.difficultly_byn != -1) { 81 | _return.difficulties[3] = { 82 | ratingClass: 3, 83 | chartDesigner: _arc_songinfo.chart_designer_byn, 84 | jacketDesigner: _arc_songinfo.jacket_designer_byn, 85 | rating: _arc_songinfo.difficultly_byn, 86 | ratingReal: _arc_songinfo.rating_byn, 87 | ratingPlus: (_arc_songinfo.difficultly_byn % 2 != 0) 88 | }; 89 | } 90 | 91 | // append rating 92 | _return.difficulties.map((element: any) => { 93 | element.rating = Math.floor(element.rating / 2); 94 | if (!element.ratingPlus) 95 | delete element.ratingPlus; 96 | return element; 97 | }); 98 | 99 | // remove empty field 100 | if (_return.title_localized.ja == '') 101 | delete _return.title_localized.ja; 102 | 103 | resolve(_return); 104 | 105 | } catch (e) { 106 | 107 | if (e instanceof APIError) 108 | return reject(e); 109 | 110 | syslog.e(TAG, e.stack); 111 | return reject(new APIError(-233, 'unknown error occurred')); 112 | 113 | } 114 | 115 | }); 116 | 117 | } 118 | -------------------------------------------------------------------------------- /source/publicapi/v2/userbest.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/userbest.ts\t'; 2 | 3 | import Utils from '../../corefunc/utils'; 4 | import syslog from '../../modules/syslog/syslog'; 5 | import APIError from '../../modules/apierror/apierror'; 6 | 7 | import arcapi_friend_add from '../../modules/arcfetch/arcapi.friend.add'; 8 | import arcapi_friend_clear from '../../modules/arcfetch/arcapi.friend.clear'; 9 | import arcapi_rank_friend from '../../modules/arcfetch/arcapi.rank.friend'; 10 | import account_alloc from '../../modules/account/alloc'; 11 | import account_recycle from '../../modules/account/recycle'; 12 | 13 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 14 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 15 | import arcrecord_update from '../../modules/database/database.arcrecord.update'; 16 | import arcplayer_update from '../../modules/database/database.arcplayer.update'; 17 | 18 | export default (argument: any): Promise => { 19 | 20 | return new Promise(async (resolve, reject): Promise => { 21 | 22 | try { 23 | 24 | // /userbest?usercode=xxx&songname=xxx&difficulty=x 25 | // validate request arguments 26 | if (typeof argument.usercode == 'undefined' || argument.usercode == '') 27 | throw new APIError(-1, 'invalid usercode'); 28 | if (typeof argument.songname == 'undefined' || argument.songname == '') 29 | throw new APIError(-2, 'invalid songname'); 30 | if (typeof argument.difficulty == 'undefined' || argument.difficulty == '') 31 | throw new APIError(-3, 'invalid difficulty'); 32 | 33 | let _arc_difficulty: any = Utils.arcMapDiffFormat(argument.difficulty, 0); 34 | if (!_arc_difficulty) 35 | throw new APIError(-4, 'invalid difficulty'); 36 | 37 | let _arc_account: any = null; 38 | let _arc_songid: any = null; 39 | let _arc_songinfo: any = null; 40 | let _arc_friendlist: any = null; 41 | let _arc_friend: any = null; 42 | let _arc_ranklist: any = null; 43 | let _arc_rank: any = null; 44 | 45 | // check songid valid 46 | try { 47 | _arc_songid = await arcsong_sid_byany(argument.songname); 48 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-5, 'this song is not recorded in the database'); } 49 | 50 | if (_arc_songid.length > 1) 51 | throw new APIError(-6, 'too many records'); 52 | _arc_songid = _arc_songid[0]; 53 | 54 | try { 55 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 56 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-7, 'internal error'); } 57 | 58 | // check for beyond is existed 59 | if (_arc_songinfo.difficultly_byn == -1 && _arc_difficulty == 3) { 60 | throw new APIError(-8, 'this song has no beyond level'); 61 | } 62 | 63 | // request an arc account 64 | try { 65 | _arc_account = await account_alloc(); 66 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-9, 'allocate an arc account failed'); } 67 | 68 | try { 69 | 70 | // clear friend list 71 | try { 72 | await arcapi_friend_clear(_arc_account); 73 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-10, 'clear friend list failed'); } 74 | 75 | // add friend 76 | try { 77 | _arc_friendlist = await arcapi_friend_add(_arc_account, argument.usercode); 78 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-11, 'add friend failed'); } 79 | 80 | // length must be 1 81 | if (_arc_friendlist.length != 1) 82 | throw new APIError(-12, 'internal error occurred'); 83 | 84 | // result of arcapi not include 85 | // user code anymore since v6 86 | _arc_friend = _arc_friendlist[0]; 87 | _arc_friend.code = argument.usercode; 88 | 89 | // get rank result 90 | try { 91 | _arc_ranklist = await arcapi_rank_friend(_arc_account, _arc_songid, _arc_difficulty, 0, 1); 92 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-13, 'internal error occurred'); } 93 | 94 | if (!_arc_ranklist.length) 95 | throw new APIError(-14, 'not played yet'); 96 | 97 | // calculate song rating 98 | _arc_rank = _arc_ranklist[0]; 99 | _arc_rank.rating = Utils.arcCalcSongRating( 100 | _arc_rank.score, 101 | _arc_songinfo[`rating_${Utils.arcMapDiffFormat(argument.difficulty, 1)}`] 102 | ); 103 | 104 | const _return = _arc_rank; 105 | delete _return.name; 106 | delete _return.user_id; 107 | 108 | // result of arcapi not include 109 | // user code anymore since v6 110 | delete _return.user_code; 111 | 112 | resolve(_return); 113 | 114 | } catch (e) { 115 | // recycle account when any error occurred 116 | if (_arc_account) 117 | account_recycle(_arc_account); 118 | // re-throw the error 119 | throw e; 120 | } 121 | 122 | // release account 123 | account_recycle(_arc_account); 124 | 125 | // update user info and recently played 126 | arcplayer_update(_arc_friend) 127 | .catch((error) => { syslog.e(error.stack); }); 128 | 129 | // insert new record into database 130 | if (_arc_friend.recent_score.length) 131 | arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 132 | .catch((error) => { syslog.e(error.stack); }); 133 | 134 | // insert new record into database 135 | arcrecord_update(_arc_friend.user_id, _arc_rank) 136 | .catch((error) => { syslog.e(error.stack); }); 137 | 138 | } catch (e) { 139 | 140 | if (e instanceof APIError) 141 | return reject(e); 142 | 143 | syslog.e(TAG, e.stack); 144 | return reject(new APIError(-233, 'unknown error occurred')); 145 | 146 | } 147 | 148 | }); 149 | 150 | } 151 | -------------------------------------------------------------------------------- /source/publicapi/v2/userinfo.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v2/userinfo.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | 6 | import arcapi_friend_add from '../../modules/arcfetch/arcapi.friend.add'; 7 | import arcapi_friend_clear from '../../modules/arcfetch/arcapi.friend.clear'; 8 | import account_alloc from '../../modules/account/alloc'; 9 | import account_recycle from '../../modules/account/recycle'; 10 | 11 | import arcrecord_update from '../../modules/database/database.arcrecord.update'; 12 | import arcplayer_update from '../../modules/database/database.arcplayer.update'; 13 | 14 | import IArcAccount from '../../modules/arcfetch/interfaces/IArcAccount'; 15 | import IArcPlayer from '../../modules/arcfetch/interfaces/IArcPlayer'; 16 | 17 | export default (argument: any): Promise => { 18 | 19 | return new Promise(async (resolve, reject) => { 20 | 21 | try { 22 | 23 | // /userinfo?usercode=xxx[&recent=true] 24 | // check for request arguments 25 | if (typeof argument.usercode == 'undefined' || argument.usercode == '') 26 | throw new APIError(-1, 'invalid usercode'); 27 | 28 | let _arc_account: IArcAccount | null = null; 29 | let _arc_friendlist: Array | null = null; 30 | let _arc_friend: IArcPlayer | null = null; 31 | 32 | // request an arc account 33 | try { 34 | _arc_account = await account_alloc(); 35 | } catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 36 | 37 | try { 38 | 39 | // clear friend list 40 | try { 41 | await arcapi_friend_clear(_arc_account); 42 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-3, 'clear friend list failed'); } 43 | 44 | // add friend 45 | try { 46 | _arc_friendlist = await arcapi_friend_add(_arc_account, argument.usercode); 47 | } catch (e) { throw new APIError(-4, 'add friend failed'); } 48 | 49 | // length must be 1 50 | if (_arc_friendlist.length != 1) 51 | throw new APIError(-5, 'internal error occurred'); 52 | 53 | // result of arcapi not include 54 | // user code anymore since v6 55 | _arc_friend = _arc_friendlist[0]; 56 | _arc_friend.code = argument.usercode; 57 | 58 | // must do deep copy 59 | const _return = JSON.parse(JSON.stringify(_arc_friend)); 60 | _return.recent_score = _arc_friend.recent_score[0]; 61 | 62 | // delete field if needn't recent data or not played yet 63 | if (argument.recent != 'true' || !_arc_friend.recent_score.length) 64 | delete _return.recent_score; 65 | 66 | // delete usercode field 67 | delete _return.code; 68 | 69 | resolve(_return); 70 | 71 | } catch (e) { 72 | // recycle account when any error occurred 73 | if (_arc_account) 74 | account_recycle(_arc_account); 75 | // re-throw the error 76 | throw e; 77 | } 78 | 79 | // release account 80 | account_recycle(_arc_account); 81 | 82 | // update user info and recently played 83 | arcplayer_update(_arc_friend) 84 | .catch((error) => { syslog.e(error.stack); }); 85 | 86 | // insert new record into database 87 | if (_arc_friend.recent_score.length) 88 | arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 89 | .catch((error) => { syslog.e(error.stack); }); 90 | 91 | } catch (e) { 92 | 93 | if (e instanceof APIError) 94 | return reject(e); 95 | 96 | syslog.e(TAG, e.stack); 97 | return reject(new APIError(-233, 'unknown error occurred')); 98 | 99 | } 100 | 101 | }); 102 | 103 | } 104 | -------------------------------------------------------------------------------- /source/publicapi/v3/arc/alloc.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/arc/alloc.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_alloc_managed from '../../../modules/account/alloc.managed'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /arc/alloc[?time=xxx][&clear=true] 14 | // validate the request arguments 15 | argument.time = parseInt(argument.time); 16 | argument.clear = argument.clear == 'true' ? true : false; 17 | 18 | // default time is 30 sec 19 | if (isNaN(argument.time) || argument.time == 0) 20 | argument.time = 30; 21 | 22 | // clamp the range 23 | if (argument.time < 30 || argument.time > 240) 24 | throw new APIError(-1, 'invalid time'); 25 | 26 | let _token = null; 27 | try { _token = await account_alloc_managed(argument.time, argument.clear); } 28 | catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 29 | 30 | const _return = { 31 | access_token: _token, 32 | valid_time: argument.time 33 | }; 34 | 35 | resolve(_return); 36 | 37 | } catch (e) { 38 | 39 | if (e instanceof APIError) 40 | return reject(e); 41 | 42 | syslog.e(TAG, e.stack); 43 | return reject(new APIError(-233, 'unknown error occurred')); 44 | 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v3/arc/forward.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/arc/forward.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcapi_any from '../../../modules/arcfetch/arcapi.any'; 6 | import account_fromtoken from '../../../modules/account/fromtoken'; 7 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 8 | 9 | export default (argument: any, method: ArcFetchMethod, 10 | path: string, header: any, databody: any): Promise => { 11 | 12 | return new Promise(async (resolve, reject) => { 13 | 14 | try { 15 | 16 | // /arc/forward[/url/to/arcapi?foo=xx&bar=xx] 17 | // get token from GET parameters 18 | let _access_token: string | null = null; 19 | if (argument.token) { 20 | _access_token = argument.token; 21 | 22 | // delete access token from parameters 23 | delete argument.token; 24 | } 25 | 26 | // compatible with arcapi request format 27 | else if (header.authorization) { 28 | const _array = header.authorization.split(' '); 29 | if (_array.length == 2 && _array[0] == 'Bearer') 30 | _access_token = _array[1]; 31 | 32 | // delete access token from header 33 | delete header.authorization; 34 | } 35 | 36 | // validate the token 37 | if (!_access_token) 38 | throw new APIError(-1, 'invalid token'); 39 | 40 | // get account from token 41 | let _account = null; 42 | try { _account = await account_fromtoken(_access_token); } 43 | catch (e) { throw new APIError(-2, 'invalid token'); } 44 | 45 | // request arcapi 46 | let _return: any = {}; 47 | try { 48 | _return = await arcapi_any(_account, method, 49 | path + '?' + new URLSearchParams(argument), databody); 50 | } 51 | catch (e) { 52 | _return.error_code = e; 53 | _return.success = 'false'; 54 | } 55 | 56 | resolve(_return); 57 | 58 | } catch (e) { 59 | if (e instanceof APIError) 60 | return reject(e); 61 | 62 | syslog.e(TAG, e.stack); 63 | return reject(new APIError(-233, 'unknown error occurred')); 64 | } 65 | 66 | }); 67 | 68 | } 69 | -------------------------------------------------------------------------------- /source/publicapi/v3/arc/recycle.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/arc/recycle.js\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_recycle_managed from '../../../modules/account/recycle.managed'; 6 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 7 | 8 | export default (argument: any, method: ArcFetchMethod, 9 | path: string, header: any, databody: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /arc/recycle[token=xxx] 16 | // get token from GET parameters 17 | let _access_token = null; 18 | if (argument.token) { 19 | _access_token = argument.token; 20 | } 21 | 22 | // compatible with arcapi request format 23 | else if (header.authorization) { 24 | const _array = header.authorization.split(' '); 25 | if (_array.length == 2 && _array[0] == 'Bearer') 26 | _access_token = _array[1]; 27 | } 28 | 29 | // validate the token 30 | if (!_access_token) 31 | throw new APIError(-1, 'invalid token'); 32 | 33 | // recycle the account 34 | try { await account_recycle_managed(_access_token); } 35 | catch (e) { throw new APIError(-2, 'invalid token'); } 36 | 37 | resolve(null); 38 | 39 | } catch (e) { 40 | if (e instanceof APIError) 41 | return reject(e); 42 | 43 | syslog.e(TAG, e.stack); 44 | return reject(new APIError(-233, 'unknown error occurred')); 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v3/connect.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/connect.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import crypto from 'crypto'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | const _date: Date = new Date(); 12 | const _table = 'qwertyuiopasdfghjklzxcvbnm1234567890'; 13 | 14 | const _secret = 15 | _date.getUTCFullYear() + 'ori' + 16 | _date.getUTCMonth() + 'wol' + 17 | _date.getUTCDate() + 'oihs' + 18 | _date.getUTCDate() + 'otas'; 19 | 20 | // calculate md5 hash 21 | const _hash = crypto.createHash('md5').update(_secret).digest('hex'); 22 | 23 | let _result = ''; 24 | for (let i = 0; i < _hash.length; ++i) { 25 | _result += _table[_hash[i].charCodeAt(0) % 36]; 26 | } 27 | 28 | const _return = `${_result[1]}${_result[20]}${_result[4]}${_result[30]}${_result[2]}${_result[11]}${_result[23]}`; 29 | 30 | resolve({ key: _return }); 31 | }); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /source/publicapi/v3/random.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/random.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_random from '../../modules/database/database.arcsong.byrand'; 6 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 7 | import IDatabaseArcSong from '../../modules/database/interfaces/IDatabaseArcSong'; 8 | 9 | export default (argument: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /random?start=xx[&end=xx][&info=true] 16 | // check for request arguments 17 | argument.start = parseInt(argument.start); 18 | if (isNaN(argument.start)) argument.start = 0; 19 | argument.end = parseInt(argument.end); 20 | if (isNaN(argument.end)) argument.end = 0; 21 | 22 | // default range 23 | // 1 1+ 2 2+ ... 10 10+ 11 24 | if (argument.start == 0 && argument.end == 0) { 25 | argument.start = 2; 26 | argument.end = 23; 27 | } 28 | 29 | if (argument.start < 2 || argument.start > 23) 30 | throw new APIError(-1, 'invalid range of start'); 31 | if (argument.end != 0 && (argument.end < argument.start || argument.end > 23)) 32 | throw new APIError(-2, 'invalid range of end'); 33 | 34 | let _arc_song: any = null; 35 | let _arc_songinfo: IDatabaseArcSong = {}; 36 | let _return: any = {}; 37 | 38 | // select song 39 | try { 40 | _arc_song = await arcsong_random(argument.start, argument.end); 41 | _return.id = _arc_song.sid; 42 | _return.rating_class = _arc_song.rating_class; 43 | } catch (e) { throw new APIError(-3, 'internal error'); } 44 | 45 | // return song info if needed 46 | if (argument.info == 'true') { 47 | 48 | try { 49 | _arc_songinfo = await arcsong_bysongid(_arc_song.sid); 50 | _return.song_info = { 51 | id: _arc_songinfo.sid, 52 | title_localized: { 53 | en: _arc_songinfo.name_en, 54 | ja: _arc_songinfo.name_jp 55 | }, 56 | artist: _arc_songinfo.artist, 57 | bpm: _arc_songinfo.bpm, 58 | bpm_base: _arc_songinfo.bpm_base, 59 | set: _arc_songinfo.pakset, 60 | audioTimeSec: _arc_songinfo.time, 61 | side: _arc_songinfo.side, 62 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 63 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 64 | date: _arc_songinfo.date, 65 | difficulties: [ 66 | { 67 | ratingClass: 0, 68 | chartDesigner: _arc_songinfo.chart_designer_pst, 69 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 70 | rating: _arc_songinfo.difficultly_pst, 71 | ratingReal: _arc_songinfo.rating_pst, 72 | ratingPlus: (_arc_songinfo.difficultly_pst % 2 != 0), 73 | totalNotes: _arc_songinfo.notes_pst 74 | }, 75 | { 76 | ratingClass: 1, 77 | chartDesigner: _arc_songinfo.chart_designer_prs, 78 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 79 | rating: _arc_songinfo.difficultly_prs, 80 | ratingReal: _arc_songinfo.rating_prs, 81 | ratingPlus: (_arc_songinfo.difficultly_prs % 2 != 0), 82 | totalNotes: _arc_songinfo.notes_prs 83 | }, 84 | { 85 | ratingClass: 2, 86 | chartDesigner: _arc_songinfo.chart_designer_ftr, 87 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 88 | rating: _arc_songinfo.difficultly_ftr, 89 | ratingReal: _arc_songinfo.rating_ftr, 90 | ratingPlus: (_arc_songinfo.difficultly_ftr % 2 != 0), 91 | totalNotes: _arc_songinfo.notes_ftr 92 | } 93 | ] 94 | }; 95 | 96 | // append beyond rating 97 | if (_arc_songinfo.difficultly_byn != -1) { 98 | _return.song_info.difficulties[3] = { 99 | ratingClass: 3, 100 | chartDesigner: _arc_songinfo.chart_designer_byn, 101 | jacketDesigner: _arc_songinfo.jacket_designer_byn, 102 | rating: _arc_songinfo.difficultly_byn, 103 | ratingReal: _arc_songinfo.rating_byn, 104 | ratingPlus: (_arc_songinfo.difficultly_byn % 2 != 0), 105 | totalNotes: _arc_songinfo.notes_byn 106 | }; 107 | } 108 | 109 | // append rating 110 | _return.song_info.difficulties.map((element: any) => { 111 | element.rating = Math.floor(element.rating / 2); 112 | if (!element.ratingPlus) 113 | delete element.ratingPlus; 114 | return element; 115 | }); 116 | 117 | // remove empty field 118 | if (_return.song_info.title_localized.ja == '') 119 | delete _return.song_info.title_localized.ja; 120 | 121 | } catch (e) { throw new APIError(-4, 'internal error'); } 122 | 123 | } 124 | 125 | resolve(_return); 126 | 127 | } catch (e) { 128 | 129 | if (e instanceof APIError) 130 | return reject(e); 131 | 132 | syslog.e(TAG, e.stack); 133 | return reject(new APIError(-233, 'unknown error occurred')); 134 | 135 | } 136 | 137 | }); 138 | 139 | } 140 | -------------------------------------------------------------------------------- /source/publicapi/v3/songalias.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/songalias.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /songalias?songname=xxx 14 | // validate request arguments 15 | if (typeof argument.songname == 'undefined' || argument.songname == '') 16 | throw new APIError(-1, 'invalid songname'); 17 | 18 | let _arc_songid: Array; 19 | 20 | // query songid by any string 21 | try { 22 | _arc_songid = await arcsong_sid_byany(argument.songname); 23 | } catch (e) { throw new APIError(-2, 'no result'); } 24 | 25 | if (_arc_songid.length > 1) 26 | throw new APIError(-3, 'too many records'); 27 | 28 | resolve({ id: _arc_songid[0] }); 29 | 30 | } catch (e) { 31 | 32 | if (e instanceof APIError) 33 | return reject(e); 34 | 35 | syslog.e(TAG, e.stack); 36 | return reject(new APIError(-233, 'unknown error occurred')); 37 | 38 | } 39 | 40 | }); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /source/publicapi/v3/songinfo.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/songinfo.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 6 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 7 | 8 | export default (argument: any): Promise => { 9 | 10 | return new Promise(async (resolve, reject) => { 11 | 12 | try { 13 | 14 | // /songinfo?songname=xxx 15 | // check for request arguments 16 | if (typeof argument.songname == 'undefined' || argument.songname == '') 17 | throw new APIError(-1, 'invalid songname'); 18 | 19 | let _arc_songid = null; 20 | let _arc_songinfo = null; 21 | 22 | // query songid by any string 23 | try { 24 | _arc_songid = await arcsong_sid_byany(argument.songname); 25 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-2, 'this song is not recorded in the database'); } 26 | 27 | if (_arc_songid.length > 1) 28 | throw new APIError(-3, 'too many records'); 29 | _arc_songid = _arc_songid[0]; 30 | 31 | // get song information by songid 32 | try { 33 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 34 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-4, 'internal error'); } 35 | 36 | const _return: any = { 37 | id: _arc_songinfo.sid, 38 | title_localized: { 39 | en: _arc_songinfo.name_en, 40 | ja: _arc_songinfo.name_jp 41 | }, 42 | artist: _arc_songinfo.artist, 43 | bpm: _arc_songinfo.bpm, 44 | bpm_base: _arc_songinfo.bpm_base, 45 | set: _arc_songinfo.pakset, 46 | audioTimeSec: _arc_songinfo.time, 47 | side: _arc_songinfo.side, 48 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 49 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 50 | date: _arc_songinfo.date, 51 | difficulties: [ 52 | { 53 | ratingClass: 0, 54 | chartDesigner: _arc_songinfo.chart_designer_pst, 55 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 56 | rating: _arc_songinfo.difficultly_pst, 57 | ratingReal: _arc_songinfo.rating_pst, 58 | ratingPlus: (_arc_songinfo.difficultly_pst % 2 != 0), 59 | totalNotes: _arc_songinfo.notes_pst 60 | }, 61 | { 62 | ratingClass: 1, 63 | chartDesigner: _arc_songinfo.chart_designer_prs, 64 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 65 | rating: _arc_songinfo.difficultly_prs, 66 | ratingReal: _arc_songinfo.rating_prs, 67 | ratingPlus: (_arc_songinfo.difficultly_prs % 2 != 0), 68 | totalNotes: _arc_songinfo.notes_prs 69 | }, 70 | { 71 | ratingClass: 2, 72 | chartDesigner: _arc_songinfo.chart_designer_ftr, 73 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 74 | rating: _arc_songinfo.difficultly_ftr, 75 | ratingReal: _arc_songinfo.rating_ftr, 76 | ratingPlus: (_arc_songinfo.difficultly_ftr % 2 != 0), 77 | totalNotes: _arc_songinfo.notes_ftr 78 | } 79 | ] 80 | }; 81 | 82 | // append beyond rating 83 | if (_arc_songinfo.difficultly_byn != -1) { 84 | _return.difficulties[3] = { 85 | ratingClass: 3, 86 | chartDesigner: _arc_songinfo.chart_designer_byn, 87 | jacketDesigner: _arc_songinfo.jacket_designer_byn, 88 | rating: _arc_songinfo.difficultly_byn, 89 | ratingReal: _arc_songinfo.rating_byn, 90 | ratingPlus: (_arc_songinfo.difficultly_byn % 2 != 0), 91 | totalNotes: _arc_songinfo.notes_byn 92 | }; 93 | } 94 | 95 | // append rating 96 | _return.difficulties.map((element: any) => { 97 | element.rating = Math.floor(element.rating / 2); 98 | if (!element.ratingPlus) 99 | delete element.ratingPlus; 100 | return element; 101 | }); 102 | 103 | // remove empty field 104 | if (_return.title_localized.ja == '') 105 | delete _return.title_localized.ja; 106 | 107 | resolve(_return); 108 | 109 | } catch (e) { 110 | 111 | if (e instanceof APIError) 112 | return reject(e); 113 | 114 | syslog.e(TAG, e.stack); 115 | return reject(new APIError(-233, 'unknown error occurred')); 116 | 117 | } 118 | 119 | }); 120 | 121 | } 122 | -------------------------------------------------------------------------------- /source/publicapi/v3/update.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/update.ts\t'; 2 | 3 | import fetch from 'node-fetch'; 4 | import syslog from '../../modules/syslog/syslog'; 5 | import APIError from '../../modules/apierror/apierror'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | try { 11 | 12 | // request remote api 13 | await fetch("https://webapi.lowiro.com/webapi/serve/static/bin/arcaea/apk") 14 | .then((response) => response.text()) 15 | .then((rawdata) => { 16 | try { 17 | return JSON.parse(rawdata); 18 | } catch (e) { throw new APIError(-1, 'service unavailable'); } 19 | }) 20 | 21 | .then((root) => { 22 | 23 | if (root.success != true) 24 | throw new APIError(-2, "fetch latest release failed"); 25 | 26 | resolve({ 27 | "url": root.value.url, 28 | "version": root.value.version 29 | }); 30 | 31 | }) 32 | 33 | } catch (e) { 34 | 35 | if (e instanceof APIError) 36 | return reject(e); 37 | 38 | syslog.e(TAG, e.stack); 39 | return reject(new APIError(-233, 'unknown error occurred')); 40 | } 41 | }); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /source/publicapi/v3/userbest.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/userbest.ts\t'; 2 | 3 | import Utils from '../../corefunc/utils'; 4 | import syslog from '../../modules/syslog/syslog'; 5 | import APIError from '../../modules/apierror/apierror'; 6 | 7 | import arcapi_friend_add from '../../modules/arcfetch/arcapi.friend.add'; 8 | import arcapi_friend_clear from '../../modules/arcfetch/arcapi.friend.clear'; 9 | import arcapi_rank_friend from '../../modules/arcfetch/arcapi.rank.friend'; 10 | import account_alloc from '../../modules/account/alloc'; 11 | import account_recycle from '../../modules/account/recycle'; 12 | 13 | import arcsong_bysongid from '../../modules/database/database.arcsong.bysongid'; 14 | import arcsong_sid_byany from '../../modules/database/database.arcsong.sid.byany'; 15 | import arcrecord_update from '../../modules/database/database.arcrecord.update'; 16 | import arcplayer_update from '../../modules/database/database.arcplayer.update'; 17 | 18 | export default (argument: any): Promise => { 19 | 20 | return new Promise(async (resolve, reject): Promise => { 21 | 22 | try { 23 | 24 | // /userbest?usercode=xxx&songname=xxx&difficulty=x 25 | // validate request arguments 26 | if (typeof argument.usercode == 'undefined' || argument.usercode == '') 27 | throw new APIError(-1, 'invalid usercode'); 28 | if (typeof argument.songname == 'undefined' || argument.songname == '') 29 | throw new APIError(-2, 'invalid songname'); 30 | if (typeof argument.difficulty == 'undefined' || argument.difficulty == '') 31 | throw new APIError(-3, 'invalid difficulty'); 32 | 33 | let _arc_difficulty: any = Utils.arcMapDiffFormat(argument.difficulty, 0); 34 | if (!_arc_difficulty) 35 | throw new APIError(-4, 'invalid difficulty'); 36 | 37 | let _arc_account: any = null; 38 | let _arc_songid: any = null; 39 | let _arc_songinfo: any = null; 40 | let _arc_friendlist: any = null; 41 | let _arc_friend: any = null; 42 | let _arc_ranklist: any = null; 43 | let _arc_rank: any = null; 44 | 45 | // check songid valid 46 | try { 47 | _arc_songid = await arcsong_sid_byany(argument.songname); 48 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-5, 'this song is not recorded in the database'); } 49 | 50 | if (_arc_songid.length > 1) 51 | throw new APIError(-6, 'too many records'); 52 | _arc_songid = _arc_songid[0]; 53 | 54 | try { 55 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 56 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-7, 'internal error'); } 57 | 58 | // check for beyond is existed 59 | if (_arc_songinfo.difficultly_byn == -1 && _arc_difficulty == 3) { 60 | throw new APIError(-8, 'this song has no beyond level'); 61 | } 62 | 63 | // request an arc account 64 | try { 65 | _arc_account = await account_alloc(); 66 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-9, 'allocate an arc account failed'); } 67 | 68 | try { 69 | 70 | // clear friend list 71 | try { 72 | await arcapi_friend_clear(_arc_account); 73 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-10, 'clear friend list failed'); } 74 | 75 | // add friend 76 | try { 77 | _arc_friendlist = await arcapi_friend_add(_arc_account, argument.usercode); 78 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-11, 'add friend failed'); } 79 | 80 | // length must be 1 81 | if (_arc_friendlist.length != 1) 82 | throw new APIError(-12, 'internal error occurred'); 83 | 84 | // result of arcapi not include 85 | // user code anymore since v6 86 | _arc_friend = _arc_friendlist[0]; 87 | _arc_friend.code = argument.usercode; 88 | 89 | // get rank result 90 | try { 91 | _arc_ranklist = await arcapi_rank_friend(_arc_account, _arc_songid, _arc_difficulty, 0, 1); 92 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-13, 'internal error occurred'); } 93 | 94 | if (!_arc_ranklist.length) 95 | throw new APIError(-14, 'not played yet'); 96 | 97 | // calculate song rating 98 | _arc_rank = _arc_ranklist[0]; 99 | _arc_rank.rating = Utils.arcCalcSongRating( 100 | _arc_rank.score, 101 | _arc_songinfo[`rating_${Utils.arcMapDiffFormat(argument.difficulty, 1)}`] 102 | ); 103 | 104 | const _return = _arc_rank; 105 | delete _return.name; 106 | delete _return.user_id; 107 | 108 | // result of arcapi not include 109 | // user code anymore since v6 110 | delete _return.user_code; 111 | 112 | resolve(_return); 113 | 114 | } catch (e) { 115 | // recycle account when any error occurred 116 | if (_arc_account) 117 | account_recycle(_arc_account); 118 | // re-throw the error 119 | throw e; 120 | } 121 | 122 | // release account 123 | account_recycle(_arc_account); 124 | 125 | // update user info and recently played 126 | arcplayer_update(_arc_friend) 127 | .catch((error) => { syslog.e(error.stack); }); 128 | 129 | // insert new record into database 130 | if (_arc_friend.recent_score.length) 131 | arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 132 | .catch((error) => { syslog.e(error.stack); }); 133 | 134 | // insert new record into database 135 | arcrecord_update(_arc_friend.user_id, _arc_rank) 136 | .catch((error) => { syslog.e(error.stack); }); 137 | 138 | } catch (e) { 139 | 140 | if (e instanceof APIError) 141 | return reject(e); 142 | 143 | syslog.e(TAG, e.stack); 144 | return reject(new APIError(-233, 'unknown error occurred')); 145 | 146 | } 147 | 148 | }); 149 | 150 | } 151 | -------------------------------------------------------------------------------- /source/publicapi/v3/userinfo.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v3/userinfo.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | 6 | import arcapi_friend_add from '../../modules/arcfetch/arcapi.friend.add'; 7 | import arcapi_friend_clear from '../../modules/arcfetch/arcapi.friend.clear'; 8 | import account_alloc from '../../modules/account/alloc'; 9 | import account_recycle from '../../modules/account/recycle'; 10 | 11 | import arcrecord_update from '../../modules/database/database.arcrecord.update'; 12 | import arcplayer_update from '../../modules/database/database.arcplayer.update'; 13 | 14 | import IArcAccount from '../../modules/arcfetch/interfaces/IArcAccount'; 15 | import IArcPlayer from '../../modules/arcfetch/interfaces/IArcPlayer'; 16 | 17 | export default (argument: any): Promise => { 18 | 19 | return new Promise(async (resolve, reject) => { 20 | 21 | try { 22 | 23 | // /userinfo?usercode=xxx[&recent=true] 24 | // check for request arguments 25 | if (typeof argument.usercode == 'undefined' || argument.usercode == '') 26 | throw new APIError(-1, 'invalid usercode'); 27 | 28 | let _arc_account: IArcAccount | null = null; 29 | let _arc_friendlist: Array | null = null; 30 | let _arc_friend: IArcPlayer | null = null; 31 | 32 | // request an arc account 33 | try { 34 | _arc_account = await account_alloc(); 35 | } catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 36 | 37 | try { 38 | 39 | // clear friend list 40 | try { 41 | await arcapi_friend_clear(_arc_account); 42 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-3, 'clear friend list failed'); } 43 | 44 | // add friend 45 | try { 46 | _arc_friendlist = await arcapi_friend_add(_arc_account, argument.usercode); 47 | } catch (e) { throw new APIError(-4, 'add friend failed'); } 48 | 49 | // length must be 1 50 | if (_arc_friendlist.length != 1) 51 | throw new APIError(-5, 'internal error occurred'); 52 | 53 | // result of arcapi not include 54 | // user code anymore since v6 55 | _arc_friend = _arc_friendlist[0]; 56 | _arc_friend.code = argument.usercode; 57 | 58 | // must do deep copy 59 | const _return = JSON.parse(JSON.stringify(_arc_friend)); 60 | _return.recent_score = _arc_friend.recent_score[0]; 61 | 62 | // delete field if needn't recent data or not played yet 63 | if (argument.recent != 'true' || !_arc_friend.recent_score.length) 64 | delete _return.recent_score; 65 | 66 | // delete usercode field 67 | delete _return.code; 68 | 69 | resolve(_return); 70 | 71 | } catch (e) { 72 | // recycle account when any error occurred 73 | if (_arc_account) 74 | account_recycle(_arc_account); 75 | // re-throw the error 76 | throw e; 77 | } 78 | 79 | // release account 80 | account_recycle(_arc_account); 81 | 82 | // update user info and recently played 83 | arcplayer_update(_arc_friend) 84 | .catch((error) => { syslog.e(error.stack); }); 85 | 86 | // insert new record into database 87 | if (_arc_friend.recent_score.length) 88 | arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 89 | .catch((error) => { syslog.e(error.stack); }); 90 | 91 | } catch (e) { 92 | 93 | if (e instanceof APIError) 94 | return reject(e); 95 | 96 | syslog.e(TAG, e.stack); 97 | return reject(new APIError(-233, 'unknown error occurred')); 98 | 99 | } 100 | 101 | }); 102 | 103 | } 104 | -------------------------------------------------------------------------------- /source/publicapi/v4/batch.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/batch.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import Utils from '../../corefunc/utils'; 5 | import APIError from '../../modules/apierror/apierror'; 6 | import safeEval from 'safe-eval'; 7 | 8 | export default (argument: any): Promise => { 9 | 10 | return new Promise(async (resolve, reject) => { 11 | 12 | // /batch?calls= 13 | // [ 14 | // { 15 | // id: 0, 16 | // bind: {"$sid": "recent_score[0].song_id"} 17 | // endpoint: "user/info?usercode=000000001" 18 | // }, 19 | // { 20 | // id: 1, 21 | // endpoint: "user/song/info?songname=$sid" 22 | // }, 23 | // { 24 | // id: 2, 25 | // endpoint: "user/song/alias?songid=$sid" 26 | // } 27 | // ] 28 | 29 | try { 30 | 31 | // validate request arguments 32 | if (typeof argument.calls == 'undefined' || argument.calls == '') 33 | throw new APIError(-1, 'invalid endpoints'); 34 | 35 | // try parse endpoints 36 | let _endpoints: any = {}; 37 | let _return: any = []; 38 | let _vm_vartable: any = {}; 39 | let _vm_reftable: any = {}; 40 | let _vm_resultbox: any = {}; 41 | 42 | try { 43 | _endpoints = JSON.parse(argument.calls); 44 | } catch (e) { syslog.e(e.stack); throw new APIError(-2, 'invalid endpoints'); } 45 | 46 | // check if endpoints is an array 47 | if (!(_endpoints instanceof Array)) 48 | throw new APIError(-3, 'invalid endpoints'); 49 | 50 | // check limitation 51 | if (_endpoints.length > BOTARCAPI_BATCH_ENDPOINTS_MAX) 52 | throw new APIError(-4, 'too many endpoints requested'); 53 | 54 | // collect bind variables 55 | _endpoints.forEach((element) => { 56 | 57 | if (!element.bind) 58 | return; 59 | 60 | // check statement 61 | if (!Utils.checkBindStatement(element.bind)) 62 | throw new APIError(-5, 'invalid bind variables'); 63 | 64 | // initialize variables 65 | Object.keys(element.bind).forEach(varname => { 66 | 67 | // check variables 68 | if (!varname.startsWith('$')) 69 | throw new APIError(-6, 'bind variables must start with character $'); 70 | 71 | _vm_vartable = { ..._vm_vartable, ...element.bind }; 72 | _vm_reftable = { ..._vm_reftable, ...{ [varname]: `result${element.id}` } }; 73 | 74 | }); 75 | }); 76 | 77 | // execute the batch operations 78 | for (let i = 0; i < _endpoints.length; ++i) { 79 | 80 | let _endret: any = {}; 81 | 82 | try { 83 | _endret = await do_single_operation 84 | (_endpoints[i], _vm_vartable, _vm_reftable, _vm_resultbox); 85 | } catch (e) { 86 | if (e instanceof APIError) 87 | _endret = { status: e.status, message: e.notify } 88 | else { 89 | _endret = { status: -233, message: 'unknown error occurred in endpoint' }; 90 | syslog.e(e.stack); 91 | } 92 | 93 | } 94 | 95 | // build results 96 | _return.push({ 97 | id: _endpoints[i].id, 98 | result: _endret 99 | }); 100 | 101 | } 102 | 103 | resolve(_return); 104 | 105 | } catch (e) { 106 | 107 | if (e instanceof APIError) 108 | return reject(e); 109 | 110 | syslog.e(TAG, e.stack); 111 | return reject(new APIError(-233, 'unknown error occurred')); 112 | 113 | } 114 | 115 | }); 116 | }; 117 | 118 | const do_single_operation = 119 | async (endpoint: any, vartable: any, reftable: any, resultbox: any) => { 120 | 121 | // apply the variable for this endpoint 122 | const regexp = /=(\$\S*?)&/g; 123 | let donext = false; 124 | let result: any = {}; 125 | 126 | do { 127 | result = regexp.exec(endpoint.endpoint + '&'); 128 | donext = !(!result); 129 | 130 | if (donext) { 131 | 132 | if (result.length != 2) 133 | continue; 134 | 135 | const varname = result[1]; 136 | const varexpr = vartable[varname]; 137 | let val = {}; 138 | 139 | // eval the value 140 | try { 141 | val = safeEval(varexpr, resultbox[reftable[varname]]); 142 | } catch (e) { throw new APIError(-1, 'run expression failed'); } 143 | 144 | // replace the variable 145 | endpoint.endpoint = endpoint.endpoint.replace(varname, val); 146 | } 147 | } while (donext) 148 | 149 | // teardown params 150 | const _url = new URL(`http://example.com/${endpoint.endpoint}`); 151 | let _path = _url.pathname; 152 | const _arguments = Utils.httpGetAllParams(_url.searchParams); 153 | 154 | let _entry: any = {}; 155 | let _return: any = {}; 156 | 157 | // refuse request batch endpoint 158 | if (_path == 'batch') 159 | throw new APIError(-2, 'batch api cannot include another batch endpoint'); 160 | 161 | // load api endpoint 162 | try { 163 | 164 | // supports forward api 165 | // batch api only supported GET method 166 | if (new RegExp(/^\/forward\/forward\//).test(_path)) { 167 | _path = _path.replace('/forward/forward/', ''); 168 | _entry = await import(`./forward/forward`); 169 | } else _entry = await import(`./${_path}.js`); 170 | 171 | _entry = _entry.default; 172 | 173 | } catch (e) { throw new APIError(-3, 'endpoint not found'); } 174 | 175 | // invoke method 176 | await _entry(_arguments, 'GET', _path) 177 | .then((result: any) => { 178 | _return.status = 0; 179 | _return.content = result; 180 | 181 | // apply this result box 182 | resultbox[`result${endpoint.id}`] = _return.content; 183 | }) 184 | .catch((error: APIError) => { 185 | _return.status = error.status; 186 | _return.message = error.notify; 187 | }); 188 | 189 | return _return; 190 | } 191 | -------------------------------------------------------------------------------- /source/publicapi/v4/connect.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/connect.ts\t'; 2 | 3 | import syslog from '../../modules/syslog/syslog'; 4 | import APIError from '../../modules/apierror/apierror'; 5 | import crypto from 'crypto'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | const _date: Date = new Date(); 12 | const _table = 'qwertyuiopasdfghjklzxcvbnm1234567890'; 13 | 14 | const _secret = 15 | _date.getUTCFullYear() + 'ori' + 16 | _date.getUTCMonth() + 'wol' + 17 | _date.getUTCDate() + 'oihs' + 18 | _date.getUTCDate() + 'otas'; 19 | 20 | // calculate md5 hash 21 | const _hash = crypto.createHash('md5').update(_secret).digest('hex'); 22 | 23 | let _result = ''; 24 | for (let i = 0; i < _hash.length; ++i) { 25 | _result += _table[_hash[i].charCodeAt(0) % 36]; 26 | } 27 | 28 | const _return = `${_result[1]}${_result[20]}${_result[4]}${_result[30]}${_result[2]}${_result[11]}${_result[23]}`; 29 | 30 | resolve({ key: _return }); 31 | }); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /source/publicapi/v4/forward/alloc.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/forward/alloc.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_alloc_managed from '../../../modules/account/alloc.managed'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /forward/alloc[?time=xxx][&clear=true] 14 | // validate the request arguments 15 | argument.time = parseInt(argument.time); 16 | argument.clear = argument.clear == 'true' ? true : false; 17 | 18 | // check time 19 | if (isNaN(argument.time) || argument.time == 0) 20 | argument.time = BOTARCAPI_FORWARD_TIMESEC_DEFAULT; 21 | 22 | // clamp the range 23 | if (argument.time < BOTARCAPI_FORWARD_TIMESEC_DEFAULT 24 | || argument.time > BOTARCAPI_FORWARD_TIMESEC_MAX) 25 | throw new APIError(-1, 'invalid time'); 26 | 27 | let _token = null; 28 | try { _token = await account_alloc_managed(argument.time, argument.clear); } 29 | catch (e) { throw new APIError(-2, 'allocate an arc account failed'); } 30 | 31 | const _return = { 32 | access_token: _token, 33 | valid_time: argument.time 34 | }; 35 | 36 | resolve(_return); 37 | 38 | } catch (e) { 39 | 40 | if (e instanceof APIError) 41 | return reject(e); 42 | 43 | syslog.e(TAG, e.stack); 44 | return reject(new APIError(-233, 'unknown error occurred')); 45 | 46 | } 47 | 48 | }); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /source/publicapi/v4/forward/feed.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/forward/feed.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_feed_managed from '../../../modules/account/feed.managed'; 6 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 7 | 8 | export default (argument: any, method: ArcFetchMethod, 9 | path: string, header: any, databody: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /forward/feed?token=xxx 16 | // get token from GET parameters 17 | let _access_token = null; 18 | if (argument.token) { 19 | _access_token = argument.token; 20 | } 21 | 22 | // compatible with arcapi request format 23 | else if (header.authorization) { 24 | const _array = header.authorization.split(' '); 25 | if (_array.length == 2 && _array[0] == 'Bearer') 26 | _access_token = _array[1]; 27 | } 28 | 29 | // validate the token 30 | if (!_access_token) 31 | throw new APIError(-1, 'invalid token'); 32 | 33 | let _time = 0; 34 | 35 | // recycle the account 36 | try { _time = await account_feed_managed(_access_token); } 37 | catch (e) { throw new APIError(-2, 'feed token failed'); } 38 | 39 | resolve({ valid_time: _time }); 40 | 41 | } catch (e) { 42 | if (e instanceof APIError) 43 | return reject(e); 44 | 45 | syslog.e(TAG, e.stack); 46 | return reject(new APIError(-233, 'unknown error occurred')); 47 | } 48 | 49 | }); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /source/publicapi/v4/forward/forward.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/arc/forward.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcapi_any from '../../../modules/arcfetch/arcapi.any'; 6 | import account_fromtoken from '../../../modules/account/fromtoken'; 7 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 8 | 9 | export default (argument: any, method: ArcFetchMethod, 10 | path: string, header: any, databody: any): Promise => { 11 | 12 | return new Promise(async (resolve, reject) => { 13 | 14 | try { 15 | 16 | // /forward/forward[/url/to/arcapi?foo=xx&bar=xx] 17 | // get token from GET parameters 18 | let _access_token: string | null = null; 19 | if (argument.token) { 20 | _access_token = argument.token; 21 | 22 | // delete access token from parameters 23 | delete argument.token; 24 | } 25 | 26 | // compatible with arcapi request format 27 | else if (header.authorization) { 28 | const _array = header.authorization.split(' '); 29 | if (_array.length == 2 && _array[0] == 'Bearer') 30 | _access_token = _array[1]; 31 | 32 | // delete access token from header 33 | delete header.authorization; 34 | } 35 | 36 | // validate the token 37 | if (!_access_token) 38 | throw new APIError(-1, 'invalid token'); 39 | 40 | // get account from token 41 | let _account = null; 42 | try { _account = await account_fromtoken(_access_token); } 43 | catch (e) { throw new APIError(-2, 'invalid token'); } 44 | 45 | // check forward whitelist 46 | let pass = false; 47 | if (BOTARCAPI_FORWARD_WHITELIST 48 | && BOTARCAPI_FORWARD_WHITELIST.length > 0) { 49 | 50 | for (const _ in BOTARCAPI_FORWARD_WHITELIST) { 51 | if (new RegExp(BOTARCAPI_FORWARD_WHITELIST[_]).test(path)) { 52 | pass = true; break; 53 | } 54 | } 55 | 56 | if (!pass) 57 | throw new APIError(-3, 'illegal forward url'); 58 | } 59 | 60 | // request arcapi 61 | let _return: any = {}; 62 | try { 63 | _return = await arcapi_any(_account, method, 64 | path + '?' + new URLSearchParams(argument), databody); 65 | } 66 | catch (e) { 67 | _return.error_code = e; 68 | _return.success = 'false'; 69 | } 70 | 71 | resolve(_return); 72 | 73 | } catch (e) { 74 | if (e instanceof APIError) 75 | return reject(e); 76 | 77 | syslog.e(TAG, e.stack); 78 | return reject(new APIError(-233, 'unknown error occurred')); 79 | } 80 | 81 | }); 82 | 83 | } 84 | -------------------------------------------------------------------------------- /source/publicapi/v4/forward/recycle.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/forward/recycle.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import account_recycle_managed from '../../../modules/account/recycle.managed'; 6 | import { ArcFetchMethod } from '../../../modules/arcfetch/arcfetch'; 7 | 8 | export default (argument: any, method: ArcFetchMethod, 9 | path: string, header: any, databody: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /forward/recycle?token=xxx 16 | // get token from GET parameters 17 | let _access_token = null; 18 | if (argument.token) { 19 | _access_token = argument.token; 20 | } 21 | 22 | // compatible with arcapi request format 23 | else if (header.authorization) { 24 | const _array = header.authorization.split(' '); 25 | if (_array.length == 2 && _array[0] == 'Bearer') 26 | _access_token = _array[1]; 27 | } 28 | 29 | // validate the token 30 | if (!_access_token) 31 | throw new APIError(-1, 'invalid token'); 32 | 33 | // recycle the account 34 | try { await account_recycle_managed(_access_token); } 35 | catch (e) { throw new APIError(-2, 'invalid token'); } 36 | 37 | resolve(null); 38 | 39 | } catch (e) { 40 | if (e instanceof APIError) 41 | return reject(e); 42 | 43 | syslog.e(TAG, e.stack); 44 | return reject(new APIError(-233, 'unknown error occurred')); 45 | } 46 | 47 | }); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /source/publicapi/v4/song/alias.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/song/alias.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import IDatabaseArcSongAlias from '../../../modules/database/interfaces/IDatabaseArcSongAlias'; 6 | 7 | import arcsong_alias_bysid from '../../../modules/database/database.arcsong.alias.byid'; 8 | 9 | export default (argument: any): Promise => { 10 | 11 | return new Promise(async (resolve, reject) => { 12 | 13 | try { 14 | 15 | // /song/alias?songid=xxx 16 | // validate request arguments 17 | if (typeof argument.songid == 'undefined' || argument.songid == '') 18 | throw new APIError(-1, 'invalid song id'); 19 | 20 | let _arc_alias: IDatabaseArcSongAlias[]; 21 | 22 | // search song alias with song id 23 | try { 24 | _arc_alias = await arcsong_alias_bysid(argument.songid); 25 | } catch (e) { throw new APIError(-2, 'internal error'); } 26 | 27 | if (_arc_alias.length == 0) 28 | throw new APIError(-3, 'no result'); 29 | 30 | resolve({ alias: _arc_alias.map((element) => { return element.alias; }) }); 31 | 32 | } catch (e) { 33 | 34 | if (e instanceof APIError) 35 | return reject(e); 36 | 37 | syslog.e(TAG, e.stack); 38 | return reject(new APIError(-233, 'unknown error occurred')); 39 | 40 | } 41 | 42 | }); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /source/publicapi/v4/song/id.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/song/id.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../../modules/database/database.arcsong.sid.byany'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /song/info?songname=xxx 14 | // check for request arguments 15 | if (typeof argument.songname == 'undefined' || argument.songname == '') 16 | throw new APIError(-1, 'invalid songname'); 17 | 18 | let _arc_songid = null; 19 | 20 | // query songid by any string 21 | try { 22 | _arc_songid = await arcsong_sid_byany(argument.songname); 23 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-2, 'this song is not recorded in the database'); } 24 | 25 | if (_arc_songid.length > 1) 26 | throw new APIError(-3, 'too many records'); 27 | 28 | resolve({ id: _arc_songid[0] }); 29 | 30 | } catch (e) { 31 | 32 | if (e instanceof APIError) 33 | return reject(e); 34 | 35 | syslog.e(TAG, e.stack); 36 | return reject(new APIError(-233, 'unknown error occurred')); 37 | 38 | } 39 | 40 | }); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /source/publicapi/v4/song/info.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/song/info.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcsong_sid_byany from '../../../modules/database/database.arcsong.sid.byany'; 6 | import arcsong_bysongid from '../../../modules/database/database.arcsong.bysongid'; 7 | 8 | export default (argument: any): Promise => { 9 | 10 | return new Promise(async (resolve, reject) => { 11 | 12 | try { 13 | 14 | // /song/info?songname=xxx 15 | // check for request arguments 16 | if (typeof argument.songname == 'undefined' || argument.songname == '') 17 | throw new APIError(-1, 'invalid songname'); 18 | 19 | let _arc_songid = null; 20 | let _arc_songinfo = null; 21 | 22 | // query songid by any string 23 | try { 24 | _arc_songid = await arcsong_sid_byany(argument.songname); 25 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-2, 'this song is not recorded in the database'); } 26 | 27 | if (_arc_songid.length > 1) 28 | throw new APIError(-3, 'too many records'); 29 | _arc_songid = _arc_songid[0]; 30 | 31 | // get song information by songid 32 | try { 33 | _arc_songinfo = await arcsong_bysongid(_arc_songid); 34 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-4, 'internal error'); } 35 | 36 | const _return: any = { 37 | id: _arc_songinfo.sid, 38 | title_localized: { 39 | en: _arc_songinfo.name_en, 40 | ja: _arc_songinfo.name_jp 41 | }, 42 | artist: _arc_songinfo.artist, 43 | bpm: _arc_songinfo.bpm, 44 | bpm_base: _arc_songinfo.bpm_base, 45 | set: _arc_songinfo.pakset, 46 | audioTimeSec: _arc_songinfo.time, 47 | side: _arc_songinfo.side, 48 | remote_dl: _arc_songinfo.remote_download == 'true' ? true : false, 49 | world_unlock: _arc_songinfo.world_unlock == 'true' ? true : false, 50 | date: _arc_songinfo.date, 51 | version: _arc_songinfo.version, 52 | difficulties: [ 53 | { 54 | ratingClass: 0, 55 | chartDesigner: _arc_songinfo.chart_designer_pst, 56 | jacketDesigner: _arc_songinfo.jacket_designer_pst, 57 | jacketOverride: _arc_songinfo.jacket_override_pst == 'true' ? true : false, 58 | rating: _arc_songinfo.difficultly_pst, 59 | ratingReal: _arc_songinfo.rating_pst, 60 | ratingPlus: (_arc_songinfo.difficultly_pst % 2 != 0), 61 | totalNotes: _arc_songinfo.notes_pst 62 | }, 63 | { 64 | ratingClass: 1, 65 | chartDesigner: _arc_songinfo.chart_designer_prs, 66 | jacketDesigner: _arc_songinfo.jacket_designer_prs, 67 | jacketOverride: _arc_songinfo.jacket_override_prs == 'true' ? true : false, 68 | rating: _arc_songinfo.difficultly_prs, 69 | ratingReal: _arc_songinfo.rating_prs, 70 | ratingPlus: (_arc_songinfo.difficultly_prs % 2 != 0), 71 | totalNotes: _arc_songinfo.notes_prs 72 | }, 73 | { 74 | ratingClass: 2, 75 | chartDesigner: _arc_songinfo.chart_designer_ftr, 76 | jacketDesigner: _arc_songinfo.jacket_designer_ftr, 77 | jacketOverride: _arc_songinfo.jacket_override_ftr == 'true' ? true : false, 78 | rating: _arc_songinfo.difficultly_ftr, 79 | ratingReal: _arc_songinfo.rating_ftr, 80 | ratingPlus: (_arc_songinfo.difficultly_ftr % 2 != 0), 81 | totalNotes: _arc_songinfo.notes_ftr 82 | } 83 | ] 84 | }; 85 | 86 | // append beyond rating 87 | if (_arc_songinfo.difficultly_byn != -1) { 88 | _return.difficulties[3] = { 89 | ratingClass: 3, 90 | chartDesigner: _arc_songinfo.chart_designer_byn, 91 | jacketDesigner: _arc_songinfo.jacket_designer_byn, 92 | jacketOverride: _arc_songinfo.jacket_override_byn == 'true' ? true : false, 93 | rating: _arc_songinfo.difficultly_byn, 94 | ratingReal: _arc_songinfo.rating_byn, 95 | ratingPlus: (_arc_songinfo.difficultly_byn % 2 != 0), 96 | totalNotes: _arc_songinfo.notes_byn 97 | }; 98 | } 99 | 100 | // append rating and remove empty field 101 | if (_return.title_localized.ja == '') 102 | delete _return.title_localized.ja; 103 | 104 | _return.difficulties.map((element: any) => { 105 | element.rating = Math.floor(element.rating / 2); 106 | if (!element.ratingPlus) 107 | delete element.ratingPlus; 108 | if (!element.jacketOverride) 109 | delete element.jacketOverride; 110 | return element; 111 | }); 112 | 113 | resolve(_return); 114 | 115 | } catch (e) { 116 | 117 | if (e instanceof APIError) 118 | return reject(e); 119 | 120 | syslog.e(TAG, e.stack); 121 | return reject(new APIError(-233, 'unknown error occurred')); 122 | 123 | } 124 | 125 | }); 126 | 127 | } 128 | -------------------------------------------------------------------------------- /source/publicapi/v4/song/random.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/song/random.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcsong_random from '../../../modules/database/database.arcsong.byrand'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | 11 | try { 12 | 13 | // /song/random?start=xx[&end=xx] 14 | // check for request arguments 15 | argument.start = parseInt(argument.start); 16 | if (isNaN(argument.start)) argument.start = 0; 17 | argument.end = parseInt(argument.end); 18 | if (isNaN(argument.end)) argument.end = 0; 19 | 20 | // default range 21 | // 1 1+ 2 2+ ... 10 10+ 11 22 | if (argument.start == 0 && argument.end == 0) { 23 | argument.start = 2; 24 | argument.end = 23; 25 | } 26 | 27 | if (argument.start < 2 || argument.start > 23) 28 | throw new APIError(-1, 'invalid range of start'); 29 | if (argument.end != 0 && (argument.end < argument.start || argument.end > 23)) 30 | throw new APIError(-2, 'invalid range of end'); 31 | 32 | let _arc_song: any = null; 33 | let _return: any = {}; 34 | 35 | // select song 36 | try { 37 | _arc_song = await arcsong_random(argument.start, argument.end); 38 | _return.id = _arc_song.sid; 39 | _return.rating_class = _arc_song.rating_class; 40 | } catch (e) { throw new APIError(-3, 'internal error'); } 41 | 42 | resolve(_return); 43 | 44 | } catch (e) { 45 | 46 | if (e instanceof APIError) 47 | return reject(e); 48 | 49 | syslog.e(TAG, e.stack); 50 | return reject(new APIError(-233, 'unknown error occurred')); 51 | 52 | } 53 | 54 | }); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /source/publicapi/v4/song/rating.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/song/rating.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | import arcsong_bysongrating from '../../../modules/database/database.arcsong.byrating'; 6 | import IDatabaseArcSongChart from "../../../modules/database/interfaces/IDatabaseArcSongChart"; 7 | 8 | export default (argument: any): Promise => { 9 | 10 | return new Promise(async (resolve, reject) => { 11 | 12 | try { 13 | 14 | // /song/rating?start=xxx[&end=xxx] 15 | // check for request arguments 16 | argument.start = parseFloat(argument.start); 17 | if (isNaN(argument.start)) 18 | throw new APIError(-1, 'invalid range start of the rating'); 19 | 20 | if (argument.start <= 0 || argument.start > 15) 21 | throw new APIError(-2, 'invalid range start of the rating'); 22 | 23 | if (isNaN(parseFloat(argument.end))) 24 | argument.end = argument.start; 25 | else argument.end = parseFloat(argument.end); 26 | 27 | if (argument.end <= 0 || argument.end > 15) 28 | throw new APIError(-3, 'invalid range end of the rating'); 29 | 30 | if (argument.end < argument.start) 31 | throw new APIError(-4, 'range of rating end smaller than its start'); 32 | 33 | let _arc_charts: IDatabaseArcSongChart[] = []; 34 | 35 | try { 36 | _arc_charts = await arcsong_bysongrating(argument.start, argument.end); 37 | 38 | } catch (e) { throw new APIError(-5, 'unknown error occurred'); } 39 | 40 | // make return results 41 | resolve({ 42 | rating: _arc_charts.map((element) => { 43 | return { 44 | sid: element.sid, 45 | rating: element.rating / 10, 46 | rating_class: element.rating_class, 47 | difficulty: element.difficultly 48 | } 49 | }) 50 | }); 51 | 52 | } catch (e) { 53 | 54 | if (e instanceof APIError) 55 | return reject(e); 56 | 57 | syslog.e(TAG, e.stack); 58 | return reject(new APIError(-233, 'unknown error occurred')); 59 | 60 | } 61 | 62 | }); 63 | 64 | } 65 | -------------------------------------------------------------------------------- /source/publicapi/v4/update.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/update.ts\t'; 2 | 3 | import fetch from 'node-fetch'; 4 | import syslog from '../../modules/syslog/syslog'; 5 | import APIError from '../../modules/apierror/apierror'; 6 | 7 | export default (argument: any): Promise => { 8 | 9 | return new Promise(async (resolve, reject) => { 10 | try { 11 | 12 | // request remote api 13 | await fetch("https://webapi.lowiro.com/webapi/serve/static/bin/arcaea/apk") 14 | .then((response) => response.text()) 15 | .then((rawdata) => { 16 | try { 17 | return JSON.parse(rawdata); 18 | } catch (e) { throw new APIError(-1, 'service unavailable'); } 19 | }) 20 | 21 | .then((root) => { 22 | 23 | if (root.success != true) 24 | throw new APIError(-2, "fetch latest release failed"); 25 | 26 | resolve({ 27 | "url": root.value.url, 28 | "version": root.value.version 29 | }); 30 | 31 | }) 32 | 33 | } catch (e) { 34 | 35 | if (e instanceof APIError) 36 | return reject(e); 37 | 38 | syslog.e(TAG, e.stack); 39 | return reject(new APIError(-233, 'unknown error occurred')); 40 | } 41 | }); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /source/publicapi/v4/user/info.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'v4/user/info.ts\t'; 2 | 3 | import syslog from '../../../modules/syslog/syslog'; 4 | import APIError from '../../../modules/apierror/apierror'; 5 | 6 | import arcapi_friend_add from '../../../modules/arcfetch/arcapi.friend.add'; 7 | import arcapi_friend_clear from '../../../modules/arcfetch/arcapi.friend.clear'; 8 | import account_alloc from '../../../modules/account/alloc'; 9 | import account_recycle from '../../../modules/account/recycle'; 10 | 11 | import arcrecord_update from '../../../modules/database/database.arcrecord.update'; 12 | import arcplayer_update from '../../../modules/database/database.arcplayer.update'; 13 | import arcrecord_byuserid from '../../../modules/database/database.arcrecord.byuserid'; 14 | import arcplayer_byany from '../../../modules/database/database.arcplayer.byany'; 15 | 16 | import IArcAccount from '../../../modules/arcfetch/interfaces/IArcAccount'; 17 | import IArcPlayer from '../../../modules/arcfetch/interfaces/IArcPlayer'; 18 | import IDatabaseArcPlayer from "../../../modules/database/interfaces/IDatabaseArcPlayer"; 19 | 20 | export default (argument: any): Promise => { 21 | 22 | return new Promise(async (resolve, reject) => { 23 | 24 | try { 25 | 26 | // /user/info?[user=xxx][usercode=xxx][&recent=7] 27 | // check for request arguments 28 | if ((typeof argument.user == 'undefined' || argument.user == '') 29 | && typeof argument.usercode == 'undefined' || argument.usercode == '') 30 | throw new APIError(-1, 'invalid username or usercode'); 31 | 32 | if (argument.usercode && !(/\d{9}/g.test(argument.usercode))) 33 | throw new APIError(-2, 'invalid usercode'); 34 | 35 | if (typeof argument.recent == 'undefined' || argument.recent == '') 36 | argument.recent = 0; 37 | else if (isNaN(parseFloat(argument.recent))) 38 | throw new APIError(-3, 'invalid recent number'); 39 | else argument.recent = parseFloat(argument.recent); 40 | 41 | if (argument.recent < 0 || argument.recent > 7) 42 | throw new APIError(-4, 'invalid recent number'); 43 | 44 | let _arc_ucode: string = ""; 45 | let _arc_account: IArcAccount | null = null; 46 | let _arc_friendlist: Array | null = null; 47 | let _arc_friend: IArcPlayer | null = null; 48 | 49 | // use this usercode directly 50 | if (argument.usercode) { 51 | _arc_ucode = argument.usercode; 52 | } 53 | 54 | // try search this user code while usercode not passing in 55 | else if (argument.user) { 56 | 57 | let users: IDatabaseArcPlayer[]; 58 | 59 | try { 60 | users = await arcplayer_byany(argument.user); 61 | } catch (e) { throw new APIError(-5, 'internal error occurred'); } 62 | 63 | if (users.length <= 0) 64 | throw new APIError(-6, 'user not found'); 65 | 66 | // too many users 67 | if (users.length > 1) 68 | throw new APIError(-7, 'too many users'); 69 | 70 | _arc_ucode = users[0].code; 71 | } 72 | 73 | // check user code again 74 | if (!_arc_ucode) 75 | throw new APIError(-8, 'internal error occurred'); 76 | 77 | // request an arc account 78 | try { 79 | _arc_account = await account_alloc(); 80 | } catch (e) { throw new APIError(-9, 'allocate an arc account failed'); } 81 | 82 | try { 83 | 84 | // clear friend list 85 | try { 86 | await arcapi_friend_clear(_arc_account); 87 | } catch (e) { syslog.e(TAG, e.stack); throw new APIError(-10, 'clear friend list failed'); } 88 | 89 | // add friend 90 | try { 91 | _arc_friendlist = await arcapi_friend_add(_arc_account, _arc_ucode); 92 | } catch (e) { throw new APIError(-11, 'add friend failed'); } 93 | 94 | // length must be 1 95 | if (_arc_friendlist.length != 1) 96 | throw new APIError(-12, 'internal error occurred'); 97 | 98 | // result of arcapi not include 99 | // user code anymore since v6 100 | _arc_friend = _arc_friendlist[0]; 101 | _arc_friend.code = _arc_ucode; 102 | 103 | // must do deep copy 104 | const _return = JSON.parse(JSON.stringify(_arc_friend)); 105 | 106 | // insert new record into database 107 | if (_arc_friend.recent_score.length) 108 | await arcrecord_update(_arc_friend.user_id, _arc_friend.recent_score) 109 | .catch((error) => { syslog.e(error.stack); }); 110 | 111 | // delete field if no need recent data 112 | if (argument.recent == 0) { 113 | delete _return.recent_score; 114 | } else { 115 | // pickup user recent records from the database 116 | _return.recent_score = await arcrecord_byuserid(_arc_friend.user_id, argument.recent); 117 | } 118 | 119 | resolve(_return); 120 | 121 | } catch (e) { 122 | // recycle account when any error occurred 123 | if (_arc_account) 124 | account_recycle(_arc_account); 125 | // re-throw the error 126 | throw e; 127 | } 128 | 129 | // release account 130 | account_recycle(_arc_account); 131 | 132 | // update user info and recently played 133 | arcplayer_update(_arc_friend) 134 | .catch((error) => { syslog.e(error.stack); }); 135 | 136 | } catch (e) { 137 | 138 | if (e instanceof APIError) 139 | return reject(e); 140 | 141 | syslog.e(TAG, e.stack); 142 | return reject(new APIError(-233, 'unknown error occurred')); 143 | 144 | } 145 | 146 | }); 147 | 148 | } 149 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | const TAG: string = 'tests/index.ts'; 2 | 3 | import config from '../source/corefunc/config'; 4 | import syslog from '../source/modules/syslog/syslog'; 5 | import database from '../source/corefunc/database'; 6 | import __loader__ from '../source/__loader__'; 7 | import databaseArcsongAllcharts from '../source/modules/database/database.arcsong.allcharts'; 8 | import IDatabaseArcSongChart from '../source/modules/database/interfaces/IDatabaseArcSongChart'; 9 | 10 | (function main() { 11 | 12 | // initialize config first 13 | config.loadConfigs(); 14 | process.title = `${BOTARCAPI_VERSTR}`; 15 | 16 | // initialize system log 17 | syslog.startLogging(); 18 | 19 | // print configs 20 | config.printConfigs(); 21 | 22 | // initialize database 23 | database.initDataBases(); 24 | 25 | setTimeout(async () => { 26 | const charts: Array | null = await databaseArcsongAllcharts(); 27 | if (charts) console.log(JSON.stringify(charts[0])); 28 | }, 1000); 29 | 30 | })(); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | "lib": ["DOM", "ES2015"], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": { 45 | // "@syslog": ["source/modules/syslog/syslog.ts"], 46 | // "@apierror": ["source/modules/apierror/apierror.ts"], 47 | // "@arcfetch/*": ["source/modules/arcfetch/*"], 48 | // "@database/*": ["source/modules/database/*"], 49 | // "@account/*": ["source/modules/account/*"], 50 | // "@modules/*": ["source/modules/*"] 51 | // }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 53 | // "typeRoots": [], /* List of folders to include type definitions from. */ 54 | // "types": [], /* Type declaration files to be included in compilation. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | /* Experimental Options */ 66 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 67 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 68 | /* Advanced Options */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } --------------------------------------------------------------------------------