[mod] TSRPC

This commit is contained in:
建喵 2023-08-31 19:28:35 +08:00
parent f7559a5f27
commit e743a53f18
35 changed files with 2494 additions and 476 deletions

9
.env Normal file
View File

@ -0,0 +1,9 @@
URLPATH = /linewebhook
PORT = 3003
# DB----------------------------------------------
DB_HOST = 192.168.0.15
DB_PORT = 3307
DB_USER = jianmiau
DB_PASSWORD = VQ*ZetC7xcc9%dTW
DB_DATABASE = badminton

368
.eslintrc.json Normal file
View File

@ -0,0 +1,368 @@
{
"env": {
"browser": true,
"es2021": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
"react-hooks",
"prettier"
],
"rules": {
"no-alert": 0, //使alert confirm prompt
"no-bitwise": 0, //使
"no-console": "off", //使console
"no-continue": 0, //使continue
"no-debugger": 2, //使debugger
"no-delete-var": 2, //var使delete
"no-div-regex": 1, //使/=foo/
"no-dupe-args": 2, //
"no-duplicate-case": 2, //switchcase
"no-else-return": "off", //ifreturn,else
"no-empty-label": "off", //使label
"no-eq-null": "off", //null使==!=
"no-extend-native": "off", //native
"no-extra-parens": "off", //
"no-extra-semi": 2, //
"no-floating-decimal": 2, //0 .5 3.
"no-implicit-coercion": "off", //
"no-inline-comments": 0, //
"no-invalid-this": 2, //this
"no-iterator": 2, //使__iterator__
"no-lonely-if": "off", //elseif
"no-mixed-requires": [
0,
false
], //
"no-mixed-spaces-and-tabs": "off", //tab
"no-multiple-empty-lines": [
1,
{
"max": 2
}
], //2
"no-nested-ternary": 0, //使
"no-new": "off", //使new
"no-new-require": 2, //使new require
"no-param-reassign": "off", //
"no-path-concat": 0, //node使__dirname__filename
"no-plusplus": 0, //使++--
"no-process-env": 0, //使process.env
"no-process-exit": 0, //使process.exit()
"no-redeclare": "off", //
"no-restricted-modules": 0, //使
"no-return-assign": "off", //return
"no-self-compare": 2, //
"no-sequences": 0, //使
"no-shadow": "off", //
"no-sync": 0, //nodejs
"no-ternary": 0, //使
"no-this-before-super": 0, //super()使thissuper
"no-throw-literal": 2, // throw "error";
"no-undef": "off", //
"no-undef-init": "off", //undefined
"no-undefined": "off", //使undefined
"no-unexpected-multiline": 2, //
"no-underscore-dangle": "off", //_
"no-unneeded-ternary": 2, // var isYes = answer === 1 ? true : false;
"no-unused-expressions": "off", //
"no-unused-vars": "off", //使
"no-use-before-define": "off", //使
"no-useless-call": "off", //callapply
"no-void": "off", //void
"no-var": 0, //varletconst
"no-warning-comments": "off", //
"no-array-constructor": "error", // 使
"no-caller": "error", // 使arguments.callerarguments.callee
"no-catch-shadow": "error", // catch
"no-class-assign": "error", //
"no-cond-assign": [
"error",
"except-parens"
], // 使
"no-constant-condition": "error", // 使 if(true) if(1)
"no-control-regex": "error", // 使
"no-dupe-keys": "error", // {a: 1, a: 1}
"no-empty": "error", //
"no-empty-character-class": "error", // []
"no-eval": "error", // 使eval
"no-ex-assign": "error", // catch
"no-extra-bind": "error", //
"no-extra-boolean-cast": "off", // bool
"no-fallthrough": "error", // switch穿
"no-func-assign": "error", //
"no-implied-eval": "error", // 使eval
"no-inner-declarations": "off", // 使
"no-invalid-regexp": "error", //
"no-irregular-whitespace": "error", //
"no-label-var": "error", // labelvar
"no-labels": "error", //
"no-lone-blocks": "error", //
"no-loop-func": "error", // 使
"no-multi-spaces": "error", //
"no-multi-str": "error", // \
"no-native-reassign": "error", // native
"no-negated-in-lhs": "error", // in !
"no-new-func": "error", // 使new Function
"no-new-object": "error", // 使new Object()
"no-new-wrappers": "error", // 使newnew String new Boolean new Number
"no-obj-calls": "error", // Math() JSON()
"no-octal": "error", // 使(0)
"no-octal-escape": "error", // 使
"no-proto": "error", // 使__proto__(__proto__)
"no-regex-spaces": "error", // 使 /foo bar/
"no-script-url": "off", // 使javascript:void(0)
"no-shadow-restricted-names": "error", // 使
"no-spaced-func": "error", // ()
"no-sparse-arrays": "error", // [1,,2]
"no-trailing-spaces": [
"error",
{
"skipBlankLines": true
}
], // ( )
"no-unreachable": "error", //
"no-const-assign": "error", // const
"no-with": "error", // with
"comma-dangle": "off", //
"comma-spacing": "error", // 西
"curly": [
"error",
"multi-line"
], // 使 {}
"eqeqeq": "off", // 使
"indent": [
"off",
"tab",
{
"SwitchCase": 1
}
], // tab
"key-spacing": [
"error",
{
"beforeColon": false,
"afterColon": true
}
], //
"keyword-spacing": "off", //
"new-parens": "error", // new const person = new Person();
"quotes": [
"error",
"double",
{
"allowTemplateLiterals": true
}
], // ''
"semi": [
"error",
"always"
], //
"semi-spacing": [
0,
{
"before": false,
"after": true
}
], // 西
"space-before-blocks": [
"error",
"always"
], // {
// "space-before-function-paren": ["error", "never"], //
"space-infix-ops": "error", // a + b
"space-unary-ops": [
"error",
{
"words": true,
"nonwords": false
}
], // / new Foo 1++
// "spaced-comment": ["error", "always", { "markers": ["*!"] }], //
"strict": [
"error",
"global"
], // 使
"use-isnan": "error", // 使NaNisNaN()
"arrow-parens": 0, //
"arrow-spacing": 0, //=>/
"accessor-pairs": 0, //使getter/setter
"block-scoped-var": 0, //使var
"brace-style": "off", //
"callback-return": "off", //
"comma-style": [
"error",
"last"
], //
"complexity": [
0,
11
], //
"computed-property-spacing": [
0,
"never"
], //
"consistent-return": 0, //return
"consistent-this": "off", //this
"constructor-super": 0, //supersuper
"default-case": "off", //switchdefault
"dot-location": 0, //访
"dot-notation": [
0,
{
"allowKeywords": true
}
], //
"eol-last": 0, //
"func-names": 0, //
"func-style": [
0,
"declaration"
], //使/
"generator-star-spacing": 0, //*
"guard-for-in": 0, //for inif
"handle-callback-err": 0, //nodejs
"id-length": 0, //
"init-declarations": 0, //
"lines-around-comment": 0, ///
"max-depth": [
0,
4
], //
"max-len": [
0,
80,
4
], //
"max-nested-callbacks": [
0,
2
], //
"max-params": [
0,
3
], //3
"max-statements": [
0,
10
], //
"new-cap": "off", //使newnew
"newline-after-var": "off", //
"object-shorthand": 0, //
"one-var": "off", //
"operator-assignment": [
0,
"always"
], // += -=
"operator-linebreak": "off", //
"padded-blocks": 0, //
"prefer-spread": 0, //
"prefer-reflect": 0, //Reflect
"quote-props": "off", //
"radix": "off", //parseInt
"id-match": 0, //
"sort-vars": 0, //
"space-after-keywords": [
0,
"always"
], //
"space-before-function-paren": [
0,
"always"
], //
"space-in-parens": [
0,
"never"
], //
"space-return-throw-case": "off", //return throw case
"spaced-comment": 0, //
"valid-jsdoc": 0, //jsdoc
"valid-typeof": "error", //使typeof
"vars-on-top": "error", //var
"wrap-iife": [
"error",
"inside"
], //
"wrap-regex": 0, //
"yoda": [
"error",
"never"
], //
"linebreak-style": [
0,
"windows"
], //
"array-bracket-spacing": [
2,
"never"
], //
"react/react-in-jsx-scope": "off",
"camelcase": "off",
"block-spacing": "error",
"no-duplicate-imports": "error",
"require-yield": "off",
"prefer-const": "off",
"object-curly-spacing": [
"error",
"always"
],
"react/jsx-curly-spacing": [
"error",
"never"
],
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/prefer-namespace-keyword": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/explicit-function-return-type": [
"off",
{
"allowExpressions": true
}
],
"@typescript-eslint/typedef": [
"warn",
{
"arrayDestructuring": false,
"arrowParameter": false,
"objectDestructuring": false,
"memberVariableDeclaration": true,
"parameter": true,
"propertyDeclaration": true,
"variableDeclaration": false,
"variableDeclarationIgnoreFunction": true
}
]
},
"settings": {
"import/resolver": {
"typescript": {}
},
"react": {
"version": "detect"
}
}
}

