push: rolling in hacked up title server
This commit is contained in:
parent
c22cc400d5
commit
ddadb82f2f
|
@ -1,4 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
js
|
js
|
||||||
package.json
|
package.json
|
||||||
*.config.*
|
*.config.*
|
||||||
|
plugins
|
10
package.json
10
package.json
|
@ -12,6 +12,7 @@
|
||||||
"typecheck": "tsc --project tsconfig.build.json --noEmit",
|
"typecheck": "tsc --project tsconfig.build.json --noEmit",
|
||||||
"start": "pnpm build && pnpm start-no-build",
|
"start": "pnpm build && pnpm start-no-build",
|
||||||
"start-no-build": "node js/main.js",
|
"start-no-build": "node js/main.js",
|
||||||
|
"migrate": "typeorm-ts-node-esm migration:run -- -d src/external/db/data-source.ts",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "beerpsi",
|
"author": "beerpsi",
|
||||||
|
@ -21,9 +22,8 @@
|
||||||
"pnpm": "7"
|
"pnpm": "7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^9.1.1",
|
"compression": "^1.7.4",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"drizzle-orm": "^0.29.0",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"fletcher": "^0.0.3",
|
"fletcher": "^0.0.3",
|
||||||
|
@ -32,9 +32,12 @@
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"micro-packed": "^0.3.2",
|
"micro-packed": "^0.3.2",
|
||||||
"raw-body": "^2.5.2",
|
"raw-body": "^2.5.2",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
"safe-json-stringify": "^1.2.0",
|
"safe-json-stringify": "^1.2.0",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typed-struct": "^2.3.0",
|
"typed-struct": "^2.3.0",
|
||||||
|
"typeorm": "^0.3.17",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1",
|
"winston-daily-rotate-file": "^4.7.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
|
@ -42,6 +45,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.7",
|
"@types/better-sqlite3": "^7.6.7",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/iconv": "^3.0.4",
|
"@types/iconv": "^3.0.4",
|
||||||
"@types/luxon": "^3.3.4",
|
"@types/luxon": "^3.3.4",
|
||||||
|
@ -49,9 +53,9 @@
|
||||||
"@types/safe-json-stringify": "^1.1.5",
|
"@types/safe-json-stringify": "^1.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
"drizzle-kit": "^0.20.4",
|
|
||||||
"eslint": "8.18.0",
|
"eslint": "8.18.0",
|
||||||
"eslint-plugin-cadence": "^0.1.0",
|
"eslint-plugin-cadence": "^0.1.0",
|
||||||
|
"tap": "^18.6.1",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3619
pnpm-lock.yaml
3619
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { AimeCard, AimeUser, FelicaCardLookup, FelicaMobileLookup } from "./entity/aimedb";
|
||||||
|
import { Arcade, Machine } from "./entity/allnet";
|
||||||
|
import { EventLog } from "./entity/base";
|
||||||
|
import { ChunithmStaticEvent } from "./entity/chunithm/static";
|
||||||
|
import { Config } from "lib/setup/config";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource({
|
||||||
|
type: "sqlite",
|
||||||
|
database: Config.DATABASE_PATH,
|
||||||
|
synchronize: true,
|
||||||
|
logging: false,
|
||||||
|
entities: [
|
||||||
|
Arcade,
|
||||||
|
AimeCard,
|
||||||
|
AimeUser,
|
||||||
|
ChunithmStaticEvent,
|
||||||
|
EventLog,
|
||||||
|
FelicaCardLookup,
|
||||||
|
FelicaMobileLookup,
|
||||||
|
Machine,
|
||||||
|
],
|
||||||
|
migrations: [],
|
||||||
|
subscribers: [],
|
||||||
|
enableWAL: true,
|
||||||
|
});
|
|
@ -1,10 +0,0 @@
|
||||||
import Database from "better-sqlite3";
|
|
||||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
||||||
import { Config } from "lib/setup/config";
|
|
||||||
|
|
||||||
const sqlite = new Database(Config.DATABASE_PATH);
|
|
||||||
|
|
||||||
export const db = drizzle(sqlite);
|
|
||||||
|
|
||||||
migrate(db, { migrationsFolder: "src/external/db/drizzle" });
|
|
|
@ -1,48 +0,0 @@
|
||||||
CREATE TABLE `aimedb_felica_card_lookup` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`idm` text(16) NOT NULL,
|
|
||||||
`access_code` text(20) NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `aimedb_felica_mobile_lookup` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`idm` text(16) NOT NULL,
|
|
||||||
`access_code` text(20) NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `allnet_arcade` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`name` text(60) DEFAULT 'Random arcade at nowhere',
|
|
||||||
`nickname` text(40) DEFAULT 'Please send help',
|
|
||||||
`country` text(3) DEFAULT 'JPN',
|
|
||||||
`region_id` integer DEFAULT 1,
|
|
||||||
`region_name0` text(48) DEFAULT 'W',
|
|
||||||
`region_name1` text(48) DEFAULT '',
|
|
||||||
`region_name2` text(48) DEFAULT '',
|
|
||||||
`region_name3` text(48) DEFAULT '',
|
|
||||||
`utc_offset` real DEFAULT 9
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `allnet_arcade_ip` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`arcade_id` integer,
|
|
||||||
`ip` text(15),
|
|
||||||
FOREIGN KEY (`arcade_id`) REFERENCES `allnet_arcade`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `allnet_machine` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`arcade_id` integer,
|
|
||||||
`serial` text(11),
|
|
||||||
`game` text(5),
|
|
||||||
`can_venue_hop` integer,
|
|
||||||
`last_authenticated` integer,
|
|
||||||
FOREIGN KEY (`arcade_id`) REFERENCES `allnet_arcade`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `aimedb_felica_card_lookup_idm_unique` ON `aimedb_felica_card_lookup` (`idm`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `aimedb_felica_card_lookup_access_code_unique` ON `aimedb_felica_card_lookup` (`access_code`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `aimedb_felica_mobile_lookup_idm_unique` ON `aimedb_felica_mobile_lookup` (`idm`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `aimedb_felica_mobile_lookup_access_code_unique` ON `aimedb_felica_mobile_lookup` (`access_code`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `allnet_arcade_ip_ip_unique` ON `allnet_arcade_ip` (`ip`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `allnet_machine_serial_unique` ON `allnet_machine` (`serial`);
|
|
|
@ -1,29 +0,0 @@
|
||||||
CREATE TABLE `aimedb_card` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`user_id` integer,
|
|
||||||
`access_code` text(20) NOT NULL,
|
|
||||||
`created_date` integer NOT NULL,
|
|
||||||
`last_login_date` integer NOT NULL,
|
|
||||||
`is_locked` integer DEFAULT false NOT NULL,
|
|
||||||
`is_banned` integer DEFAULT false NOT NULL,
|
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `aimedb_user`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `aimedb_user` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`created_date` integer NOT NULL,
|
|
||||||
`last_login_date` integer NOT NULL,
|
|
||||||
`suspend_expiration_date` integer
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `event_log` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`timestamp` integer NOT NULL,
|
|
||||||
`system` text NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`severity` text NOT NULL,
|
|
||||||
`message` text,
|
|
||||||
`details` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `aimedb_card_access_code_unique` ON `aimedb_card` (`access_code`);
|
|
|
@ -1,317 +0,0 @@
|
||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "a691ce52-7207-49ee-8450-d679c38b147a",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"tables": {
|
|
||||||
"aimedb_felica_card_lookup": {
|
|
||||||
"name": "aimedb_felica_card_lookup",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"idm": {
|
|
||||||
"name": "idm",
|
|
||||||
"type": "text(16)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"access_code": {
|
|
||||||
"name": "access_code",
|
|
||||||
"type": "text(20)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"aimedb_felica_card_lookup_idm_unique": {
|
|
||||||
"name": "aimedb_felica_card_lookup_idm_unique",
|
|
||||||
"columns": [
|
|
||||||
"idm"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
},
|
|
||||||
"aimedb_felica_card_lookup_access_code_unique": {
|
|
||||||
"name": "aimedb_felica_card_lookup_access_code_unique",
|
|
||||||
"columns": [
|
|
||||||
"access_code"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"aimedb_felica_mobile_lookup": {
|
|
||||||
"name": "aimedb_felica_mobile_lookup",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"idm": {
|
|
||||||
"name": "idm",
|
|
||||||
"type": "text(16)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"access_code": {
|
|
||||||
"name": "access_code",
|
|
||||||
"type": "text(20)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"aimedb_felica_mobile_lookup_idm_unique": {
|
|
||||||
"name": "aimedb_felica_mobile_lookup_idm_unique",
|
|
||||||
"columns": [
|
|
||||||
"idm"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
},
|
|
||||||
"aimedb_felica_mobile_lookup_access_code_unique": {
|
|
||||||
"name": "aimedb_felica_mobile_lookup_access_code_unique",
|
|
||||||
"columns": [
|
|
||||||
"access_code"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"allnet_arcade": {
|
|
||||||
"name": "allnet_arcade",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text(60)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Random arcade at nowhere'"
|
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"name": "nickname",
|
|
||||||
"type": "text(40)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Please send help'"
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"name": "country",
|
|
||||||
"type": "text(3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'JPN'"
|
|
||||||
},
|
|
||||||
"region_id": {
|
|
||||||
"name": "region_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 1
|
|
||||||
},
|
|
||||||
"region_name0": {
|
|
||||||
"name": "region_name0",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'W'"
|
|
||||||
},
|
|
||||||
"region_name1": {
|
|
||||||
"name": "region_name1",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"region_name2": {
|
|
||||||
"name": "region_name2",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"region_name3": {
|
|
||||||
"name": "region_name3",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"utc_offset": {
|
|
||||||
"name": "utc_offset",
|
|
||||||
"type": "real",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"allnet_arcade_ip": {
|
|
||||||
"name": "allnet_arcade_ip",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"arcade_id": {
|
|
||||||
"name": "arcade_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ip": {
|
|
||||||
"name": "ip",
|
|
||||||
"type": "text(15)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"allnet_arcade_ip_ip_unique": {
|
|
||||||
"name": "allnet_arcade_ip_ip_unique",
|
|
||||||
"columns": [
|
|
||||||
"ip"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"allnet_arcade_ip_arcade_id_allnet_arcade_id_fk": {
|
|
||||||
"name": "allnet_arcade_ip_arcade_id_allnet_arcade_id_fk",
|
|
||||||
"tableFrom": "allnet_arcade_ip",
|
|
||||||
"tableTo": "allnet_arcade",
|
|
||||||
"columnsFrom": [
|
|
||||||
"arcade_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"allnet_machine": {
|
|
||||||
"name": "allnet_machine",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"arcade_id": {
|
|
||||||
"name": "arcade_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"serial": {
|
|
||||||
"name": "serial",
|
|
||||||
"type": "text(11)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"game": {
|
|
||||||
"name": "game",
|
|
||||||
"type": "text(5)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"can_venue_hop": {
|
|
||||||
"name": "can_venue_hop",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"last_authenticated": {
|
|
||||||
"name": "last_authenticated",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"allnet_machine_serial_unique": {
|
|
||||||
"name": "allnet_machine_serial_unique",
|
|
||||||
"columns": [
|
|
||||||
"serial"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"allnet_machine_arcade_id_allnet_arcade_id_fk": {
|
|
||||||
"name": "allnet_machine_arcade_id_allnet_arcade_id_fk",
|
|
||||||
"tableFrom": "allnet_machine",
|
|
||||||
"tableTo": "allnet_arcade",
|
|
||||||
"columnsFrom": [
|
|
||||||
"arcade_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,494 +0,0 @@
|
||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "ca3c0d34-cebd-4ea7-806e-f4eb141e586b",
|
|
||||||
"prevId": "a691ce52-7207-49ee-8450-d679c38b147a",
|
|
||||||
"tables": {
|
|
||||||
"aimedb_card": {
|
|
||||||
"name": "aimedb_card",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"access_code": {
|
|
||||||
"name": "access_code",
|
|
||||||
"type": "text(20)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_date": {
|
|
||||||
"name": "created_date",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"last_login_date": {
|
|
||||||
"name": "last_login_date",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"is_locked": {
|
|
||||||
"name": "is_locked",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"is_banned": {
|
|
||||||
"name": "is_banned",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"aimedb_card_access_code_unique": {
|
|
||||||
"name": "aimedb_card_access_code_unique",
|
|
||||||
"columns": [
|
|
||||||
"access_code"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"aimedb_card_user_id_aimedb_user_id_fk": {
|
|
||||||
"name": "aimedb_card_user_id_aimedb_user_id_fk",
|
|
||||||
"tableFrom": "aimedb_card",
|
|
||||||
"tableTo": "aimedb_user",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"aimedb_felica_card_lookup": {
|
|
||||||
"name": "aimedb_felica_card_lookup",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"idm": {
|
|
||||||
"name": "idm",
|
|
||||||
"type": "text(16)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"access_code": {
|
|
||||||
"name": "access_code",
|
|
||||||
"type": "text(20)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"aimedb_felica_card_lookup_idm_unique": {
|
|
||||||
"name": "aimedb_felica_card_lookup_idm_unique",
|
|
||||||
"columns": [
|
|
||||||
"idm"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
},
|
|
||||||
"aimedb_felica_card_lookup_access_code_unique": {
|
|
||||||
"name": "aimedb_felica_card_lookup_access_code_unique",
|
|
||||||
"columns": [
|
|
||||||
"access_code"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"aimedb_felica_mobile_lookup": {
|
|
||||||
"name": "aimedb_felica_mobile_lookup",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"idm": {
|
|
||||||
"name": "idm",
|
|
||||||
"type": "text(16)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"access_code": {
|
|
||||||
"name": "access_code",
|
|
||||||
"type": "text(20)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"aimedb_felica_mobile_lookup_idm_unique": {
|
|
||||||
"name": "aimedb_felica_mobile_lookup_idm_unique",
|
|
||||||
"columns": [
|
|
||||||
"idm"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
},
|
|
||||||
"aimedb_felica_mobile_lookup_access_code_unique": {
|
|
||||||
"name": "aimedb_felica_mobile_lookup_access_code_unique",
|
|
||||||
"columns": [
|
|
||||||
"access_code"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"aimedb_user": {
|
|
||||||
"name": "aimedb_user",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"created_date": {
|
|
||||||
"name": "created_date",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"last_login_date": {
|
|
||||||
"name": "last_login_date",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"suspend_expiration_date": {
|
|
||||||
"name": "suspend_expiration_date",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"allnet_arcade": {
|
|
||||||
"name": "allnet_arcade",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text(60)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Random arcade at nowhere'"
|
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"name": "nickname",
|
|
||||||
"type": "text(40)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Please send help'"
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"name": "country",
|
|
||||||
"type": "text(3)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'JPN'"
|
|
||||||
},
|
|
||||||
"region_id": {
|
|
||||||
"name": "region_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 1
|
|
||||||
},
|
|
||||||
"region_name0": {
|
|
||||||
"name": "region_name0",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'W'"
|
|
||||||
},
|
|
||||||
"region_name1": {
|
|
||||||
"name": "region_name1",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"region_name2": {
|
|
||||||
"name": "region_name2",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"region_name3": {
|
|
||||||
"name": "region_name3",
|
|
||||||
"type": "text(48)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "''"
|
|
||||||
},
|
|
||||||
"utc_offset": {
|
|
||||||
"name": "utc_offset",
|
|
||||||
"type": "real",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"allnet_arcade_ip": {
|
|
||||||
"name": "allnet_arcade_ip",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"arcade_id": {
|
|
||||||
"name": "arcade_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ip": {
|
|
||||||
"name": "ip",
|
|
||||||
"type": "text(15)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"allnet_arcade_ip_ip_unique": {
|
|
||||||
"name": "allnet_arcade_ip_ip_unique",
|
|
||||||
"columns": [
|
|
||||||
"ip"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"allnet_arcade_ip_arcade_id_allnet_arcade_id_fk": {
|
|
||||||
"name": "allnet_arcade_ip_arcade_id_allnet_arcade_id_fk",
|
|
||||||
"tableFrom": "allnet_arcade_ip",
|
|
||||||
"tableTo": "allnet_arcade",
|
|
||||||
"columnsFrom": [
|
|
||||||
"arcade_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"allnet_machine": {
|
|
||||||
"name": "allnet_machine",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"arcade_id": {
|
|
||||||
"name": "arcade_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"serial": {
|
|
||||||
"name": "serial",
|
|
||||||
"type": "text(11)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"game": {
|
|
||||||
"name": "game",
|
|
||||||
"type": "text(5)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"can_venue_hop": {
|
|
||||||
"name": "can_venue_hop",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"last_authenticated": {
|
|
||||||
"name": "last_authenticated",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"allnet_machine_serial_unique": {
|
|
||||||
"name": "allnet_machine_serial_unique",
|
|
||||||
"columns": [
|
|
||||||
"serial"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"allnet_machine_arcade_id_allnet_arcade_id_fk": {
|
|
||||||
"name": "allnet_machine_arcade_id_allnet_arcade_id_fk",
|
|
||||||
"tableFrom": "allnet_machine",
|
|
||||||
"tableTo": "allnet_arcade",
|
|
||||||
"columnsFrom": [
|
|
||||||
"arcade_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"event_log": {
|
|
||||||
"name": "event_log",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"name": "timestamp",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"system": {
|
|
||||||
"name": "system",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"severity": {
|
|
||||||
"name": "severity",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"name": "message",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"details": {
|
|
||||||
"name": "details",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1700532948929,
|
|
||||||
"tag": "0000_ambitious_gorilla_man",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1700570200478,
|
|
||||||
"tag": "0001_modern_stryfe",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { ConstructableBaseEntity } from "../utils/constructable-base-entity";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
CreateDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
} from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity("aimedb_user")
|
||||||
|
export class AimeUser extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdDate!: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => AimeCard, (card) => card.user)
|
||||||
|
cards!: Array<AimeCard>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("aimedb_card")
|
||||||
|
export class AimeCard extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@ManyToOne(() => AimeUser, (user) => user.cards, { eager: true, nullable: true })
|
||||||
|
user!: AimeUser | null;
|
||||||
|
|
||||||
|
@Column({ unique: true, length: 20 })
|
||||||
|
accessCode!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdDate!: Date;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
isLocked!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
isBanned!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FelicaBaseLookup extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@Column({ unique: true, length: 16 })
|
||||||
|
idm!: string;
|
||||||
|
|
||||||
|
@Column({ unique: true, length: 20 })
|
||||||
|
accessCode!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("aimedb_felica_mobile_lookup")
|
||||||
|
export class FelicaMobileLookup extends FelicaBaseLookup {}
|
||||||
|
|
||||||
|
@Entity("aimedb_felica_card_lookup")
|
||||||
|
export class FelicaCardLookup extends FelicaBaseLookup {}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { ConstructableBaseEntity } from "../utils/constructable-base-entity";
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, OneToOne } from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity("allnet_arcade")
|
||||||
|
export class Arcade extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@Column({ length: 60 })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ length: 40 })
|
||||||
|
nickname!: string;
|
||||||
|
|
||||||
|
@Column({ length: 3 })
|
||||||
|
country!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
regionId!: integer;
|
||||||
|
|
||||||
|
@Column({ length: 48 })
|
||||||
|
regionName0!: string;
|
||||||
|
|
||||||
|
@Column({ length: 48 })
|
||||||
|
regionName1!: string;
|
||||||
|
|
||||||
|
@Column({ length: 48 })
|
||||||
|
regionName2!: string;
|
||||||
|
|
||||||
|
@Column({ length: 48 })
|
||||||
|
regionName3!: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Machine, (machine) => machine.arcade)
|
||||||
|
machines!: Array<Machine>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("allnet_machine")
|
||||||
|
export class Machine extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@OneToOne(() => Arcade)
|
||||||
|
arcade!: Arcade;
|
||||||
|
|
||||||
|
@Column({ length: 11, unique: true })
|
||||||
|
serial!: string;
|
||||||
|
|
||||||
|
@Column({ length: 5 })
|
||||||
|
game!: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
canVenueHop!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "datetime", nullable: true })
|
||||||
|
lastAuthenticated!: Date | null;
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ConstructableBaseEntity } from "../utils/constructable-base-entity";
|
||||||
|
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity("event_log")
|
||||||
|
export class EventLog extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
datetime!: Date;
|
||||||
|
|
||||||
|
@Column({ type: "text" })
|
||||||
|
system!: string;
|
||||||
|
|
||||||
|
@Column({ type: "text" })
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@Column({ length: 10 })
|
||||||
|
severity!: string;
|
||||||
|
|
||||||
|
@Column({ type: "text" })
|
||||||
|
message!: string;
|
||||||
|
|
||||||
|
@Column({ type: "simple-json" })
|
||||||
|
details!: Record<string, unknown>;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { ConstructableBaseEntity } from "../../utils/constructable-base-entity";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
CreateDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
Unique,
|
||||||
|
} from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity("chunithm_static_events")
|
||||||
|
@Unique(["version", "eventId"])
|
||||||
|
export class ChunithmStaticEvent extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
eventId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
type!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "text" })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ type: "datetime", default: () => "CURRENT_TIMESTAMP" })
|
||||||
|
startDate!: Date;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
enabled!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_static_events")
|
||||||
|
@Unique(["version", "chargeId"])
|
||||||
|
export class ChunithmStaticCharge extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
chargeId!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "text" })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
expirationDays!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
consumeType!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "boolean" })
|
||||||
|
sellingAppeal!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
enabled!: boolean;
|
||||||
|
}
|
|
@ -1,67 +0,0 @@
|
||||||
import { integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
||||||
|
|
||||||
export const felicaCardLookup = sqliteTable(
|
|
||||||
"aimedb_felica_card_lookup",
|
|
||||||
{
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
idm: text("idm", { length: 16 }).notNull(),
|
|
||||||
accessCode: text("access_code", { length: 20 }).notNull(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
unqIdm: unique().on(t.idm),
|
|
||||||
unqAc: unique().on(t.accessCode),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export type FelicaCardLookup = typeof felicaMobileLookup.$inferSelect;
|
|
||||||
export type NewFelicaCardLookup = typeof felicaMobileLookup.$inferSelect;
|
|
||||||
|
|
||||||
export const felicaMobileLookup = sqliteTable(
|
|
||||||
"aimedb_felica_mobile_lookup",
|
|
||||||
{
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
idm: text("idm", { length: 16 }).notNull(),
|
|
||||||
accessCode: text("access_code", { length: 20 }).notNull(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
unqIdm: unique().on(t.idm),
|
|
||||||
unqAc: unique().on(t.accessCode),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export type FelicaMobileLookup = typeof felicaMobileLookup.$inferSelect;
|
|
||||||
export type NewFelicaMobileLookup = typeof felicaMobileLookup.$inferInsert;
|
|
||||||
|
|
||||||
export const user = sqliteTable("aimedb_user", {
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
createdDate: integer("created_date", { mode: "timestamp" })
|
|
||||||
.notNull()
|
|
||||||
.$default(() => new Date()),
|
|
||||||
lastLoginDate: integer("last_login_date", { mode: "timestamp" })
|
|
||||||
.notNull()
|
|
||||||
.$default(() => new Date()),
|
|
||||||
suspendExpirationDate: integer("suspend_expiration_date", { mode: "timestamp" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AimeUser = typeof user.$inferSelect;
|
|
||||||
export type NewAimeUser = typeof user.$inferInsert;
|
|
||||||
|
|
||||||
export const card = sqliteTable(
|
|
||||||
"aimedb_card",
|
|
||||||
{
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
userId: integer("user_id").references(() => user.id),
|
|
||||||
accessCode: text("access_code", { length: 20 }).notNull(),
|
|
||||||
createdDate: integer("created_date", { mode: "timestamp" })
|
|
||||||
.notNull()
|
|
||||||
.$default(() => new Date()),
|
|
||||||
lastLoginDate: integer("last_login_date", { mode: "timestamp" })
|
|
||||||
.notNull()
|
|
||||||
.$default(() => new Date()),
|
|
||||||
isLocked: integer("is_locked", { mode: "boolean" }).default(false).notNull(),
|
|
||||||
isBanned: integer("is_banned", { mode: "boolean" }).default(false).notNull(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
unqAc: unique().on(t.accessCode),
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,56 +0,0 @@
|
||||||
/**
|
|
||||||
* In theory you should be able to just modify the imports here if you want to
|
|
||||||
* use a different database driver.
|
|
||||||
*/
|
|
||||||
import { integer, text, real, sqliteTable } from "drizzle-orm/sqlite-core";
|
|
||||||
|
|
||||||
export const arcade = sqliteTable("allnet_arcade", {
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
|
|
||||||
name: text("name", { length: 60 }).default("Random arcade at nowhere"),
|
|
||||||
nickname: text("nickname", { length: 40 }).default("Please send help"),
|
|
||||||
country: text("country", { length: 3 }).default("JPN"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Largest to smallest units of administrative division
|
|
||||||
*/
|
|
||||||
regionId: integer("region_id").default(1),
|
|
||||||
regionName0: text("region_name0", { length: 48 }).default("W"),
|
|
||||||
regionName1: text("region_name1", { length: 48 }).default(""),
|
|
||||||
regionName2: text("region_name2", { length: 48 }).default(""),
|
|
||||||
regionName3: text("region_name3", { length: 48 }).default(""),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client timezone. There's probably no arcades that span
|
|
||||||
* 2 timezones, right...?
|
|
||||||
*/
|
|
||||||
utcOffset: real("utc_offset").default(9),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Arcade = typeof arcade.$inferSelect;
|
|
||||||
export type NewArcade = typeof arcade.$inferInsert;
|
|
||||||
|
|
||||||
export const arcadeIp = sqliteTable("allnet_arcade_ip", {
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
|
|
||||||
arcade_id: integer("arcade_id").references(() => arcade.id),
|
|
||||||
ip: text("ip", { length: 15 }).unique(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ArcadeIp = typeof arcadeIp.$inferSelect;
|
|
||||||
export type NewArcadeIp = typeof arcadeIp.$inferInsert;
|
|
||||||
|
|
||||||
export const machine = sqliteTable("allnet_machine", {
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
arcade_id: integer("arcade_id").references(() => arcade.id),
|
|
||||||
|
|
||||||
serial: text("serial", { length: 11 }).unique(),
|
|
||||||
game: text("game", { length: 5 }),
|
|
||||||
|
|
||||||
canVenueHop: integer("can_venue_hop", { mode: "boolean" }),
|
|
||||||
|
|
||||||
lastAuthenticated: integer("last_authenticated", { mode: "timestamp" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Machine = typeof machine.$inferSelect;
|
|
||||||
export type NewMachine = typeof machine.$inferInsert;
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { integer, text, sqliteTable } from "drizzle-orm/sqlite-core";
|
|
||||||
|
|
||||||
export const eventLog = sqliteTable("event_log", {
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
timestamp: integer("timestamp", { mode: "timestamp" })
|
|
||||||
.notNull()
|
|
||||||
.$default(() => new Date()),
|
|
||||||
system: text("system").notNull(),
|
|
||||||
type: text("type").notNull(),
|
|
||||||
severity: text("severity").notNull(),
|
|
||||||
message: text("message"),
|
|
||||||
details: text("details", { mode: "json" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type EventLog = typeof eventLog.$inferSelect;
|
|
||||||
export type NewEventLog = typeof eventLog.$inferInsert;
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./aimedb";
|
|
||||||
export * from "./allnet";
|
|
||||||
export * from "./base";
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BaseEntity } from "typeorm";
|
||||||
|
|
||||||
|
export class ConstructableBaseEntity extends BaseEntity {
|
||||||
|
static construct<T extends BaseEntity>(this: new () => T, params: Partial<T>): T {
|
||||||
|
return Object.assign(new this(), params);
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,8 @@ export const enum PortalRegistration {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CompanyCodes {
|
export const enum CompanyCodes {
|
||||||
|
/** Not really, could be FeliCa Mobile. */
|
||||||
|
INVALID = 0,
|
||||||
SEGA = 1,
|
SEGA = 1,
|
||||||
BANDAI_NAMCO = 2,
|
BANDAI_NAMCO = 2,
|
||||||
KONAMI = 3,
|
KONAMI = 3,
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { CompanyCodes } from "./aimedb";
|
||||||
|
|
||||||
|
export const FELICA_MOBILE_IC_TYPES = [
|
||||||
|
0x06, 0x07, 0x10, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18,
|
||||||
|
] as const;
|
||||||
|
export const FELICA_CARD_IC_TYPES = [0x20, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7] as const;
|
||||||
|
|
||||||
|
export const DATA_FORMAT_CODE_MAP: Record<number, number> = {
|
||||||
|
0x002a: CompanyCodes.BANDAI_NAMCO,
|
||||||
|
0x003a: CompanyCodes.BANDAI_NAMCO,
|
||||||
|
0x8ca2: CompanyCodes.BANDAI_NAMCO,
|
||||||
|
0x0068: CompanyCodes.KONAMI,
|
||||||
|
0x0078: CompanyCodes.SEGA,
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
export const enum ChunithmVersions {
|
||||||
|
CHUNITHM = 100,
|
||||||
|
CHUNITHM_PLUS = 105,
|
||||||
|
AIR = 110,
|
||||||
|
AIR_PLUS = 115,
|
||||||
|
STAR = 120,
|
||||||
|
STAR_PLUS = 125,
|
||||||
|
AMAZON = 130,
|
||||||
|
AMAZON_PLUS = 135,
|
||||||
|
CRYSTAL = 140,
|
||||||
|
CRYSTAL_PLUS = 145,
|
||||||
|
/**
|
||||||
|
* @note PARADISE and PARADISE LOST share the same game version.
|
||||||
|
*/
|
||||||
|
PARADISE = 150,
|
||||||
|
NEW = 200,
|
||||||
|
NEW_PLUS = 205,
|
||||||
|
SUN = 210,
|
||||||
|
}
|
|
@ -24,34 +24,89 @@ const config: unknown = JSON5.parse(configFile);
|
||||||
|
|
||||||
const zod16bitNumber = z.number().gt(0).lte(65535);
|
const zod16bitNumber = z.number().gt(0).lte(65535);
|
||||||
const zodLogLevel = z.enum(["crit", "debug", "error", "info", "verbose", "warn"]);
|
const zodLogLevel = z.enum(["crit", "debug", "error", "info", "verbose", "warn"]);
|
||||||
const zodOptionalHexString16 = z
|
const zodHexString = z.string().regex(/^[0-9a-z]+$/iu, "value is not a hex string");
|
||||||
.string()
|
const zodHexString16 = zodHexString.length(16);
|
||||||
.length(16)
|
|
||||||
.regex(/^[0-9a-z]+$/iu, "value is not a hex string")
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
const configSchema = z.object({
|
const configSchema = z.object({
|
||||||
NAME: z.string(),
|
NAME: z.string().default("Kozukata Toa"),
|
||||||
DATABASE_PATH: z.string(),
|
DATABASE_PATH: z.string().default("data/db.sqlite3"),
|
||||||
LISTEN_ADDRESS: z.string().ip(),
|
LISTEN_ADDRESS: z.string().ip().default("0.0.0.0"),
|
||||||
LOGGER_CONFIG: z.object({
|
LOGGER_CONFIG: z.object({
|
||||||
LOG_LEVEL: zodLogLevel,
|
LOG_LEVEL: zodLogLevel.default("info"),
|
||||||
CONSOLE: z.boolean(),
|
CONSOLE: z.boolean().default(true),
|
||||||
FOLDER: z.string().optional(),
|
FOLDER: z.string().default("logs"),
|
||||||
}),
|
}),
|
||||||
ALLNET_CONFIG: z.object({
|
ALLNET_CONFIG: z.object({
|
||||||
ENABLED: z.boolean(),
|
ENABLED: z.boolean().default(true),
|
||||||
PORT: zod16bitNumber,
|
PORT: zod16bitNumber.default(80),
|
||||||
ALLOW_UNREGISTERED_SERIALS: z.boolean(),
|
ALLOW_UNREGISTERED_SERIALS: z.boolean().default(true),
|
||||||
UPDATE_CFG_FOLDER: z.string().optional(),
|
UPDATE_CFG_FOLDER: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
AIMEDB_CONFIG: z.object({
|
AIMEDB_CONFIG: z.object({
|
||||||
ENABLED: z.boolean(),
|
ENABLED: z.boolean().default(true),
|
||||||
PORT: zod16bitNumber,
|
PORT: zod16bitNumber.default(22345),
|
||||||
KEY: z.string().length(16).optional(),
|
KEY: z.string().length(16).default("Copyright(C)SEGA"),
|
||||||
AIME_MOBILE_CARD_KEY: zodOptionalHexString16,
|
AIME_MOBILE_CARD_KEY: zodHexString16.default("5CD3E81B9024F67A"),
|
||||||
RESERVED_CARD_PREFIX: z.string().length(5).optional(),
|
RESERVED_CARD_PREFIX: z.string().length(5).default("01053"),
|
||||||
RESERVED_CARD_KEY: zodOptionalHexString16,
|
RESERVED_CARD_KEY: zodHexString16.default("E8179645DB3FC02A"),
|
||||||
|
}),
|
||||||
|
TITLES_CONFIG: z.object({
|
||||||
|
ENABLED: z.boolean().default(true),
|
||||||
|
PORT: zod16bitNumber.default(8080),
|
||||||
|
HOSTNAME: z.string().default("localhost"),
|
||||||
|
}),
|
||||||
|
CHUNITHM_CONFIG: z.object({
|
||||||
|
ENABLE: z.boolean().default(true),
|
||||||
|
MODS: z
|
||||||
|
.object({
|
||||||
|
TEAM_NAME: z.string().optional(),
|
||||||
|
USE_LOGIN_BONUS: z.boolean().default(false),
|
||||||
|
FORCE_UNLOCK_ALL: z.boolean().default(false),
|
||||||
|
})
|
||||||
|
.default({ USE_LOGIN_BONUS: false, FORCE_UNLOCK_ALL: false }),
|
||||||
|
VERSIONS: z
|
||||||
|
.record(
|
||||||
|
z.object({
|
||||||
|
rom: z.string(),
|
||||||
|
data: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.default({
|
||||||
|
"200": {
|
||||||
|
rom: "2.00.00",
|
||||||
|
data: "2.00.00",
|
||||||
|
},
|
||||||
|
"205": {
|
||||||
|
rom: "2.05.00",
|
||||||
|
data: "2.05.00",
|
||||||
|
},
|
||||||
|
"210": {
|
||||||
|
rom: "2.10.00",
|
||||||
|
data: "2.10.00",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
CRYPTO: z
|
||||||
|
.object({
|
||||||
|
ENCRYPTED_ONLY: z.boolean().default(false),
|
||||||
|
KEYS: z.record(z.array(zodHexString).min(2).max(3)).optional(),
|
||||||
|
})
|
||||||
|
.refine((arg) => {
|
||||||
|
if (arg.ENCRYPTED_ONLY) {
|
||||||
|
return !!arg.KEYS && Object.keys(arg.KEYS).length >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, "Must provide keys for at least one version if running in encrypted only mode.")
|
||||||
|
.default({
|
||||||
|
ENCRYPTED_ONLY: false,
|
||||||
|
KEYS: {
|
||||||
|
"210": [
|
||||||
|
"75695c3d265f434c3953454c5830522b4b3c4d7b42482a312e5627216b2b4060",
|
||||||
|
"31277c37707044377141595058345a6b",
|
||||||
|
"04780206ca5f36f4",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,3 +119,23 @@ if (!parseResult.success) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Config = parseResult.data;
|
export const Config = parseResult.data;
|
||||||
|
|
||||||
|
// Environment Variable Validation
|
||||||
|
|
||||||
|
const nodeEnv = process.env.NODE_ENV ?? "";
|
||||||
|
|
||||||
|
if (!nodeEnv) {
|
||||||
|
logger.error(`No NODE_ENV specified in environment. Terminating.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["dev", "production", "staging", "test"].includes(nodeEnv)) {
|
||||||
|
logger.error(
|
||||||
|
`Invalid NODE_ENV set in environment. Expected dev, production, test or staging. Got ${nodeEnv}.`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Environment = {
|
||||||
|
nodeEnv: nodeEnv as "dev" | "production" | "staging" | "test",
|
||||||
|
};
|
||||||
|
|
38
src/main.ts
38
src/main.ts
|
@ -1,19 +1,41 @@
|
||||||
import { aimeDbServerFactory, allnetServer } from "./servers/index";
|
import { aimeDbServerFactory, allnetServer, titleServer } from "./servers/index";
|
||||||
|
import { AppDataSource } from "external/db/data-source";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import { Config } from "lib/setup/config";
|
import { Config } from "lib/setup/config";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
|
import "reflect-metadata";
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
logger.info(`Booting ${Config.NAME}.`, { bootInfo: true });
|
logger.info(`Booting ${Config.NAME}.`, { bootInfo: true });
|
||||||
logger.info(`Log level is set to ${Config.LOGGER_CONFIG.LOG_LEVEL}.`, { bootInfo: true });
|
logger.info(`Log level is set to ${Config.LOGGER_CONFIG.LOG_LEVEL}.`, { bootInfo: true });
|
||||||
|
|
||||||
if (Config.ALLNET_CONFIG.ENABLED) {
|
const start = process.hrtime.bigint();
|
||||||
allnetServer.listen(Config.ALLNET_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Config.AIMEDB_CONFIG.ENABLED) {
|
logger.info("Initializing database connection...");
|
||||||
const server = net.createServer(aimeDbServerFactory());
|
void AppDataSource.initialize().then(() => {
|
||||||
|
const end = process.hrtime.bigint();
|
||||||
|
|
||||||
server.listen(Config.AIMEDB_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
logger.info(`Database connected in ${(end - start) / 1000000n}ms`);
|
||||||
}
|
|
||||||
|
if (Config.ALLNET_CONFIG.ENABLED) {
|
||||||
|
logger.info(
|
||||||
|
`Starting ALL.Net server on ${Config.LISTEN_ADDRESS}:${Config.ALLNET_CONFIG.PORT}.`
|
||||||
|
);
|
||||||
|
allnetServer.listen(Config.ALLNET_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.TITLES_CONFIG.ENABLED) {
|
||||||
|
logger.info(
|
||||||
|
`Starting title server on ${Config.LISTEN_ADDRESS}:${Config.ALLNET_CONFIG.PORT}.`
|
||||||
|
);
|
||||||
|
titleServer.listen(Config.TITLES_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.AIMEDB_CONFIG.ENABLED) {
|
||||||
|
const server = net.createServer(aimeDbServerFactory());
|
||||||
|
|
||||||
|
logger.info(`Starting AimeDB on ${Config.LISTEN_ADDRESS}:${Config.ALLNET_CONFIG.PORT}.`);
|
||||||
|
server.listen(Config.AIMEDB_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -5,10 +5,8 @@ import {
|
||||||
AimeAccountExtendedQueryStruct,
|
AimeAccountExtendedQueryStruct,
|
||||||
} from "../types/aime-account";
|
} from "../types/aime-account";
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, PortalRegistration, ResultCodes } from "../utils/misc";
|
import { CommandId, PortalRegistration, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import { eq } from "drizzle-orm";
|
import { AimeCard } from "external/db/entity/aimedb";
|
||||||
import { db } from "external/db/db";
|
|
||||||
import { card } from "external/db/schemas";
|
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
|
||||||
|
@ -39,25 +37,23 @@ export const GetAimeAccountHandler: AimeDBHandlerFn<"AimeAccountResponse"> = asy
|
||||||
|
|
||||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||||
|
|
||||||
const cardRow = await db
|
const card = await AimeCard.findOne({
|
||||||
.select()
|
where: { accessCode },
|
||||||
.from(card)
|
});
|
||||||
.where(eq(card.accessCode, accessCode))
|
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (!cardRow) {
|
if (!card) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardRow.isBanned && cardRow.isLocked) {
|
if (card.isBanned && card.isLocked) {
|
||||||
header.result = ResultCodes.BAN_SYSTEM_AND_USER_LOCK;
|
header.result = ResultCodes.BAN_SYSTEM_AND_USER_LOCK;
|
||||||
} else if (cardRow.isBanned) {
|
} else if (card.isBanned) {
|
||||||
header.result = ResultCodes.BAN_SYSTEM_LOCK;
|
header.result = ResultCodes.BAN_SYSTEM_LOCK;
|
||||||
} else if (cardRow.isLocked) {
|
} else if (card.isLocked) {
|
||||||
header.result = ResultCodes.USER_LOCK;
|
header.result = ResultCodes.USER_LOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.accountId = cardRow.id;
|
resp.accountId = card.id;
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,31 +82,29 @@ export const RegisterAimeAccountHandler: AimeDBHandlerFn<"AimeAccountResponse">
|
||||||
|
|
||||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||||
|
|
||||||
const cardRow = await db
|
const card = await AimeCard.findOne({
|
||||||
.select()
|
where: {
|
||||||
.from(card)
|
accessCode,
|
||||||
.where(eq(card.accessCode, accessCode))
|
},
|
||||||
.then((r) => r[0]);
|
});
|
||||||
|
|
||||||
if (cardRow) {
|
if (card) {
|
||||||
header.result = ResultCodes.ID_ALREADY_REGISTERED;
|
header.result = ResultCodes.ID_ALREADY_REGISTERED;
|
||||||
resp.accountId = cardRow.id;
|
resp.accountId = card.id;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCardRow = await db
|
const newCard = AimeCard.construct({ accessCode });
|
||||||
.insert(card)
|
|
||||||
.values({ accessCode })
|
|
||||||
.returning()
|
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (!newCardRow) {
|
try {
|
||||||
logger.crit("Failed to insert new lookup entry into the database.", { accessCode });
|
await newCard.save();
|
||||||
|
} catch (err) {
|
||||||
|
logger.crit("Failed to insert new lookup entry into the database.", { err });
|
||||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.accountId = newCardRow.id;
|
resp.accountId = newCard.id;
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -126,7 +120,6 @@ export const GetAimeAccountExtendedHandler: AimeDBHandlerFn<"AimeAccountExtended
|
||||||
const resp = new AimeAccountExtendedResponseStruct();
|
const resp = new AimeAccountExtendedResponseStruct();
|
||||||
|
|
||||||
// TODO: Actually handle portal state when we get a webUI
|
// TODO: Actually handle portal state when we get a webUI
|
||||||
// TODO: What the fuck is an auth key
|
|
||||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
||||||
resp.accountId = -1;
|
resp.accountId = -1;
|
||||||
|
|
||||||
|
@ -140,25 +133,23 @@ export const GetAimeAccountExtendedHandler: AimeDBHandlerFn<"AimeAccountExtended
|
||||||
|
|
||||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||||
|
|
||||||
const cardRow = await db
|
const card = await AimeCard.findOne({
|
||||||
.select()
|
where: { accessCode },
|
||||||
.from(card)
|
});
|
||||||
.where(eq(card.accessCode, accessCode))
|
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (!cardRow) {
|
if (!card) {
|
||||||
resp.accountId = -1;
|
resp.accountId = -1;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardRow.isBanned && cardRow.isLocked) {
|
if (card.isBanned && card.isLocked) {
|
||||||
header.result = ResultCodes.BAN_SYSTEM_AND_USER_LOCK;
|
header.result = ResultCodes.BAN_SYSTEM_AND_USER_LOCK;
|
||||||
} else if (cardRow.isBanned) {
|
} else if (card.isBanned) {
|
||||||
header.result = ResultCodes.BAN_SYSTEM_LOCK;
|
header.result = ResultCodes.BAN_SYSTEM_LOCK;
|
||||||
} else if (cardRow.isLocked) {
|
} else if (card.isLocked) {
|
||||||
header.result = ResultCodes.USER_LOCK;
|
header.result = ResultCodes.USER_LOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.accountId = cardRow.id;
|
resp.accountId = card.id;
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,9 +5,9 @@ import {
|
||||||
StatusLogStruct,
|
StatusLogStruct,
|
||||||
} from "../types/aime-log";
|
} from "../types/aime-log";
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, LogStatus, ResultCodes } from "../utils/misc";
|
import { CommandId, LogStatus, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import { db } from "external/db/db";
|
import { AppDataSource } from "external/db/data-source";
|
||||||
import { eventLog } from "external/db/schemas";
|
import { EventLog } from "external/db/entity/base";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
|
||||||
|
@ -27,13 +27,15 @@ export const StatusLogHandler: AimeDBHandlerFn = async (header, data) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.insert(eventLog).values({
|
const eventLog = EventLog.construct({
|
||||||
system: "aimedb",
|
system: "aimedb",
|
||||||
type: `AIMEDB_LOG_${statusName}`,
|
type: `LOG_${statusName}`,
|
||||||
severity: "info",
|
severity: "info",
|
||||||
details: { aimeId: req.aimeId },
|
details: { aimeId: req.aimeId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await eventLog.save();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,9 +53,9 @@ export const AimeLogHandler: AimeDBHandlerFn = async (header, data) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.insert(eventLog).values({
|
const eventLog = EventLog.construct({
|
||||||
system: "aimedb",
|
system: "aimedb",
|
||||||
type: `AIMEDB_LOG_${statusName}`,
|
type: `LOG_${statusName}`,
|
||||||
severity: "info",
|
severity: "info",
|
||||||
details: {
|
details: {
|
||||||
aimeId: req.aimeId,
|
aimeId: req.aimeId,
|
||||||
|
@ -64,6 +66,8 @@ export const AimeLogHandler: AimeDBHandlerFn = async (header, data) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await eventLog.save();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,7 +82,7 @@ export const AimeExtendedLogHandler: AimeDBHandlerFn<"AimeLogExtendedResponse">
|
||||||
const req = new ExtendedAimeLogStruct(data);
|
const req = new ExtendedAimeLogStruct(data);
|
||||||
const resp = new AimeLogExtendedResponseStruct();
|
const resp = new AimeLogExtendedResponseStruct();
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await AppDataSource.transaction(async (em) => {
|
||||||
const ops = [];
|
const ops = [];
|
||||||
|
|
||||||
for (let i = 0; i < req.count; i++) {
|
for (let i = 0; i < req.count; i++) {
|
||||||
|
@ -100,21 +104,20 @@ export const AimeExtendedLogHandler: AimeDBHandlerFn<"AimeLogExtendedResponse">
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ops.push(
|
const eventLog = EventLog.construct({
|
||||||
tx.insert(eventLog).values({
|
system: "aimedb",
|
||||||
system: "aimedb",
|
type: `LOG_${statusName}`,
|
||||||
type: `AIMEDB_LOG_${statusName}`,
|
severity: "info",
|
||||||
severity: "info",
|
details: {
|
||||||
details: {
|
aimeId: entry.aimeId,
|
||||||
aimeId: entry.aimeId,
|
userId: entry.userId.toString(),
|
||||||
userId: entry.userId.toString(),
|
creditCount: entry.creditCount,
|
||||||
creditCount: entry.creditCount,
|
betCount: entry.betCount,
|
||||||
betCount: entry.betCount,
|
wonCount: entry.wonCount,
|
||||||
wonCount: entry.wonCount,
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
);
|
ops.push(em.save(eventLog));
|
||||||
resp.result[i] = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(ops);
|
await Promise.all(ops);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
OldCampaignResponseStruct,
|
OldCampaignResponseStruct,
|
||||||
} from "../types/campaign";
|
} from "../types/campaign";
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, ResultCodes } from "../utils/misc";
|
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
|
||||||
export const GetCampaignInfoHandler: AimeDBHandlerFn<"CampaignResponse" | "OldCampaignResponse"> = (
|
export const GetCampaignInfoHandler: AimeDBHandlerFn<"CampaignResponse" | "OldCampaignResponse"> = (
|
||||||
|
|
|
@ -17,29 +17,29 @@ import {
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CalculateAccessCode } from "../utils/access-code";
|
import { CalculateAccessCode } from "../utils/access-code";
|
||||||
import { IsSupportedFelicaMobile, IsSupportedFelica } from "../utils/felica";
|
import { IsSupportedFelicaMobile, IsSupportedFelica } from "../utils/felica";
|
||||||
import { CommandId, CompanyCodes, PortalRegistration, ResultCodes } from "../utils/misc";
|
import { AimeCard, FelicaCardLookup, FelicaMobileLookup } from "external/db/entity/aimedb";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { CommandId, CompanyCodes, PortalRegistration, ResultCodes } from "lib/constants/aimedb";
|
||||||
import { db } from "external/db/db";
|
import { DATA_FORMAT_CODE_MAP } from "lib/constants/felica";
|
||||||
import { card, felicaCardLookup, felicaMobileLookup } from "external/db/schemas/index";
|
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import { Config } from "lib/setup/config";
|
import { Config } from "lib/setup/config";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
import type { ValidFeliCaIcTypes } from "../utils/felica";
|
||||||
|
import type { FelicaBaseLookup } from "external/db/entity/aimedb";
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
async function createAndInsertNewAccessCode(
|
async function generateAndInsertNewAccessCode<T extends typeof FelicaBaseLookup>(
|
||||||
table: typeof felicaCardLookup | typeof felicaMobileLookup,
|
table: T,
|
||||||
cardType: "FeliCa Card" | "FeliCa Mobile",
|
cardType: "FeliCa Card" | "FeliCa Mobile",
|
||||||
idm: bigint,
|
idm: bigint,
|
||||||
prefix: string,
|
prefix: string,
|
||||||
key: string
|
key: string
|
||||||
) {
|
): Promise<FelicaBaseLookup | null> {
|
||||||
const mostRecentRow = await db
|
const mostRecentRow = await table.findOne({
|
||||||
.select()
|
order: {
|
||||||
.from(table)
|
id: "desc",
|
||||||
.orderBy(desc(table.id))
|
},
|
||||||
.limit(1)
|
});
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
|
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
|
||||||
|
|
||||||
|
@ -50,21 +50,59 @@ async function createAndInsertNewAccessCode(
|
||||||
accessCode,
|
accessCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const value = { idm: idm.toString(16), accessCode };
|
const card = table.construct({ idm: idm.toString(16), accessCode });
|
||||||
|
|
||||||
const result = await db
|
try {
|
||||||
.insert(felicaCardLookup)
|
await card.save();
|
||||||
.values(value)
|
} catch (err) {
|
||||||
.returning()
|
logger.error("Failed to insert new lookup entry into the database.", { err });
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
logger.crit("Failed to insert new lookup entry into the database.", value);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateNewFeliCa(idm: bigint, icType: ValidFeliCaIcTypes) {
|
||||||
|
if (!IsSupportedFelica(icType)) {
|
||||||
|
throw new Error("Invalid IC type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSupportedFelicaMobile(icType)) {
|
||||||
|
if (!Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY) {
|
||||||
|
logger.error(
|
||||||
|
"AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY is not set in config file. Cannot generate a new access code.",
|
||||||
|
{ idm, icType }
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateAndInsertNewAccessCode(
|
||||||
|
FelicaMobileLookup,
|
||||||
|
"FeliCa Mobile",
|
||||||
|
idm,
|
||||||
|
"01035",
|
||||||
|
Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Config.AIMEDB_CONFIG.RESERVED_CARD_KEY || !Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX) {
|
||||||
|
logger.error(
|
||||||
|
"AIMEDB_CONFIG.RESERVED_CARD_KEY or AIMEDB_CONFIG.RESERVED_CARD_PREFIX is not set in config file. Cannot generate a new access code.",
|
||||||
|
{ idm, icType }
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateAndInsertNewAccessCode(
|
||||||
|
FelicaCardLookup,
|
||||||
|
"FeliCa Card",
|
||||||
|
idm,
|
||||||
|
Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX,
|
||||||
|
Config.AIMEDB_CONFIG.RESERVED_CARD_KEY
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is supposed to be just a lookup handler, but my guess is that most games assume that there
|
// This is supposed to be just a lookup handler, but my guess is that most games assume that there
|
||||||
|
@ -82,44 +120,22 @@ export const FelicaLookupHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = asyn
|
||||||
const req = new FelicaLookupRequestStruct(data);
|
const req = new FelicaLookupRequestStruct(data);
|
||||||
const resp = new FelicaLookupResponseStruct();
|
const resp = new FelicaLookupResponseStruct();
|
||||||
|
|
||||||
if (!IsSupportedFelica(req.osVer)) {
|
if (!IsSupportedFelica(req.icType)) {
|
||||||
header.result = ResultCodes.INVALID_AIME_ID;
|
header.result = ResultCodes.INVALID_AIME_ID;
|
||||||
resp.felicaIndex = -1;
|
resp.felicaIndex = -1;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = IsSupportedFelicaMobile(req.osVer) ? felicaMobileLookup : felicaCardLookup;
|
const table = IsSupportedFelicaMobile(req.icType) ? FelicaMobileLookup : FelicaCardLookup;
|
||||||
|
|
||||||
let result = await db
|
let result: FelicaBaseLookup | null = await table.findOne({
|
||||||
.select()
|
where: {
|
||||||
.from(table)
|
idm: req.idm.toString(16),
|
||||||
.where(eq(table.idm, req.idm.toString(16)))
|
},
|
||||||
.then((r) => r[0]);
|
});
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// Exit early if card is FeliCa mobile, so the game can use the register endpoint
|
const tmp = await generateNewFeliCa(req.idm, req.icType);
|
||||||
if (IsSupportedFelicaMobile(req.osVer)) {
|
|
||||||
resp.felicaIndex = -1;
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Config.AIMEDB_CONFIG.RESERVED_CARD_KEY || !Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX) {
|
|
||||||
logger.error(
|
|
||||||
"AIMEDB_CONFIG.RESERVED_CARD_KEY or AIMEDB_CONFIG.RESERVED_CARD_PREFIX is not set in config file. Cannot generate a new access code.",
|
|
||||||
{ req }
|
|
||||||
);
|
|
||||||
|
|
||||||
header.result = ResultCodes.INVALID_AIME_ID;
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tmp = await createAndInsertNewAccessCode(
|
|
||||||
felicaCardLookup,
|
|
||||||
"FeliCa Card",
|
|
||||||
req.idm,
|
|
||||||
Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX,
|
|
||||||
Config.AIMEDB_CONFIG.RESERVED_CARD_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tmp) {
|
if (!tmp) {
|
||||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||||
|
@ -160,94 +176,46 @@ export const FelicaExtendedLookupHandler: AimeDBHandlerFn<"FelicaExtendedLookupR
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSupportedFelica(req.osVer)) {
|
if (!IsSupportedFelica(req.icType)) {
|
||||||
header.result = ResultCodes.INVALID_AIME_ID;
|
header.result = ResultCodes.INVALID_AIME_ID;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
const table = IsSupportedFelicaMobile(req.icType) ? FelicaMobileLookup : FelicaCardLookup;
|
||||||
|
|
||||||
if (IsSupportedFelicaMobile(req.osVer)) {
|
const result: FelicaBaseLookup | null = await table.findOne({
|
||||||
result = await db
|
where: {
|
||||||
.select()
|
idm: req.idm.toString(16),
|
||||||
.from(felicaMobileLookup)
|
},
|
||||||
.where(eq(felicaMobileLookup.idm, req.idm.toString(16)))
|
});
|
||||||
.leftJoin(card, eq(card.accessCode, felicaMobileLookup.accessCode))
|
|
||||||
.then((r) => r[0]);
|
let accessCode;
|
||||||
} else {
|
|
||||||
result = await db
|
|
||||||
.select()
|
|
||||||
.from(felicaCardLookup)
|
|
||||||
.where(eq(felicaCardLookup.idm, req.idm.toString(16)))
|
|
||||||
.leftJoin(card, eq(card.accessCode, felicaCardLookup.accessCode))
|
|
||||||
.then((r) => r[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const lookupResult =
|
accessCode = result.accessCode;
|
||||||
"aimedb_felica_mobile_lookup" in result
|
} else {
|
||||||
? result.aimedb_felica_mobile_lookup
|
const row = await generateNewFeliCa(req.idm, req.icType);
|
||||||
: result.aimedb_felica_card_lookup;
|
|
||||||
const cardResult = result.aimedb_card;
|
|
||||||
|
|
||||||
// HACK: Since we cannot possibly know who made it (even AICC cards have
|
if (!row) {
|
||||||
// the same manufacturer code `01:2e`!), we're just going to treat everything
|
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||||
// as a SEGA card.
|
return resp;
|
||||||
resp.companyCode = CompanyCodes.SEGA;
|
|
||||||
resp.accessCode.set(Buffer.from(lookupResult.accessCode, "hex"));
|
|
||||||
|
|
||||||
if (cardResult) {
|
|
||||||
resp.accountId = cardResult.id;
|
|
||||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
accessCode = row.accessCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming that FeliCa Mobile is handled by their own registration endpoint...
|
// HACK: What does official AimeDB do if the DFC is not valid?
|
||||||
if (IsSupportedFelicaMobile(req.osVer)) {
|
resp.companyCode = DATA_FORMAT_CODE_MAP[req.dataFormatCode] ?? CompanyCodes.SEGA;
|
||||||
return resp;
|
resp.accessCode.set(Buffer.from(accessCode, "hex"));
|
||||||
}
|
|
||||||
|
|
||||||
// Card is not in the lookup tables, register a new card...
|
const maybeCard = await AimeCard.findOne({
|
||||||
if (!Config.AIMEDB_CONFIG.RESERVED_CARD_KEY || !Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX) {
|
where: { accessCode },
|
||||||
logger.error(
|
});
|
||||||
"AIMEDB_CONFIG.RESERVED_CARD_KEY or AIMEDB_CONFIG.RESERVED_CARD_PREFIX is not set in config file. Cannot generate a new access code.",
|
|
||||||
{ req }
|
|
||||||
);
|
|
||||||
|
|
||||||
header.result = ResultCodes.INVALID_AIME_ID;
|
if (maybeCard) {
|
||||||
return resp;
|
// TODO: Actually handle portal state when we get a webUI
|
||||||
}
|
|
||||||
|
|
||||||
const row = await createAndInsertNewAccessCode(
|
|
||||||
felicaCardLookup,
|
|
||||||
"FeliCa Card",
|
|
||||||
req.idm,
|
|
||||||
Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX,
|
|
||||||
Config.AIMEDB_CONFIG.RESERVED_CARD_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HACK: Since we cannot possibly know who made it (even AICC cards have
|
|
||||||
// the same manufacturer code `01:2e`!), we're just going to treat everything
|
|
||||||
// as a SEGA card.
|
|
||||||
resp.companyCode = CompanyCodes.SEGA;
|
|
||||||
resp.accessCode.set(Buffer.from(row.accessCode, "hex"));
|
|
||||||
|
|
||||||
const cardResult = await db
|
|
||||||
.select()
|
|
||||||
.from(card)
|
|
||||||
.where(eq(card.accessCode, row.accessCode))
|
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (cardResult) {
|
|
||||||
resp.accountId = cardResult.id;
|
|
||||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
||||||
|
resp.accountId = maybeCard.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
@ -263,11 +231,11 @@ export const FelicaRegisterHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = as
|
||||||
const req = new FelicaLookupRequestStruct(data);
|
const req = new FelicaLookupRequestStruct(data);
|
||||||
const resp = new FelicaLookupResponseStruct();
|
const resp = new FelicaLookupResponseStruct();
|
||||||
|
|
||||||
if (!IsSupportedFelicaMobile(req.osVer)) {
|
if (!IsSupportedFelicaMobile(req.icType)) {
|
||||||
logger.error("Rejecting card because it is not FeliCa Mobile.", {
|
logger.error("Rejecting card because it is not FeliCa Mobile.", {
|
||||||
idm: req.idm,
|
idm: req.idm,
|
||||||
chipCode: req.chipCode,
|
romType: req.romType,
|
||||||
osVer: req.osVer,
|
icType: req.icType,
|
||||||
timing: req.timing,
|
timing: req.timing,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -276,12 +244,11 @@ export const FelicaRegisterHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = as
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db
|
const result = await FelicaMobileLookup.findOne({
|
||||||
.select()
|
where: {
|
||||||
.from(felicaMobileLookup)
|
idm: req.idm.toString(16),
|
||||||
.where(eq(felicaMobileLookup.idm, req.idm.toString(16)))
|
},
|
||||||
.limit(1)
|
});
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
header.result = ResultCodes.ID_ALREADY_REGISTERED;
|
header.result = ResultCodes.ID_ALREADY_REGISTERED;
|
||||||
|
@ -297,13 +264,13 @@ export const FelicaRegisterHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = as
|
||||||
{ req }
|
{ req }
|
||||||
);
|
);
|
||||||
|
|
||||||
header.result = ResultCodes.INVALID_AIME_ID;
|
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||||
resp.felicaIndex = -1;
|
resp.felicaIndex = -1;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await createAndInsertNewAccessCode(
|
const row = await generateAndInsertNewAccessCode(
|
||||||
felicaMobileLookup,
|
FelicaMobileLookup,
|
||||||
"FeliCa Mobile",
|
"FeliCa Mobile",
|
||||||
req.idm,
|
req.idm,
|
||||||
"01035",
|
"01035",
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
FelicaRegisterHandler,
|
FelicaRegisterHandler,
|
||||||
} from "./felica-conversion";
|
} from "./felica-conversion";
|
||||||
import { ServiceHealthCheckHandler } from "./status-check";
|
import { ServiceHealthCheckHandler } from "./status-check";
|
||||||
import { CommandId } from "../utils/misc";
|
import { CommandId } from "../../../lib/constants/aimedb";
|
||||||
import type { AimeDBHandlerFn, AimeDBReturnTypes } from "../types/handlers";
|
import type { AimeDBHandlerFn, AimeDBReturnTypes } from "../types/handlers";
|
||||||
|
|
||||||
type CommandIdRequest = {
|
type CommandIdRequest = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, ResultCodes } from "../utils/misc";
|
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
|
||||||
export const ServiceHealthCheckHandler: AimeDBHandlerFn = (header, _) => {
|
export const ServiceHealthCheckHandler: AimeDBHandlerFn = (header, _) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AIMEDB_HANDLERS, EXPECTED_PACKET_LENGTH } from "./handlers";
|
import { AIMEDB_HANDLERS, EXPECTED_PACKET_LENGTH } from "./handlers";
|
||||||
import { PacketHeaderStruct } from "./types/header";
|
import { PacketHeaderStruct } from "./types/header";
|
||||||
import { decryptPacket, encryptPacket } from "./utils/crypto";
|
import { decryptPacket, encryptPacket } from "./utils/crypto";
|
||||||
import { CommandId, ResultCodes } from "./utils/misc";
|
import { CommandId, ResultCodes } from "../../lib/constants/aimedb";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import { Config } from "lib/setup/config";
|
import { Config } from "lib/setup/config";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
@ -124,7 +124,7 @@ const AimeDBServerFactory = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header.keychipId === "ABCD1234567" || header.storeId === 0xfff0) {
|
if (header.keychipId === "ABCD1234567" || header.storeId === 0xfff0) {
|
||||||
logger.warning("Received request from uninitialized AMLib.", {
|
logger.warn("Received request from uninitialized AMLib.", {
|
||||||
ip: socket.remoteAddress,
|
ip: socket.remoteAddress,
|
||||||
header,
|
header,
|
||||||
body: packet.slice(0, 32).toString("hex"),
|
body: packet.slice(0, 32).toString("hex"),
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const AimeAccountExtendedResponseStruct = new Struct("AimeAccountExtended
|
||||||
.UInt32LE("accountId")
|
.UInt32LE("accountId")
|
||||||
.UInt8("portalRegistered")
|
.UInt8("portalRegistered")
|
||||||
.UInt8Array("padding", 3)
|
.UInt8Array("padding", 3)
|
||||||
.UInt8Array("authKey", 256)
|
.UInt8Array("segaIdAuthKey", 256)
|
||||||
.UInt32LE("relationId1")
|
.UInt32LE("relationId1")
|
||||||
.UInt32LE("relationId2")
|
.UInt32LE("relationId2")
|
||||||
.compile();
|
.compile();
|
||||||
|
|
|
@ -2,8 +2,8 @@ import Struct from "typed-struct";
|
||||||
|
|
||||||
export const FelicaLookupRequestStruct = new Struct("FelicaLookupRequest")
|
export const FelicaLookupRequestStruct = new Struct("FelicaLookupRequest")
|
||||||
.BigUInt64LE("idm")
|
.BigUInt64LE("idm")
|
||||||
.UInt8("chipCode")
|
.UInt8("romType")
|
||||||
.UInt8("osVer")
|
.UInt8("icType")
|
||||||
.UInt8Array("timing", 6)
|
.UInt8Array("timing", 6)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
@ -16,15 +16,15 @@ export const FelicaLookupResponseStruct = new Struct("FelicaLookupResponse")
|
||||||
export const FelicaExtendedLookupRequestStruct = new Struct("FelicaExtendedLookupRequest")
|
export const FelicaExtendedLookupRequestStruct = new Struct("FelicaExtendedLookupRequest")
|
||||||
.UInt8Array("randomChallenge", 16)
|
.UInt8Array("randomChallenge", 16)
|
||||||
.BigUInt64LE("idm")
|
.BigUInt64LE("idm")
|
||||||
.UInt8("chipCode")
|
.UInt8("romType")
|
||||||
.UInt8("osVer")
|
.UInt8("icType")
|
||||||
.UInt8Array("timing", 6)
|
.UInt8Array("timing", 6)
|
||||||
.UInt8Array("cardKeyVersion", 16)
|
.UInt8Array("cardKeyVersion", 16)
|
||||||
.UInt8Array("writeCount", 16)
|
.UInt8Array("writeCount", 16)
|
||||||
.BigUInt64LE("maca")
|
.BigUInt64LE("maca")
|
||||||
.UInt8("companyCode")
|
.UInt8("companyCode")
|
||||||
.UInt8("readerFirmwareVersion")
|
.UInt8("readerFirmwareVersion")
|
||||||
.UInt16LE("DFC")
|
.UInt16LE("dataFormatCode")
|
||||||
.UInt8Array("padding", 4)
|
.UInt8Array("padding", 4)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
@ -36,5 +36,5 @@ export const FelicaExtendedLookupResponseStruct = new Struct("FelicaExtendedLook
|
||||||
.UInt8("portalRegistered")
|
.UInt8("portalRegistered")
|
||||||
.UInt8("companyCode")
|
.UInt8("companyCode")
|
||||||
.UInt8Array("padding", 8)
|
.UInt8Array("padding", 8)
|
||||||
.UInt8Array("authKey", 256)
|
.UInt8Array("segaIdAuthKey", 256)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { CalculateAccessCode, CalculateCardKey } from "./access-code";
|
||||||
|
import t from "tap";
|
||||||
|
|
||||||
|
const TEST_CARD_KEY = "5B8CDAEF32960471";
|
||||||
|
|
||||||
|
t.test("#CalculateCardKey", (t) => {
|
||||||
|
t.equal(CalculateCardKey(1, TEST_CARD_KEY), "3653373");
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test("#CalculateAccessCode", (t) => {
|
||||||
|
t.ok(CalculateAccessCode(1, "02019", TEST_CARD_KEY), "02019068568453653373");
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
|
@ -2,19 +2,29 @@ import { Solitaire } from "./crypto";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import type { integer } from "types/misc";
|
import type { integer } from "types/misc";
|
||||||
|
|
||||||
function reverseString(data: string) {
|
/* eslint-disable no-bitwise */
|
||||||
return Array.from(data).reverse().join("");
|
export function CalculateCardKey(serial: integer, key: string) {
|
||||||
}
|
const keyBuffer = Buffer.from(key, "hex");
|
||||||
|
|
||||||
function CalculateCardKey(serial: integer, key: string) {
|
|
||||||
const paddedSerial = serial.toString().padStart(8, "0");
|
const paddedSerial = serial.toString().padStart(8, "0");
|
||||||
const realDigest = createHash("md5").update(paddedSerial).digest();
|
const digest = createHash("md5").update(paddedSerial).digest();
|
||||||
|
|
||||||
const digest = new Array(16);
|
let hash = 0;
|
||||||
|
let num = 0;
|
||||||
|
let storedBits = 0;
|
||||||
|
|
||||||
for (let i = 0; i < 16; i++) {
|
for (let i = 0; i < 16; i++) {
|
||||||
const idx = Number(`0x${key[i]}`);
|
// Extract the i-th hexadecimal digit of the key...
|
||||||
const nib = realDigest[idx];
|
const byte = keyBuffer[Math.trunc(i / 2)];
|
||||||
|
|
||||||
|
if (byte === undefined) {
|
||||||
|
throw new Error("Buffer.from returned an undefined value in a Buffer?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the upper byte and lower byte respectively...
|
||||||
|
const idx = i % 2 === 0 ? byte >>> 4 : byte & 0xf;
|
||||||
|
|
||||||
|
// Using it as the index to shuffle the serial MD5...
|
||||||
|
const nib = digest[idx];
|
||||||
|
|
||||||
if (nib === undefined) {
|
if (nib === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -22,26 +32,25 @@ function CalculateCardKey(serial: integer, key: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
digest[i] = nib;
|
// Store the byte into a temporary little-endian number...
|
||||||
|
num = num | (nib << storedBits);
|
||||||
|
storedBits = storedBits + 8;
|
||||||
|
|
||||||
|
// XOR every 23 bits or if it's the final iteration
|
||||||
|
if (storedBits > 23 || i === 15) {
|
||||||
|
// 0x7fffff is mask to extract the last 23 bits of num.
|
||||||
|
hash = hash ^ (num & 0x7fffff);
|
||||||
|
|
||||||
|
// Removed the bits we worked on
|
||||||
|
num = num >>> 23;
|
||||||
|
|
||||||
|
storedBits = storedBits - 23;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nasty ass bit string hacks that i am not good enough at math to replace
|
return hash.toString().padStart(7, "0");
|
||||||
let bitstring = reverseString(
|
|
||||||
digest.map((n) => reverseString(n.toString(2).padStart(8, "0"))).join("")
|
|
||||||
).padStart(6 * 23, "0");
|
|
||||||
let computed = 0;
|
|
||||||
|
|
||||||
while (bitstring) {
|
|
||||||
const work = Number(`0b${bitstring.slice(0, 23)}`);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
computed = computed ^ work;
|
|
||||||
|
|
||||||
bitstring = bitstring.slice(23);
|
|
||||||
}
|
|
||||||
|
|
||||||
return computed.toString().padStart(7, "0");
|
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-bitwise */
|
||||||
|
|
||||||
export function CalculateAccessCode(serial: integer, prefix: string, key: string): string {
|
export function CalculateAccessCode(serial: integer, prefix: string, key: string): string {
|
||||||
const digest = CalculateCardKey(serial, key);
|
const digest = CalculateCardKey(serial, key);
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
const FELICA_MOBILE_OS_VERSIONS = [0x06, 0x07, 0x10, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18] as const;
|
import { FELICA_CARD_IC_TYPES, FELICA_MOBILE_IC_TYPES } from "lib/constants/felica";
|
||||||
const FELICA_CARD_OS_VERSIONS = [0x20, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7] as const;
|
|
||||||
|
export type ValidFeliCaIcTypes =
|
||||||
|
| typeof FELICA_CARD_IC_TYPES[number]
|
||||||
|
| typeof FELICA_MOBILE_IC_TYPES[number];
|
||||||
|
|
||||||
export function IsSupportedFelicaMobile(
|
export function IsSupportedFelicaMobile(
|
||||||
osVer: number
|
icType: number
|
||||||
): osVer is typeof FELICA_MOBILE_OS_VERSIONS[number] {
|
): icType is typeof FELICA_MOBILE_IC_TYPES[number] {
|
||||||
return (FELICA_MOBILE_OS_VERSIONS as ReadonlyArray<number>).includes(osVer);
|
return (FELICA_MOBILE_IC_TYPES as ReadonlyArray<number>).includes(icType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IsSupportedFelicaCard(
|
export function IsSupportedFelicaCard(
|
||||||
osVer: number
|
icType: number
|
||||||
): osVer is typeof FELICA_CARD_OS_VERSIONS[number] {
|
): icType is typeof FELICA_CARD_IC_TYPES[number] {
|
||||||
return (FELICA_CARD_OS_VERSIONS as ReadonlyArray<number>).includes(osVer);
|
return (FELICA_CARD_IC_TYPES as ReadonlyArray<number>).includes(icType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IsSupportedFelica(
|
export function IsSupportedFelica(icType: number): icType is ValidFeliCaIcTypes {
|
||||||
osVer: number
|
return IsSupportedFelicaMobile(icType) || IsSupportedFelicaCard(icType);
|
||||||
): osVer is typeof FELICA_CARD_OS_VERSIONS[number] | typeof FELICA_MOBILE_OS_VERSIONS[number] {
|
|
||||||
return IsSupportedFelicaMobile(osVer) || IsSupportedFelicaCard(osVer);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import "express-async-errors";
|
import "express-async-errors";
|
||||||
|
|
||||||
// eslint-disable-next-line import/order
|
|
||||||
import mainRouter from "./router/router";
|
import mainRouter from "./router/router";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import { DFIRequestResponse } from "./middleware/dfi";
|
import { DFIRequestResponse } from "./middleware/dfi";
|
||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { RequestLoggerMiddleware } from "./middleware/request-logger";
|
import { RequestLoggerMiddleware } from "../../lib/middleware/request-logger";
|
||||||
import { IsRecord } from "utils/misc";
|
import { IsRecord } from "utils/misc";
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
@ -30,10 +29,16 @@ process.on("unhandledRejection", (reason, promise) => {
|
||||||
logger.error(reason, { promise });
|
logger.error(reason, { promise });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ALL.Net sends a form body that is sometimes deflated and base64-encoded.
|
// We do the body handling here so logging is properly handled.
|
||||||
// This also does some special handling to prevent keys/values from being
|
app.use((req, res, next) => {
|
||||||
// URL-escaped, which ALL.Net doesn't do. Pray they don't need an & or a =.
|
if (req.path === "/sys/servlet/PowerOn") {
|
||||||
app.use(DFIRequestResponse);
|
DFIRequestResponse(true)(req, res, next);
|
||||||
|
} else if (req.path === "/report-api/Report") {
|
||||||
|
express.json({ limit: "4mb", type: (_) => true })(req, res, next);
|
||||||
|
} else {
|
||||||
|
DFIRequestResponse(false)(req, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.method !== "GET" && (typeof req.body !== "object" || req.body === null)) {
|
if (req.method !== "GET" && (typeof req.body !== "object" || req.body === null)) {
|
||||||
|
@ -50,6 +55,12 @@ app.use(RequestLoggerMiddleware);
|
||||||
app.use("/", mainRouter);
|
app.use("/", mainRouter);
|
||||||
|
|
||||||
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) => {
|
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) => {
|
||||||
|
// eslint-disable-next-line cadence/no-instanceof
|
||||||
|
if (err instanceof SyntaxError && req.path === "/report-api/Report") {
|
||||||
|
logger.error("Could not parse download report as JSON.", { err });
|
||||||
|
return res.status(200).send("NG");
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`MAIN_ERR_HANDLER hit by request.`, { url: req.originalUrl });
|
logger.info(`MAIN_ERR_HANDLER hit by request.`, { url: req.originalUrl });
|
||||||
|
|
||||||
const unknownErr = err as unknown;
|
const unknownErr = err as unknown;
|
||||||
|
|
|
@ -6,59 +6,61 @@ import type { RequestHandler } from "express";
|
||||||
|
|
||||||
const inflateAsync = promisify(inflate);
|
const inflateAsync = promisify(inflate);
|
||||||
|
|
||||||
export const DFIRequestResponse: RequestHandler = async (req, res, next) => {
|
export const DFIRequestResponse: (mustUseDfi: boolean) => RequestHandler = (mustUseDfi) => {
|
||||||
if (Number(req.headers["content-length"] ?? 0) === 0) {
|
return async (req, res, next) => {
|
||||||
|
if (Number(req.headers["content-length"] ?? 0) === 0) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUsingDfi = req.headers.pragma?.toUpperCase() === "DFI";
|
||||||
|
|
||||||
|
if (mustUseDfi && !isUsingDfi) {
|
||||||
|
return res.status(200).send("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBody = await getRawBody(req, { encoding: "utf-8" });
|
||||||
|
|
||||||
|
let body: string;
|
||||||
|
|
||||||
|
if (isUsingDfi) {
|
||||||
|
const compressedBuffer = Buffer.from(rawBody, "base64");
|
||||||
|
const buffer = await inflateAsync(compressedBuffer);
|
||||||
|
|
||||||
|
body = buffer.toString("utf-8").trim();
|
||||||
|
} else {
|
||||||
|
body = rawBody.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys and values are not URL escaped.
|
||||||
|
// This should be fine. I think.
|
||||||
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
req.body = Object.fromEntries(body.split("&").map((s) => s.split("=")));
|
||||||
|
req.safeBody = req.body as Record<string, unknown>;
|
||||||
|
|
||||||
|
const originalSend = res.send;
|
||||||
|
|
||||||
|
res.send = (params) => {
|
||||||
|
const body = `${new URLSearchParams(params).toString()}\n`;
|
||||||
|
|
||||||
|
const encoding = req.body.encode ?? "EUC-JP";
|
||||||
|
const encodedBody = iconv.encode(body, encoding);
|
||||||
|
|
||||||
|
res.header("Content-Type", `text/plain; charset=${encoding}`);
|
||||||
|
|
||||||
|
// TODO: I don't know what black magic SEGA did, but I have been woefully
|
||||||
|
// unable to DFI-encode my responses...
|
||||||
|
return originalSend.apply(res, [encodedBody]);
|
||||||
|
|
||||||
|
// if (req.headers.pragma?.toUpperCase() !== "DFI") {
|
||||||
|
// return originalSend.apply(res, [encodedBody]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// res.header("Pragma", "DFI");
|
||||||
|
|
||||||
|
// return originalSend.apply(res, [deflateSync(encodedBody).toString("base64")]);
|
||||||
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUsingDfi = req.headers.pragma?.toUpperCase() === "DFI";
|
|
||||||
|
|
||||||
const rawBody = await getRawBody(req, { encoding: "utf-8" });
|
|
||||||
|
|
||||||
let body: string;
|
|
||||||
|
|
||||||
if (isUsingDfi) {
|
|
||||||
const compressedBuffer = Buffer.from(rawBody, "base64");
|
|
||||||
const buffer = await inflateAsync(compressedBuffer);
|
|
||||||
|
|
||||||
body = buffer.toString("utf-8").trim();
|
|
||||||
} else {
|
|
||||||
body = rawBody.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys and values are not URL escaped.
|
|
||||||
// This should be fine. I think.
|
|
||||||
// eslint-disable-next-line require-atomic-updates
|
|
||||||
req.body = Object.fromEntries(body.split("&").map((s) => s.split("=")));
|
|
||||||
|
|
||||||
const originalSend = res.send;
|
|
||||||
|
|
||||||
res.send = (params) => {
|
|
||||||
const body =
|
|
||||||
typeof params === "object"
|
|
||||||
? `${Object.entries(params)
|
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
|
||||||
.join("&")}\n`
|
|
||||||
: params;
|
|
||||||
|
|
||||||
const encoding = req.body.encode ?? "EUC-JP";
|
|
||||||
const encodedBody = iconv.encode(body, encoding);
|
|
||||||
|
|
||||||
res.header("Content-Type", `text/plain; charset=${encoding}`);
|
|
||||||
|
|
||||||
// TODO: I don't know what black magic SEGA did, but I have been woefully
|
|
||||||
// unable to DFI-encode my responses...
|
|
||||||
return originalSend.apply(res, [encodedBody]);
|
|
||||||
|
|
||||||
// if (req.headers.pragma?.toUpperCase() !== "DFI") {
|
|
||||||
// return originalSend.apply(res, [encodedBody]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// res.header("Pragma", "DFI");
|
|
||||||
|
|
||||||
// return originalSend.apply(res, [deflateSync(encodedBody).toString("base64")]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import { Machine } from "external/db/entity/allnet";
|
||||||
|
import { EventLog } from "external/db/entity/base";
|
||||||
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import { Config } from "lib/setup/config";
|
||||||
|
import { DownloadReportSchema } from "servers/allnet/types/download-report";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
const parseResult = DownloadReportSchema.safeParse(req.safeBody);
|
||||||
|
|
||||||
|
if (!parseResult.success) {
|
||||||
|
logger.error("Invalid download progress report.", {
|
||||||
|
err: fromZodError(parseResult.error).message,
|
||||||
|
});
|
||||||
|
return res.status(200).send("NG");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportData = parseResult.data.appimage ?? parseResult.data.optimage;
|
||||||
|
|
||||||
|
if (!reportData) {
|
||||||
|
logger.error(
|
||||||
|
"Invalid download progress report. Neither appimage nor optimage was available."
|
||||||
|
);
|
||||||
|
return res.status(200).send("NG");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Config.ALLNET_CONFIG.ALLOW_UNREGISTERED_SERIALS) {
|
||||||
|
const machine = await Machine.findOne({
|
||||||
|
where: {
|
||||||
|
serial: reportData.serial,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!machine) {
|
||||||
|
logger.error("Rejected download progress report from unknown serial.", { reportData });
|
||||||
|
return res.status(200).send("NG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `${reportData.serial} reported download state for ${reportData.gd} v${
|
||||||
|
reportData.dav
|
||||||
|
}: ${reportData.tdsc}/${reportData.tsc} segments downloaded for working files ${
|
||||||
|
reportData.wfl
|
||||||
|
} with ${reportData.dfl ?? "none"} complete.`;
|
||||||
|
|
||||||
|
const eventLog = EventLog.construct({
|
||||||
|
system: "allnet",
|
||||||
|
type: "DOWNLOAD_REPORT",
|
||||||
|
severity: "info",
|
||||||
|
message,
|
||||||
|
details: reportData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await eventLog.save();
|
||||||
|
|
||||||
|
return res.status(200).send("OK");
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import reportRouter from "./Report/router";
|
||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
router.use("/Report", reportRouter);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,3 +1,4 @@
|
||||||
|
import reportRouter from "./report-api/router";
|
||||||
import sysServletRouter from "./sys/servlet/router";
|
import sysServletRouter from "./sys/servlet/router";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
|
@ -8,5 +9,6 @@ router.all("/naomitest.html", (_, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use("/sys/servlet", sysServletRouter);
|
router.use("/sys/servlet", sysServletRouter);
|
||||||
|
router.use("/report-api", reportRouter);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,19 +1,44 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
import { Machine } from "external/db/entity/allnet";
|
||||||
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import { Config } from "lib/setup/config";
|
||||||
import {
|
import {
|
||||||
DownloadOrderRequestSchema,
|
DownloadOrderRequestSchema,
|
||||||
DownloadOrderStatus,
|
DownloadOrderStatus,
|
||||||
} from "servers/allnet/types/download-order";
|
} from "servers/allnet/types/download-order";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
import type { DownloadOrderResponse } from "servers/allnet/types/download-order";
|
import type { DownloadOrderResponse } from "servers/allnet/types/download-order";
|
||||||
|
|
||||||
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
const router: Router = Router({ mergeParams: true });
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
const parseResult = DownloadOrderRequestSchema.safeParse(req.safeBody);
|
const parseResult = DownloadOrderRequestSchema.safeParse(req.safeBody);
|
||||||
|
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
|
logger.error("Received invalid download order request.", {
|
||||||
|
error: fromZodError(parseResult.error).message,
|
||||||
|
body: req.safeBody,
|
||||||
|
});
|
||||||
return res.status(403).send("");
|
return res.status(403).send("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = parseResult.data;
|
||||||
|
|
||||||
|
if (!Config.ALLNET_CONFIG.ALLOW_UNREGISTERED_SERIALS) {
|
||||||
|
const machine = await Machine.findOne({
|
||||||
|
where: {
|
||||||
|
serial: data.serial,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!machine) {
|
||||||
|
logger.error("Rejected download order request from unknown serial.", { data });
|
||||||
|
return res.status(200).send({ stat: 1, serial: "", uri: "null" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Allow network delivery.
|
// TODO: Allow network delivery.
|
||||||
const response = {
|
const response = {
|
||||||
stat: DownloadOrderStatus.FAILURE,
|
stat: DownloadOrderStatus.FAILURE,
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { db } from "external/db/db";
|
import { Machine } from "external/db/entity/allnet";
|
||||||
import { arcade, machine } from "external/db/schemas";
|
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import { Config } from "lib/setup/config";
|
import { Config } from "lib/setup/config";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
@ -33,11 +31,10 @@ router.post("/", async (req, res) => {
|
||||||
return res.status(500).send();
|
return res.status(500).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatVer = req.safeBody.format_ver ?? "1";
|
const formatVer = (req.safeBody.format_ver as string | undefined) ?? "1";
|
||||||
|
|
||||||
let parseResult;
|
let parseResult;
|
||||||
|
|
||||||
// TODO: ALL.Net China
|
|
||||||
if (formatVer === "1") {
|
if (formatVer === "1") {
|
||||||
parseResult = PowerOnRequestV1Schema.safeParse(req.safeBody);
|
parseResult = PowerOnRequestV1Schema.safeParse(req.safeBody);
|
||||||
} else if (formatVer === "2") {
|
} else if (formatVer === "2") {
|
||||||
|
@ -57,8 +54,10 @@ router.post("/", async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
logger.error(`Received invalid PowerOn request: ${fromZodError(parseResult.error)}`, {
|
logger.error(`Received invalid PowerOn request.`, {
|
||||||
|
err: fromZodError(parseResult.error).message,
|
||||||
formatVer,
|
formatVer,
|
||||||
|
body: req.safeBody,
|
||||||
});
|
});
|
||||||
return res.status(400).send("");
|
return res.status(400).send("");
|
||||||
}
|
}
|
||||||
|
@ -96,14 +95,13 @@ router.post("/", async (req, res) => {
|
||||||
// TODO: Implement store authentication + fetch arcade information
|
// TODO: Implement store authentication + fetch arcade information
|
||||||
// Reference implementation: https://sega.bsnk.me/allnet/auth/power-on
|
// Reference implementation: https://sega.bsnk.me/allnet/auth/power-on
|
||||||
// For now, we just check if there's a cab registered in the database.
|
// For now, we just check if there's a cab registered in the database.
|
||||||
const cabAndStore = await db
|
const result = await Machine.findOne({
|
||||||
.select()
|
where: {
|
||||||
.from(machine)
|
serial,
|
||||||
.leftJoin(arcade, eq(arcade.id, machine.arcade_id))
|
},
|
||||||
.where(eq(machine.serial, serial))
|
});
|
||||||
.then((r) => r[0]);
|
|
||||||
|
|
||||||
if (!cabAndStore && !Config.ALLNET_CONFIG.ALLOW_UNREGISTERED_SERIALS) {
|
if (!result && !Config.ALLNET_CONFIG.ALLOW_UNREGISTERED_SERIALS) {
|
||||||
logger.error("Unregistered serial attempted ALL.Net authentication.", {
|
logger.error("Unregistered serial attempted ALL.Net authentication.", {
|
||||||
gameId,
|
gameId,
|
||||||
serial,
|
serial,
|
||||||
|
@ -115,21 +113,19 @@ router.post("/", async (req, res) => {
|
||||||
// TODO: Verify that title exists and is enabled.
|
// TODO: Verify that title exists and is enabled.
|
||||||
const serverTime = DateTime.now().setZone("Asia/Tokyo");
|
const serverTime = DateTime.now().setZone("Asia/Tokyo");
|
||||||
const baseResponse = {
|
const baseResponse = {
|
||||||
// Same thing
|
|
||||||
stat: 1,
|
stat: 1,
|
||||||
|
place_id: (result?.arcade.id ?? 0x123).toString(16).toUpperCase(),
|
||||||
place_id: (0x123).toString(16),
|
|
||||||
// uri: `http://localhost:8080/${gameId}/${titleVer.replace(/\./u, "")}/`,
|
// uri: `http://localhost:8080/${gameId}/${titleVer.replace(/\./u, "")}/`,
|
||||||
uri: `http://localhost:8080/${titleVer.replace(/\./u, "")}/`,
|
uri: `http://localhost:8080/${titleVer.replace(/\./u, "")}/`,
|
||||||
host: "localhost:8080",
|
host: "localhost:8080",
|
||||||
name: cabAndStore?.allnet_arcade?.name ?? Config.NAME,
|
name: result?.arcade.name ?? Config.NAME,
|
||||||
nickname: cabAndStore?.allnet_arcade?.nickname ?? "kozukata-toa",
|
nickname: result?.arcade.nickname ?? "kozukatatoa",
|
||||||
setting: 1,
|
setting: 1,
|
||||||
region0: cabAndStore?.allnet_arcade?.regionId ?? 1,
|
region0: result?.arcade.regionId ?? 1,
|
||||||
region_name0: cabAndStore?.allnet_arcade?.regionName0 ?? "W",
|
region_name0: result?.arcade.regionName0 ?? "W",
|
||||||
region_name1: cabAndStore?.allnet_arcade?.regionName1 ?? "",
|
region_name1: result?.arcade.regionName1 ?? "",
|
||||||
region_name2: cabAndStore?.allnet_arcade?.regionName2 ?? "",
|
region_name2: result?.arcade.regionName2 ?? "",
|
||||||
region_name3: cabAndStore?.allnet_arcade?.regionName3 ?? "",
|
region_name3: result?.arcade.regionName3 ?? "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
@ -164,7 +160,7 @@ router.post("/", async (req, res) => {
|
||||||
allnet_id: 456,
|
allnet_id: 456,
|
||||||
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||||
country: "JPN",
|
country: "JPN",
|
||||||
client_timezone: "+0900",
|
client_timezone: " 0900",
|
||||||
token: request.token ?? "null",
|
token: request.token ?? "null",
|
||||||
} satisfies PowerOnResponseV3;
|
} satisfies PowerOnResponseV3;
|
||||||
} else if (request.format_ver === "3" && "game_ver" in request) {
|
} else if (request.format_ver === "3" && "game_ver" in request) {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const zodOptionalInteger = z.number().int().optional();
|
||||||
|
|
||||||
|
export const DownloadOptImageReportSchema = z.object({
|
||||||
|
serial: z.string().min(1).max(11),
|
||||||
|
dfl: z.array(z.string().min(1)).optional(),
|
||||||
|
wfl: z.array(z.string().min(1)).optional(),
|
||||||
|
tsc: z.number().int(),
|
||||||
|
tdsc: z.number().int(),
|
||||||
|
at: zodOptionalInteger,
|
||||||
|
ot: zodOptionalInteger,
|
||||||
|
rt: zodOptionalInteger,
|
||||||
|
as: z.number().int().gte(1).lte(2),
|
||||||
|
rf_state: zodOptionalInteger,
|
||||||
|
gd: z.string(),
|
||||||
|
dav: z.string(),
|
||||||
|
dov: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DownloadAppImageReportSchema = DownloadOptImageReportSchema.extend({
|
||||||
|
wdav: z.string(),
|
||||||
|
wdov: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DownloadReportSchema = z
|
||||||
|
.object({
|
||||||
|
appimage: DownloadAppImageReportSchema.optional(),
|
||||||
|
optimage: DownloadOptImageReportSchema.optional(),
|
||||||
|
})
|
||||||
|
// One of these must be present
|
||||||
|
.refine(
|
||||||
|
(arg) => !!arg.appimage || !!arg.optimage,
|
||||||
|
"At least one of 'appimage' or 'optimage' must be available"
|
||||||
|
);
|
|
@ -1,16 +1,18 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { integer } from "types/misc";
|
import type { integer } from "types/misc";
|
||||||
|
|
||||||
|
const zodCoerceOptionalFiveDigitInteger = z.coerce.number().int().gte(-99999).lte(99999).optional();
|
||||||
|
|
||||||
export const PowerOnRequestV1Schema = z.object({
|
export const PowerOnRequestV1Schema = z.object({
|
||||||
game_id: z.string().max(5),
|
game_id: z.string().max(5),
|
||||||
ver: z.string().max(5),
|
ver: z.string().max(5),
|
||||||
serial: z.string().max(11),
|
serial: z.string().max(11),
|
||||||
ip: z.string().ip(),
|
ip: z.string().ip(),
|
||||||
firm_ver: z.coerce.number().optional(),
|
firm_ver: zodCoerceOptionalFiveDigitInteger,
|
||||||
boot_ver: z.coerce.number().optional(),
|
boot_ver: zodCoerceOptionalFiveDigitInteger,
|
||||||
encode: z.enum(["Shift_JIS", "EUC-JP", "UTF-8"]).default("EUC-JP"),
|
encode: z.enum(["Shift_JIS", "EUC-JP", "UTF-8"]).default("EUC-JP"),
|
||||||
format_ver: z.literal("1"),
|
format_ver: z.literal("1").optional(),
|
||||||
hops: z.coerce.number().default(-1),
|
hops: z.coerce.number().int().gte(-99999).lte(99999).default(-1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PowerOnRequestV1 = z.infer<typeof PowerOnRequestV1Schema>;
|
export type PowerOnRequestV1 = z.infer<typeof PowerOnRequestV1Schema>;
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as allnetServer } from "./allnet";
|
export { default as allnetServer } from "./allnet";
|
||||||
export { default as aimeDbServerFactory } from "./aimedb";
|
export { default as aimeDbServerFactory } from "./aimedb";
|
||||||
|
export { default as titleServer } from "./titles";
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { VERSIONS } from "./versions";
|
||||||
|
import { GetClassMethods } from "../utils/reflection";
|
||||||
|
import compression from "compression";
|
||||||
|
import { Router, json } from "express";
|
||||||
|
import { ChunithmVersions } from "lib/constants/game-versions";
|
||||||
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import { RequestLoggerMiddleware } from "lib/middleware/request-logger";
|
||||||
|
import { Config, Environment } from "lib/setup/config";
|
||||||
|
import getRawBody from "raw-body";
|
||||||
|
import { createDecipheriv, pbkdf2Sync } from "crypto";
|
||||||
|
import type { RequestHandler } from "express";
|
||||||
|
|
||||||
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
|
const router: Router = Router({ mergeParams: false });
|
||||||
|
|
||||||
|
router.use(compression({ threshold: 0 }));
|
||||||
|
|
||||||
|
router.use(async (req, res, next) => {
|
||||||
|
if ((req.headers["content-length"] ?? 0) === 0) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /SDHD/210/UpsertUserAllApi
|
||||||
|
const splitPath = req.path.split("/");
|
||||||
|
const version = splitPath[2];
|
||||||
|
const versionNum = Number(version);
|
||||||
|
const endpoint = splitPath.at(-1);
|
||||||
|
|
||||||
|
if (!endpoint || !version || Number.isNaN(versionNum)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEncryptedRequest =
|
||||||
|
endpoint.length === 32 && [...endpoint].every((c) => "0123456789abcdefABCDEF".includes(c));
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isEncryptedRequest &&
|
||||||
|
Config.CHUNITHM_CONFIG.CRYPTO.ENCRYPTED_ONLY &&
|
||||||
|
versionNum >= ChunithmVersions.CRYSTAL_PLUS
|
||||||
|
) {
|
||||||
|
logger.error(
|
||||||
|
"Rejecting unencrypted request, because the server is configured to accept only encrypted requests.",
|
||||||
|
{ version, endpoint }
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({ stat: "0" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEncryptedRequest) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Received encrypted request.", { version, endpoint });
|
||||||
|
|
||||||
|
const keys = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[version];
|
||||||
|
|
||||||
|
if (!keys) {
|
||||||
|
logger.error("No keys were configured for the requested version.", {
|
||||||
|
version,
|
||||||
|
endpoint,
|
||||||
|
});
|
||||||
|
return res.status(200).send({ stat: "0" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostly as a convenience, so versions PARADISE LOST and older don't have to provide
|
||||||
|
// the path salt.
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
|
||||||
|
if (versionNum <= ChunithmVersions.PARADISE && userAgent) {
|
||||||
|
const unhashedEndpoint = userAgent.split("#")[0];
|
||||||
|
|
||||||
|
if (unhashedEndpoint) {
|
||||||
|
req.url = req.url.replace(endpoint, unhashedEndpoint);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"User-Agent did not contain the unhashed endpoint for version PARADISE (LOST) or older?",
|
||||||
|
{ version, userAgent }
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({ stat: "0" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = keys[0];
|
||||||
|
const iv = keys[1];
|
||||||
|
|
||||||
|
if (!key || !iv) {
|
||||||
|
logger.error("Key or IV was not provided for the requested version.", {
|
||||||
|
version,
|
||||||
|
key,
|
||||||
|
iv,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({ stat: "0" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = createDecipheriv("aes-256-cbc", Buffer.from(key, "hex"), Buffer.from(iv, "hex"));
|
||||||
|
const body = await getRawBody(req);
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
req.body = Buffer.concat([cipher.update(body), cipher.final()]);
|
||||||
|
|
||||||
|
// TODO: Encrypt response
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
if (Environment.nodeEnv === "dev" && req.header("x-debug") !== undefined) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get express.json() to recognize
|
||||||
|
req.headers["content-encoding"] = "deflate";
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// All requests to the title server will be JSON, I promise!
|
||||||
|
router.use(json({ type: (_) => true }));
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
// Always mount an empty req body. We operate under the assumption that req.body is
|
||||||
|
// always defined as atleast an object.
|
||||||
|
if (req.method !== "GET" && (typeof req.body !== "object" || req.body === null)) {
|
||||||
|
req.body = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.safeBody *is* just a type-safe req.body!
|
||||||
|
req.safeBody = req.body as Record<string, unknown>;
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use(RequestLoggerMiddleware);
|
||||||
|
|
||||||
|
for (const Title of VERSIONS) {
|
||||||
|
const inst = new Title();
|
||||||
|
|
||||||
|
const endpointSaltString = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[inst.version]?.[2];
|
||||||
|
const iterations = inst.numericVersion >= ChunithmVersions.SUN ? 70 : 44;
|
||||||
|
|
||||||
|
const key = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[inst.version]?.[0];
|
||||||
|
const iv = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[inst.version]?.[1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
Config.CHUNITHM_CONFIG.CRYPTO.ENCRYPTED_ONLY &&
|
||||||
|
inst.numericVersion >= ChunithmVersions.CRYSTAL_PLUS &&
|
||||||
|
(!key || !iv)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`Disabling ${inst.gameCode} v${inst.version}, because ENCRYPTED_ONLY was enabled but no keys are provided.`,
|
||||||
|
{
|
||||||
|
gameCode: inst.gameCode,
|
||||||
|
version: inst.version,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Config.CHUNITHM_CONFIG.CRYPTO.ENCRYPTED_ONLY &&
|
||||||
|
inst.numericVersion >= ChunithmVersions.NEW &&
|
||||||
|
!endpointSaltString
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`Disabling ${inst.gameCode} v${inst.version}, because ENCRYPTED_ONLY was enabled but no endpoint salt was provided.`,
|
||||||
|
{
|
||||||
|
gameCode: inst.gameCode,
|
||||||
|
version: inst.version,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subrouter: Router = Router({ mergeParams: false });
|
||||||
|
const methods = GetClassMethods(Title).filter((m) => m.startsWith("handle_"));
|
||||||
|
const endpointSalt = endpointSaltString ? Buffer.from(endpointSaltString, "hex") : null;
|
||||||
|
|
||||||
|
for (const method of methods) {
|
||||||
|
const methodName = method.split("_")[1];
|
||||||
|
|
||||||
|
if (!methodName) {
|
||||||
|
logger.warn(
|
||||||
|
"Method starts with 'handle_', but cannot get the part after the underscore?",
|
||||||
|
{
|
||||||
|
method,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl = (inst as unknown as Record<string, unknown>)[method] as RequestHandler;
|
||||||
|
const implWrapper: RequestHandler = (req, res, next) => {
|
||||||
|
impl.apply(inst, [req, res, next]);
|
||||||
|
};
|
||||||
|
|
||||||
|
subrouter.post(`/${methodName}`, implWrapper);
|
||||||
|
|
||||||
|
if (endpointSalt) {
|
||||||
|
const hash = pbkdf2Sync(methodName, endpointSalt, iterations, 16, "sha1").toString(
|
||||||
|
"hex"
|
||||||
|
);
|
||||||
|
|
||||||
|
subrouter.post(`/${hash}`, implWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.use(`/${inst.gameCode}/${inst.version}/${inst.servletName}`, subrouter);
|
||||||
|
|
||||||
|
// TODO: Add to a title registry
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { ChunithmStaticCharge, ChunithmStaticEvent } from "external/db/entity/chunithm/static";
|
||||||
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { BaseTitle } from "servers/titles/types/titles";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { ITitle } from "servers/titles/types/titles";
|
||||||
|
|
||||||
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
|
export class Chunithm extends BaseTitle implements ITitle {
|
||||||
|
private readonly dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
super(gameCode ?? "SDBT", version ?? "100", servletName ?? "ChuniServlet");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createGetGameSettingsApiResponse(_req: Request, _res: Response) {
|
||||||
|
// HACK: This is not the way to go if you're somehow hosting an arcade server
|
||||||
|
// from my code, and wants your machines to actually restart sometimes.
|
||||||
|
const rebootStartTime = DateTime.now().minus({ hours: 4 }).toFormat(this.dateTimeFormat);
|
||||||
|
const rebootEndTime = DateTime.now().minus({ hours: 3 }).toFormat(this.dateTimeFormat);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameSetting: {
|
||||||
|
dataVersion: "1.00.00",
|
||||||
|
isMaintenance: "false",
|
||||||
|
requestInterval: "10",
|
||||||
|
rebootStartTime,
|
||||||
|
rebootEndTime,
|
||||||
|
isBackgroundDistribute: "false",
|
||||||
|
maxCountCharacter: "300",
|
||||||
|
maxCountItem: "300",
|
||||||
|
maxCountMusic: "300",
|
||||||
|
},
|
||||||
|
isDumpUpload: "false",
|
||||||
|
isAou: "false",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note override {@link createGetGameSettingsApiResponse} in order to modify this
|
||||||
|
* method's response
|
||||||
|
* @since CHUNITHM
|
||||||
|
*/
|
||||||
|
handle_GetGameSettingsApi(req: Request, res: Response) {
|
||||||
|
const resp = this.createGetGameSettingsApiResponse(req, res);
|
||||||
|
|
||||||
|
return res.status(200).send(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_UpsertClientSettingApi(req: Request, res: Response) {
|
||||||
|
return res.status(200).send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_UpsertClientTestmodeApi(req: Request, res: Response) {
|
||||||
|
return res.status(200).send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetGameIdlistApi(req: Request, res: Response) {
|
||||||
|
return res.status(200).send({ type: req.safeBody.type, length: "0", gameIdlistList: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle_GetGameEventApi(req: Request, res: Response) {
|
||||||
|
const events = await ChunithmStaticEvent.find({
|
||||||
|
where: {
|
||||||
|
enabled: true,
|
||||||
|
version: this.numericVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
logger.warn("No events are enabled.", {
|
||||||
|
gameCode: this.gameCode,
|
||||||
|
version: this.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: We may want to rotate events...
|
||||||
|
return res.send({
|
||||||
|
type: req.safeBody.type,
|
||||||
|
length: events.length.toString(),
|
||||||
|
gameEventList: events.map((e) => ({
|
||||||
|
id: e.eventId.toString(),
|
||||||
|
type: e.type.toString(),
|
||||||
|
startDate: DateTime.fromJSDate(e.startDate).toFormat(this.dateTimeFormat),
|
||||||
|
endDate: "2099-12-31 00:00:00",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetGameRankingApi(req: Request, res: Response) {
|
||||||
|
// TODO
|
||||||
|
return res.send({
|
||||||
|
type: req.safeBody.type,
|
||||||
|
length: "0",
|
||||||
|
gameRankingList: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle_GetGameChargeApi(req: Request, res: Response) {
|
||||||
|
const charges = await ChunithmStaticCharge.find({
|
||||||
|
where: {
|
||||||
|
enabled: true,
|
||||||
|
version: this.numericVersion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (charges.length === 0) {
|
||||||
|
logger.warn("No charges (tickets) are enabled.", {
|
||||||
|
gameCode: this.gameCode,
|
||||||
|
version: this.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK
|
||||||
|
return res.send({
|
||||||
|
length: charges.length.toString(),
|
||||||
|
gameChargeList: charges.map((c, i) => ({
|
||||||
|
orderId: i.toString(),
|
||||||
|
chargeId: c.chargeId.toString(),
|
||||||
|
price: "1",
|
||||||
|
startDate: "2017-12-05 07:00:00.0",
|
||||||
|
endDate: "2099-12-31 00:00:00.0",
|
||||||
|
salePrice: "1",
|
||||||
|
saleStartDate: "2017-12-05 07:00:00.0",
|
||||||
|
saleEndDate: "2099-12-31 00:00:00.0",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserPreviewApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GameLoginApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserDataApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserOptionApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserCharacterApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserActivityApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserItemApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserRecentRatingApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserMusicApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserRegionApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserFavoriteItemApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserLoginBonusApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserMapAreaApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GetUserSymbolChatSettingApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since NEW
|
||||||
|
*/
|
||||||
|
handle_GetUserNetBattleDataApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_GameLogoutApi(req: Request, res: Response) {
|
||||||
|
return res.status(200).send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Chunithm } from "./100-base";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
|
||||||
|
export class ChunithmAirPlus extends Chunithm {
|
||||||
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
super(gameCode ?? "SDBT", version ?? "125", servletName ?? "ChuniServlet");
|
||||||
|
}
|
||||||
|
|
||||||
|
override createGetGameSettingsApiResponse(req: Request, res: Response) {
|
||||||
|
const resp = super.createGetGameSettingsApiResponse(req, res);
|
||||||
|
|
||||||
|
resp.gameSetting.dataVersion = "1.25.00";
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since AIR PLUS
|
||||||
|
*/
|
||||||
|
handle_GetUserCourseApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ChunithmAirPlus } from "./125-airplus";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
|
||||||
|
export class ChunithmAmazon extends ChunithmAirPlus {
|
||||||
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
super(gameCode ?? "SDBT", version ?? "130", servletName ?? "ChuniServlet");
|
||||||
|
}
|
||||||
|
|
||||||
|
override createGetGameSettingsApiResponse(req: Request, res: Response) {
|
||||||
|
const resp = super.createGetGameSettingsApiResponse(req, res);
|
||||||
|
|
||||||
|
resp.gameSetting.dataVersion = "1.30.00";
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since AMAZON
|
||||||
|
*/
|
||||||
|
handle_GetUserDuelApi(req: Request, res: Response) {
|
||||||
|
throw new Error("Unimplemented");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { ChunithmAmazon } from "./130-amazon";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
|
||||||
|
export class ChunithmAmazonPlus extends ChunithmAmazon {
|
||||||
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
super(gameCode ?? "SDBT", version ?? "135", servletName ?? "ChuniServlet");
|
||||||
|
}
|
||||||
|
|
||||||
|
override createGetGameSettingsApiResponse(req: Request, res: Response) {
|
||||||
|
const resp = super.createGetGameSettingsApiResponse(req, res);
|
||||||
|
|
||||||
|
resp.gameSetting.dataVersion = "1.35.00";
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since AMAZON PLUS
|
||||||
|
*/
|
||||||
|
handle_GetUserFavoriteMusicApi(req: Request, res: Response) {
|
||||||
|
// TODO: Add a web UI for favorites
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
length: "0",
|
||||||
|
userFavoriteMusicList: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { ChunithmAmazonPlus } from "./135-amazonplus";
|
||||||
|
import { Config } from "lib/setup/config";
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
|
||||||
|
export class ChunithmCrystal extends ChunithmAmazonPlus {
|
||||||
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
super(gameCode ?? "SDBT", version ?? "140", servletName ?? "ChuniServlet");
|
||||||
|
}
|
||||||
|
|
||||||
|
override createGetGameSettingsApiResponse(req: Request, res: Response) {
|
||||||
|
const resp = super.createGetGameSettingsApiResponse(req, res);
|
||||||
|
|
||||||
|
resp.gameSetting.dataVersion = "1.40.00";
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since CRYSTAL
|
||||||
|
*/
|
||||||
|
handle_GetUserTeamApi(req: Request, res: Response) {
|
||||||
|
// TODO
|
||||||
|
const teamName = Config.CHUNITHM_CONFIG.MODS.TEAM_NAME;
|
||||||
|
|
||||||
|
if (!teamName) {
|
||||||
|
return res.send({
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
teamId: "0",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
teamId: "1",
|
||||||
|
teamRank: "1",
|
||||||
|
teamName,
|
||||||
|
userTeamPoint: {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
teamId: 1,
|
||||||
|
orderId: 1,
|
||||||
|
teamPoint: 1,
|
||||||
|
aggrDate: req.safeBody.playDate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since CRYSTAL
|
||||||
|
*/
|
||||||
|
handle_GetTeamCourseSettingApi(req: Request, res: Response) {
|
||||||
|
// TODO
|
||||||
|
return res.send({
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
length: "0",
|
||||||
|
nextIndex: "0",
|
||||||
|
teamCourseSettingList: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since CRYSTAL
|
||||||
|
*/
|
||||||
|
handle_GetTeamCourseRuleApi(req: Request, res: Response) {
|
||||||
|
// TODO
|
||||||
|
return res.send({
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
length: "0",
|
||||||
|
nextIndex: "0",
|
||||||
|
teamCourseRuleList: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Chunithm } from "./100-base";
|
||||||
|
import { ChunithmAirPlus } from "./125-airplus";
|
||||||
|
import { ChunithmAmazon } from "./130-amazon";
|
||||||
|
import { ChunithmAmazonPlus } from "./135-amazonplus";
|
||||||
|
import { ChunithmCrystal } from "./140-crystal";
|
||||||
|
import type { BaseTitle } from "servers/titles/types/titles";
|
||||||
|
|
||||||
|
export const VERSIONS: Array<typeof BaseTitle> = [
|
||||||
|
Chunithm,
|
||||||
|
ChunithmAirPlus,
|
||||||
|
ChunithmAmazon,
|
||||||
|
ChunithmAmazonPlus,
|
||||||
|
ChunithmCrystal,
|
||||||
|
];
|
|
@ -0,0 +1,31 @@
|
||||||
|
// THIS IMPORT **MUST** GO HERE. DO NOT MOVE IT. IT MUST OCCUR BEFORE ANYTHING HAPPENS WITH EXPRESS
|
||||||
|
// BUT AFTER EXPRESS IS IMPORTED.
|
||||||
|
/* eslint-disable import/order */
|
||||||
|
import express from "express";
|
||||||
|
import "express-async-errors";
|
||||||
|
|
||||||
|
import chunithmRouter from "./chunithm";
|
||||||
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import type { Express } from "express";
|
||||||
|
|
||||||
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
|
const app: Express = express();
|
||||||
|
|
||||||
|
// Pass the IP of the user up our increasingly insane chain of nginx/docker nonsense
|
||||||
|
app.set("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
|
||||||
|
|
||||||
|
// we don't allow nesting in query strings.
|
||||||
|
app.set("query parser", "simple");
|
||||||
|
|
||||||
|
// taken from https://nodejs.org/api/process.html#process_event_unhandledrejection
|
||||||
|
// to avoid future deprecation.
|
||||||
|
process.on("unhandledRejection", (reason, promise) => {
|
||||||
|
// @ts-expect-error reason is an error, and the logger can handle errors
|
||||||
|
// it just refuses.
|
||||||
|
logger.error(reason, { promise });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/", chunithmRouter);
|
||||||
|
|
||||||
|
export default app;
|
|
@ -0,0 +1,20 @@
|
||||||
|
export interface ITitle {
|
||||||
|
gameCode: string;
|
||||||
|
version: string;
|
||||||
|
servletName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseTitle implements ITitle {
|
||||||
|
gameCode: string;
|
||||||
|
version: string;
|
||||||
|
servletName: string;
|
||||||
|
numericVersion: number;
|
||||||
|
|
||||||
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
this.gameCode = gameCode ?? "";
|
||||||
|
this.version = version?.replace(/\./gu, "") ?? "";
|
||||||
|
this.servletName = servletName ?? "";
|
||||||
|
|
||||||
|
this.numericVersion = Number(this.version);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* eslint-disable cadence/no-instanceof */
|
||||||
|
|
||||||
|
function IsRecord<T = string>(v: unknown): v is Record<string, T> {
|
||||||
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetClassMethods(className: unknown): Array<string> {
|
||||||
|
if (typeof className !== "function" || !("prototype" in className)) {
|
||||||
|
throw new Error("Not a class");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRecord(className.prototype)) {
|
||||||
|
throw new Error("Not a class");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = new Set<string>();
|
||||||
|
|
||||||
|
function methods(obj: Record<number | string | symbol, unknown> | null | undefined) {
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ps = Object.getOwnPropertyNames(obj);
|
||||||
|
|
||||||
|
for (const p of ps) {
|
||||||
|
try {
|
||||||
|
if (obj[p] instanceof Function) {
|
||||||
|
ret.add(p);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
methods(Object.getPrototypeOf(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
methods(className.prototype);
|
||||||
|
|
||||||
|
return Array.from(ret);
|
||||||
|
}
|
|
@ -15,8 +15,8 @@
|
||||||
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
"lib": ["ES2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
"lib": ["ES2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
"experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
/* Type Checking */
|
/* Type Checking */
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
"strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
|
Loading…
Reference in New Issue