8
.gitignore vendored
View File

@ -1,7 +1,3 @@
node_modules node_modules
.env dist
package-lock.json .DS_STORE
*.pem
.foreverignore
.vscode
/yarn.lock

11
.mocharc.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
require: [
'ts-node/register',
],
timeout: 999999,
exit: true,
spec: [
'./test/**/*.test.ts'
],
'preserve-symlinks': true
}

22
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
// 使 IntelliSense
//
// : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to Remote",
// "address": "jianmiau.tk:9229/87f42d5b-97bf-4d25-a4d7-37306985459a",
"address": "192.168.5.36:9229/87f42d5b-97bf-4d25-a4d7-37306985459a",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "D:/Users/JianMiau/Downloads/guesswhoiams/backend",
// "remoteRoot": "/volume1/homes/JianMiau/www/line-bot-ts",
"skipFiles": [
"<node_internals>/**"
]
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@ -1,25 +1,30 @@
# sudo docker build -t linebotts . FROM node
# sudo docker exec -it 2e8e3995aa52 /bin/bash
# 選擇node # 使用淘宝 NPM 镜像(国内机器构建推荐启用)
FROM node:19.4.0 # RUN npm config set registry https://registry.npm.taobao.org/
# 指定NODE_ENV為production # npm install
ENV NODE_ENV=production ADD package*.json /src/
WORKDIR /src
RUN npm i
# build
ADD . /src
RUN npm run build
# clean
RUN npm prune --production
# move
RUN rm -rf /app \
&& mv dist /app \
&& mv node_modules /app/ \
&& rm -rf /src
# ENV
ENV NODE_ENV production
EXPOSE 3000
# 指定預設/工作資料夾
WORKDIR /app WORKDIR /app
CMD node index.js
# 只copy package.json檔案
COPY ["package.json", "./"]
# 安裝dependencies
# If you are building your code for production
# RUN npm ci --only=production
RUN npm install
# copy其餘目錄及檔案
COPY . .
# 指定啟動container後執行命令
CMD [ "npm", "start" ]

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# TSRPC Server
## Usage
### Local dev server
Dev server would restart automatically when code changed.
```
npm run dev
```
### Build
```
npm run build
```
### Generate API document
Generate API document in swagger/openapi and markdown format.
```shell
npm run doc
```
### Run unit Test
Execute `npm run dev` first, then execute:
```
npm run test
```
---

164
docs/openapi.json Normal file
View File

@ -0,0 +1,164 @@
{
"openapi": "3.0.0",
"info": {
"title": "TSRPC Open API",
"version": "1.0.0"
},
"paths": {
"/Send": {
"post": {
"operationId": "Send",
"requestBody": {
"description": "Req<Send>",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PtlSend_ReqSend"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"description": "ApiReturn<ResSend>",
"properties": {
"isSucc": {
"type": "boolean",
"enum": [
true
],
"default": true
},
"res": {
"$ref": "#/components/schemas/PtlSend_ResSend"
}
}
}
}
}
},
"default": {
"description": "Error",
"$ref": "#/components/responses/error"
}
}
}
}
},
"components": {
"schemas": {
"MsgChat_MsgChat": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"time": {
"type": "string",
"format": "date-time"
}
},
"required": [
"content",
"time"
]
},
"PtlSend_ReqSend": {
"type": "object",
"properties": {
"content": {
"type": "string"
}
},
"required": [
"content"
]
},
"PtlSend_ResSend": {
"type": "object",
"properties": {
"time": {
"type": "string",
"format": "date-time"
}
},
"required": [
"time"
]
},
"?bson_ObjectID": {
"type": "string"
},
"?bson_ObjectId": {
"type": "string"
},
"?mongodb_ObjectID": {
"type": "string"
},
"?mongodb_ObjectId": {
"type": "string"
}
},
"responses": {
"error": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"title": "API 错误",
"description": "业务错误ApiError返回 HTTP 状态码 200其它错误返回 HTTP 状态码 500",
"properties": {
"isSucc": {
"type": "boolean",
"enum": [
false
],
"default": false
},
"err": {
"type": "object",
"description": "TsrpcError",
"properties": {
"message": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"ApiError",
"NetworkError",
"ServerError",
"ClientError"
]
},
"code": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"nullable": true
}
},
"required": [
"message",
"type"
]
}
}
}
}
}
}
}
}
}

34
docs/tsapi.md Normal file
View File

@ -0,0 +1,34 @@
# TSRPC API 接口文档
## 通用说明
- 所有请求方法均为 `POST`
- 所有请求均需加入以下 Header :
- `Content-Type: application/json`
## 目录
- [Send](#/Send)
---
## Send <a id="/Send"></a>
**路径**
- POST `/Send`
**请求**
```ts
interface ReqSend {
content: string
}
```
**响应**
```ts
interface ResSend {
time: /*datetime*/ string
}
```

View File

@ -1,35 +1,29 @@
{ {
"name": "line-bot-ts", "name": "guesswhoiams-backend",
"version": "1.0.0", "version": "0.1.0",
"description": "", "main": "index.js",
"main": "src/app.ts", "private": true,
"scripts": { "scripts": {
"start": "nodemon src/app.ts", "dev": "tsrpc-cli dev",
"test": "nodemon src/app.ts", "debug": "nodemon --exec \"node --require ts-node/register --inspect=192.168.5.36:9229 src/index.ts\"",
"dev": "nodemon --exec \"node --require ts-node/register --inspect=192.168.5.36:9229 src/app.ts\"", "build": "tsrpc-cli build",
"build": "tsc --project ./" "doc": "tsrpc-cli doc",
"test": "mocha test/**/*.test.ts",
"proto": "tsrpc-cli proto",
"sync": "tsrpc-cli sync",
"api": "tsrpc-cli api"
}, },
"author": "",
"license": "ISC",
"devDependencies": { "devDependencies": {
"@types/dateformat": "^5.0.0", "@types/mocha": "^8.2.3",
"@types/express": "^4.17.15", "@types/node": "^15.14.9",
"@types/mysql": "^2.15.21", "mocha": "^9.2.2",
"@types/node": "^18.11.18", "onchange": "^7.1.0",
"typescript": "^4.9.4" "ts-node": "^10.9.1",
"tsrpc-cli": "^2.4.5",
"typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"@line/bot-sdk": "^7.5.2", "dayjs": "^1.11.9",
"@types/ws": "^8.5.5", "tsrpc": "^3.4.12"
"dateformat": "^4.5.1",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"fs": "^0.0.1-security",
"mysql": "^2.18.1",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"ws": "^8.13.0",
"xmlhttprequest": "^1.8.0"
} }
} }

View File

@ -1,106 +0,0 @@
import _ws from "ws"
import { Encoding } from "../Engine/CatanEngine/CSharp/System/Text/Encoding"
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse"
import WebSocketServerClass from "../NetManager/WebSocketServerClass"
/**
* Client
*/
export default class Client {
//#region private
private ws: _ws = undefined
private clientCount: number = undefined
//#endregion
//#region Lifecycle
/**
*
*/
constructor(ws: _ws, clientCount: number) {
this.ws = ws
this.clientCount = clientCount
// 當收到client消息時
ws.on('message', this.onMessage.bind(this))
// 當連線關閉
ws.on('close', this.onClose.bind(this))
}
//#endregion
//#region Custom
private onMessage(buffer: _ws.RawData): void {
// 收回來是 Buffer 格式、需轉成字串
const dataStr: string = "[" + buffer.toString().split("[").slice(1).join("[")
const json = JSON.parse(dataStr)
const method = <string>json[0]
let status = 0
const data = json[1]
const resp = {
Method: method,
Status: status,
Data: data,
IsValid: method && status === 0,
WS: this
}
if (true) {
if (data) {
console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}(${JSON.stringify(resp.Data)})`)
} else {
console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}()`)
}
}
WebSocketServerClass.Instance.OnDataReceived.DispatchCallback(resp)
// /// 發送消息給client
// this.SendClient(data)
// WebSocketServerClass.Instance.SendAllClient(data)
}
private onClose(): void {
console.log(`Client_${this.clientCount} Close connected`)
}
/** 發送給client */
public SendClient(req: INetResponse<any>): void {
const status = 0
const json: any[] = [req.Method]
//@ts-ignore
if (req.Data != null && req.Data != undefined && req.Data != NaN) {
json[1] = [status, req.Data]
}
if (true) {
//@ts-ignore
if (req.Data != null && req.Data != undefined && req.Data != NaN) {
console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}(${JSON.stringify(req.Data)})`)
} else {
console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}()`)
}
}
const str = JSON.stringify(json)
if (str.length > 65535) {
throw new Error('要傳的資料太大囉')
}
const strary = Encoding.UTF8.GetBytes(str)
const buffer = new Uint8Array(4 + strary.byteLength)
const u16ary = new Uint16Array(buffer.buffer, 0, 3)
u16ary[0] = strary.byteLength
buffer[3] = 0x01
buffer.set(strary, 4)
this.ws.send(buffer)
}
//#endregion
}

View File

@ -1,28 +0,0 @@
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse";
import Lobby from "../Lobby/Lobby";
import WebSocketServerClass from "../NetManager/WebSocketServerClass";
export default class MainControlData {
constructor() {
WebSocketServerClass.Instance.OnDataReceived.AddCallback(this._serverData, this)
}
/** SERVER主動通知 */
private _serverData(req: INetResponse<any>): void {
if (req.IsValid) {
switch (req.Method) {
case "lobby.list":
Lobby.List(req);
break
case "lobby.create":
Lobby.Create(req);
break
default:
// if (GameMain.Instance && GameMain.Instance.node && GameMain.Instance.node.parent) {
// GameMain.Instance.SettingBase.OnNetDataReceived(resp)
// }
break
}
}
}
}

View File

@ -1,50 +0,0 @@
import { LobbyCreateRequest, LobbyListRequest } from "../define/Request/LobbyRequest";
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse";
import Room from "../Room/Room";
/**
* Lobby
*/
export default class Lobby {
//#region private
private static list: Room[] = []
private static serialNumber: number = 0
//#endregion
//#region Custom
/** GetList */
public static List(req: INetResponse<any>): void {
const data = []
for (let i = 0; i < this.list.length; i++) {
const room = this.list[i];
data.push(room.SerialNumber)
}
const resp: LobbyListRequest = new LobbyListRequest(data, 0)
req.WS.SendClient(resp)
}
/** Create */
public static Create(req: INetResponse<any>): void {
const room: Room = new Room(Lobby.serialNumber, req.WS)
Lobby.serialNumber++;
this.list.push(room)
const resp: LobbyCreateRequest = new LobbyCreateRequest()
req.WS.SendClient(resp)
}
/** Join */
public static Join(req: INetResponse<any>): void {
//
}
/** Exit */
public static Exit(req: INetResponse<any>): void {
//
}
//#endregion
}

View File

@ -1,21 +0,0 @@
// export class MainControl {
// //#region 網路相關
// /**連線(目前沒有重連機制) */
// public * ConnectAsync() {
// if (NetManager.IsConnected) {
// return
// }
// this._conn = new NetConnector(BusinessTypeSetting.UseHost, BusinessTypeSetting.UsePort/*, this._realIp*/)
// this._conn.OnDataReceived.AddCallback(this._onNetDataReceived, this)
// this._conn.OnDisconnected.AddCallback(this._onNetDisconnected, this)
// this._conn.OnLoadUIMask.AddCallback(this._oOnLoadUIMask, this)
// NetManager.Initialize(this._conn)
// cc.log("[socket] connecting...")
// // 同個connector要再次連線, 可以不用叫CasinoNetManager.Initialize(), 但要先叫CasinoNetManager.Disconnect()
// yield NetManager.ConnectAsync()
// }
// //#endregion
// }

View File

@ -1,79 +0,0 @@
import express from "express"
import fs from "fs"
import _ws from "ws"
import Client from "../Client/Client"
import { Action } from "../Engine/CatanEngine/CSharp/System/Action"
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse"
import BaseSingleton from "../Engine/Utils/Singleton/BaseSingleton"
const SocketServer: typeof _ws.Server = _ws.Server
/**
* WebSocketServer
*/
export default class WebSocketServerClass extends BaseSingleton<WebSocketServerClass>() {
readonly OnDataReceived: Action<INetResponse<any>> = new Action<INetResponse<any>>()
//#region private
private wss: _ws.Server = undefined
private clientCount: number = 0
//#endregion
//#region Lifecycle
/**
*
*/
constructor() {
super()
//讀取憑證及金鑰
const prikey: string = fs.readFileSync("./certificate/RSA-privkey.pem", "utf8")
const cert: string = fs.readFileSync("./certificate/RSA-cert.pem", "utf8")
const cafile: string = fs.readFileSync("./certificate/RSA-chain.pem", "utf-8")
//建立憑證及金鑰
const credentials: Object = {
key: prikey,
cert: cert,
ca: cafile
}
// 用於辨識Line Channel的資訊
const config: any = {
channelSecret: process.env.channelSecret,
channelAccessToken: process.env.channelAccessToken || ""
}
const port: number = +process.env.PORT || 3000
const server = express().listen(port, () => {
console.log(`Listening on ${port}`)
})
//將 express 交給 SocketServer 開啟 WebSocket 的服務
this.wss = new SocketServer({ server })
//當有 client 連線成功時
this.wss.on('connection', this.onConnection.bind(this))
}
//#endregion
//#region Custom
private onConnection(ws: _ws): void {
const clientNum: number = this.clientCount
console.log(`Client_${clientNum} connected`)
new Client(ws, clientNum)
this.clientCount++
}
/** 發送給所有client */
public SendAllClient(data: any): void {
let clients = this.wss.clients //取得所有連接中的 client
clients.forEach(client => {
client.send(data) // 發送至每個 client
})
}
//#endregion
}

View File

@ -1,44 +0,0 @@
import mysql from "mysql"
import Tools from "./Tools"
/**
* DBTools
*/
export default class DBTools {
//#region Custom
public static async Query(query: string): Promise<any> {
const conn: mysql.Connection = this.connect()
let resp: any = null
let run: boolean = true
conn.query(query, function (err: mysql.MysqlError, rows: any, fields: mysql.FieldInfo[]): void {
if (err) {
console.error(`${query} Error: \n${err.message}`)
run = false
}
resp = rows
run = false
})
while (run) {
await Tools.Sleep(100)
}
conn.end()
return resp
}
private static connect(): mysql.Connection {
const conn: mysql.Connection = mysql.createConnection({
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE
})
conn.connect()
return conn
}
//#endregion
}

View File

@ -1,13 +0,0 @@
/**
* Tools
*/
export default class Tools {
//#region Custom
public static Sleep(ms: number): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
//#endregion
}

View File

@ -0,0 +1,24 @@
import { ApiCall, BaseConnection } from "tsrpc";
import Client from "../component/Client/Client";
import User from "../component/Client/User";
import Lobby from "../component/Lobby/Lobby";
import { ReqAccountLogin, ResAccountLogin } from "../shared/protocols/PtlAccountLogin";
export default async function (call: ApiCall<ReqAccountLogin, ResAccountLogin>) {
// Error
if (!call.req.name) {
call.error('Name is empty')
return;
}
// Success
const { sn, req } = call
const { name } = req
const conn: BaseConnection<any> = call.conn
console.log(`name: ${name} is Login`)
const user = new User(name)
const client = new Client(conn, sn)
client.setUser(user)
Lobby.AddClient(client)
call.succ(0)
}

12
src/api/ApiLobbyList.ts Normal file
View File

@ -0,0 +1,12 @@
import { ApiCall } from "tsrpc";
import Lobby from "../component/Lobby/Lobby";
import { ReqLobbyList, ResLobbyList } from "../shared/protocols/PtlLobbyList";
export default async function (call: ApiCall<ReqLobbyList, ResLobbyList>) {
const data: any[] = []
for (let i = 0; i < Lobby.Room.length; i++) {
const room = Lobby.Room[i]
data.push(room.SerialNumber)
}
call.succ(data)
}

26
src/api/ApiSend.ts Normal file
View File

@ -0,0 +1,26 @@
import { ApiCall } from "tsrpc";
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
// This is a demo code file
// Feel free to delete it
export default async function (call: ApiCall<ReqSend, ResSend>) {
// Error
if (call.req.content.length === 0) {
call.error('Content is empty')
return;
}
// Success
let time = new Date();
call.succ({
time: time
});
// Broadcast
server.broadcastMsg('Chat', {
content: call.req.content,
time: time
})
}

View File

@ -1,18 +0,0 @@
// 背景執行 forever start -c ts-node -a -l line-bot-ts.log src/app.ts
// 重新背景執行 forever restart -a -l line-bot-ts.log src/app.ts
// 監聽檔案變化 "npm start"
// 連線Debug "npm run dev"
import dayjs from "dayjs"
import "dayjs/locale/zh-tw"
import dotenv from "dotenv"
import MainControlData from "./DataReceived/MainControlData"
import "./Engine/CatanEngine/CSharp/String"
import "./Engine/Utils/CCExtensions/ArrayExtension"
import "./Engine/Utils/CCExtensions/NumberExtension"
import WebSocketServerClass from "./NetManager/WebSocketServerClass"
dayjs.locale("zh-tw")
dotenv.config()
new WebSocketServerClass()
new MainControlData()

View File

@ -0,0 +1,128 @@
import { BaseConnection } from "tsrpc"
import User from "./User"
/**
* Client
*/
export default class Client {
//#region private
private conn: BaseConnection<any> = undefined
private ws: any = undefined
private sn: number = undefined
//#endregion
//#region get set
public get User(): User {
return this.user
}
private user: User = undefined
//#endregion
//#region Lifecycle
/**
*
*/
constructor(conn: BaseConnection<any>, sn: number) {
this.conn = conn
this.ws = conn["ws"]
this.sn = sn
// // 當收到client消息時
// ws.on('message', this.onMessage.bind(this))
// // 當連線關閉
// ws.on('close', this.onClose.bind(this))
}
//#endregion
//#region Custom
/**
* setUser
*/
public setUser(user: User) {
this.user = user
}
//#endregion
//#region Server
// private onMessage(buffer: _ws.RawData): void {
// // 收回來是 Buffer 格式、需轉成字串
// const dataStr: string = "[" + buffer.toString().split("[").slice(1).join("[")
// const json = JSON.parse(dataStr)
// const method = <string>json[0]
// let status = 0
// const data = json[1]
// const resp = {
// Method: method,
// Status: status,
// Data: data,
// IsValid: method && status === 0,
// WS: this
// }
// if (true) {
// if (data) {
// console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}(${JSON.stringify(resp.Data)})`)
// } else {
// console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}()`)
// }
// }
// WebSocketServerClass.Instance.OnDataReceived.DispatchCallback(resp)
// // /// 發送消息給client
// // this.SendClient(data)
// // WebSocketServerClass.Instance.SendAllClient(data)
// }
// private onClose(): void {
// console.log(`Client_${this.clientCount} Close connected`)
// }
// /** 發送給client */
// public SendClient(req: INetResponse<any>): void {
// const status = 0
// const json: any[] = [req.Method]
// //@ts-ignore
// if (req.Data != null && req.Data != undefined && req.Data != NaN) {
// json[1] = [status, req.Data]
// }
// if (true) {
// //@ts-ignore
// if (req.Data != null && req.Data != undefined && req.Data != NaN) {
// console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}(${JSON.stringify(req.Data)})`)
// } else {
// console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}()`)
// }
// }
// const str = JSON.stringify(json)
// if (str.length > 65535) {
// throw new Error('要傳的資料太大囉')
// }
// const strary = Encoding.UTF8.GetBytes(str)
// const buffer = new Uint8Array(4 + strary.byteLength)
// const u16ary = new Uint16Array(buffer.buffer, 0, 3)
// u16ary[0] = strary.byteLength
// buffer[3] = 0x01
// buffer.set(strary, 4)
// this.ws.send(buffer)
// }
//#endregion
}

View File

@ -0,0 +1,29 @@
/**
* User
*/
export default class User {
//#region get set
public get Name(): string {
return this.name
}
private name: string = undefined
//#endregion
//#region Lifecycle
constructor(name: string) {
this.name = name
}
//#endregion
//#region Custom
//#endregion
}

View File

@ -0,0 +1,77 @@
import Client from "../Client/Client";
import Room from "../Room/Room";
/**
* Lobby
*/
export default class Lobby {
//#region private
private static clients: Client[] = []
private static serialNumber: number = 0
//#endregion
//#region get set
public static get Room(): Room[] { return this.room }
private static room: Room[] = []
//#endregion
//#region Custom
/** AddClient */
public static AddClient(client: Client): void {
this.clients.push(client)
}
// /** List */
// public static List(req: INetResponse<RpcLobbyListRequest>): void {
// const data = []
// for (let i = 0; i < this.list.length; i++) {
// const room = this.list[i]
// data.push(room.SerialNumber)
// }
// const resp: LobbyListRequest = new LobbyListRequest(data, 0)
// req.WS.SendClient(resp)
// }
// /** Create */
// public static Create(req: INetResponse<RpcLobbyCreateRequest>): void {
// const room: Room = new Room(Lobby.serialNumber, req.WS)
// Lobby.serialNumber++
// this.list.push(room)
// const resp: LobbyCreateRequest = new LobbyCreateRequest()
// req.WS.SendClient(resp)
// }
// /** Join */
// public static Join(req: INetResponse<RpcLobbyJoinRequest>): void {
// const serialNumber: number = req.Data
// for (let i = 0; i < this.list.length; i++) {
// const room = this.list[i]
// if (room.SerialNumber === serialNumber) {
// room.Join(req.WS)
// break
// }
// }
// }
// /** Exit */
// public static Exit(req: INetResponse<RpcLobbyExitRequest>): void {
// const serialNumber: number = req.Data
// for (let i = 0; i < this.list.length; i++) {
// const room = this.list[i]
// if (room.SerialNumber === serialNumber) {
// this.list.splice(i, 1)
// // room.Exit()
// // room = null
// break
// }
// }
// }
//#endregion
}

View File

@ -31,5 +31,13 @@ export default class Room {
//#region Custom //#region Custom
/** Join */
public Join(ws: Client): void {
this.wsArr.forEach(otherWS => {
// otherWS.SendClient()
})
this.wsArr.push(ws)
}
//#endregion //#endregion
} }

View File

@ -1,28 +0,0 @@
import { NetRequest } from "../../Engine/CatanEngine/NetManagerV2/NetRequest";
// #region Request
export type RpcExampleCodeRequest = null
export type RpcExampleCodeResponse = ExampleCodeData[]
export class ExampleCodeRequest extends NetRequest<RpcExampleCodeRequest, RpcExampleCodeResponse> {
get Method(): string {
return "example.code";
}
constructor() {
super();
}
}
// #endregion
// #region Type
export type ExampleCodeData = [
id: number,
title: number,
content: number,
time: number,
];
// #endregion

View File

@ -1,23 +0,0 @@
import { NetResponse } from "../../Engine/CatanEngine/NetManagerV2/NetResponse"
// #region Request
export type RpcLobbyListRequest = any[]
export class LobbyListRequest extends NetResponse {
protected data: RpcLobbyListRequest
protected method: string = "lobby.list"
constructor(data: RpcLobbyListRequest = undefined, status: number = 0) { super(data, status) }
}
export type RpcLobbyCreateRequest = undefined
export class LobbyCreateRequest extends NetResponse {
protected data: RpcLobbyListRequest
protected method: string = "lobby.create"
constructor(data: RpcLobbyCreateRequest = undefined, status: number = 0) { super(data, status) }
}
// #endregion
// #region Type
// #endregion

34
src/index.ts Normal file
View File

@ -0,0 +1,34 @@
import dayjs from "dayjs";
import "dayjs/locale/zh-tw";
import * as path from "path";
import { WsServer } from "tsrpc";
import { BaseEnumerator } from "./Engine/CatanEngine/CoroutineV2/Core/BaseEnumerator";
import "./Engine/CatanEngine/CSharp/String";
import "./Engine/Utils/CCExtensions/ArrayExtension";
import "./Engine/Utils/CCExtensions/NumberExtension";
import { serviceProto } from './shared/protocols/serviceProto';
BaseEnumerator.Init();
dayjs.locale("zh-tw")
// Create the Server
export const server = new WsServer(serviceProto, {
port: 3003,
// Remove this to use binary mode (remove from the client too)
json: true
});
// Initialize before server start
async function init() {
await server.autoImplementApi(path.resolve(__dirname, 'api'));
// TODO
// Prepare something... (e.g. connect the db)
};
// Entry function
async function main() {
await init();
await server.start();
}
main();

1
src/shared/protocols Submodule

@ -0,0 +1 @@
Subproject commit 3aab251c77cbd76451676b0cab3283cf345b0043

53
test/api/ApiSend.test.ts Normal file
View File

@ -0,0 +1,53 @@
import assert from 'assert';
import { WsClient } from 'tsrpc';
import { serviceProto } from '../../src/shared/protocols/serviceProto';
// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
// 2. EXECUTE `npm test` TO START UNIT TEST
describe('ApiSend', function () {
let client = new WsClient(serviceProto, {
server: 'ws://127.0.0.1:3003',
json: true,
logger: console
});
// 連線
before(async function () {
let res = await client.connect();
assert.strictEqual(res.isSucc, true, 'Failed to connect to server, have you executed `npm run dev` already?');
})
it('AccountLogin', async function () {
let ret = await client.callApi('AccountLogin', {
name: 'Test'
});
assert.ok(ret.isSucc)
});
it('LobbyList', async function () {
let ret = await client.callApi('LobbyList', null);
assert.ok(ret.isSucc)
});
// it('Success', async function () {
// let ret = await client.callApi('Send', {
// content: 'Test'
// });
// assert.ok(ret.isSucc)
// });
// it('Check content is empty', async function () {
// let ret = await client.callApi('Send', {
// content: ''
// });
// assert.deepStrictEqual(ret, {
// isSucc: false,
// err: new TsrpcError('Content is empty')
// });
// })
after(async function () {
await client.disconnect();
})
})

19
test/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": [
".",
"../src"
]
}

View File

@ -1,16 +1,23 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"lib": [ "lib": [
"es2015", "es2018"
"es2017",
"dom"
], ],
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "module": "commonjs",
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "target": "es2018",
"strict": false, /* Enable all strict type-checking options. */ "outDir": "dist",
"skipLibCheck": true /* Skip type checking all .d.ts files. */, "strict": false,
"outDir": "dist" // jsdist "esModuleInterop": true,
} "skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
// "paths": {
// "@/*": [
// "./src/*"
// ]
// }
},
"include": [
"src"
]
} }

38
tsrpc.config.ts Normal file
View File

@ -0,0 +1,38 @@
import type { TsrpcConfig } from 'tsrpc-cli';
export default <TsrpcConfig>{
// Generate ServiceProto
proto: [
{
ptlDir: 'src/shared/protocols', // Protocol dir
output: 'src/shared/protocols/serviceProto.ts', // Path for generated ServiceProto
apiDir: 'src/api', // API dir
docDir: 'docs', // API documents dir
ptlTemplate: { baseFile: 'src/shared/protocols/base.ts' },
// msgTemplate: { baseFile: 'src/shared/protocols/base.ts' },
}
],
// Sync shared code
sync: [
{
from: 'src/shared',
to: '../frontend/src/shared',
type: 'symlink' // Change this to 'copy' if your environment not support symlink
}
],
// Dev server
dev: {
autoProto: true, // Auto regenerate proto
autoSync: true, // Auto sync when file changed
autoApi: true, // Auto create API when ServiceProto updated
watch: 'src', // Restart dev server when these files changed
entry: 'src/index.ts', // Dev server command: node -r ts-node/register {entry}
},
// Build config
build: {
autoProto: true, // Auto generate proto before build
autoSync: true, // Auto sync before build
autoApi: true, // Auto generate API before build
outDir: 'dist', // Clean this dir before build
}
}

1335
yarn.lock Normal file

File diff suppressed because it is too large Load Diff